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

Gabriel39 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 61906ce9a1c [fix](be) Preserve signed decimal bounds in Iceberg ORC 
writes (#69888)
61906ce9a1c is described below

commit 61906ce9a1c89d76bb450f1d6b4a10acbb712efe
Author: daidai <[email protected]>
AuthorDate: Tue Nov 3 14:39:46 2026 +0800

    [fix](be) Preserve signed decimal bounds in Iceberg ORC writes (#69888)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Related PR: N/A
    
    Problem Summary:
    
    ORC `Int128` statistics already contain two's-complement bits, but the
    Iceberg ORC writer negates negative values again before encoding them. A
    file containing `[-100, -1]` consequently reports bounds `[100, 1]`, so
    a negative predicate can prune a file that contains matching rows.
    
    ORC footer statistics also trim trailing fractional zeros. For example,
    `-1.00` is returned as `value=-1, scale=0`; a `DECIMAL(...,2)` manifest
    needs the unscaled value `-100`. Preserving the sign alone therefore
    leaves incorrect bounds.
    
    Restore each bound to the declared column scale with the existing ORC
    scaling utility, then serialize its original signed bits using minimal
    big-endian encoding. Preserve necessary sign prefixes such as `128 -> 00
    80` and `-129 -> FF 7F`.
    
    The fix applies to newly collected bounds. Existing incorrect metrics
    require recomputation or data-file rewriting.
    
    ### Release note
    
    Correct decimal bounds emitted by Iceberg ORC writes to prevent
    incorrect predicate pruning for negative values and values with trailing
    fractional zeros.
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [x] Regression test
        - [x] Unit Test
        - [x] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [ ] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason
    
    Added BE tests cover 29 encoding cases, real ORC statistics at
    precisions 1/9/18/38, scale restoration through scale 38, nullable
    columns, and all-null columns. The external regression checks manifest
    pruning for negative and mixed decimal data across decimal32/64/128.
    
    Local validation:
    - An isolated codec harness built from the production function passed
    all 29 encoding cases.
    - Five real ORC files passed independent Iceberg 1.12.0 manifest
    write/read and predicate-pruning probes.
    - clang-format 16, build hygiene, changed-line clang-tidy, and Groovy
    4.0.19 syntax checks passed. Static analysis used real ASAN UT
    compilation commands with PCH expanded as headers.
    - The full local BE-UT build was stopped during the monolithic
    dependency build before tests executed.
    - Regenerated `test_iceberg_write_stats2.out` with the standard
    regression runner after restoring decimal scales. Only `sql_4/5/7/9`
    changed, and the decimal bounds match both stored rows and the Parquet
    metrics.
    - Re-ran `test_iceberg_write_stats2` and
    `test_iceberg_orc_decimal_bounds` locally using this PR's CI FE/BE
    artifacts: 2 suites passed, 0 failed, 0 skipped.
    
    - Behavior changed:
        - [ ] No.
    - [x] Yes. Newly written ORC decimal bounds preserve the sign and the
    column scale.
    
    - Does this need documentation?
        - [x] No.
        - [ ] Yes.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label
---
 be/src/format/transformer/vorc_transformer.cpp     |  38 +++---
 be/src/format/transformer/vorc_transformer.h       |   2 +-
 .../format/transformer/vorc_transformer_test.cpp   | 150 +++++++++++++++++++++
 .../iceberg/write/test_iceberg_write_stats2.out    |   8 +-
 .../iceberg/test_iceberg_orc_decimal_bounds.groovy | 103 ++++++++++++++
 5 files changed, 275 insertions(+), 26 deletions(-)

diff --git a/be/src/format/transformer/vorc_transformer.cpp 
b/be/src/format/transformer/vorc_transformer.cpp
index 842f3bfa5cf..28799eb8a63 100644
--- a/be/src/format/transformer/vorc_transformer.cpp
+++ b/be/src/format/transformer/vorc_transformer.cpp
@@ -677,32 +677,27 @@ bool VOrcTransformer::_collect_column_bounds(const 
orc::ColumnStatistics* col_st
                        dynamic_cast<const 
orc::DecimalColumnStatistics*>(col_stats)) {
         if (decimal_stats->hasMinimum() && decimal_stats->hasMaximum()) {
             has_bounds = true;
-            (*lower_bounds)[field_id] = 
_decimal_to_bytes(decimal_stats->getMinimum());
-            (*upper_bounds)[field_id] = 
_decimal_to_bytes(decimal_stats->getMaximum());
+            const auto scale = 
static_cast<int32_t>(remove_nullable(data_type)->get_scale());
+            (*lower_bounds)[field_id] = 
_decimal_to_bytes(decimal_stats->getMinimum(), scale);
+            (*upper_bounds)[field_id] = 
_decimal_to_bytes(decimal_stats->getMaximum(), scale);
         }
     }
 
     return has_bounds;
 }
 
-std::string VOrcTransformer::_decimal_to_bytes(const orc::Decimal& decimal) {
-    orc::Int128 val = decimal.value;
-    if (val == 0) {
-        char zero = 0;
-        return std::string(&zero, 1);
-    }
-
-    // Convert Int128 -> signed big-endian minimal bytes
-    bool negative = val < 0;
-    auto high = static_cast<uint64_t>(val.getHighBits());
-    auto low = val.getLowBits();
+std::string VOrcTransformer::_decimal_to_bytes(const orc::Decimal& decimal, 
int32_t scale) {
+    // ORC footer statistics trim trailing zeros: -1.00 becomes value=-1, 
scale=0.
+    // Iceberg requires the unscaled value at the column's declared scale, 
which is -100 here.
+    DORIS_CHECK_GE(scale, decimal.scale);
+    bool overflow = false;
+    auto val = orc::scaleUpInt128ByPowerOfTen(decimal.value, scale - 
decimal.scale, overflow);
+    DORIS_CHECK(!overflow);
 
-    // If negative, convert to two's complement explicitly
-    if (negative) {
-        // two's complement for 128-bit
-        low = ~low + 1;
-        high = ~high + (low == 0 ? 1 : 0);
-    }
+    // Int128 already exposes two's-complement bits; negating -1 again would 
encode +1.
+    const bool negative = val < 0;
+    const auto high = static_cast<uint64_t>(val.getHighBits());
+    const auto low = val.getLowBits();
 
     // Serialize to big-endian bytes
     uint8_t buf[16];
@@ -711,9 +706,10 @@ std::string VOrcTransformer::_decimal_to_bytes(const 
orc::Decimal& decimal) {
         buf[i + 8] = static_cast<uint8_t>(low >> (56 - i * 8));
     }
 
-    // Strip leading sign-extension bytes (Iceberg minimal encoding)
+    // Strip redundant sign extension, retaining one byte for zero and the 
sign prefix
+    // when the next byte has the opposite sign: 128 -> 00 80, -129 -> FF 7F.
     int start = 0;
-    uint8_t sign_byte = negative ? 0xFF : 0x00;
+    const uint8_t sign_byte = negative ? 0xFF : 0x00;
     while (start < 15 && buf[start] == sign_byte &&
            ((buf[start + 1] & 0x80) == (sign_byte & 0x80))) {
         ++start;
diff --git a/be/src/format/transformer/vorc_transformer.h 
b/be/src/format/transformer/vorc_transformer.h
index 6515439cc2c..3afd5810fb6 100644
--- a/be/src/format/transformer/vorc_transformer.h
+++ b/be/src/format/transformer/vorc_transformer.h
@@ -117,7 +117,7 @@ private:
                                 const DataTypePtr& data_type,
                                 std::map<int32_t, std::string>* lower_bounds,
                                 std::map<int32_t, std::string>* upper_bounds);
-    std::string _decimal_to_bytes(const orc::Decimal& decimal);
+    static std::string _decimal_to_bytes(const orc::Decimal& decimal, int32_t 
scale);
     std::shared_ptr<io::FileSystem> _fs = nullptr;
     doris::io::FileWriter* _file_writer = nullptr;
     std::vector<std::string> _column_names;
diff --git a/be/test/format/transformer/vorc_transformer_test.cpp 
b/be/test/format/transformer/vorc_transformer_test.cpp
index fdf81b03161..3da58120862 100644
--- a/be/test/format/transformer/vorc_transformer_test.cpp
+++ b/be/test/format/transformer/vorc_transformer_test.cpp
@@ -17,6 +17,7 @@
 
 #include "format/transformer/vorc_transformer.h"
 
+#include <fmt/format.h>
 #include <gtest/gtest.h>
 
 #include <orc/OrcFile.hh>
@@ -31,6 +32,7 @@
 #include "core/column/column_vector.h"
 #include "core/data_type/data_type_array.h"
 #include "core/data_type/data_type_date_or_datetime_v2.h"
+#include "core/data_type/data_type_decimal.h"
 #include "core/data_type/data_type_map.h"
 #include "core/data_type/data_type_nullable.h"
 #include "core/data_type/data_type_number.h"
@@ -48,6 +50,7 @@
 #include "io/fs/local_file_system.h"
 #include "runtime/runtime_state.h"
 #include "testutil/mock/mock_slot_ref.h"
+#include "util/slice.h"
 #include "util/timezone_utils.h"
 #include "util/uid_util.h"
 
@@ -243,6 +246,153 @@ TEST_F(VOrcTransformerTest, 
CollectsBoundsForTopLevelFieldAfterStruct) {
     EXPECT_EQ("hello", stats.upper_bounds.at(3));
 }
 
+TEST_F(VOrcTransformerTest, DecimalBoundsUseMinimalTwosComplement) {
+    struct TestCase {
+        const char* value;
+        int32_t footer_scale;
+        int32_t column_scale;
+        std::string expected;
+    };
+    const std::vector<TestCase> cases = {
+            {"-99999999999999999999999999999999999999", 0, 0,
+             
std::string("\xb4\xc4\xb3\x57\xa5\x79\x3b\x85\xf6\x75\xdd\xc0\x00\x00\x00\x01", 
16)},
+            {"-18446744073709551616", 0, 0, 
std::string("\xff\x00\x00\x00\x00\x00\x00\x00\x00", 9)},
+            {"-9223372036854775808", 0, 0, 
std::string("\x80\x00\x00\x00\x00\x00\x00\x00", 8)},
+            {"-32769", 0, 0, std::string("\xff\x7f\xff", 3)},
+            {"-32768", 0, 0, std::string("\x80\x00", 2)},
+            {"-256", 0, 0, std::string("\xff\x00", 2)},
+            {"-255", 0, 0, std::string("\xff\x01", 2)},
+            {"-129", 0, 0, std::string("\xff\x7f", 2)},
+            {"-128", 0, 0, std::string("\x80", 1)},
+            {"-100", 0, 0, std::string("\x9c", 1)},
+            {"-1", 0, 0, std::string("\xff", 1)},
+            {"0", 0, 0, std::string("\x00", 1)},
+            {"1", 0, 0, std::string("\x01", 1)},
+            {"127", 0, 0, std::string("\x7f", 1)},
+            {"128", 0, 0, std::string("\x00\x80", 2)},
+            {"255", 0, 0, std::string("\x00\xff", 2)},
+            {"256", 0, 0, std::string("\x01\x00", 2)},
+            {"9223372036854775807", 0, 0, 
std::string("\x7f\xff\xff\xff\xff\xff\xff\xff", 8)},
+            {"18446744073709551616", 0, 0, 
std::string("\x01\x00\x00\x00\x00\x00\x00\x00\x00", 9)},
+            {"99999999999999999999999999999999999999", 0, 0,
+             
std::string("\x4b\x3b\x4c\xa8\x5a\x86\xc4\x7a\x09\x8a\x22\x3f\xff\xff\xff\xff", 
16)},
+            {"-1", 0, 1, std::string("\xf6", 1)},
+            {"-1", 0, 2, std::string("\x9c", 1)},
+            {"-10", 1, 2, std::string("\x9c", 1)},
+            {"1", 0, 2, std::string("\x64", 1)},
+            {"128", 0, 2, std::string("\x32\x00", 2)},
+            {"0", 0, 38, std::string("\x00", 1)},
+            {"-1", 1, 38,
+             
std::string("\xf8\x7a\x11\xef\x2a\x25\xb9\x26\xff\x0b\xc9\x60\x00\x00\x00\x00", 
16)},
+            {"1", 38, 38, std::string("\x01", 1)},
+            {"-1", 38, 38, std::string("\xff", 1)},
+    };
+    for (const auto& test : cases) {
+        SCOPED_TRACE(test.value);
+        SCOPED_TRACE(test.footer_scale);
+        SCOPED_TRACE(test.column_scale);
+        const orc::Decimal decimal(orc::Int128(test.value), test.footer_scale);
+        EXPECT_EQ(test.expected, VOrcTransformer::_decimal_to_bytes(decimal, 
test.column_scale));
+    }
+}
+
+TEST_F(VOrcTransformerTest, CollectsDecimalBoundsAtColumnScale) {
+    struct TestCase {
+        int precision;
+        int scale;
+        std::vector<std::string> values;
+        std::string lower;
+        std::string upper;
+    };
+    const std::vector<TestCase> cases = {
+            {1, 0, {"-9", "9"}, std::string("\xf7", 1), std::string("\x09", 
1)},
+            {9, 0, {"-100", "-1"}, std::string("\x9c", 1), std::string("\xff", 
1)},
+            {9, 0, {"-100", "128"}, std::string("\x9c", 1), 
std::string("\x00\x80", 2)},
+            {9, 2, {"-100.00", "-1.00"}, std::string("\xd8\xf0", 2), 
std::string("\x9c", 1)},
+            {18, 2, {"-100.00", "1.28"}, std::string("\xd8\xf0", 2), 
std::string("\x00\x80", 2)},
+            {18, 2, {"1.00", "128.00"}, std::string("\x64", 1), 
std::string("\x32\x00", 2)},
+            {38,
+             0,
+             {"-99999999999999999999999999999999999999", 
"99999999999999999999999999999999999999"},
+             
std::string("\xb4\xc4\xb3\x57\xa5\x79\x3b\x85\xf6\x75\xdd\xc0\x00\x00\x00\x01", 
16),
+             
std::string("\x4b\x3b\x4c\xa8\x5a\x86\xc4\x7a\x09\x8a\x22\x3f\xff\xff\xff\xff", 
16)},
+            {38,
+             38,
+             {"-0.10000000000000000000000000000000000000",
+              "0.00000000000000000000000000000000000001"},
+             
std::string("\xf8\x7a\x11\xef\x2a\x25\xb9\x26\xff\x0b\xc9\x60\x00\x00\x00\x00", 
16),
+             std::string("\x01", 1)},
+            {38,
+             38,
+             {"0.00000000000000000000000000000000000000"},
+             std::string("\x00", 1),
+             std::string("\x00", 1)},
+    };
+    for (const auto& test : cases) {
+        for (bool nullable : {false, true}) {
+            SCOPED_TRACE(test.precision);
+            SCOPED_TRACE(test.scale);
+            SCOPED_TRACE(nullable);
+            auto decimal_type = create_decimal(test.precision, test.scale, 
false);
+            auto type = nullable ? make_nullable(decimal_type) : decimal_type;
+            auto column = type->create_column();
+            auto serde = type->get_serde();
+            DataTypeSerDe::FormatOptions options;
+            for (const auto& value : test.values) {
+                Slice slice(value);
+                ASSERT_TRUE(serde->deserialize_one_cell_from_json(*column, 
slice, options).ok());
+            }
+            // A null beside the extrema must not change either decimal bound.
+            if (nullable) {
+                column->insert_default();
+            }
+            auto exprs = MockSlotRef::create_mock_contexts(DataTypes {type});
+            const auto json = fmt::format(
+                    
R"json({{"type":"struct","fields":[{{"id":1,"name":"d","required":false,"type":"decimal({},{})"}}]}})json",
+                    test.precision, test.scale);
+            auto schema = iceberg::SchemaParser::from_json(json);
+            io::FileWriterPtr file_writer;
+            ASSERT_TRUE(_fs->create_file(_file_path, &file_writer).ok());
+            RuntimeState state;
+            VOrcTransformer transformer(&state, file_writer.get(), exprs, "", 
{"d"}, false,
+                                        TFileCompressType::PLAIN, 
schema.get(), _fs);
+            ASSERT_TRUE(transformer.open().ok());
+            ASSERT_TRUE(transformer.write(Block({{std::move(column), type, 
"d"}})).ok());
+            ASSERT_TRUE(transformer.close().ok());
+            TIcebergColumnStats stats;
+            
ASSERT_TRUE(transformer.collect_file_statistics_after_close(&stats).ok());
+            ASSERT_TRUE(stats.lower_bounds.contains(1));
+            ASSERT_TRUE(stats.upper_bounds.contains(1));
+            EXPECT_EQ(test.lower, stats.lower_bounds.at(1));
+            EXPECT_EQ(test.upper, stats.upper_bounds.at(1));
+        }
+    }
+}
+
+TEST_F(VOrcTransformerTest, AllNullDecimalsHaveNoBounds) {
+    auto type = make_nullable(create_decimal(38, 2, false));
+    auto column = type->create_column();
+    column->insert_default();
+    column->insert_default();
+    auto exprs = MockSlotRef::create_mock_contexts(DataTypes {type});
+    const std::string json =
+            
R"json({"type":"struct","fields":[{"id":1,"name":"d","required":false,"type":"decimal(38,2)"}]})json";
+    auto schema = iceberg::SchemaParser::from_json(json);
+    io::FileWriterPtr file_writer;
+    ASSERT_TRUE(_fs->create_file(_file_path, &file_writer).ok());
+    RuntimeState state;
+    VOrcTransformer transformer(&state, file_writer.get(), exprs, "", {"d"}, 
false,
+                                TFileCompressType::PLAIN, schema.get(), _fs);
+    ASSERT_TRUE(transformer.open().ok());
+    ASSERT_TRUE(transformer.write(Block({{std::move(column), type, 
"d"}})).ok());
+    ASSERT_TRUE(transformer.close().ok());
+    TIcebergColumnStats stats;
+    ASSERT_TRUE(transformer.collect_file_statistics_after_close(&stats).ok());
+    EXPECT_FALSE(stats.lower_bounds.contains(1));
+    EXPECT_FALSE(stats.upper_bounds.contains(1));
+    EXPECT_EQ(2, stats.null_value_counts.at(1));
+}
+
 TEST_F(VOrcTransformerTest, IcebergBinaryTypesOverrideLegacyStringCarrier) {
     const std::string schema_json = R"({
         "type": "struct",
diff --git 
a/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_stats2.out
 
b/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_stats2.out
index df313184df9..68295992854 100644
--- 
a/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_stats2.out
+++ 
b/regression-test/data/external_table_p0/iceberg/write/test_iceberg_write_stats2.out
@@ -14,20 +14,20 @@ false       22      222     2.2     2.2     2222    
8765.4321       8765.432100     987654321098765432.987654321099
 true   11      111     1.1     1.1     1111    1234.5678       1234.567890     
123456789012345678.123456789012 aaa     2023-01-01      2023-01-01T12:34:56
 
 -- !sql_4 --
-0      ORC     2       {1:2, 2:2, 3:2, 4:2, 5:2, 6:2, 7:2, 8:2, 9:2, 10:2, 
11:2, 12:2} {}      {1:0x00, 2:0x0B00000000000000, 3:0x6F00000000000000, 
4:0xCDCC8C3F, 5:0x9A9999999999F13F, 6:0x0457, 7:0x00BC614E, 8:0x075BCD15, 
9:0x018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000, 
12:0x005CE70F33F10500}       {1:0x01, 2:0x1600000000000000, 
3:0xDE00000000000000, 4:0xCDCC0C40, 5:0x9A99999999990140, 6:0x08AE, 
7:0x05397FB1, 8:0x05397FB1, 9:0x0C7748819DFFB62505316873CB, 10:0x626262, 
11:0x434C0000, 12:0x40D91FA833FE0500}
+0      ORC     2       {1:2, 2:2, 3:2, 4:2, 5:2, 6:2, 7:2, 8:2, 9:2, 10:2, 
11:2, 12:2} {}      {1:0x00, 2:0x0B00000000000000, 3:0x6F00000000000000, 
4:0xCDCC8C3F, 5:0x9A9999999999F13F, 6:0x0457, 7:0x00BC614E, 8:0x499602D2, 
9:0x018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000, 
12:0x005CE70F33F10500}       {1:0x01, 2:0x1600000000000000, 
3:0xDE00000000000000, 4:0xCDCC0C40, 5:0x9A99999999990140, 6:0x08AE, 
7:0x05397FB1, 8:0x020A75E124, 9:0x0C7748819DFFB62505316873CB, 10:0x626262, 
11:0x434C0000, 12:0x40D91FA833FE0500}
 
 -- !sql_5 --
-{"bigint_col":{"column_size":null, "value_count":2, "null_value_count":null, 
"nan_value_count":null, "lower_bound":111, "upper_bound":222}, 
"boolean_col":{"column_size":null, "value_count":2, "null_value_count":null, 
"nan_value_count":null, "lower_bound":0, "upper_bound":1}, 
"date_col":{"column_size":null, "value_count":2, "null_value_count":null, 
"nan_value_count":null, "lower_bound":"2023-01-01", 
"upper_bound":"2023-06-15"}, "datetime_col1":{"column_size":null, 
"value_count":2, "null_v [...]
+{"bigint_col":{"column_size":null, "value_count":2, "null_value_count":null, 
"nan_value_count":null, "lower_bound":111, "upper_bound":222}, 
"boolean_col":{"column_size":null, "value_count":2, "null_value_count":null, 
"nan_value_count":null, "lower_bound":0, "upper_bound":1}, 
"date_col":{"column_size":null, "value_count":2, "null_value_count":null, 
"nan_value_count":null, "lower_bound":"2023-01-01", 
"upper_bound":"2023-06-15"}, "datetime_col1":{"column_size":null, 
"value_count":2, "null_v [...]
 
 -- !sql_6 --
 {1:0x00, 2:0x0B000000, 3:0x6F00000000000000, 4:0xCDCC8C3F, 
5:0x9A9999999999F13F, 6:0x0457, 7:0x00BC614E, 8:0x499602D2, 
9:0x018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000, 
12:0x005CE70F33F10500}
 
 -- !sql_7 --
-{1:0x00, 2:0x0B00000000000000, 3:0x6F00000000000000, 4:0xCDCC8C3F, 
5:0x9A9999999999F13F, 6:0x0457, 7:0x00BC614E, 8:0x075BCD15, 
9:0x018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000, 
12:0x005CE70F33F10500}
+{1:0x00, 2:0x0B00000000000000, 3:0x6F00000000000000, 4:0xCDCC8C3F, 
5:0x9A9999999999F13F, 6:0x0457, 7:0x00BC614E, 8:0x499602D2, 
9:0x018EE90FF6C373E0393713FA14, 10:0x616161, 11:0x9E4B0000, 
12:0x005CE70F33F10500}
 
 -- !sql_8 --
 {1:0x01, 2:0x16000000, 3:0xDE00000000000000, 4:0xCDCC0C40, 
5:0x9A99999999990140, 6:0x08AE, 7:0x05397FB1, 8:0x020A75E124, 
9:0x0C7748819DFFB62505316873CB, 10:0x626262, 11:0x434C0000, 
12:0x40D91FA833FE0500}
 
 -- !sql_9 --
-{1:0x01, 2:0x1600000000000000, 3:0xDE00000000000000, 4:0xCDCC0C40, 
5:0x9A99999999990140, 6:0x08AE, 7:0x05397FB1, 8:0x05397FB1, 
9:0x0C7748819DFFB62505316873CB, 10:0x626262, 11:0x434C0000, 
12:0x40D91FA833FE0500}
+{1:0x01, 2:0x1600000000000000, 3:0xDE00000000000000, 4:0xCDCC0C40, 
5:0x9A99999999990140, 6:0x08AE, 7:0x05397FB1, 8:0x020A75E124, 
9:0x0C7748819DFFB62505316873CB, 10:0x626262, 11:0x434C0000, 
12:0x40D91FA833FE0500}
 
diff --git 
a/regression-test/suites/external_table_p0/iceberg/test_iceberg_orc_decimal_bounds.groovy
 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_orc_decimal_bounds.groovy
new file mode 100644
index 00000000000..0a64c8121c6
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/iceberg/test_iceberg_orc_decimal_bounds.groovy
@@ -0,0 +1,103 @@
+// 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_iceberg_orc_decimal_bounds", "p0,external") {
+    String enabled = context.config.otherConfigs.get("enableIcebergTest")
+    if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+        logger.info("disable iceberg test.")
+        return
+    }
+
+    String rest_port = context.config.otherConfigs.get("iceberg_rest_uri_port")
+    String minio_port = context.config.otherConfigs.get("iceberg_minio_port")
+    String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+    String catalog_name = "test_iceberg_orc_decimal_bounds"
+    String db_name = "test_iceberg_orc_decimal_bounds_db"
+
+    sql """drop catalog if exists ${catalog_name}"""
+    sql """create catalog ${catalog_name} properties (
+        'type' = 'iceberg',
+        'iceberg.catalog.type' = 'rest',
+        'uri' = 'http://${externalEnvIp}:${rest_port}',
+        's3.access_key' = 'admin',
+        's3.secret_key' = 'password',
+        's3.endpoint' = 'http://${externalEnvIp}:${minio_port}',
+        's3.region' = 'us-east-1'
+    )"""
+    sql """create database if not exists ${catalog_name}.${db_name}"""
+    sql """use ${catalog_name}.${db_name}"""
+    // EXPLAIN must finish planning splits so it exposes manifest pruning.
+    sql "set enable_external_table_batch_mode = false"
+
+    sql "drop table if exists orc_decimal_bounds_negative"
+    sql """create table orc_decimal_bounds_negative (
+        id int,
+        d32 decimal(9, 0),
+        d64 decimal(18, 2),
+        d128 decimal(38, 2)
+    ) properties ('write-format' = 'orc')"""
+    sql """insert into orc_decimal_bounds_negative values
+        (1, -100, -100.00, -999999999999999999999999999999999999.99),
+        (2, -1, -1.00, -1.00)"""
+
+    ["d32", "d64", "d128"].each { column ->
+        // Negating the footer's two's-complement value makes both bounds 
positive
+        // and incorrectly excludes this file from every negative range.
+        explain {
+            sql("select id from orc_decimal_bounds_negative where ${column} < 
0")
+            contains "inputSplitNum=1"
+        }
+        explain {
+            sql("select id from orc_decimal_bounds_negative where ${column} > 
0")
+            contains "inputSplitNum=0"
+        }
+    }
+    // ORC serializes -100.00 as value=-100, scale=0. Fixing only its sign
+    // still leaves -1.00 in the manifest unless the column scale is restored.
+    explain {
+        sql("select id from orc_decimal_bounds_negative where d64 = -100.00")
+        contains "inputSplitNum=1"
+    }
+
+    sql "drop table if exists orc_decimal_bounds_mixed"
+    sql """create table orc_decimal_bounds_mixed (
+        id int,
+        d32 decimal(9, 0),
+        d64 decimal(18, 2),
+        d128 decimal(38, 2)
+    ) properties ('write-format' = 'orc')"""
+    sql """insert into orc_decimal_bounds_mixed values
+        (1, -100, -100.00, -100.00),
+        (2, 128, 128.00, 128.00),
+        (3, null, null, null)"""
+
+    ["d32", "d64", "d128"].each { column ->
+        explain {
+            sql("select id from orc_decimal_bounds_mixed where ${column} = 
-100")
+            contains "inputSplitNum=1"
+        }
+        // Positive values with trailing zeros must retain their scale too.
+        explain {
+            sql("select id from orc_decimal_bounds_mixed where ${column} = 
128")
+            contains "inputSplitNum=1"
+        }
+        explain {
+            sql("select id from orc_decimal_bounds_mixed where ${column} > 
128")
+            contains "inputSplitNum=0"
+        }
+    }
+}


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

Reply via email to