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 75ab5ba1082 [fix](be) Fix ORC condition-cache seeks past filtered 
stripe suffixes (#70079)
75ab5ba1082 is described below

commit 75ab5ba108293af01da96e84f612e0b05bdce9db
Author: Gabriel <[email protected]>
AuthorDate: Wed Nov 4 13:57:35 2026 +0800

    [fix](be) Fix ORC condition-cache seeks past filtered stripe suffixes 
(#70079)
    
    ### What problem does this PR solve?
    
    Related PR: https://github.com/apache/doris-thirdparty/pull/431 (merge
    the ORC dependency fix first)
    
    A condition-cache granule can start in a stripe's SARG-filtered suffix
    while containing a matching row in the next stripe. On an initialized
    stripe, ORC's `seekToRow()` uses the stripe-end sentinel as a row-group
    index. This can access past the index when the stripe ends on an index
    boundary, or prematurely return EOF when the last row group is partial.
    
    The same-stripe fast path originates in [Apache ORC
    ORC-1170](https://github.com/apache/orc/pull/1114); the same missing
    stripe-end handling remains in the current upstream C++ reader.
    
    Pin the ORC fix that initializes the next selected stripe before
    positioning its column readers. Preserve SARG pruning, lazy reads and
    split boundaries.
    
    Add a Doris reader regression and a 1.5 KB synthetic ORC fixture with
    its generator. The fixture has two 20,000-row stripes, a 10,000-row
    index stride, and one matching row at the start of each stripe. A cache
    hit seeks to row 18,432 in the first stripe's filtered suffix. The SQL
    regression compares cache-disabled and cached results with lazy reads
    and min/max pruning enabled or disabled, and checks the profile for
    actual cache hits and skipped rows.
    
    ### Release note
    
    Fix potential crashes and missing rows when ORC condition-cache seeks
    reach a SARG-filtered stripe suffix.
    
    ### Check List (For Author)
    
    - Test:
    - [x] Unit Test: 247 ORC-related BE tests passed under AddressSanitizer,
    including the new Doris condition-cache case and 32 dependency boundary
    cases.
    - [x] Unit Test: 56 focused standalone ORC tests passed under
    AddressSanitizer. Reverting the fix makes all 16 initialized-stripe
    boundary cases fail.
    - [ ] Regression test: SQL suite added; Groovy compilation checked. The
    external SQL suite has not been executed locally.
      - [x] clang-format 16 and BE build-hygiene checks passed.
    - [ ] Local full-file clang-tidy analysis did not complete within ten
    minutes; no completed result is available.
    - Behavior changed:
      - [x] Yes. Safely advance to the next matching stripe or EOF.
    - Does this need documentation?
      - [x] No.
---
 be/test/format_v2/orc/orc_reader_test.cpp          |  66 +++++++++++++
 contrib/apache-orc                                 |   2 +-
 .../test_data/GenerateOrcConditionCacheSeek.cpp    |  51 ++++++++++
 .../tvf/test_data/condition_cache_seek.orc         | Bin 0 -> 1458 bytes
 .../tvf/test_orc_condition_cache_seek.groovy       | 110 +++++++++++++++++++++
 5 files changed, 228 insertions(+), 1 deletion(-)

diff --git a/be/test/format_v2/orc/orc_reader_test.cpp 
b/be/test/format_v2/orc/orc_reader_test.cpp
index b2a725dfad4..ac8e0a7df8a 100644
--- a/be/test/format_v2/orc/orc_reader_test.cpp
+++ b/be/test/format_v2/orc/orc_reader_test.cpp
@@ -6343,6 +6343,72 @@ TEST_F(NewOrcReaderTest, 
ConditionCacheHitSkipsFalseGranulesBeforeColumnRead) {
     EXPECT_EQ(rows, 0);
 }
 
+TEST_F(NewOrcReaderTest, ConditionCacheHitSeeksPastSargFilteredStripeSuffix) {
+    const auto file_path = find_repo_file(
+            
"regression-test/data/external_table_p0/tvf/test_data/condition_cache_seek.orc");
+    ASSERT_FALSE(file_path.empty());
+    for (const bool lazy : {false, true}) {
+        for (const bool min_max : {false, true}) {
+            SCOPED_TRACE(fmt::format("lazy={}, min_max={}", lazy, min_max));
+            auto io_ctx = std::make_shared<io::IOContext>();
+            auto reader = create_reader_for_path(file_path.string(), nullptr, 
io_ctx);
+            TQueryOptions options;
+            options.__set_enable_orc_lazy_mat(lazy);
+            options.__set_enable_orc_filter_by_min_max(min_max);
+            RuntimeState state {options, TQueryGlobals()};
+            ASSERT_TRUE(reader->init(&state).ok());
+            reader->set_batch_size(ConditionCacheContext::GRANULE_SIZE);
+
+            std::vector<format::ColumnDefinition> schema;
+            ASSERT_TRUE(reader->get_schema(&schema).ok());
+            auto request = std::make_shared<format::FileScanRequest>();
+            request->predicate_columns = {field_projection(0)};
+            request->non_predicate_columns = {field_projection(1)};
+            request->local_positions = {{format::LocalColumnId(0), 
format::LocalIndex(0)},
+                                        {format::LocalColumnId(1), 
format::LocalIndex(1)}};
+            request->conjuncts.push_back(VExprContext::create_shared(
+                    std::make_shared<NullableInt32GreaterThanExpr>(0, 5000)));
+            ASSERT_TRUE(reader->open(request).ok());
+
+            auto ctx = std::make_shared<ConditionCacheContext>();
+            ctx->is_hit = true;
+            ctx->filter_result = std::make_shared<std::vector<bool>>(20, 
false);
+            (*ctx->filter_result)[0] = true;
+            (*ctx->filter_result)[9] = true;
+            reader->set_condition_cache_context(ctx);
+            int seeks = 0;
+            ScopedDebugPoint debug_point(
+                    
"OrcReader._skip_condition_cache_false_granules.before_seek_to_row",
+                    [&]() { ++seeks; });
+
+            // Granule 9 contains the next stripe's matching row, but starts 
in the
+            // current stripe's filtered suffix. Both columns must resume in 
stripe 2.
+            std::vector<int32_t> ids;
+            std::vector<int64_t> payloads;
+            bool eof = false;
+            while (!eof) {
+                Block block = build_file_block(schema);
+                size_t rows = 0;
+                ASSERT_TRUE(reader->get_block(&block, &rows, &eof).ok());
+                const auto& id_column = assert_cast<const ColumnInt32&>(
+                        assert_cast<const 
ColumnNullable&>(*block.get_by_position(0).column)
+                                .get_nested_column());
+                const auto& payload_column = assert_cast<const ColumnInt64&>(
+                        assert_cast<const 
ColumnNullable&>(*block.get_by_position(1).column)
+                                .get_nested_column());
+                for (size_t row = 0; row < rows; ++row) {
+                    ids.push_back(id_column.get_element(row));
+                    payloads.push_back(payload_column.get_element(row));
+                }
+            }
+            EXPECT_EQ(ids, (std::vector<int32_t> {10000, 10001}));
+            EXPECT_EQ(payloads, (std::vector<int64_t> {0, 20000}));
+            EXPECT_GT(seeks, 0);
+            EXPECT_GT(io_ctx->condition_cache_filtered_rows, 0);
+        }
+    }
+}
+
 TEST_F(NewOrcReaderTest, ConditionCacheSeekReturnsCleanEofWhenCancelled) {
     constexpr int64_t row_count = ConditionCacheContext::GRANULE_SIZE * 2;
     const auto file_path = (_test_dir / 
"condition_cache_cancelled_seek.orc").string();
diff --git a/contrib/apache-orc b/contrib/apache-orc
index 5860f94768b..5962bb071d6 160000
--- a/contrib/apache-orc
+++ b/contrib/apache-orc
@@ -1 +1 @@
-Subproject commit 5860f94768b94406f31698ac66f4cc3d2a781324
+Subproject commit 5962bb071d6345cc29c5353d64d85a203525e4d9
diff --git 
a/regression-test/data/external_table_p0/tvf/test_data/GenerateOrcConditionCacheSeek.cpp
 
b/regression-test/data/external_table_p0/tvf/test_data/GenerateOrcConditionCacheSeek.cpp
new file mode 100644
index 00000000000..be4ec1bbb61
--- /dev/null
+++ 
b/regression-test/data/external_table_p0/tvf/test_data/GenerateOrcConditionCacheSeek.cpp
@@ -0,0 +1,51 @@
+// 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.
+
+#include <memory>
+#include <orc/OrcFile.hh>
+#include <orc/Writer.hh>
+
+// Link against the ORC library used by Doris and pass the output fixture path.
+// Each stripe has 20,000 rows and two 10,000-row index groups. Only its first 
row
+// matches id > 5000. The second match makes cache granule [18432, 20480) true,
+// so a cache hit seeks into the first stripe's SARG-filtered suffix at row 
18432.
+int main(int argc, char** argv) {
+    if (argc != 2) {
+        return 1;
+    }
+    auto stream = orc::writeLocalFile(argv[1]);
+    auto type = std::unique_ptr<orc::Type>(
+            orc::Type::buildTypeFromString("struct<id:int,payload:bigint>"));
+    orc::WriterOptions options;
+    
options.setStripeSize(1).setRowIndexStride(10000).setCompression(orc::CompressionKind_NONE);
+    auto writer = orc::createWriter(*type, stream.get(), options);
+    auto batch = writer->createRowBatch(20000);
+    auto& root = dynamic_cast<orc::StructVectorBatch&>(*batch);
+    auto& ids = dynamic_cast<orc::LongVectorBatch&>(*root.fields[0]);
+    auto& payloads = dynamic_cast<orc::LongVectorBatch&>(*root.fields[1]);
+    for (int64_t stripe = 0; stripe < 2; ++stripe) {
+        for (int64_t row = 0; row < 20000; ++row) {
+            ids.data[row] = row == 0 ? 10000 + stripe : 0;
+            payloads.data[row] = stripe * 20000 + row;
+        }
+        root.numElements = ids.numElements = payloads.numElements = 20000;
+        writer->add(*batch);
+    }
+    writer->close();
+    stream->close();
+    return 0;
+}
diff --git 
a/regression-test/data/external_table_p0/tvf/test_data/condition_cache_seek.orc 
b/regression-test/data/external_table_p0/tvf/test_data/condition_cache_seek.orc
new file mode 100644
index 00000000000..3cb2b0524c0
Binary files /dev/null and 
b/regression-test/data/external_table_p0/tvf/test_data/condition_cache_seek.orc 
differ
diff --git 
a/regression-test/suites/external_table_p0/tvf/test_orc_condition_cache_seek.groovy
 
b/regression-test/suites/external_table_p0/tvf/test_orc_condition_cache_seek.groovy
new file mode 100644
index 00000000000..e6a056f5747
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/tvf/test_orc_condition_cache_seek.groovy
@@ -0,0 +1,110 @@
+// 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
+import org.apache.doris.regression.action.ProfileAction
+
+suite("test_orc_condition_cache_seek", "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 = "orc_condition_cache_seek/${UUID.randomUUID()}/values.orc"
+    File fixture = new File(context.dataPath, 
"test_data/condition_cache_seek.orc")
+    def blobClient = null
+    def s3Client = null
+    boolean uploaded = false
+    Map<String, String> originalSettings = [:]
+    for (String variable : ["enable_condition_cache", 
"enable_orc_lazy_materialization",
+                            "enable_orc_filter_by_min_max", "batch_size", 
"enable_sql_cache",
+                            "enable_query_cache", "enable_profile", 
"profile_level", "parallel_pipeline_task_num",
+                            "max_file_scanners_concurrency"]) {
+        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'='orc',
+                'provider'='${provider}', 's3.endpoint'='${endpoint}', 
's3.region'='${getS3Region()}',
+                's3.access_key'='${getS3AK()}', 
's3.secret_key'='${getS3SK()}')"""
+        sql "SET enable_sql_cache = false"
+        sql "SET enable_query_cache = false"
+        sql "SET enable_profile = true"
+        sql "SET profile_level = 2"
+        sql "SET parallel_pipeline_task_num = 1"
+        sql "SET max_file_scanners_concurrency = 1"
+        sql "SET batch_size = 2048"
+        // A full LRU-K shard needs one scan for ghost history and another for 
admission.
+        // Allow a third visit to one backend even when scans are spread 
across all backends.
+        int attempts = 2 * sql("SHOW BACKENDS").size() + 1
+        for (boolean minMax : [false, true]) {
+            sql "SET enable_orc_filter_by_min_max = ${minMax}"
+            for (boolean lazy : [false, true]) {
+                sql "SET enable_orc_lazy_materialization = ${lazy}"
+                sql "SET enable_condition_cache = false"
+                quickTest("uncached", "SELECT id, payload FROM ${source} WHERE 
id > 5000 ORDER BY id",
+                        "SELECT 10000, CAST(0 AS BIGINT) UNION ALL SELECT 
10001, CAST(20000 AS BIGINT) ORDER BY 1")
+                sql "SET enable_condition_cache = true"
+                boolean cacheHit = false
+                for (int attempt = 0; attempt < attempts && !cacheHit; 
++attempt) {
+                    String token = 
"orc_seek_${UUID.randomUUID().toString().replace('-', '')}"
+                    // The first match populates granule 0. The next stripe's 
match populates
+                    // granule 9, whose start (18432) lies in the first 
stripe's pruned suffix.
+                    quickTest("cached",
+                            "SELECT id, payload FROM (SELECT id, payload, 
'${token}' AS marker " +
+                                    "FROM ${source} WHERE id > 5000) t ORDER 
BY id",
+                            "SELECT 10000, CAST(0 AS BIGINT) UNION ALL " +
+                                    "SELECT 10001, CAST(20000 AS BIGINT) ORDER 
BY 1")
+                    String profile = new 
ProfileAction(context).getProfileBySql(token,
+                            ["ConditionCacheHit", 
"ConditionCacheFilteredRows"])
+                    cacheHit = (profile =~ 
/(?m)ConditionCacheHit:\s*[1-9]/).find()
+                    if (cacheHit) {
+                        assertTrue((profile =~ 
/(?m)ConditionCacheFilteredRows:\s*[1-9]/).find(), profile)
+                    }
+                }
+                assertTrue(cacheHit, "No condition cache hit after ${attempts} 
scans")
+            }
+        }
+    } 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