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 156bae07933 [fix](be) Read dictionary-encoded Parquet BOOLEAN columns 
(#69967)
156bae07933 is described below

commit 156bae0793386477fb98b08649a2d5090a7a5f0d
Author: Gabriel <[email protected]>
AuthorDate: Tue Nov 3 10:36:46 2026 +0800

    [fix](be) Read dictionary-encoded Parquet BOOLEAN columns (#69967)
    
    ### What problem does this PR solve?
    
    Issue Number: N/A
    
    Problem Summary:
    
    The native Parquet reader rejects every BOOLEAN dictionary page,
    although PLAIN dictionaries with RLE_DICTIONARY or legacy
    PLAIN_DICTIONARY data pages are valid. Reading a nullable BOOLEAN
    interoperability fixture fails with `Boolean type cannot have a
    dictionary page`.
    
    Create the existing fixed-length dictionary decoder for BOOLEAN and
    expand its LSB-first packed entries into tracked, contiguous one-byte
    values for SerDe. Validate the packed extent before allocation and
    preserve the common dictionary-index checks for reads, selections, and
    skips.
    
    The 257-byte interoperability fixture and synthetic tests cover byte
    boundaries, invalid extents, dictionary replacement, both dictionary
    encoding enums, V1/V2 pages, skipped/filtered values, and
    dictionary-to-PLAIN transitions.
    
    ### Release note
    
    Support reading dictionary-encoded BOOLEAN columns from Parquet files.
    
    ### Check List (For Author)
    
    - Test
    - [x] Regression test: S3 TVF reads compare against SQL literal rows,
    covering predicates, aggregates, NULL alignment, and lazy
    materialization on/off. Groovy compilation and a suite control-flow
    check using the real FE variable manager pass, including settings
    restoration after success or an injected failure; full SQL execution is
    pending CI.
    - [x] Unit Test: ASAN BE tests; all 199 tests in
    `ParquetV2NativeDecoderTest.*:NewParquetReaderTest.*` pass. The five new
    tests fail before the fix and pass after it.
        - [ ] Manual test
    - Behavior changed:
    - [x] Yes. Valid BOOLEAN dictionaries are readable instead of being
    rejected.
    - Does this need documentation?
        - [x] No. Compatibility fix for existing Parquet reads.
---
 be/src/format_v2/parquet/reader/native/decoder.cpp |   3 +-
 .../reader/native/fix_length_dict_decoder.hpp      |  32 +++-
 be/test/format_v2/parquet/native_decoder_test.cpp  | 180 +++++++++++++++++++++
 be/test/format_v2/parquet/parquet_reader_test.cpp  |  53 ++++++
 .../test_data/boolean_rle_dictionary.parquet       | Bin 0 -> 257 bytes
 .../tvf/test_data/boolean_rle_dictionary.parquet   | Bin 0 -> 257 bytes
 .../tvf/test_parquet_boolean_dictionary.groovy     | 102 ++++++++++++
 7 files changed, 362 insertions(+), 8 deletions(-)

diff --git a/be/src/format_v2/parquet/reader/native/decoder.cpp 
b/be/src/format_v2/parquet/reader/native/decoder.cpp
index 845c9deacd7..68ec067104d 100644
--- a/be/src/format_v2/parquet/reader/native/decoder.cpp
+++ b/be/src/format_v2/parquet/reader/native/decoder.cpp
@@ -279,7 +279,8 @@ Status create_plain_decoder(tparquet::Type::type type, 
std::unique_ptr<Decoder>&
 Status create_dictionary_decoder(tparquet::Type::type type, 
std::unique_ptr<Decoder>& decoder) {
     switch (type) {
     case tparquet::Type::BOOLEAN:
-        return Status::InternalError("Boolean type cannot have a dictionary 
page");
+        decoder = 
std::make_unique<FixLengthDictDecoder<tparquet::Type::BOOLEAN>>();
+        return Status::OK();
     case tparquet::Type::BYTE_ARRAY:
         decoder = std::make_unique<ByteArrayDictDecoder>();
         return Status::OK();
diff --git a/be/src/format_v2/parquet/reader/native/fix_length_dict_decoder.hpp 
b/be/src/format_v2/parquet/reader/native/fix_length_dict_decoder.hpp
index 1f1166421a4..280b97cc853 100644
--- a/be/src/format_v2/parquet/reader/native/fix_length_dict_decoder.hpp
+++ b/be/src/format_v2/parquet/reader/native/fix_length_dict_decoder.hpp
@@ -18,6 +18,7 @@
 #pragma once
 
 #include <limits>
+#include <utility>
 
 #include "format_v2/parquet/reader/native/decoder.h"
 
@@ -41,16 +42,33 @@ public:
 
     Status set_dict(DorisUniqueBufferPtr<uint8_t>& dict, int32_t length,
                     size_t num_values) override {
-        if (UNLIKELY(_type_length <= 0 || length < 0 ||
-                     num_values > std::numeric_limits<size_t>::max() /
-                                          static_cast<size_t>(_type_length) ||
-                     num_values * static_cast<size_t>(_type_length) !=
-                             static_cast<size_t>(length))) {
-            return Status::Corruption("Wrong dictionary data for fixed length 
type");
-        }
         if (UNLIKELY(dict == nullptr)) {
             return Status::Corruption("Fixed-length Parquet dictionary is 
null");
         }
+        if constexpr (PhysicalType == tparquet::Type::BOOLEAN) {
+            // A PLAIN BOOLEAN dictionary is LSB-first bit-packed, so nine 
entries need two bytes.
+            // Validate that extent before expanding; division first also 
avoids count + 7 overflow.
+            const size_t packed_bytes = num_values / 8 + (num_values % 8 != 
0U);
+            if (UNLIKELY(std::cmp_not_equal(packed_bytes, length))) {
+                return Status::Corruption("Wrong dictionary data for boolean 
type");
+            }
+            // SerDe consumes contiguous one-byte values: e.g. 0x02 becomes 
{false, true}.
+            auto unpacked = make_unique_buffer<uint8_t>(num_values);
+            for (size_t row = 0; row < num_values; ++row) {
+                unpacked[row] = static_cast<uint8_t>((dict[row / 8] >> (row % 
8)) & 1);
+            }
+            dict = std::move(unpacked);
+            _type_length = sizeof(uint8_t);
+        } else {
+            // Other physical types store one complete fixed-width value per 
dictionary entry.
+            if (UNLIKELY(_type_length <= 0 || length < 0 ||
+                         num_values > std::numeric_limits<size_t>::max() /
+                                              
static_cast<size_t>(_type_length) ||
+                         num_values * static_cast<size_t>(_type_length) !=
+                                 static_cast<size_t>(length))) {
+                return Status::Corruption("Wrong dictionary data for fixed 
length type");
+            }
+        }
         _dict = std::move(dict);
         _num_dictionary_values = num_values;
         ++_dictionary_generation;
diff --git a/be/test/format_v2/parquet/native_decoder_test.cpp 
b/be/test/format_v2/parquet/native_decoder_test.cpp
index 1dc88ace240..63f972a4946 100644
--- a/be/test/format_v2/parquet/native_decoder_test.cpp
+++ b/be/test/format_v2/parquet/native_decoder_test.cpp
@@ -295,6 +295,93 @@ public:
     std::vector<uint32_t> indices;
 };
 
+TEST(ParquetV2NativeDecoderTest, BooleanDictionaryUnpacksPlainEntries) {
+    for (const auto encoding :
+         {tparquet::Encoding::RLE_DICTIONARY, 
tparquet::Encoding::PLAIN_DICTIONARY}) {
+        SCOPED_TRACE(encoding);
+        for (const size_t count : {1, 2, 8, 9, 129}) {
+            SCOPED_TRACE(count);
+            std::unique_ptr<Decoder> decoder;
+            ASSERT_TRUE(Decoder::get_decoder(tparquet::Type::BOOLEAN, 
encoding, decoder).ok());
+            decoder->set_type_length(-1);
+            const size_t bytes = (count + 7) / 8;
+            auto dictionary = make_unique_buffer<uint8_t>(bytes);
+            memset(dictionary.get(), 0xaa, bytes);
+            ASSERT_TRUE(decoder->set_dict(dictionary, bytes, count).ok());
+            CaptureFixedConsumer values;
+            CaptureBinaryConsumer unused;
+            ASSERT_TRUE(decoder->decode_dictionary(values, unused).ok());
+            EXPECT_EQ(values.width, 1);
+            ASSERT_EQ(values.bytes.size(), count);
+            for (size_t row = 0; row < count; ++row) {
+                EXPECT_EQ(values.bytes[row], row % 2);
+            }
+        }
+    }
+}
+
+TEST(ParquetV2NativeDecoderTest, BooleanDictionaryReadSkipSelectionAndBounds) {
+    for (const auto encoding :
+         {tparquet::Encoding::RLE_DICTIONARY, 
tparquet::Encoding::PLAIN_DICTIONARY}) {
+        SCOPED_TRACE(encoding);
+        std::unique_ptr<Decoder> decoder;
+        ASSERT_TRUE(Decoder::get_decoder(tparquet::Type::BOOLEAN, encoding, 
decoder).ok());
+        auto dictionary = make_unique_buffer<uint8_t>(1);
+        dictionary[0] = 2;
+        ASSERT_TRUE(decoder->set_dict(dictionary, 1, 2).ok());
+        faststring encoded;
+        RleEncoder<uint32_t> encoder(&encoded, 2);
+        const std::vector<uint32_t> ids {1, 0, 1, 1, 0, 0, 1, 0};
+        for (const auto id : ids) {
+            encoder.Put(id);
+        }
+        encoder.Flush();
+        std::string payload(1, 2);
+        payload.append(reinterpret_cast<const char*>(encoded.data()), 
encoded.size());
+        Slice data(payload);
+        ASSERT_TRUE(decoder->set_data(&data).ok());
+        std::vector<uint32_t> actual;
+        ASSERT_TRUE(decoder->decode_dictionary_indices(2, &actual).ok());
+        EXPECT_EQ(actual, (std::vector<uint32_t> {1, 0}));
+        ASSERT_TRUE(decoder->skip_values(2).ok());
+        ParquetSelection selection {
+                .total_values = 4, .selected_values = 2, .ranges = {{.first = 
1, .count = 2}}};
+        actual.clear();
+        ASSERT_TRUE(decoder->decode_selected_dictionary_indices(selection, 
&actual).ok());
+        EXPECT_EQ(actual, (std::vector<uint32_t> {0, 1}));
+
+        const auto generation = decoder->dictionary_generation();
+        auto replacement = make_unique_buffer<uint8_t>(1);
+        replacement[0] = 1;
+        ASSERT_TRUE(decoder->set_dict(replacement, 1, 1).ok());
+        EXPECT_GT(decoder->dictionary_generation(), generation);
+        CaptureFixedConsumer values;
+        CaptureBinaryConsumer unused;
+        ASSERT_TRUE(decoder->decode_dictionary(values, unused).ok());
+        EXPECT_EQ(values.bytes, (std::vector<uint8_t> {1}));
+        ASSERT_TRUE(decoder->set_data(&data).ok());
+        EXPECT_TRUE(decoder->skip_values(1).is<ErrorCode::CORRUPTION>());
+        ASSERT_TRUE(decoder->set_data(&data).ok());
+        EXPECT_TRUE(decoder->decode_dictionary_indices(1, 
&actual).is<ErrorCode::CORRUPTION>());
+    }
+}
+
+TEST(ParquetV2NativeDecoderTest, BooleanDictionaryRejectsInvalidPackedExtent) {
+    std::unique_ptr<Decoder> decoder;
+    ASSERT_TRUE(Decoder::get_decoder(tparquet::Type::BOOLEAN, 
tparquet::Encoding::RLE_DICTIONARY,
+                                     decoder)
+                        .ok());
+    auto dictionary = make_unique_buffer<uint8_t>(2);
+    EXPECT_TRUE(decoder->set_dict(dictionary, 0, 
1).is<ErrorCode::CORRUPTION>());
+    EXPECT_TRUE(decoder->set_dict(dictionary, 1, 
9).is<ErrorCode::CORRUPTION>());
+    EXPECT_TRUE(decoder->set_dict(dictionary, 2, 
2).is<ErrorCode::CORRUPTION>());
+    EXPECT_TRUE(decoder->set_dict(dictionary, -1, 
2).is<ErrorCode::CORRUPTION>());
+    EXPECT_TRUE(decoder->set_dict(dictionary, 1, 
std::numeric_limits<size_t>::max())
+                        .is<ErrorCode::CORRUPTION>());
+    DorisUniqueBufferPtr<uint8_t> missing;
+    EXPECT_TRUE(decoder->set_dict(missing, 1, 2).is<ErrorCode::CORRUPTION>());
+}
+
 TEST(ParquetV2NativeDecoderTest, FragmentedPlainSelectionUsesOneConsumerBatch) 
{
     std::array<int32_t, 32> input {};
     std::iota(input.begin(), input.end(), 0);
@@ -609,6 +696,99 @@ std::vector<uint8_t> serialize_page(tparquet::PageHeader 
header,
     return bytes;
 }
 
+TEST(ParquetV2NativeDecoderTest, BooleanDictionaryAcrossPagesAndPlainFallback) 
{
+    for (const bool page_v2 : {false, true}) {
+        for (const auto encoding :
+             {tparquet::Encoding::RLE_DICTIONARY, 
tparquet::Encoding::PLAIN_DICTIONARY}) {
+            SCOPED_TRACE(page_v2);
+            SCOPED_TRACE(encoding);
+            tparquet::PageHeader dict_header;
+            dict_header.__set_type(tparquet::PageType::DICTIONARY_PAGE);
+            dict_header.__set_compressed_page_size(1);
+            dict_header.__set_uncompressed_page_size(1);
+            tparquet::DictionaryPageHeader dict_info;
+            dict_info.__set_num_values(2);
+            dict_info.__set_encoding(tparquet::Encoding::PLAIN);
+            dict_header.__set_dictionary_page_header(dict_info);
+            // A nonzero dictionary offset distinguishes this chunk from one 
without a dictionary.
+            std::vector<uint8_t> bytes(1, 0);
+            const auto dictionary = serialize_page(dict_header, {2});
+            bytes.insert(bytes.end(), dictionary.begin(), dictionary.end());
+            const auto data_offset = bytes.size();
+            for (size_t page = 0; page < 3; ++page) {
+                const auto page_encoding = page == 2 ? 
tparquet::Encoding::PLAIN : encoding;
+                // Width 1 + a literal run of eight IDs (0x55); PLAIN uses the 
opposite polarity.
+                const std::vector<uint8_t> payload =
+                        page == 2 ? std::vector<uint8_t> {0xaa} : 
std::vector<uint8_t> {1, 3, 0x55};
+                tparquet::PageHeader header;
+                header.__set_type(page_v2 ? tparquet::PageType::DATA_PAGE_V2
+                                          : tparquet::PageType::DATA_PAGE);
+                header.__set_compressed_page_size(payload.size());
+                header.__set_uncompressed_page_size(payload.size());
+                if (page_v2) {
+                    tparquet::DataPageHeaderV2 info;
+                    info.__set_num_values(8);
+                    info.__set_num_rows(8);
+                    info.__set_num_nulls(0);
+                    info.__set_encoding(page_encoding);
+                    info.__set_definition_levels_byte_length(0);
+                    info.__set_repetition_levels_byte_length(0);
+                    info.__set_is_compressed(false);
+                    header.__set_data_page_header_v2(info);
+                } else {
+                    tparquet::DataPageHeader info;
+                    info.__set_num_values(8);
+                    info.__set_encoding(page_encoding);
+                    
info.__set_definition_level_encoding(tparquet::Encoding::RLE);
+                    
info.__set_repetition_level_encoding(tparquet::Encoding::RLE);
+                    header.__set_data_page_header(info);
+                }
+                const auto serialized = serialize_page(header, payload);
+                bytes.insert(bytes.end(), serialized.begin(), 
serialized.end());
+            }
+            MemoryBufferedReader stream(bytes);
+            tparquet::ColumnChunk chunk;
+            chunk.meta_data.__set_type(tparquet::Type::BOOLEAN);
+            
chunk.meta_data.__set_codec(tparquet::CompressionCodec::UNCOMPRESSED);
+            chunk.meta_data.__set_num_values(24);
+            chunk.meta_data.__set_total_compressed_size(bytes.size() - 1);
+            chunk.meta_data.__set_dictionary_page_offset(1);
+            chunk.meta_data.__set_data_page_offset(data_offset);
+            NativeFieldSchema field;
+            field.physical_type = tparquet::Type::BOOLEAN;
+            ParquetPageReadContext page_context(false, "");
+            ColumnChunkReader<false, false> reader(&stream, &chunk, &field, 
nullptr, 24, nullptr,
+                                                   page_context);
+            ASSERT_TRUE(reader.init().ok());
+            DataTypeUInt8 type;
+            auto column = type.create_column();
+            ParquetDecodeContext context;
+            context.physical_type = ParquetPhysicalType::BOOLEAN;
+            ParquetMaterializationState state;
+            for (size_t page = 0; page < 3; ++page) {
+                ASSERT_TRUE(reader.parse_page_header().ok());
+                ASSERT_TRUE(reader.load_page_data().ok());
+                ASSERT_TRUE(reader.skip_values(2).ok());
+                const std::vector<uint8_t> selected {1, 0, 1, 1, 0, 1};
+                FilterMap filter;
+                ASSERT_TRUE(filter.init(selected.data(), selected.size(), 
false).ok());
+                ColumnSelectVector selection;
+                ASSERT_TRUE(selection.init({6, 0}, 6, nullptr, &filter, 
0).ok());
+                const auto status = reader.materialize_values(column, 
*type.get_serde(), context,
+                                                              state, 
selection);
+                ASSERT_TRUE(status.ok()) << status;
+                EXPECT_EQ(reader.remaining_num_values(), 0);
+                ASSERT_TRUE(reader.next_page().ok());
+            }
+            EXPECT_FALSE(reader.has_next_page());
+            const std::vector<uint8_t> expected {1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 
1, 1};
+            ASSERT_EQ(column->size(), expected.size());
+            const auto& values = assert_cast<const 
ColumnUInt8&>(*column).get_data();
+            EXPECT_TRUE(std::equal(expected.begin(), expected.end(), 
values.begin()));
+        }
+    }
+}
+
 std::vector<uint8_t> serialize_plain_int32_page(const std::vector<int32_t>& 
values) {
     tparquet::PageHeader header;
     header.type = tparquet::PageType::DATA_PAGE;
diff --git a/be/test/format_v2/parquet/parquet_reader_test.cpp 
b/be/test/format_v2/parquet/parquet_reader_test.cpp
index 027f20ae89f..db173ec83ca 100644
--- a/be/test/format_v2/parquet/parquet_reader_test.cpp
+++ b/be/test/format_v2/parquet/parquet_reader_test.cpp
@@ -1973,6 +1973,59 @@ TEST_F(NewParquetReaderTest, 
UuidPlainDictionaryNullableAndMappingMatrix) {
     }
 }
 
+TEST_F(NewParquetReaderTest, ReadsNullableBooleanDictionaryFromExternalWriter) 
{
+    const char* source_root = std::getenv("ROOT");
+    ASSERT_NE(source_root, nullptr);
+    // Public interoperability fixture from StarRocks/starrocks PR #81788.
+    _file_path = std::string(source_root) +
+                 
"/be/test/format_v2/parquet/test_data/boolean_rle_dictionary.parquet";
+    for (const size_t batch_size : {1, 3, 1024}) {
+        SCOPED_TRACE(batch_size);
+        auto reader = create_reader();
+        reader->set_batch_size(batch_size);
+        RuntimeState state {TQueryOptions(), TQueryGlobals()};
+        ASSERT_TRUE(reader->init(&state).ok());
+        std::vector<format::ColumnDefinition> schema;
+        ASSERT_TRUE(reader->get_schema(&schema).ok());
+        ASSERT_EQ(schema.size(), 2);
+        auto request = std::make_shared<format::FileScanRequest>();
+        for (size_t column = 0; column < schema.size(); ++column) {
+            
request->non_predicate_columns.push_back(field_projection(schema[column].local_id));
+            
request->local_positions.emplace(format::LocalColumnId(schema[column].local_id),
+                                             format::LocalIndex(column));
+        }
+        ASSERT_TRUE(reader->open(request).ok());
+        const std::array<int, 10> expected {1, 0, -1, 1, 1, 0, -1, 0, 1, 1};
+        size_t total = 0;
+        bool eof = false;
+        while (!eof) {
+            Block block;
+            for (const auto& column : schema) {
+                block.insert({column.type->create_column(), column.type, 
column.name});
+            }
+            size_t rows = 0;
+            const auto status = reader->get_block(&block, &rows, &eof);
+            ASSERT_TRUE(status.ok()) << status;
+            ASSERT_LE(total + rows, expected.size());
+            const auto& booleans =
+                    assert_cast<const 
ColumnNullable&>(*block.get_by_position(1).column);
+            for (size_t row = 0; row < rows; ++row) {
+                
EXPECT_EQ(block.get_by_position(0).type->to_string(*block.get_by_position(0).column,
+                                                                   row),
+                          std::to_string(total + row));
+                EXPECT_EQ(booleans.is_null_at(row), expected[total + row] == 
-1);
+                if (expected[total + row] != -1) {
+                    EXPECT_EQ(assert_cast<const 
ColumnUInt8&>(booleans.get_nested_column())
+                                      .get_element(row),
+                              expected[total + row]);
+                }
+            }
+            total += rows;
+        }
+        EXPECT_EQ(total, expected.size());
+    }
+}
+
 TEST_F(NewParquetReaderTest, InitializesProfileOnlyOncePerFile) {
     class CountingParquetReader final : public format::parquet::ParquetReader {
     public:
diff --git a/be/test/format_v2/parquet/test_data/boolean_rle_dictionary.parquet 
b/be/test/format_v2/parquet/test_data/boolean_rle_dictionary.parquet
new file mode 100644
index 00000000000..f075f6e21ce
Binary files /dev/null and 
b/be/test/format_v2/parquet/test_data/boolean_rle_dictionary.parquet differ
diff --git 
a/regression-test/data/external_table_p0/tvf/test_data/boolean_rle_dictionary.parquet
 
b/regression-test/data/external_table_p0/tvf/test_data/boolean_rle_dictionary.parquet
new file mode 100644
index 00000000000..f075f6e21ce
Binary files /dev/null and 
b/regression-test/data/external_table_p0/tvf/test_data/boolean_rle_dictionary.parquet
 differ
diff --git 
a/regression-test/suites/external_table_p0/tvf/test_parquet_boolean_dictionary.groovy
 
b/regression-test/suites/external_table_p0/tvf/test_parquet_boolean_dictionary.groovy
new file mode 100644
index 00000000000..d8cf489945a
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/tvf/test_parquet_boolean_dictionary.groovy
@@ -0,0 +1,102 @@
+// 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.
+
+import com.azure.storage.blob.BlobClientBuilder
+import com.azure.storage.common.StorageSharedKeyCredential
+
+suite("test_parquet_boolean_dictionary", "p0,external") {
+    String provider = getS3Provider()
+    String endpoint = getS3Endpoint()
+    if (provider.equalsIgnoreCase("AZURE")) {
+        if (endpoint == "azure-endpoint") {
+            endpoint = "https://${getS3AK()}.blob.core.windows.net"
+        } else if (!endpoint.contains("://")) {
+            endpoint = "https://${endpoint}";
+        }
+    }
+    String bucket = context.config.otherConfigs.get('s3BucketName')
+    String key = 
"parquet_boolean_dictionary/${UUID.randomUUID()}/values.parquet"
+    File fixture = new File(context.dataPath, 
"test_data/boolean_rle_dictionary.parquet")
+    def blobClient = null
+    def s3Client = null
+    boolean uploaded = false
+    Map<String, String> originalSettings = [:]
+    // Use the public SQL name: SHOW VARIABLES silently returns no rows for 
unknown names.
+    for (String variable : ['enable_sql_cache', 'enable_query_cache', 
'enable_parquet_lazy_materialization']) {
+        originalSettings[variable] = sql("SHOW VARIABLES LIKE 
'${variable}'")[0][1].toString()
+    }
+    try {
+        if (provider.equalsIgnoreCase("AZURE")) {
+            blobClient = new BlobClientBuilder()
+                    .endpoint(endpoint)
+                    .containerName(bucket)
+                    .blobName(key)
+                    .credential(new StorageSharedKeyCredential(getS3AK(), 
getS3SK()))
+                    .buildClient()
+            blobClient.uploadFromFile(fixture.absolutePath)
+        } else {
+            s3Client = getS3Client()
+            s3Client.putObject(bucket, key, fixture)
+        }
+        uploaded = true
+        String source = """S3('uri'='s3://${bucket}/${key}', 
'format'='parquet',
+                'provider'='${provider}', 's3.endpoint'='${endpoint}', 
's3.region'='${getS3Region()}',
+                's3.access_key'='${getS3AK()}', 
's3.secret_key'='${getS3SK()}')"""
+        // OUTFILE writes PLAIN booleans, so use a fixture whose BOOLEAN 
values require dictionary
+        // decoding. Compare with SQL literals to check NULL alignment without 
hand-written output.
+        String expected = """(
+                SELECT 0 AS id, CAST(TRUE AS BOOLEAN) AS b
+                UNION ALL SELECT 1, FALSE
+                UNION ALL SELECT 2, CAST(NULL AS BOOLEAN)
+                UNION ALL SELECT 3, TRUE
+                UNION ALL SELECT 4, TRUE
+                UNION ALL SELECT 5, FALSE
+                UNION ALL SELECT 6, CAST(NULL AS BOOLEAN)
+                UNION ALL SELECT 7, FALSE
+                UNION ALL SELECT 8, TRUE
+                UNION ALL SELECT 9, TRUE
+            ) expected_boolean_dictionary"""
+        sql "SET enable_sql_cache = false"
+        sql "SET enable_query_cache = false"
+        for (boolean lazyMaterialization : [false, true]) {
+            sql "SET enable_parquet_lazy_materialization = 
${lazyMaterialization}"
+            for (String predicate : ['', 'WHERE b = TRUE', 'WHERE b = FALSE', 
'WHERE b IS NULL',
+                    'WHERE b <> TRUE OR b IS NULL', 'WHERE id IN (1, 2, 5, 6, 
9)']) {
+                quickTest("boolean_dictionary_rows",
+                        "SELECT id, b FROM ${source} ${predicate} ORDER BY id",
+                        "SELECT id, b FROM ${expected} ${predicate} ORDER BY 
id")
+            }
+            quickTest("boolean_dictionary_aggregates",
+                    """SELECT COUNT(*), COUNT(b), SUM(IF(b, 1, 0)), SUM(IF(b 
IS NULL, 1, 0))
+                       FROM ${source}""",
+                    """SELECT COUNT(*), COUNT(b), SUM(IF(b, 1, 0)), SUM(IF(b 
IS NULL, 1, 0))
+                       FROM ${expected}""")
+        }
+    } finally {
+        try {
+            originalSettings.each { variable, value -> sql "SET ${variable} = 
${value}" }
+        } finally {
+            if (uploaded) {
+                if (blobClient != null) {
+                    blobClient.delete()
+                } else {
+                    s3Client.deleteObject(bucket, key)
+                }
+            }
+        }
+    }
+}


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

Reply via email to