This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 7dc29855af7 [fix](types) Fix binary value ownership and timestamp 
primitives (#68301)
7dc29855af7 is described below

commit 7dc29855af7318419ff1fe4db4dfab6b44348043
Author: Gabriel <[email protected]>
AuthorDate: Tue Sep 22 15:55:30 2026 +0800

    [fix](types) Fix binary value ownership and timestamp primitives (#68301)
    
    ### What problem does this PR solve?
    
    This ports #68297 to `master`, preserving the first of five planned
    extractions from #67784.
    
    Binary `Field` values can retain references to released source storage,
    and Hive binary text needs its own Base64 contract. TIMESTAMPTZ output
    can lose historical offset seconds, format invalid NULL payloads, or
    fail again while reporting a boundary cast error.
    
    - Own long binary Field values while keeping short values inline.
    Preserve execution type lengths and decoder bytes, and add Hive Base64
    and hexadecimal decoding support.
    - Explicitly reject unsupported binary hash keys, IN, aggregates,
    predicates and computed partition transforms. Keep the existing FE
    comparison/group/join restrictions and existing binary scalar functions.
    Reject unsupported collection kernels in each function's legality check
    before coercion.
    - Preserve historical second offsets in both TIMESTAMPTZ formatting and
    parsing. Skip masked NULL payloads, reject unrepresentable local years,
    and preserve cast error/NULL behavior at boundaries.
    
    Arrow convertor migration, Parquet/ORC semantics, external writer
    changes and catalog mapping migration belong to the subsequent
    extractions. This PR does not enable native VARBINARY storage.
    
    ### Master adaptation
    
    - Retain the fixed-offset normalization and tests already present on
    master.
    - Use the current void-returning
    `VInPredicate::_prepare_zonemap_min_max` interface in both the guard and
    its test.
    - Retain master header cleanup and existing timestamp-nanosecond tests.
    - Retain the existing master binary-literal encoder and its StringView
    input contract; the older std::string-based caller fix is not
    applicable.
    
    ### Testing
    - TIMESTAMPTZ regression follow-up: reproduced both binary-output and
    stream-load failures using the master PR CI artifact, regenerated the
    two snapshots through `run-regression-test.sh`, and passed both suites
    in comparison mode from each branch checkout. Explicit `Asia/Shanghai`
    session settings were verified with the server default session zone set
    to UTC. Only historical offset seconds changed in the generated results.
    - Function-local validation update: 18 FE tests passed with Checkstyle
    enabled, covering direct legality checks, nested/mixed/variadic
    VARBINARY arguments, both `collect_set` arities, supported ordinary
    types, SQL analysis, and existing array rewrites. The new
    direct-legality tests reproduced missing rejection before the change.
    
    - BE ASAN build and **199 tests passed** across 17 suites using
    `run-be-ut.sh`, including binary lifetime/SerDe/rejection, timestamp
    parsing/casts, and existing Arrow/Variant serialization coverage.
    - `VarBinaryUnsupportedCollectionTest`: **passed** (13 unsupported
    expressions plus supported byte-preserving collection analysis). The FE
    test reactor and repository Checkstyle passed after cleaning stale
    branch build artifacts.
    - Repository clang-format 16 check and build-header hygiene checks:
    **passed**; 31 changed C++ source/header files.
    - Groovy compilation of the three regression suites: **passed**. Live
    SQL regression execution remains pending CI.
    - clang-tidy was attempted but could not complete because master already
    contains an unmatched `NOLINTEND` in `be/src/core/types.h`. A diagnostic
    run with the compiler resource directory corrected reproduced that
    blocker; the other reported findings in `column_varbinary.cpp` were
    outside changed lines. This is not a clean clang-tidy result.
    
    The focused BE test source list and local test/build settings were
    restored before committing. No build configuration changes are included.
    
    ### Release note
    
    Fix binary value lifetime and serialization, reject unsupported binary
    computation paths, and preserve TIMESTAMPTZ historical offsets and
    boundary error behavior.
    
    ### Check List (For Author)
    
    - Test
    - [x] Regression test (three self-checking suites added; execution
    pending CI)
      - [x] Unit Test
    - Behavior changed:
    - [x] Yes. Binary rejection and timestamp boundary behavior are
    described above.
    - Does this need documentation?
    - [x] No. This fixes existing type behavior without introducing a
    configuration option.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
    
    
    ### Scoped review follow-up
    
    This follow-up only fixes correctness/stability defects introduced by
    this PR. Compatibility preservation, pre-existing limitations,
    additional VARBINARY computation/validation, performance refactors, and
    test-style-only rewrites are excluded.
    
    - Separate historical TIMESTAMPTZ wire-offset parsing from session
    fixed-zone limits in both parser paths.
    - Use UTC diagnostics for the TIMESTAMP_NS cast/comparison failures
    affected by the new local-year formatting exception.
    - Validation: 31 focused ASAN BE tests passed, including ordinary
    DATE/DATETIME parsing. Three targeted tests failed before the fixes.
    clang-format 16 and build hygiene passed. Full clang-tidy remains
    affected by pre-existing diagnostics.
    - Branch-specific UTC/GMT normalization and FE folding fixes are handled
    in #68297; the corresponding master behavior predates this PR or already
    defers folding.
    
    ### CI test follow-up
    
    - Keep the binary literal test's owning Field alive while reading its
    StringView. Branch-4.1 now has the corresponding short/long embedded-NUL
    coverage using its execution API.
    - Replace the obsolete +15:00 rejection input with +24:00. Add generated
    historical-offset checks in both cast modes; all prior snapshot results
    are unchanged.
    - Validation: 34 focused ASAN BE tests passed on each branch. The
    lifetime error and the original SQL mismatch were reproduced. The
    complete cast regression suite passed in comparison mode from both
    branch checkouts against the reported master CI artifact. clang-format
    16 passed; full clang-tidy remains blocked by pre-existing diagnostics.
    
    This follow-up changes tests only and retains the agreed scope: no
    compatibility work or additional binary computation support. Existing
    muted failures are outside this fix.
---
 be/src/core/column/column_varbinary.cpp            |  20 +++
 be/src/core/column/column_varbinary.h              |   6 +
 be/src/core/data_type/data_type_factory.cpp        |   4 +
 .../data_type_serde/data_type_varbinary_serde.cpp  |  80 +++++++++++
 .../data_type_serde/data_type_varbinary_serde.h    |  16 +++
 be/src/core/field.cpp                              |  73 +++++++++-
 be/src/core/field.h                                |   7 +-
 be/src/core/value/timestamptz_value.cpp            |  16 +++
 be/src/exec/common/hash_table/hash_key_type.h      |   7 +
 .../sink/writer/iceberg/partition_transformers.cpp |   6 +
 .../aggregate/aggregate_function_min_max_impl.h    |   4 +
 be/src/exprs/create_predicate_function.h           |   3 +
 be/src/exprs/function/cast/cast_to_date.h          |   6 +-
 .../function/cast/cast_to_datetimev2_impl.hpp      |  89 +++++++++---
 be/src/exprs/function/cast/cast_to_string.h        |  19 ++-
 be/src/exprs/function/cast/cast_to_timestamp_ns.h  |   3 +-
 be/src/exprs/function/cast/cast_to_timestamptz.h   |   8 +-
 be/src/exprs/function/functions_comparison.h       |   4 +-
 be/src/exprs/function/in.h                         |   4 +
 be/src/exprs/vin_predicate.cpp                     |   4 +-
 be/src/util/raw_value.h                            |   6 +
 be/src/util/timezone_utils.h                       |   1 +
 be/test/core/column/column_varbinary_test.cpp      | 154 +++++++++++++++++++++
 .../core/data_type/data_type_varbinary_test.cpp    |  18 ++-
 .../data_type_serde_varbinary_test.cpp             |  69 +++++++++
 .../exec/common/hash_table/hash_key_type_test.cpp  |  10 ++
 .../writer/iceberg/partition_transformers_test.cpp |  14 ++
 be/test/exprs/aggregate/agg_min_max_test.cpp       |  12 ++
 be/test/exprs/expr_zonemap_filter_test.cpp         |  17 +++
 .../function/cast/cast_to_string_api_test.cpp      |  46 ++++++
 .../function/cast/cast_to_timestamptz_test.cpp     |  79 +++++++++++
 be/test/exprs/function/function_varbinary_test.cpp |  15 ++
 be/test/exprs/vexpr_test.cpp                       |   4 +-
 be/test/runtime/timestamptz_value_test.cpp         | 133 ++++++++++++++++++
 .../expressions/functions/agg/CollectSet.java      |   6 +
 .../functions/scalar/ArrayContains.java            |   5 +
 .../functions/scalar/ArrayContainsAll.java         |   5 +
 .../functions/scalar/ArrayDistinct.java            |   1 +
 .../functions/scalar/ArrayEnumerateUniq.java       |   1 +
 .../expressions/functions/scalar/ArrayExcept.java  |   5 +
 .../functions/scalar/ArrayFunctionUtils.java       |  42 ++++++
 .../functions/scalar/ArrayIntersect.java           |   1 +
 .../functions/scalar/ArrayPosition.java            |   1 +
 .../expressions/functions/scalar/ArrayRemove.java  |   1 +
 .../expressions/functions/scalar/ArrayUnion.java   |   1 +
 .../functions/scalar/ArraysOverlap.java            |   1 +
 .../expressions/functions/scalar/CountEqual.java   |   1 +
 .../functions/VarBinaryCollectionLegalityTest.java | 114 +++++++++++++++
 .../types/VarBinaryUnsupportedCollectionTest.java  |  62 +++++++++
 .../stream_load/test_timestamptz_stream_load.out   |   8 +-
 .../timestamptz/test_cast_timestamptz.out          |   6 +
 .../timestamptz/test_timestamptz_binary_output.out |  16 +--
 .../test_timestamptz_stream_load.groovy            |   2 +
 .../timestamptz/test_cast_timestamptz.groovy       |  14 +-
 .../test_timestamptz_binary_output.groovy          |   4 +
 .../test_timestamptz_historical_offset.groovy      |  53 +++++++
 .../test_timestamptz_null_string.groovy            |  48 +++++++
 .../test_timestamptz_output_boundary.groovy        |  71 ++++++++++
 58 files changed, 1371 insertions(+), 55 deletions(-)

diff --git a/be/src/core/column/column_varbinary.cpp 
b/be/src/core/column/column_varbinary.cpp
index 4a54ca421d0..71dd2f91e6b 100644
--- a/be/src/core/column/column_varbinary.cpp
+++ b/be/src/core/column/column_varbinary.cpp
@@ -31,6 +31,26 @@
 #include "exec/sort/sort_block.h"
 
 namespace doris {
+
+void ColumnVarbinary::insert_many_continuous_binary_data(const char* data, 
const uint32_t* offsets,
+                                                         size_t num) {
+    reserve(size() + num);
+    for (size_t row = 0; row < num; ++row) {
+        insert_data(data + offsets[row], offsets[row + 1] - offsets[row]);
+    }
+}
+
+void ColumnVarbinary::insert_many_dict_data(const int32_t* data_array, size_t 
start_index,
+                                            const StringRef* dict, size_t 
data_num,
+                                            uint32_t dict_num) {
+    reserve(size() + data_num);
+    // Decoder pages can be released after the call; copy long dictionary 
entries into our arena.
+    for (size_t row = start_index; row < start_index + data_num; ++row) {
+        const auto& value = dict[data_array[row]];
+        insert_data(value.data, value.size);
+    }
+}
+
 MutableColumnPtr ColumnVarbinary::clone_resized(size_t size) const {
     auto res = create();
     if (size > 0) {
diff --git a/be/src/core/column/column_varbinary.h 
b/be/src/core/column/column_varbinary.h
index caad77e28ad..2efc8da06fd 100644
--- a/be/src/core/column/column_varbinary.h
+++ b/be/src/core/column/column_varbinary.h
@@ -30,6 +30,7 @@
 #include "core/string_view.h"
 
 namespace doris {
+// Binary IO does not enable hash computation; inherit IColumn's unsupported 
methods.
 class ColumnVarbinary final : public COWHelper<IColumn, ColumnVarbinary> {
 private:
     using Self = ColumnVarbinary;
@@ -189,6 +190,11 @@ public:
     void insert_many_strings_overflow(const StringRef* strings, size_t num,
                                       size_t max_length) override;
 
+    void insert_many_continuous_binary_data(const char* data, const uint32_t* 
offsets,
+                                            size_t num) override;
+    void insert_many_dict_data(const int32_t* data_array, size_t start_index, 
const StringRef* dict,
+                               size_t data_num, uint32_t dict_num = 0) 
override;
+
     void sort_column(const ColumnSorter* sorter, EqualFlags& flags, 
IColumn::Permutation& perms,
                      EqualRange& range, bool last_column) const override;
 
diff --git a/be/src/core/data_type/data_type_factory.cpp 
b/be/src/core/data_type/data_type_factory.cpp
index 4b00064eb99..8167c7a7a03 100644
--- a/be/src/core/data_type/data_type_factory.cpp
+++ b/be/src/core/data_type/data_type_factory.cpp
@@ -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) {
+            // 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());
diff --git a/be/src/core/data_type_serde/data_type_varbinary_serde.cpp 
b/be/src/core/data_type_serde/data_type_varbinary_serde.cpp
index 16ff1b20e8a..5832ffb1f30 100644
--- a/be/src/core/data_type_serde/data_type_varbinary_serde.cpp
+++ b/be/src/core/data_type_serde/data_type_varbinary_serde.cpp
@@ -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.
+    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(
+        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;
+    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];
diff --git a/be/src/core/data_type_serde/data_type_varbinary_serde.h 
b/be/src/core/data_type_serde/data_type_varbinary_serde.h
index b4f5f26eab5..4ac78c371ad 100644
--- a/be/src/core/data_type_serde/data_type_varbinary_serde.h
+++ b/be/src/core/data_type_serde/data_type_varbinary_serde.h
@@ -49,6 +49,19 @@ public:
     Status deserialize_one_cell_from_json(IColumn& column, Slice& slice,
                                           const FormatOptions& options) const 
override;
 
+    Status deserialize_one_cell_from_hive_text(
+            IColumn& column, Slice& slice, const FormatOptions& options,
+            int hive_text_complex_type_delimiter_level = 1) const override;
+
+    Status 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 = 1) const override;
+
+    Status serialize_one_cell_to_hive_text(
+            const IColumn& column, int64_t row_num, BufferWritable& bw, 
FormatOptions& options,
+            int hive_text_complex_type_delimiter_level = 1) const override;
+
     Status deserialize_column_from_json_vector(IColumn& column, 
std::vector<Slice>& slices,
                                                uint64_t* num_deserialized,
                                                const FormatOptions& options) 
const override {
@@ -95,6 +108,9 @@ public:
 
     void to_string(const IColumn& column, size_t row_num, BufferWritable& bw,
                    const FormatOptions& options) const override;
+
+    Status from_string(StringRef& str, IColumn& column,
+                       const FormatOptions& options) const override;
 };
 
 } // namespace doris
diff --git a/be/src/core/field.cpp b/be/src/core/field.cpp
index d6ee59009a6..ec42f6b86c6 100644
--- a/be/src/core/field.cpp
+++ b/be/src/core/field.cpp
@@ -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;
+    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();
diff --git a/be/src/core/field.h b/be/src/core/field.h
index cf350b4c469..155278fa337 100644
--- a/be/src/core/field.h
+++ b/be/src/core/field.h
@@ -191,13 +191,15 @@ public:
     Field(PrimitiveType w) : type(w) {}
     template <PrimitiveType T>
     static Field create_field(const typename PrimitiveTypeTraits<T>::CppType& 
data) {
-        auto f = Field(T);
+        // Publish the type only after construction succeeds, so allocation 
failures cannot
+        // destroy uninitialized owned storage (including long binary values).
+        auto f = Field();
         f.template create_concrete<T>(data);
         return f;
     }
     template <PrimitiveType T>
     static Field create_field(typename PrimitiveTypeTraits<T>::CppType&& data) 
{
-        auto f = Field(T);
+        auto f = Field();
         f.template create_concrete<T>(std::move(data));
         return f;
     }
@@ -243,6 +245,7 @@ public:
         if (this != &rhs) {
             if (type != rhs.type) {
                 destroy();
+                type = TYPE_NULL;
                 create(std::move(rhs));
             } else {
                 assign(std::move(rhs));
diff --git a/be/src/core/value/timestamptz_value.cpp 
b/be/src/core/value/timestamptz_value.cpp
index ffa9bf530e2..05114342b30 100644
--- a/be/src/core/value/timestamptz_value.cpp
+++ b/be/src/core/value/timestamptz_value.cpp
@@ -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) {
+        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;
+    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)};
 }
 
diff --git a/be/src/exec/common/hash_table/hash_key_type.h 
b/be/src/exec/common/hash_table/hash_key_type.h
index 8ce7882f3a6..09bd3d60642 100644
--- a/be/src/exec/common/hash_table/hash_key_type.h
+++ b/be/src/exec/common/hash_table/hash_key_type.h
@@ -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) {
+            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);
     }
diff --git a/be/src/exec/sink/writer/iceberg/partition_transformers.cpp 
b/be/src/exec/sink/writer/iceberg/partition_transformers.cpp
index e38e0e4c5eb..d52ea289df1 100644
--- a/be/src/exec/sink/writer/iceberg/partition_transformers.cpp
+++ b/be/src/exec/sink/writer/iceberg/partition_transformers.cpp
@@ -46,6 +46,12 @@ const std::chrono::sys_days 
PartitionColumnTransformUtils::EPOCH = std::chrono::
 std::unique_ptr<PartitionColumnTransform> PartitionColumnTransforms::create(
         const doris::iceberg::PartitionField& field, const DataTypePtr& 
source_type) {
     auto& transform = field.transform();
+    // Identity/void only carry values; computed binary partition transforms 
are unsupported.
+    if (source_type->get_primitive_type() == TYPE_VARBINARY && transform != 
"identity" &&
+        transform != "void") {
+        throw Exception(ErrorCode::NOT_IMPLEMENTED_ERROR,
+                        "VARBINARY partition transform {} is not supported", 
transform);
+    }
     static const std::regex has_width(R"((\w+)\[(\d+)\])");
     std::smatch width_match;
 
diff --git a/be/src/exprs/aggregate/aggregate_function_min_max_impl.h 
b/be/src/exprs/aggregate/aggregate_function_min_max_impl.h
index 9717cc0461c..0c49c79aa8a 100644
--- a/be/src/exprs/aggregate/aggregate_function_min_max_impl.h
+++ b/be/src/exprs/aggregate/aggregate_function_min_max_impl.h
@@ -141,6 +141,10 @@ AggregateFunctionPtr 
create_aggregate_function_single_value(const String& name,
         return creator_without_type::create_unary_arguments<
                 
AggregateFunctionsSingleValue<Data<SingleValueDataComplexType>>>(
                 argument_types, result_is_nullable, attr);
+    case PrimitiveType::TYPE_VARBINARY:
+        // Owning binary values for IO must not implicitly enable single-value 
aggregates.
+        throw Exception(ErrorCode::NOT_IMPLEMENTED_ERROR, "VARBINARY aggregate 
{} is not supported",
+                        name);
     default:
         return nullptr;
     }
diff --git a/be/src/exprs/create_predicate_function.h 
b/be/src/exprs/create_predicate_function.h
index 43c32ebb0b5..afdfe80e798 100644
--- a/be/src/exprs/create_predicate_function.h
+++ b/be/src/exprs/create_predicate_function.h
@@ -108,6 +108,9 @@ typename Traits::BasePtr 
create_predicate_function(PrimitiveType type, bool null
     using Creator = PredicateFunctionCreator<Traits>;
 
     switch (type) {
+    case TYPE_VARBINARY:
+        // Binary read/write support does not provide storage or runtime 
predicate kernels.
+        throw Exception(ErrorCode::NOT_IMPLEMENTED_ERROR, "VARBINARY 
predicates are not supported");
     case TYPE_BOOLEAN: {
         return Creator::template create<TYPE_BOOLEAN, N>(null_aware);
     }
diff --git a/be/src/exprs/function/cast/cast_to_date.h 
b/be/src/exprs/function/cast/cast_to_date.h
index eecfcb6d552..dad009409a6 100644
--- a/be/src/exprs/function/cast/cast_to_date.h
+++ b/be/src/exprs/function/cast/cast_to_date.h
@@ -504,9 +504,11 @@ public:
             TimestampTzValue from_tz {col_from[i]};
             DateV2Value<DateTimeV2ValueType> dt;
             if (!from_tz.to_datetime(dt, local_time_zone, dt_scale, tz_scale)) 
{
+                // The failed local conversion may also be unformattable. 
Render the stored
+                // UTC fields so reporting the cast error cannot throw a 
second exception.
                 return Status::InvalidArgument(
-                        "can not cast from  timestamptz : {} to datetime in 
timezone : {}",
-                        from_tz.to_string(local_time_zone), 
context->state()->timezone());
+                        "can not cast from  timestamptz : {} UTC to datetime 
in timezone : {}",
+                        from_tz.utc_dt().to_string(tz_scale), 
context->state()->timezone());
             }
             col_to_data[i] = dt.to_date_int_val();
         }
diff --git a/be/src/exprs/function/cast/cast_to_datetimev2_impl.hpp 
b/be/src/exprs/function/cast/cast_to_datetimev2_impl.hpp
index 21c78ca0f09..9c1999d2ead 100644
--- a/be/src/exprs/function/cast/cast_to_datetimev2_impl.hpp
+++ b/be/src/exprs/function/cast/cast_to_datetimev2_impl.hpp
@@ -695,6 +695,7 @@ FRAC:
             const char sign = *ptr;
             ++ptr;
             part[1] = 0;
+            uint32_t second_offset = 0;
 
             uint32_t length = count_digits(ptr, end);
             // hour
@@ -705,7 +706,9 @@ FRAC:
                 SET_PARAMS_RET_FALSE_IFN((consume_digit<UInt32, 2>(ptr, end, 
part[0])),
                                          "invalid hour offset '{}'", 
std::string {ptr, end});
             }
-            SET_PARAMS_RET_FALSE_IFN(part[0] <= 14, "invalid hour offset 
'{}'", part[0]);
+            SET_PARAMS_RET_FALSE_IFN(
+                    part[0] < (type == DataTimeCastEnumType::TIMESTAMP_TZ ? 
24U : 15U),
+                    "invalid hour offset '{}'", part[0]);
             if (ptr < end) {
                 if (*ptr == ':') {
                     ++ptr;
@@ -713,16 +716,37 @@ FRAC:
                 // minute
                 SET_PARAMS_RET_FALSE_IFN((consume_digit<UInt32, 2>(ptr, end, 
part[1])),
                                          "invalid minute offset '{}'", 
std::string {ptr, end});
-                SET_PARAMS_RET_FALSE_IFN((part[1] == 0 || part[1] == 30 || 
part[1] == 45),
-                                         "invalid minute offset '{}'", 
part[1]);
+                if constexpr (type == DataTimeCastEnumType::TIMESTAMP_TZ) {
+                    // TIMESTAMPTZ output preserves historical offsets, 
including seconds and
+                    // non-quarter-hour minutes. Keep the legacy DATETIME 
parser unchanged.
+                    SET_PARAMS_RET_FALSE_IFN(part[1] < 60, "invalid minute 
offset '{}'", part[1]);
+                    if (ptr < end && *ptr == ':') {
+                        ++ptr;
+                        SET_PARAMS_RET_FALSE_IFN(
+                                (consume_digit<UInt32, 2>(ptr, end, 
second_offset)),
+                                "invalid second offset '{}'", std::string 
{ptr, end});
+                        SET_PARAMS_RET_FALSE_IFN(second_offset < 60, "invalid 
second offset '{}'",
+                                                 second_offset);
+                    }
+                } else {
+                    SET_PARAMS_RET_FALSE_IFN((part[1] == 0 || part[1] == 30 || 
part[1] == 45),
+                                             "invalid minute offset '{}'", 
part[1]);
+                }
+            }
+            if constexpr (type == DataTimeCastEnumType::TIMESTAMP_TZ) {
+                // Wire offsets include historical zones outside the session 
fixed-zone range.
+                // Use the exact offset even when it has no seconds (for 
example, Guam's -14:21).
+                const auto offset = static_cast<int>(part[0] * 3600 + part[1] 
* 60 + second_offset);
+                parsed_tz = cctz::fixed_time_zone(cctz::seconds(sign == '-' ? 
-offset : offset));
+            } else {
+                SET_PARAMS_RET_FALSE_IFN(part[0] != 14 || part[1] == 0,
+                                         "invalid timezone offset '{}'",
+                                         combine_tz_offset(sign, part[0], 
part[1]));
+                SET_PARAMS_RET_FALSE_IFN(
+                        TimezoneUtils::find_cctz_time_zone(
+                                combine_tz_offset(sign, part[0], part[1]), 
parsed_tz),
+                        "invalid timezone offset '{}'", 
combine_tz_offset(sign, part[0], part[1]));
             }
-            SET_PARAMS_RET_FALSE_IFN(part[0] != 14 || part[1] == 0, "invalid 
timezone offset '{}'",
-                                     combine_tz_offset(sign, part[0], 
part[1]));
-
-            SET_PARAMS_RET_FALSE_IFN(TimezoneUtils::find_cctz_time_zone(
-                                             combine_tz_offset(sign, part[0], 
part[1]), parsed_tz),
-                                     "invalid timezone offset '{}'",
-                                     combine_tz_offset(sign, part[0], 
part[1]));
         } else {
             // timezone name
             const auto* start = ptr;
@@ -959,7 +983,7 @@ inline bool 
CastToDatetimeV2::from_string_non_strict_mode_internal(
             // offset
             const char sign = *ptr;
             ++ptr;
-            uint32_t hour_offset, minute_offset = 0;
+            uint32_t hour_offset, minute_offset = 0, second_offset = 0;
 
             uint32_t length = count_digits(ptr, end);
             // hour
@@ -968,26 +992,45 @@ inline bool 
CastToDatetimeV2::from_string_non_strict_mode_internal(
             } else {
                 PROPAGATE_FALSE((consume_digit<UInt32, 2>(ptr, end, 
hour_offset)));
             }
-            SET_PARAMS_RET_FALSE_IFN(hour_offset <= 14, "invalid hour offset 
'{}'", hour_offset);
+            SET_PARAMS_RET_FALSE_IFN(
+                    hour_offset < (type == DataTimeCastEnumType::TIMESTAMP_TZ 
? 24U : 15U),
+                    "invalid hour offset '{}'", hour_offset);
             if (ptr < end) {
                 if (*ptr == ':') {
                     ++ptr;
                 }
                 // minute
                 PROPAGATE_FALSE((consume_digit<UInt32, 2>(ptr, end, 
minute_offset)));
+                if constexpr (type == DataTimeCastEnumType::TIMESTAMP_TZ) {
+                    SET_PARAMS_RET_FALSE_IFN(minute_offset < 60, "invalid 
minute offset '{}'",
+                                             minute_offset);
+                    if (ptr < end && *ptr == ':') {
+                        ++ptr;
+                        PROPAGATE_FALSE((consume_digit<UInt32, 2>(ptr, end, 
second_offset)));
+                        SET_PARAMS_RET_FALSE_IFN(second_offset < 60, "invalid 
second offset '{}'",
+                                                 second_offset);
+                    }
+                } else {
+                    SET_PARAMS_RET_FALSE_IFN(
+                            (minute_offset == 0 || minute_offset == 30 || 
minute_offset == 45),
+                            "invalid minute offset {}", minute_offset);
+                }
+            }
+            if constexpr (type == DataTimeCastEnumType::TIMESTAMP_TZ) {
+                // Match strict parsing: a serialized historical offset is not 
a session setting.
+                const auto offset =
+                        static_cast<int>(hour_offset * 3600 + minute_offset * 
60 + second_offset);
+                parsed_tz = cctz::fixed_time_zone(cctz::seconds(sign == '-' ? 
-offset : offset));
+            } else {
+                SET_PARAMS_RET_FALSE_IFN(hour_offset != 14 || minute_offset == 
0,
+                                         "invalid timezone offset '{}'",
+                                         combine_tz_offset(sign, hour_offset, 
minute_offset));
                 SET_PARAMS_RET_FALSE_IFN(
-                        (minute_offset == 0 || minute_offset == 30 || 
minute_offset == 45),
-                        "invalid minute offset {}", minute_offset);
+                        TimezoneUtils::find_cctz_time_zone(
+                                combine_tz_offset(sign, hour_offset, 
minute_offset), parsed_tz),
+                        "invalid timezone offset '{}'",
+                        combine_tz_offset(sign, hour_offset, minute_offset));
             }
-            SET_PARAMS_RET_FALSE_IFN(hour_offset != 14 || minute_offset == 0,
-                                     "invalid timezone offset '{}'",
-                                     combine_tz_offset(sign, hour_offset, 
minute_offset));
-
-            SET_PARAMS_RET_FALSE_IFN(
-                    TimezoneUtils::find_cctz_time_zone(
-                            combine_tz_offset(sign, hour_offset, 
minute_offset), parsed_tz),
-                    "invalid timezone offset '{}'",
-                    combine_tz_offset(sign, hour_offset, minute_offset));
         } else {
             // timezone name
             const auto* start = ptr;
diff --git a/be/src/exprs/function/cast/cast_to_string.h 
b/be/src/exprs/function/cast/cast_to_string.h
index 3e4e188b7cc..8f205386c76 100644
--- a/be/src/exprs/function/cast/cast_to_string.h
+++ b/be/src/exprs/function/cast/cast_to_string.h
@@ -17,6 +17,8 @@
 
 #pragma once
 
+#include <algorithm>
+
 #include "core/data_type_serde/data_type_serde.h"
 #include "core/types.h"
 #include "core/value/time_value.h"
@@ -581,7 +583,22 @@ public:
             limited_col = col_from.cut(0, input_rows_count);
             col_to_serialize = limited_col.get();
         }
-        type.get_serde()->to_string_batch(*col_to_serialize, *col_to, options);
+        const auto serde = type.get_serde();
+        if (null_map != nullptr && std::any_of(null_map, null_map + 
input_rows_count,
+                                               [](auto value) { return value 
!= 0; })) {
+            // Nested payloads of NULL rows may be uninitialized or outside 
the type's
+            // domain. Do not format them before the nullable wrapper restores 
the mask.
+            col_to->reserve(input_rows_count);
+            VectorBufferWriter write_buffer(*col_to);
+            for (size_t row = 0; row < input_rows_count; ++row) {
+                if (!null_map[row]) {
+                    serde->to_string(*col_to_serialize, row, write_buffer, 
options);
+                }
+                write_buffer.commit();
+            }
+        } else {
+            serde->to_string_batch(*col_to_serialize, *col_to, options);
+        }
 
         block.replace_by_position(result, std::move(col_to));
         return Status::OK();
diff --git a/be/src/exprs/function/cast/cast_to_timestamp_ns.h 
b/be/src/exprs/function/cast/cast_to_timestamp_ns.h
index 2ba379d940b..7e664553967 100644
--- a/be/src/exprs/function/cast/cast_to_timestamp_ns.h
+++ b/be/src/exprs/function/cast/cast_to_timestamp_ns.h
@@ -382,9 +382,10 @@ public:
                     col_to->get_data()[i].from_datetime(datetime);
             if (!converted) {
                 if constexpr (CastMode == CastModeType::StrictMode) {
+                    // The session-local year may be unrepresentable even 
though UTC is valid.
                     return Status::InvalidArgument(
                             "can not cast timestamptz {} to TIMESTAMP_NS in 
timezone {}",
-                            source.to_string(local_time_zone), 
context->state()->timezone());
+                            source.utc_dt().to_string(source_scale), 
context->state()->timezone());
                 }
                 col_null->get_data()[i] = true;
             }
diff --git a/be/src/exprs/function/cast/cast_to_timestamptz.h 
b/be/src/exprs/function/cast/cast_to_timestamptz.h
index 1e31cf24be5..f09bf602a03 100644
--- a/be/src/exprs/function/cast/cast_to_timestamptz.h
+++ b/be/src/exprs/function/cast/cast_to_timestamptz.h
@@ -200,7 +200,6 @@ public:
 
         auto col_to = ColumnTimeStampTz::create(input_rows_count);
         auto& col_to_data = col_to->get_data();
-        const auto& local_time_zone = context->state()->timezone_obj();
 
         const auto from_scale = 
block.get_by_position(arguments[0]).type->get_scale();
         const auto to_scale = block.get_by_position(result).type->get_scale();
@@ -214,10 +213,11 @@ public:
             auto& to_tz = col_to_data[i];
 
             if (!transform_date_scale(to_scale, from_scale, to_tz, from_tz)) {
+                // Error reporting must not format an overflowing timestamp in 
a session
+                // timezone whose local year may itself be outside the 
supported range.
                 return Status::InvalidArgument(
-                        "can not cast from  timestamptz : {} to timestamptz in 
timezone : {}",
-                        TimestampTzValue {from_tz}.to_string(local_time_zone, 
from_scale),
-                        context->state()->timezone());
+                        "can not cast from  timestamptz : {} UTC to 
timestamptz in timezone : {}",
+                        from_tz.utc_dt().to_string(from_scale), 
context->state()->timezone());
             }
         }
         block.get_by_position(result).column = std::move(col_to);
diff --git a/be/src/exprs/function/functions_comparison.h 
b/be/src/exprs/function/functions_comparison.h
index ffb6958be4d..0bfce7e67f5 100644
--- a/be/src/exprs/function/functions_comparison.h
+++ b/be/src/exprs/function/functions_comparison.h
@@ -703,10 +703,10 @@ private:
                 const auto scale = temporal_type->get_scale();
                 if (!temporal.to_datetime(local_datetime, 
context->state()->timezone_obj(), scale,
                                           scale)) [[unlikely]] {
+                    // Preserve the comparison error instead of re-entering 
the failing formatter.
                     return Status::InvalidArgument(
                             "can not compare timestamptz {} with TIMESTAMP_NS 
in timezone {}",
-                            
temporal.to_string(context->state()->timezone_obj(), scale),
-                            context->state()->timezone());
+                            temporal.utc_dt().to_string(scale), 
context->state()->timezone());
                 }
                 comparison = compare_timestamp_ns_with_temporal(timestamp, 
local_datetime);
             } else {
diff --git a/be/src/exprs/function/in.h b/be/src/exprs/function/in.h
index 9075415e636..10e101324f6 100644
--- a/be/src/exprs/function/in.h
+++ b/be/src/exprs/function/in.h
@@ -105,6 +105,10 @@ public:
         if (scope == FunctionContext::THREAD_LOCAL) {
             return Status::OK();
         }
+        // Binary IO must not route IN through the shared string/storage 
predicate implementation.
+        if (context->get_arg_type(0)->get_primitive_type() == TYPE_VARBINARY) {
+            return Status::NotSupported("VARBINARY IN/NOT IN is not 
supported");
+        }
         std::shared_ptr<InState> state = std::make_shared<InState>();
         context->set_function_state(scope, state);
         DCHECK(context->get_num_args() >= 1);
diff --git a/be/src/exprs/vin_predicate.cpp b/be/src/exprs/vin_predicate.cpp
index 64a0f60a405..d956c2173a0 100644
--- a/be/src/exprs/vin_predicate.cpp
+++ b/be/src/exprs/vin_predicate.cpp
@@ -176,7 +176,9 @@ void VInPredicate::_prepare_zonemap_min_max(VExprContext* 
context) {
     // dictionary, and raw evaluation direct-slot-only while Bloom may consume 
a nested leaf.
     const auto data_type = remove_nullable(bloom_probe->value_type);
     DORIS_CHECK(data_type != nullptr);
-    if (is_complex_type(data_type->get_primitive_type())) {
+    // Binary IN is rejected by the SQL function; do not build storage 
predicates for its keys.
+    if (is_complex_type(data_type->get_primitive_type()) ||
+        data_type->get_primitive_type() == TYPE_VARBINARY) {
         return;
     }
 
diff --git a/be/src/util/raw_value.h b/be/src/util/raw_value.h
index ca9914e4064..4ed839e0bc5 100644
--- a/be/src/util/raw_value.h
+++ b/be/src/util/raw_value.h
@@ -23,6 +23,7 @@
 #include <string>
 
 #include "common/consts.h"
+#include "common/exception.h"
 #include "common/logging.h"
 #include "core/data_type/define_primitive_type.h"
 #include "core/packed_int128.h"
@@ -44,6 +45,11 @@ public:
 // Because crc32 hardware is not equal with zlib crc32
 inline uint32_t RawValue::zlib_crc32(const void* v, size_t len, const 
PrimitiveType& type,
                                      uint32_t seed) {
+    // Reject binary even for NULL instead of reaching the default-type 
assertion or hash path.
+    if (type == TYPE_VARBINARY) {
+        throw Exception(ErrorCode::NOT_IMPLEMENTED_ERROR,
+                        "VARBINARY tablet routing hash is not supported");
+    }
     // Hash_combine with v = 0
     if (v == nullptr) {
         uint32_t value = 0x9e3779b9;
diff --git a/be/src/util/timezone_utils.h b/be/src/util/timezone_utils.h
index 3ae0a23d1d1..1f37c7e2e9e 100644
--- a/be/src/util/timezone_utils.h
+++ b/be/src/util/timezone_utils.h
@@ -18,6 +18,7 @@
 
 #pragma once
 
+#include <cstdint>
 #include <string>
 
 namespace cctz {
diff --git a/be/test/core/column/column_varbinary_test.cpp 
b/be/test/core/column/column_varbinary_test.cpp
index 0360b14cda8..20e07dfa8af 100644
--- a/be/test/core/column/column_varbinary_test.cpp
+++ b/be/test/core/column/column_varbinary_test.cpp
@@ -36,9 +36,138 @@
 #include "core/string_ref.h"
 #include "core/string_view.h"
 #include "core/types.h"
+#include "exec/common/sip_hash.h"
+#include "runtime/memory/mem_tracker_limiter.h"
+#include "runtime/thread_context.h"
+#include "util/defer_op.h"
+#include "util/raw_value.h"
 
 namespace doris {
 
+TEST(ColumnVarbinaryStorageTest, FieldsOwnValuesAcrossInlineBoundary) {
+    for (size_t size : {0U, 1U, 12U, 13U, 64U}) {
+        SCOPED_TRACE(size);
+        const std::string expected(size, '\xff');
+        std::string source = expected;
+        auto field = Field::create_field<TYPE_VARBINARY>(StringView(source));
+        Field copied = field;
+        source.assign(size, 'x');
+        EXPECT_EQ(field.get<TYPE_VARBINARY>().str(), expected);
+        EXPECT_EQ(copied.get<TYPE_VARBINARY>().str(), expected);
+        field = Field::create_field<TYPE_VARBINARY>(StringView("replacement"));
+        Field moved = std::move(copied);
+        EXPECT_EQ(moved.get<TYPE_VARBINARY>().str(), expected);
+        moved = field;
+        EXPECT_EQ(moved.get<TYPE_VARBINARY>().str(), "replacement");
+    }
+}
+
+TEST(ColumnVarbinaryStorageTest, FieldsOwnLongBinaryValues) {
+    const std::string expected(64, '\xff');
+    Field copy;
+    {
+        auto column = ColumnVarbinary::create();
+        column->insert_data(expected.data(), expected.size());
+        Field value = (*column)[0];
+        EXPECT_NE(value.get<TYPE_VARBINARY>().data(), 
column->get_data_at(0).data);
+        copy = value;
+        EXPECT_NE(copy.get<TYPE_VARBINARY>().data(), 
value.get<TYPE_VARBINARY>().data());
+        column->clear();
+    }
+    EXPECT_EQ(copy.get<TYPE_VARBINARY>().str(), expected);
+    copy = Field::create_field<TYPE_VARBINARY>(StringView("a"));
+    EXPECT_EQ(copy.get<TYPE_VARBINARY>().str(), "a");
+}
+
+TEST(ColumnVarbinaryStorageTest, FieldsChargeAndReleaseTrackedMemory) {
+    const std::string payload(64, '\xff');
+    const std::string smaller(16, 's');
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                    "binary-field-ownership", 
1024);
+    SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(tracker);
+    const auto consumption = [&] {
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        return tracker->consumption();
+    };
+    {
+        auto field = Field::create_field<TYPE_VARBINARY>(StringView(payload));
+        EXPECT_EQ(consumption(), 64);
+        Field copied = field;
+        EXPECT_EQ(consumption(), 128);
+        // Replacement must release the old allocation using its original size.
+        copied = Field::create_field<TYPE_VARBINARY>(StringView(smaller));
+        EXPECT_EQ(consumption(), 80);
+        EXPECT_EQ(copied.get<TYPE_VARBINARY>().str(), smaller);
+        copied = Field::create_field<TYPE_VARBINARY>(StringView("inline"));
+        EXPECT_EQ(consumption(), 64);
+        auto inline_field = 
Field::create_field<TYPE_VARBINARY>(StringView("inline"));
+        EXPECT_EQ(consumption(), 64);
+        EXPECT_EQ(inline_field.get<TYPE_VARBINARY>().str(), "inline");
+        field.get<TYPE_VARBINARY>() = StringView("shorter view");
+        EXPECT_EQ(consumption(), 64);
+    }
+    EXPECT_EQ(consumption(), 0);
+}
+
+TEST(ColumnVarbinaryStorageTest, FieldsRespectTrackedMemoryLimit) {
+    const std::string payload(64, '\xff');
+    const std::string oversized(128, 'x');
+    auto tracker = 
MemTrackerLimiter::create_shared(MemTrackerLimiter::Type::OTHER,
+                                                    "binary-field-limit", 96);
+    SCOPED_SWITCH_THREAD_MEM_TRACKER_LIMITER(tracker);
+    ++enable_thread_catch_bad_alloc;
+    Defer restore_catch_bad_alloc {[] { --enable_thread_catch_bad_alloc; }};
+    const auto consumption = [&] {
+        thread_context()->thread_mem_tracker_mgr->flush_untracked_mem();
+        return tracker->consumption();
+    };
+    const auto expect_allocation_failure = [](auto&& operation) {
+        try {
+            operation();
+            FAIL() << "Expected the binary payload to respect the memory 
limit";
+        } catch (const Exception& e) {
+            EXPECT_EQ(e.code(), ErrorCode::MEM_ALLOC_FAILED);
+        }
+    };
+    expect_allocation_failure(
+            [&] { auto field = 
Field::create_field<TYPE_VARBINARY>(StringView(oversized)); });
+    EXPECT_EQ(consumption(), 0);
+    {
+        auto field = Field::create_field<TYPE_VARBINARY>(StringView(payload));
+        EXPECT_EQ(consumption(), 64);
+        // A deep copy must check the peak while the source is still retained.
+        expect_allocation_failure([&] { Field copied = field; });
+        EXPECT_EQ(consumption(), 64);
+        auto destination = 
Field::create_field<TYPE_VARBINARY>(StringView("inline"));
+        expect_allocation_failure([&] { destination = field; });
+        EXPECT_EQ(destination.get<TYPE_VARBINARY>().str(), "inline");
+        EXPECT_EQ(field.get<TYPE_VARBINARY>().str(), payload);
+        EXPECT_EQ(consumption(), 64);
+        const std::string smaller(16, 's');
+        destination = Field::create_field<TYPE_VARBINARY>(StringView(smaller));
+        EXPECT_EQ(consumption(), 80);
+        expect_allocation_failure([&] { destination = field; });
+        EXPECT_EQ(destination.get<TYPE_VARBINARY>().str(), smaller);
+        EXPECT_EQ(consumption(), 80);
+    }
+    EXPECT_EQ(consumption(), 0);
+}
+
+TEST(ColumnVarbinaryStorageTest, 
StorageDecoderInsertionPreservesBinaryPayloads) {
+    auto column = ColumnVarbinary::create();
+    const std::string payload("\0a\0\xff", 4);
+    const uint32_t offsets[] = {0, 0, 1, 4};
+    ASSERT_NO_THROW(column->insert_many_continuous_binary_data(payload.data(), 
offsets, 3));
+    EXPECT_EQ(column->get_data_at(0).to_string(), "");
+    EXPECT_EQ(column->get_data_at(1).to_string(), std::string("\0", 1));
+    EXPECT_EQ(column->get_data_at(2).to_string(), payload.substr(1));
+    const StringRef dictionary[] = {{payload.data(), payload.size()}, {"", 0}};
+    const int32_t codes[] = {1, 0, 1};
+    ASSERT_NO_THROW(column->insert_many_dict_data(codes, 1, dictionary, 2, 2));
+    EXPECT_EQ(column->get_data_at(3).to_string(), payload);
+    EXPECT_EQ(column->get_data_at(4).to_string(), "");
+}
+
 class ColumnVarbinaryTest : public ::testing::Test {
 protected:
     void SetUp() override {}
@@ -113,6 +242,31 @@ TEST_F(ColumnVarbinaryTest, BasicInsertGetPopClear) {
     EXPECT_EQ(col->byte_size(), 0U);
 }
 
+TEST_F(ColumnVarbinaryTest, TabletRoutingHashIsNotSupported) {
+    for (const char* value : {static_cast<const char*>(nullptr), "", 
"binary"}) {
+        EXPECT_THROW(RawValue::zlib_crc32(value, value == nullptr ? 0 : 
strlen(value),
+                                          TYPE_VARBINARY, 0),
+                     Exception);
+    }
+}
+
+TEST_F(ColumnVarbinaryTest, HashingIsNotSupported) {
+    auto binary = ColumnVarbinary::create();
+    binary->insert_data("\0\xff", 2);
+    SipHash sip;
+    uint64_t hash64 = 17;
+    uint32_t hash32 = 23;
+    EXPECT_THROW(binary->update_hash_with_value(0, sip), Exception);
+    EXPECT_THROW(binary->update_hashes_with_value(&hash64, nullptr), 
Exception);
+    EXPECT_THROW(binary->update_xxHash_with_value(0, 1, hash64, nullptr), 
Exception);
+    EXPECT_THROW(binary->update_crcs_with_value(&hash32, TYPE_VARBINARY, 1, 0, 
nullptr), Exception);
+    EXPECT_THROW(binary->update_crc_with_value(0, 1, hash32, nullptr), 
Exception);
+    EXPECT_THROW(binary->update_crc32c_batch(&hash32, nullptr), Exception);
+    EXPECT_THROW(binary->update_crc32c_single(0, 1, hash32, nullptr), 
Exception);
+    EXPECT_EQ(hash64, 17);
+    EXPECT_EQ(hash32, 23);
+}
+
 TEST_F(ColumnVarbinaryTest, InsertFromAndRanges) {
     auto src = ColumnVarbinary::create();
     std::vector<std::string> vals = {make_bytes(1, 0x01), make_bytes(2, 0x02),
diff --git a/be/test/core/data_type/data_type_varbinary_test.cpp 
b/be/test/core/data_type/data_type_varbinary_test.cpp
index d71710ceb25..3e4ee03ced2 100644
--- a/be/test/core/data_type/data_type_varbinary_test.cpp
+++ b/be/test/core/data_type/data_type_varbinary_test.cpp
@@ -35,12 +35,14 @@
 #include "core/column/column_varbinary.h"
 #include "core/data_type/common_data_type_serder_test.h"
 #include "core/data_type/common_data_type_test.h"
+#include "core/data_type/data_type_factory.hpp"
 #include "core/data_type/data_type_string.h"
 #include "core/data_type_serde/data_type_serde.h"
 #include "core/field.h"
 #include "core/string_buffer.hpp"
 #include "core/string_view.h"
 #include "core/types.h"
+#include "storage/olap_common.h"
 #include "util/mysql_row_buffer.h"
 
 namespace doris {
@@ -257,9 +259,9 @@ TEST_F(DataTypeVarbinaryTest, SerDeWriteColumnToMysql) {
     EXPECT_GT(rb_bin.length(), 0);
 }
 
-TEST_F(DataTypeVarbinaryTest, GetStorageFieldTypeThrows) {
+TEST_F(DataTypeVarbinaryTest, GetStorageFieldType) {
     DataTypeVarbinary dt;
-    EXPECT_THROW({ (void)dt.get_storage_field_type(); }, doris::Exception);
+    EXPECT_THROW(dt.get_storage_field_type(), doris::Exception);
 }
 
 TEST_F(DataTypeVarbinaryTest, GetFieldFromTExprNodeWithEmbeddedNull) {
@@ -285,6 +287,16 @@ TEST_F(DataTypeVarbinaryTest, ToProtobufDefaultLen) {
     EXPECT_EQ(scalar.len(), -1);
 }
 
+TEST_F(DataTypeVarbinaryTest, ProtobufPreservesDeclaredLength) {
+    PTypeDesc type;
+    auto* node = type.add_types();
+    node->set_type(TTypeNodeType::SCALAR);
+    node->mutable_scalar_type()->set_type(TPrimitiveType::VARBINARY);
+    node->mutable_scalar_type()->set_len(2);
+    auto restored = DataTypeFactory::instance().create_data_type(type, false);
+    EXPECT_EQ(assert_cast<const DataTypeVarbinary&>(*restored).len(), 2);
+}
+
 TEST_F(DataTypeVarbinaryTest, GetFieldWithDataTypeNonInline) {
     DataTypeVarbinary dt;
     auto col = dt.create_column();
@@ -299,4 +311,4 @@ TEST_F(DataTypeVarbinaryTest, 
GetFieldWithDataTypeNonInline) {
     ASSERT_EQ(memcmp(sv.data(), big.data(), sv.size()), 0);
 }
 
-} // namespace doris
\ No newline at end of file
+} // namespace doris
diff --git a/be/test/core/data_type_serde/data_type_serde_varbinary_test.cpp 
b/be/test/core/data_type_serde/data_type_serde_varbinary_test.cpp
index 5f03f363c86..4a42e769766 100644
--- a/be/test/core/data_type_serde/data_type_serde_varbinary_test.cpp
+++ b/be/test/core/data_type_serde/data_type_serde_varbinary_test.cpp
@@ -27,6 +27,7 @@
 #include <memory>
 #include <orc/OrcFile.hh>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "core/arena.h"
@@ -57,6 +58,74 @@ static std::string make_bytes(size_t n, uint8_t seed = 0x31) 
{
 
 class DataTypeVarbinarySerDeTest : public ::testing::Test {};
 
+TEST_F(DataTypeVarbinarySerDeTest, HiveTextBinaryUsesBase64InsteadOfJsonBytes) 
{
+    DataTypeVarbinarySerDe serde;
+    auto column = ColumnVarbinary::create();
+    auto options = DataTypeSerDe::get_default_format_options();
+    // Hive LazyBinary decodes Base64, whereas JSON/CSV and binary file 
readers keep raw bytes.
+    const std::vector<std::pair<std::string, std::string>> cases = {
+            {"dGVzdDI=", "test2"},
+            {"AP8=", std::string("\0\xff", 2)},
+            {"", ""},
+            {"not!base64", "not!base64"},
+            {"====", "===="},
+            {"dGVzdDI", "test2"},
+            {"dG Vz\tdDI=\r\n", "test2"},
+            {"-_8=", std::string("\xfb\xff", 2)},
+            {"YWJjZ", "abc"}};
+    for (const auto& [encoded, expected] : cases) {
+        Slice slice(encoded);
+        ASSERT_TRUE(serde.deserialize_one_cell_from_hive_text(*column, slice, 
options).ok());
+        EXPECT_EQ(expected, column->get_data_at(column->size() - 
1).to_string());
+    }
+    auto output = ColumnString::create();
+    VectorBufferWriter writer(*output);
+    auto binary = ColumnVarbinary::create();
+    binary->insert_data("\0\xff", 2);
+    ASSERT_TRUE(serde.serialize_one_cell_to_hive_text(*binary, 0, writer, 
options).ok());
+    writer.commit();
+    EXPECT_EQ("AP8=", output->get_data_at(0).to_string());
+
+    std::string encoded = "dGVzdDI=";
+    Slice raw(encoded);
+    ASSERT_TRUE(serde.deserialize_one_cell_from_json(*column, raw, 
options).ok());
+    EXPECT_EQ(encoded, column->get_data_at(column->size() - 1).to_string());
+
+    auto vector_column = ColumnVarbinary::create();
+    std::vector<Slice> slices;
+    for (const auto& [text, expected] : cases) {
+        slices.emplace_back(text);
+    }
+    uint64_t count = 0;
+    ASSERT_TRUE(serde.deserialize_column_from_hive_text_vector(*vector_column, 
slices, &count,
+                                                               options, 2)
+                        .ok());
+    ASSERT_EQ(cases.size(), count);
+    for (size_t i = 0; i < cases.size(); ++i) {
+        EXPECT_EQ(cases[i].second, vector_column->get_data_at(i).to_string());
+    }
+}
+
+TEST_F(DataTypeVarbinarySerDeTest, FromHexStringPreservesBinaryPartitionBytes) 
{
+    DataTypeVarbinarySerDe serde;
+    auto column = ColumnVarbinary::create();
+    auto options = DataTypeSerDe::get_default_format_options();
+    for (const std::string text : {"0x00FF", "0x", 
"0x123E4567E89B12D3A456426614174000"}) {
+        StringRef input(text);
+        ASSERT_TRUE(serde.from_string(input, *column, options).ok());
+    }
+    ASSERT_EQ(3, column->size());
+    EXPECT_EQ(std::string("\0\xff", 2), column->get_data_at(0).to_string());
+    EXPECT_EQ(0, column->get_data_at(1).size);
+    
EXPECT_EQ(std::string("\x12\x3e\x45\x67\xe8\x9b\x12\xd3\xa4\x56\x42\x66\x14\x17\x40\x00",
 16),
+              column->get_data_at(2).to_string());
+    for (const std::string text : {"0x0", "0xGG", "1234"}) {
+        StringRef input(text);
+        EXPECT_FALSE(serde.from_string(input, *column, options).ok());
+        EXPECT_EQ(3, column->size());
+    }
+}
+
 TEST_F(DataTypeVarbinarySerDeTest, Name) {
     DataTypeVarbinarySerDe serde;
     EXPECT_EQ(serde.get_name(), std::string("Varbinary"));
diff --git a/be/test/exec/common/hash_table/hash_key_type_test.cpp 
b/be/test/exec/common/hash_table/hash_key_type_test.cpp
index 4d68ff70801..770f3433f1d 100644
--- a/be/test/exec/common/hash_table/hash_key_type_test.cpp
+++ b/be/test/exec/common/hash_table/hash_key_type_test.cpp
@@ -24,9 +24,19 @@
 #include "core/data_type/data_type_nullable.h"
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_struct.h"
+#include "core/data_type/data_type_varbinary.h"
 
 namespace doris {
 
+TEST(HashKeyTypeTest, BinaryKeysAreNotSupported) {
+    auto type = std::make_shared<DataTypeVarbinary>();
+    for (const auto& key : DataTypes {type, make_nullable(type)}) {
+        EXPECT_THROW(get_hash_key_type({key}), Exception);
+        EXPECT_THROW(get_hash_key_type({key, 
std::make_shared<DataTypeInt32>()}), Exception);
+        EXPECT_THROW(get_hash_key_type({std::make_shared<DataTypeInt32>(), 
key}), Exception);
+    }
+}
+
 TEST(HashKeyTypeTest, FixedWidthStructUsesSerializedKey) {
     const auto group_key = make_nullable(std::make_shared<DataTypeInt32>());
 
diff --git a/be/test/exec/sink/writer/iceberg/partition_transformers_test.cpp 
b/be/test/exec/sink/writer/iceberg/partition_transformers_test.cpp
index 974eb817e88..60d1cc976ae 100644
--- a/be/test/exec/sink/writer/iceberg/partition_transformers_test.cpp
+++ b/be/test/exec/sink/writer/iceberg/partition_transformers_test.cpp
@@ -22,6 +22,8 @@
 #include <limits>
 
 #include "core/data_type/data_type_date_or_datetime_v2.h"
+#include "core/data_type/data_type_varbinary.h"
+#include "format/table/iceberg/partition_spec.h"
 
 namespace doris {
 
@@ -31,6 +33,18 @@ public:
     virtual ~PartitionTransformersTest() = default;
 };
 
+TEST_F(PartitionTransformersTest, 
binary_computation_transforms_are_not_supported) {
+    const auto type = std::make_shared<DataTypeVarbinary>();
+    for (const auto& source_type : DataTypes {type, make_nullable(type)}) {
+        for (const auto& transform : {"truncate[1]", "bucket[16]"}) {
+            EXPECT_THROW(
+                    PartitionColumnTransforms::create(
+                            iceberg::PartitionField(1, 1000, "binary_key", 
transform), source_type),
+                    Exception);
+        }
+    }
+}
+
 TEST_F(PartitionTransformersTest, test_integer_truncate_transform) {
     const std::vector<int32_t> values({1, -1});
     auto column = ColumnInt32::create();
diff --git a/be/test/exprs/aggregate/agg_min_max_test.cpp 
b/be/test/exprs/aggregate/agg_min_max_test.cpp
index 86b19d7462b..c02cbd78841 100644
--- a/be/test/exprs/aggregate/agg_min_max_test.cpp
+++ b/be/test/exprs/aggregate/agg_min_max_test.cpp
@@ -36,6 +36,7 @@
 #include "core/data_type/data_type_number.h"
 #include "core/data_type/data_type_string.h"
 #include "core/data_type/data_type_timestamp_ns.h"
+#include "core/data_type/data_type_varbinary.h"
 #include "core/field.h"
 #include "core/string_ref.h"
 #include "core/types.h"
@@ -49,6 +50,17 @@ namespace doris {
 // declare function
 void register_aggregate_function_minmax(AggregateFunctionSimpleFactory& 
factory);
 
+TEST(BinaryAggregateTest, SingleValueAggregatesAreNotSupported) {
+    AggregateFunctionSimpleFactory factory;
+    register_aggregate_function_minmax(factory);
+    for (const auto& type : DataTypes {std::make_shared<DataTypeVarbinary>(),
+                                       
make_nullable(std::make_shared<DataTypeVarbinary>())}) {
+        for (const auto& name : {"min", "max"}) {
+            EXPECT_THROW(factory.get(name, {type}, nullptr, 
type->is_nullable(), -1), Exception);
+        }
+    }
+}
+
 class AggMinMaxTest : public ::testing::TestWithParam<std::string> {};
 
 TEST_P(AggMinMaxTest, min_max_test) {
diff --git a/be/test/exprs/expr_zonemap_filter_test.cpp 
b/be/test/exprs/expr_zonemap_filter_test.cpp
index 88dee1cb974..27dcfdd8344 100644
--- a/be/test/exprs/expr_zonemap_filter_test.cpp
+++ b/be/test/exprs/expr_zonemap_filter_test.cpp
@@ -40,6 +40,7 @@
 #include "core/data_type/data_type_string.h"
 #include "core/data_type/data_type_struct.h"
 #include "core/data_type/data_type_time.h"
+#include "core/data_type/data_type_varbinary.h"
 #include "core/field.h"
 #include "core/string_ref.h"
 #include "core/value/vdatetime_value.h"
@@ -76,6 +77,7 @@
 #endif
 
 namespace doris {
+
 namespace {
 
 Field int_field(int32_t value) {
@@ -1507,6 +1509,21 @@ TEST(ExprZonemapFilterTest, 
VInPredicateDictionaryAndBloomProbePreparedSet) {
               in_predicate->evaluate_bloom_filter(matching_bloom_ctx));
 }
 
+TEST(ExprZonemapFilterTest, BinaryInDoesNotMaterializeStoragePredicates) {
+    auto type = std::make_shared<DataTypeVarbinary>();
+    for (bool negative : {false, true}) {
+        auto predicate = 
std::make_shared<VInPredicate>(make_in_predicate_node(negative, 2));
+        predicate->add_child(make_slot(0, type));
+        auto field = Field::create_field<TYPE_VARBINARY>(StringView("\0\xff", 
2));
+        predicate->add_child(
+                std::make_shared<VLiteral>(create_texpr_node_from(field, 
TYPE_VARBINARY, 0, 0)));
+        ASSERT_NO_THROW(predicate->_prepare_zonemap_min_max(nullptr));
+        EXPECT_FALSE(predicate->can_evaluate_zonemap_filter());
+        EXPECT_FALSE(predicate->can_evaluate_dictionary_filter());
+        EXPECT_FALSE(predicate->can_evaluate_bloom_filter());
+    }
+}
+
 TEST(ExprZonemapFilterTest, VInPredicatePreparesNestedBloomValuesDuringOpen) {
     auto leaf_type = int_type();
     auto struct_type = std::make_shared<DataTypeStruct>(DataTypes {leaf_type}, 
Strings {"value"});
diff --git a/be/test/exprs/function/cast/cast_to_string_api_test.cpp 
b/be/test/exprs/function/cast/cast_to_string_api_test.cpp
index 537090699b1..2d59760824c 100644
--- a/be/test/exprs/function/cast/cast_to_string_api_test.cpp
+++ b/be/test/exprs/function/cast/cast_to_string_api_test.cpp
@@ -17,6 +17,8 @@
 
 #include <gtest/gtest.h>
 
+#include "core/data_type/data_type_string.h"
+#include "core/data_type/data_type_timestamptz.h"
 #include "core/types.h"
 #include "core/value/ipv4_value.h"
 #include "core/value/vdatetime_value.h"
@@ -27,6 +29,50 @@
 
 namespace doris {
 
+TEST(CastToStringTest, NullableTimestampSkipsInvalidPayload) {
+    TimestampTzValue valid;
+    valid.unchecked_set_time(2024, 1, 2, 3, 4, 5, 123456);
+    TimestampTzValue invalid;
+    // A NULL row may retain arbitrary bytes from an earlier expression's 
allocation.
+    invalid.unchecked_set_time(64251, 1, 1, 0, 0, 0);
+    auto input = ColumnTimeStampTz::create();
+    input->insert_value(valid);
+    input->insert_value(invalid);
+    input->insert_value(valid);
+    input->insert_value(invalid);
+    const NullMap null_map {0, 1, 0, 1};
+    ColumnPtr input_column = std::move(input);
+
+    for (size_t rows : {0, 1, 2, 3, 4}) {
+        Block block {{input_column, std::make_shared<DataTypeTimeStampTz>(6), 
"input"},
+                     {nullptr, std::make_shared<DataTypeString>(), "result"}};
+        ASSERT_NO_THROW({
+            ASSERT_TRUE(CastToStringFunction::execute_impl(nullptr, block, 
{0}, 1, rows,
+                                                           null_map.data())
+                                .ok());
+        });
+        const auto& result = assert_cast<const 
ColumnString&>(*block.get_by_position(1).column);
+        ASSERT_EQ(result.size(), rows);
+        for (size_t row = 0; row < rows; ++row) {
+            EXPECT_EQ(result.get_data_at(row).to_string(),
+                      null_map[row] ? "" : "2024-01-02 03:04:05.123456+00:00");
+        }
+    }
+}
+
+TEST(CastToStringTest, NonNullInvalidTimestampIsStillRejected) {
+    TimestampTzValue invalid;
+    invalid.unchecked_set_time(64251, 1, 1, 0, 0, 0);
+    auto input = ColumnTimeStampTz::create();
+    input->insert_value(invalid);
+    Block block {{std::move(input), std::make_shared<DataTypeTimeStampTz>(6), 
"input"},
+                 {nullptr, std::make_shared<DataTypeString>(), "result"}};
+    const NullMap null_map {0};
+    EXPECT_THROW(static_cast<void>(CastToStringFunction::execute_impl(nullptr, 
block, {0}, 1, 1,
+                                                                      
null_map.data())),
+                 Exception);
+}
+
 TEST(CastToStringTest, test) {
     {
         UInt8 num = 1;
diff --git a/be/test/exprs/function/cast/cast_to_timestamptz_test.cpp 
b/be/test/exprs/function/cast/cast_to_timestamptz_test.cpp
index cc08b4a69e2..ee925e95c53 100644
--- a/be/test/exprs/function/cast/cast_to_timestamptz_test.cpp
+++ b/be/test/exprs/function/cast/cast_to_timestamptz_test.cpp
@@ -32,6 +32,7 @@
 #include "exprs/function/cast/cast_to_date.h"
 #include "exprs/function/cast/cast_to_timestamp_ns.h"
 #include "exprs/function/cast/cast_wrapper_decls.h"
+#include "exprs/function/functions_comparison.h"
 #include "testutil/column_helper.h"
 #include "testutil/datetime_ut_util.h"
 #include "testutil/mock/mock_runtime_state.h"
@@ -459,4 +460,82 @@ TEST_F(CastTimeStampTzTest, 
from_timestamptz_non_strict_mode_to_datetime) {
     }
 }
 
+TEST_F(CastTimeStampTzTest, 
boundary_cast_errors_preserve_status_and_null_semantics) {
+    const auto maximum = make_timestamptz(9999, 12, 31, 23, 59, 59, 999999);
+    for (const bool to_datetime : {false, true}) {
+        auto make_block = [&]() {
+            auto block = 
ColumnHelper::create_block<DataTypeTimeStampTz>({maximum});
+            block.get_by_position(0).type = 
std::make_shared<DataTypeTimeStampTz>(6);
+            DataTypePtr target = to_datetime
+                                         ? 
DataTypePtr(std::make_shared<DataTypeDateTimeV2>(6))
+                                         : 
DataTypePtr(std::make_shared<DataTypeTimeStampTz>(0));
+            block.insert(ColumnWithTypeAndName {nullptr, target, "result"});
+            return block;
+        };
+        auto strict_block = make_block();
+        Status status;
+        // Local display overflow must not replace the cast's error status 
with an exception.
+        if (to_datetime) {
+            CastToImpl<CastModeType::StrictMode, DataTypeTimeStampTz, 
DataTypeDateTimeV2> cast;
+            ASSERT_NO_THROW(
+                    status = cast.execute_impl(&context, strict_block, 
arguments, result, 1));
+        } else {
+            CastToImpl<CastModeType::StrictMode, DataTypeTimeStampTz, 
DataTypeTimeStampTz> cast;
+            ASSERT_NO_THROW(
+                    status = cast.execute_impl(&context, strict_block, 
arguments, result, 1));
+        }
+        // TRY_CAST must recognize a conversion failure instead of propagating 
an execution error.
+        EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT);
+        EXPECT_NE(status.to_string().find("9999-12-31 23:59:59.999999"), 
std::string::npos);
+
+        auto nullable_block = make_block();
+        nullable_block.get_by_position(result).type =
+                make_nullable(nullable_block.get_by_position(result).type);
+        if (to_datetime) {
+            CastToImpl<CastModeType::NonStrictMode, DataTypeTimeStampTz, 
DataTypeDateTimeV2> cast;
+            status = cast.execute_impl(&context, nullable_block, arguments, 
result, 1, nullptr);
+        } else {
+            CastToImpl<CastModeType::NonStrictMode, DataTypeTimeStampTz, 
DataTypeTimeStampTz> cast;
+            status = cast.execute_impl(&context, nullable_block, arguments, 
result, 1, nullptr);
+        }
+        ASSERT_TRUE(status.ok()) << status;
+        const auto& nullable =
+                assert_cast<const 
ColumnNullable&>(*nullable_block.get_by_position(result).column);
+        EXPECT_TRUE(nullable.get_null_map_data()[0]);
+    }
+}
+
+// NOLINTNEXTLINE(readability-function-cognitive-complexity): GTest exception 
macros add branches.
+TEST_F(CastTimeStampTzTest, timestamp_ns_local_year_overflow_returns_status) {
+    for (const bool upper : {false, true}) {
+        _state._timezone_obj = cctz::fixed_time_zone(std::chrono::hours(upper 
? 8 : -8));
+        const auto value = upper ? make_timestamptz(9999, 12, 31, 23, 59, 59, 
999999)
+                                 : make_timestamptz(0, 1, 1, 0, 0, 0, 0);
+        auto block = ColumnHelper::create_block<DataTypeTimeStampTz>({value});
+        block.get_by_position(0).type = 
std::make_shared<DataTypeTimeStampTz>(6);
+        block.insert({nullptr, std::make_shared<DataTypeTimeStampNs>(), 
"result"});
+        CastToImpl<CastModeType::StrictMode, DataTypeTimeStampTz, 
DataTypeTimeStampNs> cast;
+        Status status;
+        ASSERT_NO_THROW(status = cast.execute_impl(&context, block, {0}, 1, 
1));
+        EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT);
+        EXPECT_NE(status.to_string().find("can not cast timestamptz"), 
std::string::npos);
+
+        CastToImpl<CastModeType::NonStrictMode, DataTypeTimeStampTz, 
DataTypeTimeStampNs> try_cast;
+        ASSERT_TRUE(try_cast.execute_impl(&context, block, {0}, 1, 1).ok());
+        EXPECT_TRUE(block.get_by_position(1).column->is_null_at(0));
+
+        auto ns_column = ColumnTimeStampNs::create();
+        ns_column->insert_default();
+        block.get_by_position(1).column = std::move(ns_column);
+        block.insert({nullptr, std::make_shared<DataTypeUInt8>(), 
"comparison"});
+        FunctionComparison<EqualsOp, NameEquals> equals;
+        // Error reporting must not try to display the unrepresentable 
session-local year.
+        for (const ColumnNumbers& inputs : {ColumnNumbers {0, 1}, 
ColumnNumbers {1, 0}}) {
+            ASSERT_NO_THROW(status = equals.execute_impl(&context, block, 
inputs, 2, 1));
+            EXPECT_EQ(status.code(), ErrorCode::INVALID_ARGUMENT);
+            EXPECT_NE(status.to_string().find("can not compare timestamptz"), 
std::string::npos);
+        }
+    }
+}
+
 } // namespace doris
diff --git a/be/test/exprs/function/function_varbinary_test.cpp 
b/be/test/exprs/function/function_varbinary_test.cpp
index c0fe3b02f1c..3de706661ec 100644
--- a/be/test/exprs/function/function_varbinary_test.cpp
+++ b/be/test/exprs/function/function_varbinary_test.cpp
@@ -19,11 +19,26 @@
 
 #include "core/data_type/data_type_varbinary.h"
 #include "exprs/function/function_test_util.h"
+#include "exprs/function/in.h"
 
 namespace doris {
 
 using namespace ut_type;
 
+TEST(function_binary_test, in_and_not_in_are_not_supported) {
+    RuntimeState state;
+    for (const auto& type : DataTypes {std::make_shared<DataTypeVarbinary>(),
+                                       
make_nullable(std::make_shared<DataTypeVarbinary>())}) {
+        const DataTypes arguments {type, type};
+        auto context = FunctionContext::create_context(
+                &state, make_nullable(std::make_shared<DataTypeUInt8>()), 
arguments);
+        EXPECT_EQ(FunctionIn<false>().open(context.get(), 
FunctionContext::FRAGMENT_LOCAL).code(),
+                  ErrorCode::NOT_IMPLEMENTED_ERROR);
+        EXPECT_EQ(FunctionIn<true>().open(context.get(), 
FunctionContext::FRAGMENT_LOCAL).code(),
+                  ErrorCode::NOT_IMPLEMENTED_ERROR);
+    }
+}
+
 TEST(function_binary_test, function_binary_length_test) {
     std::string func_name = "length";
     InputTypeSet input_types = {PrimitiveType::TYPE_VARBINARY};
diff --git a/be/test/exprs/vexpr_test.cpp b/be/test/exprs/vexpr_test.cpp
index ddcc5234e0f..10bdfb815d7 100644
--- a/be/test/exprs/vexpr_test.cpp
+++ b/be/test/exprs/vexpr_test.cpp
@@ -725,7 +725,9 @@ TEST(TEST_VEXPR, LITERALTEST) {
 
             ColumnPtr result_column;
             ASSERT_TRUE(literal.execute_column(nullptr, nullptr, nullptr, 1, 
result_column).ok());
-            auto sv = (*result_column)[0].get<TYPE_VARBINARY>();
+            // The view borrows the Field's owned bytes, so keep the Field 
alive for the assertion.
+            const auto result_field = (*result_column)[0];
+            const auto& sv = result_field.get<TYPE_VARBINARY>();
             EXPECT_EQ(value, std::string(sv.data(), sv.size()));
         }
     }
diff --git a/be/test/runtime/timestamptz_value_test.cpp 
b/be/test/runtime/timestamptz_value_test.cpp
index ad35e8681c3..35a3e6ec594 100644
--- a/be/test/runtime/timestamptz_value_test.cpp
+++ b/be/test/runtime/timestamptz_value_test.cpp
@@ -21,9 +21,13 @@
 #include <cctz/time_zone.h>
 #include <gtest/gtest.h>
 
+#include <chrono>
 #include <string>
+#include <utility>
 
+#include "common/exception.h"
 #include "exprs/function/cast/cast_base.h"
+#include "exprs/function/cast/cast_to_timestamptz_impl.hpp"
 #include "testutil/datetime_ut_util.h"
 #include "util/timezone_utils.h"
 
@@ -34,6 +38,98 @@ TEST(TimeStampTzValueTest, make_time) {
     EXPECT_EQ(tz.to_date_int_val(), MIN_DATETIME_V2);
 }
 
+TEST(TimeStampTzValueTest, ToStringPreservesHistoricalOffsetSeconds) {
+    TimezoneUtils::load_offsets_to_cache();
+    const auto utc = cctz::utc_time_zone();
+    cctz::time_zone shanghai;
+    cctz::time_zone new_york;
+    cctz::time_zone kathmandu;
+    ASSERT_TRUE(cctz::load_time_zone("Asia/Shanghai", &shanghai));
+    ASSERT_TRUE(cctz::load_time_zone("America/New_York", &new_york));
+    ASSERT_TRUE(cctz::load_time_zone("Asia/Kathmandu", &kathmandu));
+    struct TestCase {
+        cctz::time_zone zone;
+        int year;
+        const char* civil;
+        const char* offset;
+    };
+    const TestCase cases[] = {
+            {.zone = shanghai, .year = 1890, .civil = "1890-01-01 08:05:43", 
.offset = "+08:05:43"},
+            {.zone = new_york, .year = 1880, .civil = "1879-12-31 19:03:58", 
.offset = "-04:56:02"},
+            // Pre-standard offsets vary across tzdata versions. Fixed zones 
keep coverage
+            // of offsets beyond 14 hours independent of the host's historical 
records.
+            {.zone = cctz::fixed_time_zone(std::chrono::seconds(-57368)),
+             .year = 1800,
+             .civil = "1799-12-31 08:03:52",
+             .offset = "-15:56:08"},
+            {.zone = cctz::fixed_time_zone(std::chrono::seconds(-51660)),
+             .year = 1800,
+             .civil = "1799-12-31 09:39:00",
+             .offset = "-14:21"},
+            {.zone = shanghai, .year = 2024, .civil = "2024-01-01 08:00:00", 
.offset = "+08:00"},
+            {.zone = new_york, .year = 2024, .civil = "2023-12-31 19:00:00", 
.offset = "-05:00"},
+            {.zone = kathmandu, .year = 2024, .civil = "2024-01-01 05:45:00", 
.offset = "+05:45"},
+            {.zone = utc, .year = 2024, .civil = "2024-01-01 00:00:00", 
.offset = "+00:00"},
+    };
+    for (const auto& test_case : cases) {
+        const auto& zone = test_case.zone;
+        for (const auto scale : {0, 3, 6}) {
+            SCOPED_TRACE(testing::Message() << zone.name() << ", scale=" << 
scale);
+            const auto micros = scale == 6 ? 123456 : scale == 3 ? 123000 : 0;
+            const auto value = make_timestamptz(test_case.year, 1, 1, 0, 0, 0, 
micros);
+            const std::string fraction = scale == 6 ? ".123456" : scale == 3 ? 
".123" : "";
+            const auto formatted = value.to_string(zone, scale);
+            EXPECT_EQ(formatted, std::string(test_case.civil) + fraction + 
test_case.offset);
+
+            // The client-visible offset must describe the same instant, 
including historical
+            // sub-minute offsets; parsing in UTC must not depend on the 
display session zone.
+            for (const bool strict : {false, true}) {
+                TimestampTzValue parsed;
+                CastParameters params;
+                params.is_strict = strict;
+                ASSERT_TRUE(parsed.from_string(StringRef(formatted), &utc, 
params, scale))
+                        << params.status.to_string();
+                EXPECT_EQ(parsed, value) << formatted;
+            }
+        }
+    }
+}
+
+TEST(TimeStampTzValueTest, ToStringRejectsUnrepresentableLocalYear) {
+    TimezoneUtils::load_offsets_to_cache();
+    const auto utc = cctz::utc_time_zone();
+    const auto east = cctz::fixed_time_zone(std::chrono::hours(8));
+    const auto west = cctz::fixed_time_zone(std::chrono::hours(-8));
+    for (const auto scale : {0, 3, 6}) {
+        const auto micros = scale == 6 ? 999999 : scale == 3 ? 999000 : 0;
+        const auto minimum = make_timestamptz(0, 1, 1, 0, 0, 0, 0);
+        const auto maximum = make_timestamptz(9999, 12, 31, 23, 59, 59, 
micros);
+        // A valid UTC instant must not turn into an offset-only protocol 
value.
+        for (const auto& entry : {std::make_pair(minimum, west), 
std::make_pair(maximum, east)}) {
+            try {
+                static_cast<void>(entry.first.to_string(entry.second, scale));
+                FAIL() << "Expected an unrepresentable local year error";
+            } catch (const Exception& e) {
+                EXPECT_EQ(e.code(), ErrorCode::INVALID_ARGUMENT);
+                EXPECT_NE(std::string(e.what()).find("TIMESTAMPTZ local year 
is outside [0, 9999]"),
+                          std::string::npos);
+            }
+        }
+        for (const auto& entry : {std::make_pair(minimum, utc), 
std::make_pair(maximum, utc),
+                                  std::make_pair(minimum, east), 
std::make_pair(maximum, west)}) {
+            const auto wire = entry.first.to_string(entry.second, scale);
+            for (const bool strict : {false, true}) {
+                TimestampTzValue parsed;
+                CastParameters params;
+                params.is_strict = strict;
+                ASSERT_TRUE(parsed.from_string(StringRef(wire), &utc, params, 
scale))
+                        << wire << ": " << params.status.to_string();
+                EXPECT_EQ(parsed, entry.first);
+            }
+        }
+    }
+}
+
 TEST(TimeStampTzValueTest, from_string) {
     cctz::time_zone time_zone = cctz::fixed_time_zone(std::chrono::hours(8));
     TimezoneUtils::load_offsets_to_cache();
@@ -110,6 +206,43 @@ TEST(TimeStampTzValueTest, from_string) {
     }
 }
 
+TEST(TimeStampTzValueTest, HistoricalOffsetsInStrictAndFallbackParsers) {
+    const auto utc = cctz::utc_time_zone();
+    const auto expected = make_timestamptz(1890, 1, 1, 0, 0, 0, 123456);
+    for (const std::string input :
+         {"1890-01-01 08:05:43.123456+08:05:43", "1889-12-31 
19:03:58.123456-04:56:02",
+          "1890-01-01 00:00:30.123456+00:00:30", "1889-12-31 
23:59:30.123456-00:00:30",
+          "1890-01-01 08:05:00.123456+08:05", "1889-12-31 
08:03:52.123456-15:56:08",
+          "1889-12-31 09:39:00.123456-14:21", "1890-01-01 
15:00:00.123456+15:00",
+          "1889-12-31 11:59:59.123456-12:00:01"}) {
+        SCOPED_TRACE(input);
+        for (const bool fallback : {false, true}) {
+            TimestampTzValue parsed;
+            CastParameters params;
+            params.is_strict = !fallback;
+            const bool success =
+                    fallback
+                            ? 
CastToTimestampTz::from_string_non_strict_mode_impl(
+                                      StringRef(input), parsed, params, &utc, 
6)
+                            : 
CastToTimestampTz::from_string_strict_mode<DatelikeParseMode::STRICT>(
+                                      StringRef(input), parsed, params, &utc, 
6);
+            EXPECT_TRUE(success) << params.status.to_string();
+            EXPECT_EQ(parsed, expected);
+        }
+    }
+    for (const std::string offset : {"+08:60:00", "+08:05:60", "+08:05:", 
"+08:05:4", "+08:05:430",
+                                     "+24:00:00", "-24:00:00", "+99:00:00"}) {
+        SCOPED_TRACE(offset);
+        const auto input = "1890-01-01 00:00:00" + offset;
+        for (const bool strict : {false, true}) {
+            TimestampTzValue parsed;
+            CastParameters params;
+            params.is_strict = strict;
+            EXPECT_FALSE(parsed.from_string(StringRef(input), &utc, params, 
6));
+        }
+    }
+}
+
 TEST(TimeStampTzValueTest, from_datetime) {
     cctz::time_zone time_zone = cctz::fixed_time_zone(std::chrono::hours(8));
     TimezoneUtils::load_offsets_to_cache();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CollectSet.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CollectSet.java
index f93714aca03..775e800d630 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CollectSet.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/agg/CollectSet.java
@@ -112,6 +112,12 @@ public class CollectSet extends 
NotNullableAggregateFunction
 
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        // The BE set kernel cannot hash raw VARBINARY; reject it before 
implicit casts change its type.
+        for (Expression argument : getArguments()) {
+            if (argument.getDataType().isVarBinaryType()) {
+                throw new AnalysisException("collect_set does not support 
VARBINARY arguments");
+            }
+        }
         if (arity() == 2 && !getArgument(1).isConstant()) {
             throw new AnalysisException(
                     "collect_set requires second parameter must be a constant: 
"
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayContains.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayContains.java
index 7f0089dd96d..86d94f80756 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayContains.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayContains.java
@@ -59,6 +59,11 @@ public class ArrayContains extends ScalarFunction
         super(functionParams);
     }
 
+    @Override
+    public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
+    }
+
     /**
      * withChildren.
      */
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayContainsAll.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayContainsAll.java
index 50613ffde50..b2bd31898cd 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayContainsAll.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayContainsAll.java
@@ -56,6 +56,11 @@ public class ArrayContainsAll extends ScalarFunction 
implements ExplicitlyCastab
         super(functionParams);
     }
 
+    @Override
+    public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
+    }
+
     /**
      * withChildren.
      */
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayDistinct.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayDistinct.java
index 9d957b137a2..e6f40d46c03 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayDistinct.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayDistinct.java
@@ -61,6 +61,7 @@ public class ArrayDistinct extends ScalarFunction
      */
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
         DataType argType = getArgument(0).getDataType();
         if (argType.isArrayType()) {
             DataType itemType = ((ArrayType) argType).getItemType();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayEnumerateUniq.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayEnumerateUniq.java
index beac5aaf0ab..db21fce3365 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayEnumerateUniq.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayEnumerateUniq.java
@@ -65,6 +65,7 @@ public class ArrayEnumerateUniq extends ScalarFunction
      */
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
         for (Expression arg : getArguments()) {
             DataType argType = arg.getDataType();
             if (argType.isArrayType()) {
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayExcept.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayExcept.java
index 03fb5b459b5..e3d19299416 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayExcept.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayExcept.java
@@ -54,6 +54,11 @@ public class ArrayExcept extends ScalarFunction implements 
ExplicitlyCastableSig
         super(functionParams);
     }
 
+    @Override
+    public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
+    }
+
     /**
      * withChildren.
      */
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayFunctionUtils.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayFunctionUtils.java
new file mode 100644
index 00000000000..49cd49d481c
--- /dev/null
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayFunctionUtils.java
@@ -0,0 +1,42 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions.scalar;
+
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DataType;
+
+/** Argument validation shared by array functions. */
+final class ArrayFunctionUtils {
+    private ArrayFunctionUtils() {
+    }
+
+    static void checkNoVarBinaryArguments(ScalarFunction function) {
+        // Inspect original arguments before coercion can hide unsupported 
binary comparison/hash inputs.
+        for (Expression argument : function.getArguments()) {
+            DataType type = argument.getDataType();
+            while (type instanceof ArrayType) {
+                type = ((ArrayType) type).getItemType();
+            }
+            if (type.isVarBinaryType()) {
+                throw new AnalysisException(function.getName() + " does not 
support VARBINARY arguments");
+            }
+        }
+    }
+}
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayIntersect.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayIntersect.java
index c48b54305ed..9bd8bcc039b 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayIntersect.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayIntersect.java
@@ -62,6 +62,7 @@ public class ArrayIntersect extends ScalarFunction implements 
ExplicitlyCastable
      */
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
         DataType itemType = NullType.INSTANCE;
         for (Expression child : getArguments()) {
             DataType argType = child.getDataType();
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayPosition.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayPosition.java
index 490b428b062..945aee4831e 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayPosition.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayPosition.java
@@ -77,6 +77,7 @@ public class ArrayPosition extends ScalarFunction
      */
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
         DataType argType = getArgument(0).getDataType();
         if (argType.isArrayType() && ((ArrayType) 
argType).getItemType().isComplexType()) {
             throw new AnalysisException("array_position does not support 
complex types: " + toSql());
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayRemove.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayRemove.java
index 2610f84c8f7..9977b7efbe1 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayRemove.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayRemove.java
@@ -72,6 +72,7 @@ public class ArrayRemove extends ScalarFunction
 
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
         DataType argType = getArgument(0).getDataType();
         if (argType.isArrayType() && (((ArrayType) 
argType).getItemType().isComplexType()
                     || ((ArrayType) argType).getItemType().isVariantType()
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayUnion.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayUnion.java
index fb4fc04e2cd..ecc880972b9 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayUnion.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArrayUnion.java
@@ -68,6 +68,7 @@ public class ArrayUnion extends ScalarFunction implements 
ExplicitlyCastableSign
 
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
         DataType argType = getArgument(0).getDataType();
         if (argType.isArrayType() && (((ArrayType) 
argType).getItemType().isComplexType()
                     || ((ArrayType) argType).getItemType().isVariantType()
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArraysOverlap.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArraysOverlap.java
index 2f9402f5444..50d79d58b10 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArraysOverlap.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/ArraysOverlap.java
@@ -68,6 +68,7 @@ public class ArraysOverlap extends ScalarFunction implements 
ExplicitlyCastableS
 
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
         DataType argType = getArgument(0).getDataType();
         if (argType.isArrayType() && (((ArrayType) 
argType).getItemType().isComplexType()
                     || ((ArrayType) argType).getItemType().isVariantType()
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CountEqual.java
 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CountEqual.java
index 20c6a9c208a..3e97e46ff2a 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CountEqual.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/expressions/functions/scalar/CountEqual.java
@@ -64,6 +64,7 @@ public class CountEqual extends ScalarFunction
 
     @Override
     public void checkLegalityBeforeTypeCoercion() {
+        ArrayFunctionUtils.checkNoVarBinaryArguments(this);
         DataType argType = getArgument(0).getDataType();
         if (argType.isArrayType() && (((ArrayType) 
argType).getItemType().isComplexType()
                     || ((ArrayType) argType).getItemType().isVariantType()
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/VarBinaryCollectionLegalityTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/VarBinaryCollectionLegalityTest.java
new file mode 100644
index 00000000000..a811152f76c
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/expressions/functions/VarBinaryCollectionLegalityTest.java
@@ -0,0 +1,114 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.trees.expressions.functions;
+
+import org.apache.doris.nereids.exceptions.AnalysisException;
+import org.apache.doris.nereids.trees.expressions.Expression;
+import org.apache.doris.nereids.trees.expressions.SlotReference;
+import org.apache.doris.nereids.trees.expressions.functions.agg.CollectSet;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayContains;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayContainsAll;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayDistinct;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayEnumerateUniq;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayExcept;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayIntersect;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayPosition;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayRemove;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.ArrayUnion;
+import 
org.apache.doris.nereids.trees.expressions.functions.scalar.ArraysOverlap;
+import org.apache.doris.nereids.trees.expressions.functions.scalar.CountEqual;
+import org.apache.doris.nereids.trees.expressions.literal.IntegerLiteral;
+import org.apache.doris.nereids.types.ArrayType;
+import org.apache.doris.nereids.types.DataType;
+import org.apache.doris.nereids.types.IntegerType;
+import org.apache.doris.nereids.types.StringType;
+import org.apache.doris.nereids.types.VarBinaryType;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class VarBinaryCollectionLegalityTest {
+    private List<BoundFunction> collections(Expression array, Expression 
value) {
+        return Arrays.asList(
+                new ArrayContains(array, value), new ArrayPosition(array, 
value), new CountEqual(array, value),
+                new ArrayDistinct(array), new ArrayRemove(array, value), new 
ArrayEnumerateUniq(array),
+                new ArrayContainsAll(array, array), new ArraysOverlap(array, 
array), new ArrayUnion(array, array),
+                new ArrayExcept(array, array), new ArrayIntersect(array, 
array));
+    }
+
+    private void assertRejectsVarBinary(List<BoundFunction> functions) {
+        Assertions.assertAll(functions.stream().map(function -> () -> {
+            AnalysisException error = 
Assertions.assertThrows(AnalysisException.class,
+                    function::checkLegalityBeforeTypeCoercion, 
function.getName());
+            Assertions.assertEquals(function.getName() + " does not support 
VARBINARY arguments", error.getMessage());
+        }));
+    }
+
+    @Test
+    public void testFunctionsRejectVarBinaryBeforeCoercion() {
+        Expression value = new SlotReference("bytes", VarBinaryType.INSTANCE);
+        Expression array = new SlotReference("items", 
ArrayType.of(VarBinaryType.INSTANCE));
+        assertRejectsVarBinary(collections(array, value));
+        assertRejectsVarBinary(Arrays.asList(new CollectSet(value), new 
CollectSet(value, new IntegerLiteral(2))));
+    }
+
+    @Test
+    public void testNestedArraysRejectVarBinary() {
+        Expression value = new SlotReference("bytes", 
ArrayType.of(VarBinaryType.INSTANCE));
+        Expression array = new SlotReference("items", 
ArrayType.of(ArrayType.of(VarBinaryType.INSTANCE)));
+        assertRejectsVarBinary(collections(array, value));
+    }
+
+    @Test
+    public void testChecksAllArgumentsBeforeCoercion() {
+        Expression value = new SlotReference("bytes", VarBinaryType.INSTANCE);
+        Expression binaryArray = new SlotReference("bytes_array", 
ArrayType.of(VarBinaryType.INSTANCE));
+        Expression stringArray = new SlotReference("text_array", 
ArrayType.of(StringType.INSTANCE));
+        assertRejectsVarBinary(Arrays.asList(
+                new ArrayContains(stringArray, value), new 
ArrayPosition(stringArray, value),
+                new CountEqual(stringArray, value), new 
ArrayRemove(stringArray, value),
+                new ArrayContainsAll(stringArray, binaryArray), new 
ArraysOverlap(stringArray, binaryArray),
+                new ArrayExcept(stringArray, binaryArray), new 
ArrayUnion(stringArray, stringArray, binaryArray),
+                new ArrayIntersect(stringArray, stringArray, binaryArray),
+                new ArrayEnumerateUniq(stringArray, binaryArray), new 
CollectSet(new IntegerLiteral(1), value)));
+    }
+
+    @Test
+    public void testOrdinaryTypesRemainLegal() {
+        for (DataType type : Arrays.asList(IntegerType.INSTANCE, 
StringType.INSTANCE)) {
+            Expression value = new SlotReference("value", type);
+            Expression array = new SlotReference("items", ArrayType.of(type));
+            for (BoundFunction function : collections(array, value)) {
+                
Assertions.assertDoesNotThrow(function::checkLegalityBeforeTypeCoercion, 
function.getName());
+            }
+            Assertions.assertDoesNotThrow(new 
CollectSet(value)::checkLegalityBeforeTypeCoercion);
+            Assertions.assertDoesNotThrow(new CollectSet(value, new 
IntegerLiteral(2))::checkLegalityBeforeTypeCoercion);
+        }
+    }
+
+    @Test
+    public void testCollectSetRetainsConstantLimitCheck() {
+        CollectSet function = new CollectSet(new IntegerLiteral(1), new 
SlotReference("limit", IntegerType.INSTANCE));
+        AnalysisException error = 
Assertions.assertThrows(AnalysisException.class,
+                function::checkLegalityBeforeTypeCoercion);
+        Assertions.assertTrue(error.getMessage().contains("second parameter 
must be a constant"));
+    }
+}
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/nereids/types/VarBinaryUnsupportedCollectionTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/types/VarBinaryUnsupportedCollectionTest.java
new file mode 100644
index 00000000000..39f5368009d
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/nereids/types/VarBinaryUnsupportedCollectionTest.java
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.apache.doris.nereids.types;
+
+import org.apache.doris.nereids.util.PlanChecker;
+import org.apache.doris.utframe.TestWithFeService;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class VarBinaryUnsupportedCollectionTest extends TestWithFeService {
+    @Override
+    protected void runBeforeAll() throws Exception {
+        createDatabaseAndUse("binary_collections");
+        createTable("create table source_bytes (id int, encoded string) 
duplicate key(id) "
+                + "distributed by hash(id) buckets 1 properties 
('replication_num'='1')");
+    }
+
+    @Test
+    public void testUnsupportedBinaryCollectionsFailDuringAnalysis() {
+        String values = "array(cast(encoded as varbinary), X'', X'0080FF', 
NULL)";
+        for (String expression : new String[] {
+                "array_contains(" + values + ", X'0080FF')",
+                "array_position(" + values + ", X'0080FF')",
+                "countequal(" + values + ", X'0080FF')",
+                "array_distinct(" + values + ")",
+                "array_remove(" + values + ", X'0080FF')",
+                "array_enumerate_uniq(" + values + ")",
+                "array_contains_all(" + values + ", " + values + ")",
+                "arrays_overlap(" + values + ", " + values + ")",
+                "array_union(" + values + ", " + values + ")",
+                "array_except(" + values + ", " + values + ")",
+                "array_intersect(" + values + ", " + values + ")",
+                "collect_set(cast(encoded as varbinary))",
+                "collect_set(cast(encoded as varbinary), 2)"}) {
+            org.apache.doris.nereids.exceptions.AnalysisException error = 
Assertions.assertThrows(
+                    
org.apache.doris.nereids.exceptions.AnalysisException.class,
+                    () -> PlanChecker.from(connectContext).analyze("select " + 
expression + " from source_bytes"),
+                    expression);
+            Assertions.assertTrue(error.getMessage().contains("does not 
support VARBINARY"), error.getMessage());
+        }
+        // Byte-agnostic array construction and element access remain 
supported.
+        PlanChecker.from(connectContext).analyze("select array(cast(encoded as 
varbinary))[1] from source_bytes");
+        PlanChecker.from(connectContext).analyze("select 
collect_list(cast(encoded as varbinary)) from source_bytes");
+    }
+
+}
diff --git 
a/regression-test/data/datatype_p0/timestamptz/load/stream_load/test_timestamptz_stream_load.out
 
b/regression-test/data/datatype_p0/timestamptz/load/stream_load/test_timestamptz_stream_load.out
index a59b2e04876..8c0d0dd2ba2 100644
--- 
a/regression-test/data/datatype_p0/timestamptz/load/stream_load/test_timestamptz_stream_load.out
+++ 
b/regression-test/data/datatype_p0/timestamptz/load/stream_load/test_timestamptz_stream_load.out
@@ -3,7 +3,7 @@
 \N     -1
 \N     12
 \N     12
-0000-01-01 08:05:43+08:05      0
+0000-01-01 08:05:43+08:05:43   0
 2023-01-01 17:00:00+08:00      1
 2023-02-02 17:00:00+08:00      2
 2023-03-04 01:00:00+08:00      3
@@ -13,7 +13,7 @@
 
 -- !dup_key_strict0 --
 \N     -1
-0000-01-01 08:05:43+08:05      0
+0000-01-01 08:05:43+08:05:43   0
 2023-01-01 17:00:00+08:00      1
 2023-02-02 17:00:00+08:00      2
 2023-03-04 01:00:00+08:00      3
@@ -26,7 +26,7 @@
 -- !dup_key_null_to_not_null_non_strict0 --
 
 -- !dup_key_null_to_not_null_non_strict1 --
-0000-01-01 08:05:43+08:05      0
+0000-01-01 08:05:43+08:05:43   0
 2023-01-01 17:00:00+08:00      1
 2023-02-02 17:00:00+08:00      2
 2023-03-04 01:00:00+08:00      3
@@ -35,7 +35,7 @@
 2023-12-12 11:12:12+08:00      12
 
 -- !dup_key_null_to_not_null_strict0 --
-0000-01-01 08:05:43+08:05      0
+0000-01-01 08:05:43+08:05:43   0
 2023-01-01 17:00:00+08:00      1
 2023-02-02 17:00:00+08:00      2
 2023-03-04 01:00:00+08:00      3
diff --git 
a/regression-test/data/datatype_p0/timestamptz/test_cast_timestamptz.out 
b/regression-test/data/datatype_p0/timestamptz/test_cast_timestamptz.out
index d9021431ae7..c1c674e3416 100644
--- a/regression-test/data/datatype_p0/timestamptz/test_cast_timestamptz.out
+++ b/regression-test/data/datatype_p0/timestamptz/test_cast_timestamptz.out
@@ -14,6 +14,12 @@
 -- !cast_str_to_timetz_invalid --
 \N     \N      \N      \N      \N      \N      \N      \N
 
+-- !cast_str_to_timetz_historical_offsets --
+2019-12-31 16:00:00+07:00      1800-01-01 22:56:08+07:00       1800-01-01 
21:21:00+07:00
+
+-- !cast_str_to_timetz_historical_offsets --
+2019-12-31 16:00:00+07:00      1800-01-01 22:56:08+07:00       1800-01-01 
21:21:00+07:00
+
 -- !sql --
 2020-01-01 00:00:00.124+07:00
 
diff --git 
a/regression-test/data/datatype_p0/timestamptz/test_timestamptz_binary_output.out
 
b/regression-test/data/datatype_p0/timestamptz/test_timestamptz_binary_output.out
index b40c83ee923..461cd06f347 100644
--- 
a/regression-test/data/datatype_p0/timestamptz/test_timestamptz_binary_output.out
+++ 
b/regression-test/data/datatype_p0/timestamptz/test_timestamptz_binary_output.out
@@ -15,11 +15,11 @@
 
 -- !all_bin0 --
 \N     \N      0
-\N     0000-01-01 08:05:43+08:05       1
+\N     0000-01-01 08:05:43+08:05:43    1
 \N     2023-08-08 20:20:20+08:00       2
 \N     9999-12-31 23:59:59+08:00       -1
-0000-01-01 08:05:43+08:05      0000-01-01 08:05:43+08:05       0
-0000-01-01 08:05:43+08:05      0000-01-01 08:05:43+08:05       1
+0000-01-01 08:05:43+08:05:43   0000-01-01 08:05:43+08:05:43    0
+0000-01-01 08:05:43+08:05:43   0000-01-01 08:05:43+08:05:43    1
 2023-01-01 12:00:00+08:00      2023-01-01 12:00:00+08:00       0
 2023-08-08 20:20:20+08:00      2023-08-08 20:20:20+08:00       1
 2023-12-12 12:12:12+08:00      2023-12-12 12:12:12+08:00       2
@@ -47,11 +47,11 @@
 
 -- !all_bin_scale0 --
 \N     \N      -1
-0000-01-01 08:05:43.000000+08:05       0000-01-01 08:05:43.000000+08:05        0
-0000-01-01 08:05:43.000000+08:05       0000-01-01 08:05:43.000000+08:05        0
-0000-01-01 08:05:43.000001+08:05       0000-01-01 08:05:43.000001+08:05        0
-0000-01-01 08:05:43.123456+08:05       0000-01-01 08:05:43.123456+08:05        0
-0000-01-01 08:05:43.999999+08:05       0000-01-01 08:05:43.999999+08:05        
10
+0000-01-01 08:05:43.000000+08:05:43    0000-01-01 08:05:43.000000+08:05:43     0
+0000-01-01 08:05:43.000000+08:05:43    0000-01-01 08:05:43.000000+08:05:43     0
+0000-01-01 08:05:43.000001+08:05:43    0000-01-01 08:05:43.000001+08:05:43     0
+0000-01-01 08:05:43.123456+08:05:43    0000-01-01 08:05:43.123456+08:05:43     0
+0000-01-01 08:05:43.999999+08:05:43    0000-01-01 08:05:43.999999+08:05:43     
10
 2023-08-09 04:20:20.000000+08:00       2023-08-09 04:20:20.000000+08:00        
8
 2023-08-09 04:20:20.000000+08:00       2023-08-09 04:20:20.000000+08:00        
8
 2023-08-09 04:20:20.000001+08:00       2023-08-09 04:20:20.000001+08:00        
8
diff --git 
a/regression-test/suites/datatype_p0/timestamptz/load/stream_load/test_timestamptz_stream_load.groovy
 
b/regression-test/suites/datatype_p0/timestamptz/load/stream_load/test_timestamptz_stream_load.groovy
index 5bb5fe4d370..2b2487cfc39 100644
--- 
a/regression-test/suites/datatype_p0/timestamptz/load/stream_load/test_timestamptz_stream_load.groovy
+++ 
b/regression-test/suites/datatype_p0/timestamptz/load/stream_load/test_timestamptz_stream_load.groovy
@@ -16,6 +16,8 @@
 // under the License.
 
 suite("test_timestamptz_stream_load") {
+    // Named zones retain historical second offsets when loaded timestamps are 
rendered.
+    sql "set time_zone = 'Asia/Shanghai'"
     def csvFile = """test_timestamptz_stream_load.csv"""
     def prepare_table_dup_key = {
         sql """ DROP TABLE IF EXISTS test_timestamptz_stream_load_dup_key"""
diff --git 
a/regression-test/suites/datatype_p0/timestamptz/test_cast_timestamptz.groovy 
b/regression-test/suites/datatype_p0/timestamptz/test_cast_timestamptz.groovy
index 7bd92c87588..660409bc526 100644
--- 
a/regression-test/suites/datatype_p0/timestamptz/test_cast_timestamptz.groovy
+++ 
b/regression-test/suites/datatype_p0/timestamptz/test_cast_timestamptz.groovy
@@ -58,6 +58,7 @@ suite("test_cast_timestamptz") {
         cast('2020-12-31 23:59:59' as TIMESTAMPTZ) as ts_no_tz_winter;
     """
 
+    // Wire offsets can exceed session-zone limits; an offset of 24 hours is 
invalid.
     qt_cast_str_to_timetz_invalid """
     SELECT 
         cast('2020-13-01 00:00:00 +03:00' as TIMESTAMPTZ) as ts_invalid_month,
@@ -65,12 +66,23 @@ suite("test_cast_timestamptz") {
         cast('2020-01-01 24:00:00 +03:00' as TIMESTAMPTZ) as ts_invalid_hour,
         cast('2020-01-01 00:60:00 +03:00' as TIMESTAMPTZ) as ts_invalid_minute,
         cast('2020-01-01 00:00:60 +03:00' as TIMESTAMPTZ) as ts_invalid_second,
-        cast('2020-01-01 00:00:00 +15:00' as TIMESTAMPTZ) as 
ts_invalid_tz_hour,
+        cast('2020-01-01 00:00:00 +24:00' as TIMESTAMPTZ) as 
ts_invalid_tz_hour,
         cast('2020-01-01 00:00:00 +03:60' as TIMESTAMPTZ) as 
ts_invalid_tz_minute,
         cast('invalid-string' as TIMESTAMPTZ) as ts_invalid_string;
     """
 
 
+    // Historical wire offsets must parse in both cast modes, independently of 
session-zone limits.
+    for (boolean strict : [false, true]) {
+        sql "set enable_strict_cast=${strict}"
+        qt_cast_str_to_timetz_historical_offsets """
+            select cast('2020-01-01 00:00:00+15:00' as timestamptz),
+                   cast('1800-01-01 00:00:00-15:56:08' as timestamptz),
+                   cast('1800-01-01 00:00:00-14:21' as timestamptz);
+        """
+    }
+    sql "set enable_strict_cast=false"
+
     qt_sql """
         select cast(cast("2020-01-01 00:00:00.1236" as datetime(4)) as 
timestamptz(3));
     """
diff --git 
a/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_binary_output.groovy
 
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_binary_output.groovy
index bd7198a7ee8..e3186e0a3da 100644
--- 
a/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_binary_output.groovy
+++ 
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_binary_output.groovy
@@ -64,6 +64,8 @@ suite("test_timestamptz_binary_output") {
     String url = getServerPrepareJdbcUrl(context.config.jdbcUrl, 
"regression_test_datatype_p0_timestamptz");
     logger.info("jdbc prepare statement url: ${url}")
     def result1 = connect(user, password, url) {
+        // A prepared connection has its own session; exercise historical 
second offsets explicitly.
+        sql "set time_zone = 'Asia/Shanghai'"
         qt_all_bin0 """
             SELECT * FROM test_timestamptz_binary_output_no_scale ORDER BY 1, 
2, 3;
         """
@@ -106,6 +108,8 @@ suite("test_timestamptz_binary_output") {
         SELECT * FROM test_timestamptz_binary_output_with_scale ORDER BY 1, 2, 
3;
     """
     def result2 = connect(user, password, url) {
+        // Keep the scaled binary-protocol check independent of the server's 
default time zone.
+        sql "set time_zone = 'Asia/Shanghai'"
         qt_all_bin_scale0 """
             SELECT * FROM test_timestamptz_binary_output_with_scale ORDER BY 
1, 2, 3;
         """
diff --git 
a/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_historical_offset.groovy
 
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_historical_offset.groovy
new file mode 100644
index 00000000000..ca9c5161f32
--- /dev/null
+++ 
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_historical_offset.groovy
@@ -0,0 +1,53 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_timestamptz_historical_offset") {
+    def originalZone = sql("select @@time_zone")[0][0]
+    def originalStrict = sql("select @@enable_strict_cast")[0][0]
+    def cases = [
+        ["Asia/Shanghai", "1890-01-01 00:00:00.123456+00:00", "1890-01-01 
08:05:43.123456+08:05:43"],
+        ["America/New_York", "1880-01-01 00:00:00.123456+00:00", "1879-12-31 
19:03:58.123456-04:56:02"],
+        ["Asia/Shanghai", "2024-01-01 00:00:00.123456+00:00", "2024-01-01 
08:00:00.123456+08:00"],
+        ["America/New_York", "2024-01-01 00:00:00.123456+00:00", "2023-12-31 
19:00:00.123456-05:00"],
+        ["Asia/Kathmandu", "2024-01-01 00:00:00.123456+00:00", "2024-01-01 
05:45:00.123456+05:45"]
+    ]
+    try {
+        for (def testCase : cases) {
+            sql "set time_zone = '${testCase[0]}'"
+            for (def strict : [false, true]) {
+                sql "set enable_strict_cast = ${strict}"
+                // A nonconstant input exercises BE protocol formatting and 
parsing instead of
+                // FE constant folding. The offset must retain the instant 
when sent back by a client.
+                def wire = sql("""
+                    select cast(concat('${testCase[1]}', substring(cast(number 
as string), 2))
+                                as timestamptz(6))
+                    from numbers('number' = '1')
+                """)[0][0].toString()
+                assertEquals(testCase[2], wire)
+                def roundTrip = sql("""
+                    select cast(concat('${wire}', substring(cast(number as 
string), 2))
+                                as timestamptz(6)) = cast('${testCase[1]}' as 
timestamptz(6))
+                    from numbers('number' = '1')
+                """)
+                assertEquals([[true]], roundTrip)
+            }
+        }
+    } finally {
+        sql "set time_zone = '${originalZone}'"
+        sql "set enable_strict_cast = ${originalStrict}"
+    }
+}
diff --git 
a/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_null_string.groovy
 
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_null_string.groovy
new file mode 100644
index 00000000000..201e969a0ed
--- /dev/null
+++ 
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_null_string.groovy
@@ -0,0 +1,48 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_timestamptz_null_string") {
+    def originalZone = sql("select @@time_zone")[0][0]
+    def originalStrict = sql("select @@enable_strict_cast")[0][0]
+    def originalSkipFold = sql("select @@debug_skip_fold_constant")[0][0]
+    try {
+        sql "set time_zone = '+08:00'"
+        sql "set enable_strict_cast = false"
+        for (def skipFold : [false, true]) {
+            sql "set debug_skip_fold_constant = ${skipFold}"
+            // Invalid casts and NULL inputs leave no valid timestamp payload 
for the
+            // following formatter; only the null map determines whether a row 
is readable.
+            assertEquals([[null]], sql("""
+                select cast(second_floor('9999-12-31 23:59:59.999999-02:00', 
5) as string)
+            """))
+            assertEquals([[null]], sql("""
+                select cast(second_floor(cast(null as timestamptz(6)), 5) as 
string)
+            """))
+            assertEquals([[null], ['2024-01-02 11:04:05.000000+08:00'],
+                          [null], ['2024-01-02 11:04:05.000000+08:00']], 
sql("""
+                select cast(second_floor(
+                    if(number % 2 = 0, cast(null as timestamptz(6)),
+                       cast('2024-01-02 03:04:05.123456+00:00' as 
timestamptz(6))), 5) as string)
+                from numbers('number' = '4') order by number
+            """))
+        }
+    } finally {
+        sql "set time_zone = '${originalZone}'"
+        sql "set enable_strict_cast = ${originalStrict}"
+        sql "set debug_skip_fold_constant = ${originalSkipFold}"
+    }
+}
diff --git 
a/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_output_boundary.groovy
 
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_output_boundary.groovy
new file mode 100644
index 00000000000..f1cde7a38de
--- /dev/null
+++ 
b/regression-test/suites/datatype_p0/timestamptz/test_timestamptz_output_boundary.groovy
@@ -0,0 +1,71 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+suite("test_timestamptz_output_boundary") {
+    def originalZone = sql("select @@time_zone")[0][0]
+    def originalStrict = sql("select @@enable_strict_cast")[0][0]
+    def minimum = "0000-01-01 00:00:00.000000+00:00"
+    def maximum = "9999-12-31 23:59:59.999999+00:00"
+    def readTimestamp = { value ->
+        // Nonconstant inputs reach the BE formatter instead of FE constant 
folding.
+        """select cast(concat('${value}', substring(cast(number as string), 
2)) as timestamptz(6))
+           from numbers('number' = '1')"""
+    }
+    try {
+        for (def strict : [false, true]) {
+            sql "set enable_strict_cast = ${strict}"
+            sql "set time_zone = '+08:00'"
+            for (def target : [["datetimev2(6)", "to datetime in timezone"],
+                               ["timestamptz(0)", "to timestamptz in 
timezone"]]) {
+                // Failed casts must retain their error/NULL contract even 
when the input
+                // cannot be displayed in the session timezone while reporting 
the error.
+                def query = """
+                    select cast(cast(concat('${maximum}', 
substring(cast(number as string), 2))
+                                     as timestamptz(6)) as ${target[0]})
+                    from numbers('number' = '1')
+                """
+                if (strict) {
+                    test {
+                        sql query
+                        exception target[1]
+                    }
+                } else {
+                    assertEquals([[null]], sql(query))
+                }
+            }
+            for (def entry : [["-08:00", minimum], ["+08:00", maximum]]) {
+                sql "set time_zone = '${entry[0]}'"
+                test {
+                    sql readTimestamp(entry[1])
+                    exception "TIMESTAMPTZ local year is outside [0, 9999]"
+                }
+            }
+            for (def entry : [["UTC", minimum, minimum], ["UTC", maximum, 
maximum],
+                              ["+08:00", minimum, "0000-01-01 
08:00:00.000000+08:00"],
+                              ["-08:00", maximum, "9999-12-31 
15:59:59.999999-08:00"]]) {
+                sql "set time_zone = '${entry[0]}'"
+                def wire = sql(readTimestamp(entry[1]))[0][0].toString()
+                assertEquals(entry[2], wire)
+                sql "set time_zone = 'UTC'"
+                assertEquals(entry[1], 
sql(readTimestamp(wire))[0][0].toString())
+            }
+        }
+    } finally {
+        sql "set time_zone = '${originalZone}'"
+        sql "set enable_strict_cast = ${originalStrict}"
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to