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

Gabriel39 pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 5d46ecfd7e3 [fix](external) Backport SCHEMATA, ORC and JSON/WAL fixes 
to 4.1 (#70319)
5d46ecfd7e3 is described below

commit 5d46ecfd7e376d1422cad8b9addeda1a089f326d
Author: Gabriel <[email protected]>
AuthorDate: Mon Nov 9 16:56:07 2026 +0800

    [fix](external) Backport SCHEMATA, ORC and JSON/WAL fixes to 4.1 (#70319)
    
    ### What problem does this PR solve?
    
    Backport three master fixes to `branch-4.1`: avoid unnecessary
    database-object loading for SCHEMATA scans, prevent ORC condition-cache
    seek crashes or missing rows, and preserve JSON/WAL row counts when no
    file columns are projected.
    
    The commits retain their original authors and are applied in master
    merge order:
    
    1. https://github.com/apache/doris/pull/70010 — Avoid database-object
    enumeration when SCHEMATA only needs names.
    2. https://github.com/apache/doris/pull/70079 — Advance safely past
    SARG-filtered ORC stripe suffixes during condition-cache seeks.
    3. https://github.com/apache/doris/pull/70154 — Track logical JSON
    records independently of projected columns and capture WAL row counts
    before column ownership is transferred.
    
    Branch adaptations:
    
    - Keep Thrift field 7 reserved and add the optional `include_db_ids`
    flag at upstream field 8 with its default `true`. Preserve existing
    privilege checks, pattern matching and schema-name formatting for both
    enumeration paths.
    - Use the upstream fixed ORC dependency revision
    `5962bb071d6345cc29c5353d64d85a203525e4d9`, which descends from the
    branch's existing revision.
    - Resolve the JSON reader conflicts by retaining 4.1's error handling
    and adding logical row accounting without importing unrelated
    missing-field/load-rejection changes. Remove the rooted-array pre-count
    that consumes the ondemand cursor before JSON-path materialization;
    retain the upstream rooted-array coverage.
    
    ### Validation
    
    - The branch's FE production and test sources compiled with the existing
    Java 8 target settings. All **7 targeted FE tests passed**, including
    the existing `FrontendServiceImplTest#testGetDBNames`, the
    100,000-namespace case, filtering and legacy-request compatibility. FE
    Checkstyle passed.
    - A fresh ASAN build of the exact pinned ORC dependency passed **56
    focused tests**, including **32 filtered-stripe-suffix boundary cases**.
    - All **7 affected C++ implementation/test/generator translation units
    compiled**. Clang-format 16 passed for all **8 affected C++
    source/header files**.
    - The ORC fixture generator reproduced the committed fixture byte for
    byte. All **3 added Groovy suites compiled**.
    - Full Doris BE unit-test execution and external SQL regression
    execution remain for CI; they were not run locally for this backport.
    
    ### Release note
    
    Improve SCHEMATA enumeration for large catalogs. Fix potential crashes
    or missing rows during ORC condition-cache seeks and incorrect JSON/WAL
    row counts with empty file projections.
    
    ### Check List (For Author)
    
    - Test
      - [x] Unit tests and focused dependency tests
    - [x] Regression tests included and compiled; full SQL execution pending
    CI
    - Behavior changed:
    - [x] Yes. Apply the upstream fixes while preserving branch-specific
    validation and protocol compatibility.
    - Does this need documentation?
      - [x] No.
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Add branch pick label
    
    ### ARM64 link dependency follow-up
    
    The ORC revision enables libdeflate ZLIB decoding on every architecture.
    Remove the old x86-only guard around `add_thirdparty(deflate)` so ARM64
    BE links also receive the required library, matching master. An adjacent
    comment records this dependency invariant.
    
    Validation: probes include each branch's actual
    `be/cmake/thirdparty.cmake` and select `aarch64`, `arm64`, `x86` and
    `x86_64` dependency configurations. Before the fix, both ARM
    configurations fail to link with missing libdeflate symbols while both
    x86 controls pass. After the fix, all eight repository/configuration
    combinations link and pass a DEFLATE compression/decompression round
    trip. These probes run natively on x86_64 and validate CMake dependency
    selection; a native ARM64/full BE build remains for CI. Clang-format 16
    passed for the PR-affected C++ files.
---
 be/cmake/thirdparty.cmake                          |   6 +-
 be/src/format_v2/json/json_reader.cpp              |  55 ++++--
 be/src/format_v2/json/json_reader.h                |   3 +
 be/src/format_v2/wal/wal_reader.cpp                |   5 +-
 .../information_schema/schema_schemata_scanner.cpp |   2 +
 be/test/format_v2/json/json_reader_test.cpp        | 162 ++++++++++++++++++
 be/test/format_v2/orc/orc_reader_test.cpp          |  66 ++++++++
 be/test/format_v2/wal/wal_reader_test.cpp          | 120 +++++++++++++
 contrib/apache-orc                                 |   2 +-
 .../apache/doris/service/FrontendServiceImpl.java  |  54 ++++--
 .../service/FrontendServiceGetDbNamesTest.java     | 186 +++++++++++++++++++++
 gensrc/thrift/FrontendService.thrift               |   2 +
 .../test_data/GenerateOrcConditionCacheSeek.cpp    |  51 ++++++
 .../tvf/test_data/condition_cache_seek.orc         | Bin 0 -> 1458 bytes
 .../test_group_commit_wal_row_count.out            |  17 ++
 .../hive/test_hive_json_partition_only.groovy      |  85 ++++++++++
 .../tvf/test_orc_condition_cache_seek.groovy       | 110 ++++++++++++
 .../test_group_commit_wal_row_count.groovy         |  85 ++++++++++
 18 files changed, 976 insertions(+), 35 deletions(-)

diff --git a/be/cmake/thirdparty.cmake b/be/cmake/thirdparty.cmake
index b67c8e9d0aa..5471fa30979 100644
--- a/be/cmake/thirdparty.cmake
+++ b/be/cmake/thirdparty.cmake
@@ -186,10 +186,8 @@ if (OS_MACOSX)
     add_thirdparty(intl)
 endif()
 
-# Only used on x86 or x86_64
-if ("${CMAKE_BUILD_TARGET_ARCH}" STREQUAL "x86" OR 
"${CMAKE_BUILD_TARGET_ARCH}" STREQUAL "x86_64")
-    add_thirdparty(deflate)
-endif()
+# ORC's ZLIB decoder requires libdeflate on every architecture, including 
ARM64.
+add_thirdparty(deflate)
 
 add_thirdparty(icuuc LIB64)
 add_thirdparty(icui18n LIB64)
diff --git a/be/src/format_v2/json/json_reader.cpp 
b/be/src/format_v2/json/json_reader.cpp
index 78601f90584..e2a963093fa 100644
--- a/be/src/format_v2/json/json_reader.cpp
+++ b/be/src/format_v2/json/json_reader.cpp
@@ -306,13 +306,13 @@ Status JsonReader::get_block(Block* file_block, size_t* 
rows, bool* eof) {
         return Status::InternalError("JSON v2 reader is not open");
     }
 
-    const auto batch_size = _runtime_state->batch_size();
+    const auto batch_size = cast_set<size_t>(_runtime_state->batch_size());
     const auto max_block_bytes = _runtime_state->preferred_block_size_bytes();
     *rows = 0;
     *eof = false;
+    _logical_rows = 0;
 
-    while (file_block->rows() < batch_size && !_reader_eof &&
-           file_block->bytes() < max_block_bytes) {
+    while (_logical_rows < batch_size && !_reader_eof && file_block->bytes() < 
max_block_bytes) {
         if (_read_json_by_line && _skip_first_line) {
             RETURN_IF_ERROR(_line_reader->skip_split_prefix(
                     _reader_range.start_offset, _line_delimiter, &_reader_eof, 
_io_ctx.get()));
@@ -320,7 +320,7 @@ Status JsonReader::get_block(Block* file_block, size_t* 
rows, bool* eof) {
             continue;
         }
 
-        const size_t original_rows = file_block->rows();
+        const size_t original_rows = _logical_rows;
         size_t size = 0;
         bool is_empty_row = false;
         Status st = Status::OK();
@@ -349,12 +349,13 @@ Status JsonReader::get_block(Block* file_block, size_t* 
rows, bool* eof) {
         }
         // An ignored or empty JSON object can produce no row. Avoid spinning 
forever on a document
         // that was consumed but produced no materialized value.
-        if (!is_empty_row && file_block->rows() == original_rows) {
+        if (!is_empty_row && _logical_rows == original_rows) {
             break;
         }
     }
 
-    *rows = file_block->rows();
+    *rows = _logical_rows;
+    DCHECK(file_block->columns() == 0 || file_block->rows() == *rows);
     _record_scan_rows(cast_set<int64_t>(*rows));
     {
         SCOPED_TIMER(_filter_time);
@@ -671,10 +672,8 @@ Status JsonReader::_extract_json_value(size_t size, bool* 
eof, bool* is_empty_ro
         return Status::DataQualityError(
                 "JSON data is not an array-object, `strip_outer_array` must be 
FALSE.");
     }
-    if (!_parsed_jsonpaths.empty() && _strip_outer_array &&
-        _json_value.count_elements().value() == 0) {
-        *is_empty_row = true;
-    }
+    // A rooted ondemand array has one cursor. Counting it here would consume 
records
+    // before jsonpaths materialization; the append path detects an empty 
array itself.
     return Status::OK();
 }
 
@@ -725,7 +724,7 @@ Status JsonReader::_append_flat_array_jsonpath_rows(Block* 
block, bool* is_empty
     DORIS_CHECK(block != nullptr);
     DORIS_CHECK(is_empty_row != nullptr);
     DORIS_CHECK(eof != nullptr);
-    const size_t original_rows = block->rows();
+    const size_t original_rows = _logical_rows;
     bool valid = true;
     _array = _json_value.get_array();
     _array_iter = _array.begin();
@@ -753,7 +752,7 @@ Status JsonReader::_append_flat_array_jsonpath_rows(Block* 
block, bool* is_empty
         RETURN_IF_ERROR(_write_columns_by_jsonpath(&object_value, block, 
&valid));
         ++_array_iter;
     }
-    *is_empty_row = block->rows() == original_rows;
+    *is_empty_row = _logical_rows == original_rows;
     return Status::OK();
 }
 
@@ -777,7 +776,7 @@ Status 
JsonReader::_set_column_values_from_object(simdjson::ondemand::object* ob
     DORIS_CHECK(block != nullptr);
     DORIS_CHECK(valid != nullptr);
     std::vector<bool> seen_columns(block->columns(), false);
-    const size_t cur_row_count = block->rows();
+    const size_t cur_row_count = _logical_rows;
     bool has_valid_value = false;
     size_t key_index = 0;
 
@@ -821,21 +820,46 @@ Status 
JsonReader::_set_column_values_from_object(simdjson::ondemand::object* ob
         }
     }
     *valid = true;
+    ++_logical_rows;
     if (!has_valid_value) {
         return Status::OK();
     }
     return Status::OK();
 }
 
+Status JsonReader::_has_matching_jsonpath(simdjson::ondemand::object* 
object_value,
+                                          bool* matches) const {
+    *matches = false;
+    for (const auto& path : _parsed_jsonpaths) {
+        // A root path selects the current document without a field lookup.
+        if (JsonFunctions::is_root_path(path)) {
+            *matches = true;
+            continue;
+        }
+        simdjson::ondemand::value value;
+        const auto status = JsonFunctions::extract_from_object(*object_value, 
path, &value);
+        if (!status.ok() && !status.is<ErrorCode::NOT_FOUND>()) {
+            return status;
+        }
+        *matches = *matches || status.ok();
+    }
+    return Status::OK();
+}
+
 Status JsonReader::_write_columns_by_jsonpath(simdjson::ondemand::object* 
object_value,
                                               Block* block, bool* valid) {
     DORIS_CHECK(object_value != nullptr);
     DORIS_CHECK(block != nullptr);
     DORIS_CHECK(valid != nullptr);
     bool has_valid_value = false;
-    const size_t cur_row_count = block->rows();
+    const size_t cur_row_count = _logical_rows;
     _cached_string_values.clear();
 
+    // Even without projected fields, jsonpaths still decides whether the 
object contributes a row.
+    if (_requested_columns.empty()) {
+        RETURN_IF_ERROR(_has_matching_jsonpath(object_value, 
&has_valid_value));
+    }
+
     for (size_t i = 0; i < _requested_columns.size(); ++i) {
         const auto& requested = _requested_columns[i];
         auto* column_ptr = 
block->get_by_position(i).column->assert_mutable().get();
@@ -889,6 +913,7 @@ Status 
JsonReader::_write_columns_by_jsonpath(simdjson::ondemand::object* object
         return Status::OK();
     }
     *valid = true;
+    ++_logical_rows;
     return Status::OK();
 }
 
@@ -1086,6 +1111,7 @@ Status JsonReader::_append_null_for_malformed_json(Block* 
block) {
         assert_cast<ColumnNullable*>(column.get())->insert_default();
         column_with_type.column = std::move(column);
     }
+    ++_logical_rows;
     return Status::OK();
 }
 
@@ -1097,6 +1123,7 @@ Status JsonReader::_handle_json_error(const Status& 
status, Block* block, size_t
     // block to the row count before this document before either surfacing the 
error or appending
     // the ignore-malformed null row.
     _truncate_block_to_rows(block, original_rows);
+    _logical_rows = original_rows;
     if (_openx_json_ignore_malformed && 
status.is<ErrorCode::DATA_QUALITY_ERROR>()) {
         RETURN_IF_ERROR(_append_null_for_malformed_json(block));
         *is_empty_row = false;
diff --git a/be/src/format_v2/json/json_reader.h 
b/be/src/format_v2/json/json_reader.h
index f5e6614b2da..075d5aca534 100644
--- a/be/src/format_v2/json/json_reader.h
+++ b/be/src/format_v2/json/json_reader.h
@@ -147,6 +147,7 @@ private:
     Status _append_nested_jsonpath_row(Block* block, bool* is_empty_row, bool* 
eof);
     Status _set_column_values_from_object(simdjson::ondemand::object* 
object_value, Block* block,
                                           bool* valid);
+    Status _has_matching_jsonpath(simdjson::ondemand::object* object_value, 
bool* matches) const;
     Status _write_columns_by_jsonpath(simdjson::ondemand::object* 
object_value, Block* block,
                                       bool* valid);
     template <bool use_string_cache>
@@ -190,6 +191,8 @@ private:
     std::unique_ptr<Decompressor> _decompressor;
     std::unique_ptr<NewPlainTextLineReader> _line_reader;
     int64_t _current_offset = 0;
+    // Partition-only scans have no file columns, so Block::rows() cannot 
count their records.
+    size_t _logical_rows = 0;
     bool _reader_eof = false;
     bool _skip_first_line = false;
     bool _single_document_read = false;
diff --git a/be/src/format_v2/wal/wal_reader.cpp 
b/be/src/format_v2/wal/wal_reader.cpp
index 55e88796c98..5f57a422ff8 100644
--- a/be/src/format_v2/wal/wal_reader.cpp
+++ b/be/src/format_v2/wal/wal_reader.cpp
@@ -195,8 +195,11 @@ Status WalReader::get_block(Block* file_block, size_t* 
rows, bool* eof) {
         return Status::Corruption("WAL block has {} columns but header 
declares {}",
                                   source_block.columns(), _column_ids.size());
     }
+    // Empty projections still contribute every WAL row. Capture the count 
before materialization
+    // moves source columns into the output block and can leave the source 
with no owned columns.
+    const size_t source_rows = source_block.rows();
     RETURN_IF_ERROR(_materialize_requested_columns(&source_block, file_block));
-    *rows = file_block->rows();
+    *rows = source_rows;
     _record_scan_rows(cast_set<int64_t>(*rows));
     RETURN_IF_ERROR(
             apply_materialized_reader_filters(_request.get(), _io_ctx.get(), 
file_block, rows));
diff --git a/be/src/information_schema/schema_schemata_scanner.cpp 
b/be/src/information_schema/schema_schemata_scanner.cpp
index e9d25684780..15e2528a89f 100644
--- a/be/src/information_schema/schema_schemata_scanner.cpp
+++ b/be/src/information_schema/schema_schemata_scanner.cpp
@@ -52,6 +52,8 @@ Status SchemaSchemataScanner::start(RuntimeState* state) {
         return Status::InternalError("used before initial.");
     }
     TGetDbsParams db_params;
+    // SCHEMATA never reads database IDs; avoid creating one database object 
per namespace in FE.
+    db_params.__set_include_db_ids(false);
     if (nullptr != _param->common_param->wild) {
         db_params.__set_pattern(*(_param->common_param->wild));
     }
diff --git a/be/test/format_v2/json/json_reader_test.cpp 
b/be/test/format_v2/json/json_reader_test.cpp
index 1d94e244f64..f25fd3d8b05 100644
--- a/be/test/format_v2/json/json_reader_test.cpp
+++ b/be/test/format_v2/json/json_reader_test.cpp
@@ -43,6 +43,7 @@
 #include "exprs/vexpr_context.h"
 #include "format/json/new_json_reader.h"
 #include "format_v2/column_data.h"
+#include "format_v2/table_reader.h"
 #include "io/io_common.h"
 #include "runtime/descriptors.h"
 #include "runtime/runtime_profile.h"
@@ -802,4 +803,165 @@ TEST(JsonReaderTest, 
AppliesDeleteAndNormalConjunctsWithPredicateFilterAccountin
     EXPECT_EQ(io_ctx->predicate_filtered_rows, 1);
 }
 
+// Partition-only scans have no file-local output columns, but still need one 
logical row per
+// accepted JSON record so TableReader can materialize the partition constants.
+TEST(JsonReaderTest, EmptyFileProjectionPreservesRows) {
+    ObjectPool pool;
+    const auto slots = build_slots(&pool);
+    for (const auto& source_slots : {slots, std::vector<SlotDescriptor*> {}}) {
+        auto result = read_once("empty_projection.jsonl", 
"{\"id\":1}\n\n{}\n{\"id\":3}\n",
+                                json_scan_params(), source_slots, {}, true, 
true);
+        ASSERT_TRUE(result.status.ok()) << result.status;
+        EXPECT_EQ(result.block.columns(), 0);
+        EXPECT_EQ(result.rows, 3);
+        EXPECT_FALSE(result.eof);
+        EXPECT_TRUE(result.second_status.ok());
+        EXPECT_EQ(result.second_rows, 0);
+        EXPECT_TRUE(result.second_eof);
+    }
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionPreservesOuterArrayRows) {
+    auto result = read_once("empty_projection_array.json", 
R"([{"id":1},{},{"id":3}])",
+                            json_scan_params(false, true), {}, {}, true, true);
+    ASSERT_TRUE(result.status.ok()) << result.status;
+    EXPECT_EQ(result.rows, 3);
+    EXPECT_TRUE(result.second_status.ok());
+    EXPECT_TRUE(result.second_eof);
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionPreservesJsonRootRows) {
+    auto result = read_once("empty_projection_root.jsonl",
+                            
"{\"payload\":{\"id\":1}}\n{\"payload\":{\"id\":2}}\n",
+                            json_scan_params(true, false, "", "$.payload"), 
{}, {}, true, true);
+    ASSERT_TRUE(result.status.ok()) << result.status;
+    EXPECT_EQ(result.rows, 2);
+    EXPECT_TRUE(result.second_status.ok());
+    EXPECT_TRUE(result.second_eof);
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionHonorsJsonPaths) {
+    ObjectPool pool;
+    const auto slots = build_slots(&pool);
+    auto result = read_once("empty_projection_jsonpaths.jsonl",
+                            
"{\"payload\":{\"id\":1}}\n{\"other\":2}\n{\"payload\":{\"id\":3}}\n",
+                            json_scan_params(true, false, 
R"(["$.payload.id","$.payload.user"])"),
+                            slots, {}, true, true);
+    ASSERT_TRUE(result.status.ok()) << result.status;
+    EXPECT_EQ(result.rows, 2);
+    EXPECT_TRUE(result.second_status.ok());
+    EXPECT_TRUE(result.second_eof);
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionPreservesRootJsonPathRows) {
+    auto result = read_once("empty_projection_root_path.jsonl", 
"{\"id\":1}\n{}\n",
+                            json_scan_params(true, false, R"(["$."])"), {}, 
{}, true, true);
+    ASSERT_TRUE(result.status.ok()) << result.status;
+    EXPECT_EQ(result.rows, 2);
+    EXPECT_TRUE(result.second_eof);
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionPreservesRootedArrayJsonPathRows) {
+    auto result = read_once("empty_projection_rooted_array.json",
+                            
R"({"rows":[{"payload":{"id":1}},{"other":2},{"payload":{"id":3}}]})",
+                            json_scan_params(false, true, 
R"(["$.payload.id"])", "$.rows"), {}, {},
+                            true, true);
+    ASSERT_TRUE(result.status.ok()) << result.status;
+    EXPECT_EQ(result.rows, 2);
+    EXPECT_TRUE(result.second_status.ok());
+    EXPECT_TRUE(result.second_eof);
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionPreservesMalformedNullRows) {
+    auto result = read_once("empty_projection_malformed.jsonl", 
"not-json\n{\"id\":1}\n",
+                            json_scan_params(true, false, "", "", true), {}, 
{}, true, true);
+    ASSERT_TRUE(result.status.ok()) << result.status;
+    EXPECT_EQ(result.rows, 2);
+    EXPECT_TRUE(result.second_status.ok());
+    EXPECT_TRUE(result.second_eof);
+
+    auto strict = read_once("empty_projection_strict.jsonl", "not-json\n", 
json_scan_params(), {},
+                            {}, false, true);
+    EXPECT_FALSE(strict.status.ok());
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionPreservesEmptyInput) {
+    for (const std::string content : {"", "\n", "[]\n"}) {
+        auto result = read_once("empty_projection_empty.jsonl", content,
+                                json_scan_params(true, true), {}, {}, false, 
true);
+        ASSERT_TRUE(result.status.ok()) << result.status;
+        EXPECT_EQ(result.rows, 0);
+        EXPECT_TRUE(result.eof);
+    }
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionHonorsBatchSize) {
+    const auto path = write_json_file("empty_projection_batches.jsonl",
+                                      "{\"id\":1}\n{\"id\":2}\n{\"id\":3}\n"
+                                      "{\"id\":4}\n{\"id\":5}\n");
+    auto properties = std::make_shared<io::FileSystemProperties>();
+    properties->system_type = TFileType::FILE_LOCAL;
+    auto desc = file_description(path.string());
+    auto params = json_scan_params();
+    RuntimeProfile profile("json_empty_projection_batches");
+    MockRuntimeState state;
+    state._batch_size = 2;
+    JsonReader reader(properties, desc, std::make_shared<io::IOContext>(), 
&profile, &params,
+                      file_range(path), {});
+    ASSERT_TRUE(reader.init(&state).ok());
+    ASSERT_TRUE(reader.open(std::make_shared<FileScanRequest>()).ok());
+    for (const size_t expected_rows : {2, 2, 1, 0}) {
+        Block block;
+        size_t rows = 0;
+        bool eof = false;
+        ASSERT_TRUE(reader.get_block(&block, &rows, &eof).ok());
+        EXPECT_EQ(block.columns(), 0);
+        EXPECT_EQ(rows, expected_rows);
+        EXPECT_EQ(eof, expected_rows == 0);
+    }
+}
+
+TEST(JsonReaderTest, EmptyFileProjectionMaterializesPartitionRows) {
+    const auto path =
+            write_json_file("partition_only.jsonl", 
"{\"id\":1}\n{\"id\":2}\n{\"id\":3}\n");
+    auto params = json_scan_params();
+    const std::vector<SlotDescriptor*> file_slots;
+    ColumnDefinition partition;
+    partition.identifier = Field::create_field<TYPE_STRING>("dt");
+    partition.local_id = 0;
+    partition.name = "dt";
+    partition.type = std::make_shared<DataTypeString>();
+    partition.is_partition_key = true;
+    RuntimeProfile profile("json_partition_only");
+    MockRuntimeState state;
+    state._batch_size = 2;
+    TableReader reader;
+    ASSERT_TRUE(reader.init({.projected_columns = {partition},
+                             .conjuncts = {},
+                             .format = FileFormat::JSON,
+                             .scan_params = &params,
+                             .io_ctx = std::make_shared<io::IOContext>(),
+                             .runtime_state = &state,
+                             .scanner_profile = &profile,
+                             .file_slot_descs = &file_slots})
+                        .ok());
+    SplitReadOptions split;
+    split.current_range = file_range(path);
+    split.current_split_format = FileFormat::JSON;
+    split.partition_values.emplace("dt", 
Field::create_field<TYPE_STRING>("p1"));
+    ASSERT_TRUE(reader.prepare_split(split).ok());
+    bool eos = false;
+    for (const size_t expected_rows : {2, 1, 0}) {
+        Block block({{partition.type->create_column(), partition.type, 
partition.name}});
+        ASSERT_TRUE(reader.get_block(&block, &eos).ok());
+        ASSERT_EQ(block.rows(), expected_rows);
+        EXPECT_EQ(eos, expected_rows == 0);
+        const auto column = 
block.get_by_position(0).column->convert_to_full_column_if_const();
+        for (size_t row = 0; row < expected_rows; ++row) {
+            EXPECT_EQ(column->get_data_at(row).to_string(), "p1");
+        }
+    }
+    EXPECT_TRUE(reader.close().ok());
+}
+
 } // namespace doris::format::json
diff --git a/be/test/format_v2/orc/orc_reader_test.cpp 
b/be/test/format_v2/orc/orc_reader_test.cpp
index 1817793936e..28564cf88dd 100644
--- a/be/test/format_v2/orc/orc_reader_test.cpp
+++ b/be/test/format_v2/orc/orc_reader_test.cpp
@@ -6313,6 +6313,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/be/test/format_v2/wal/wal_reader_test.cpp 
b/be/test/format_v2/wal/wal_reader_test.cpp
index 77c14afb12f..65e37bd5f89 100644
--- a/be/test/format_v2/wal/wal_reader_test.cpp
+++ b/be/test/format_v2/wal/wal_reader_test.cpp
@@ -22,6 +22,8 @@
 #include <chrono>
 #include <filesystem>
 #include <memory>
+#include <tuple>
+#include <vector>
 
 #include "agent/be_exec_version_manager.h"
 #include "core/block/block.h"
@@ -60,8 +62,126 @@ PBlock serialize_block(const Block& block) {
     return pblock;
 }
 
+void expect_wal_column(const IColumn& column, const std::string& name, size_t 
expected_rows,
+                       size_t offset) {
+    ASSERT_EQ(column.size(), expected_rows);
+    for (size_t row = 0; row < expected_rows; ++row) {
+        EXPECT_EQ(column.get_data_at(row).to_string(), name + 
std::to_string(offset + row));
+    }
+}
+
+void expect_wal_batch(WalReader* reader, Block* output, size_t 
expected_columns,
+                      size_t expected_rows, size_t offset) {
+    output->clear_column_data();
+    size_t rows = 0;
+    bool eof = true;
+    ASSERT_TRUE(reader->get_block(output, &rows, &eof).ok());
+    // Projection can remove every output column while the WAL block still 
contributes rows.
+    EXPECT_EQ(rows, expected_rows);
+    EXPECT_FALSE(eof);
+    ASSERT_EQ(output->columns(), expected_columns);
+    for (const auto& column : output->get_columns_with_type_and_name()) {
+        ASSERT_NO_FATAL_FAILURE(
+                expect_wal_column(*column.column, column.name, expected_rows, 
offset));
+    }
+}
+
+void initialize_wal_reader(WalReader* reader, const std::string& path, bool 
discover_schema) {
+    reader->_wal_reader = std::make_shared<WalFileReader>(path);
+    ASSERT_TRUE(reader->_wal_reader->init().ok());
+    std::string encoded_ids;
+    ASSERT_TRUE(reader->_wal_reader->read_header(reader->_version, 
encoded_ids).ok());
+    ASSERT_TRUE(parse_wal_column_ids(encoded_ids, &reader->_column_ids).ok());
+    if (discover_schema) {
+        std::vector<ColumnDefinition> schema;
+        ASSERT_TRUE(reader->get_schema(&schema).ok());
+        ASSERT_EQ(schema.size(), 2);
+    }
+}
+
+void expect_wal_eof(WalReader* reader, Block* output) {
+    output->clear_column_data();
+    size_t rows = 99;
+    bool eof = false;
+    ASSERT_TRUE(reader->get_block(output, &rows, &eof).ok());
+    EXPECT_EQ(rows, 0);
+    EXPECT_TRUE(eof);
+    EXPECT_EQ(output->rows(), 0);
+}
+
+class WalReaderV2ProjectionTest
+        : public testing::TestWithParam<std::tuple<std::vector<int>, bool>> {
+protected:
+    void SetUp() override {
+        _wal_path = temporary_wal_path();
+        const auto type = std::make_shared<DataTypeString>();
+        WalWriter writer(_wal_path);
+        ASSERT_TRUE(writer.init(io::global_local_filesystem()).ok());
+        ASSERT_TRUE(writer.append_header("17,4").ok());
+        size_t offset = 0;
+        for (const size_t batch_rows : {3, 2}) {
+            Block source;
+            for (const auto& name : {"left_value", "right_value"}) {
+                auto column = type->create_column();
+                for (size_t row = 0; row < batch_rows; ++row) {
+                    const auto value = std::string(name) + 
std::to_string(offset + row);
+                    column->insert_data(value.data(), value.size());
+                }
+                source.insert({std::move(column), type, name});
+            }
+            auto pblock = serialize_block(source);
+            ASSERT_TRUE(writer.append_blocks({&pblock}).ok());
+            offset += batch_rows;
+        }
+        ASSERT_TRUE(writer.finalize().ok());
+    }
+
+    void TearDown() override {
+        std::filesystem::remove_all(
+                
std::filesystem::path(_wal_path).parent_path().parent_path().parent_path());
+    }
+
+    std::string _wal_path;
+};
+
 } // namespace
 
+TEST_P(WalReaderV2ProjectionTest, PreservesRowsAcrossBlocksAndEof) {
+    const auto& [projected_columns, discover_schema] = GetParam();
+    std::shared_ptr<io::FileSystemProperties> properties;
+    std::unique_ptr<io::FileDescription> description;
+    WalReader reader(properties, description, nullptr, nullptr, {});
+    ASSERT_NO_FATAL_FAILURE(initialize_wal_reader(&reader, _wal_path, 
discover_schema));
+
+    auto request = std::make_shared<FileScanRequest>();
+    Block output;
+    const auto type = std::make_shared<DataTypeString>();
+    const std::vector<std::string> names {"left_value", "right_value"};
+    for (size_t position = 0; position < projected_columns.size(); ++position) 
{
+        const auto source_position = projected_columns[position];
+        request->local_positions.emplace(LocalColumnId(source_position), 
LocalIndex(position));
+        output.insert({type->create_column(), type, names[source_position]});
+    }
+    ASSERT_TRUE(reader.open(std::move(request)).ok());
+
+    size_t offset = 0;
+    for (const size_t expected_rows : {3, 2}) {
+        ASSERT_NO_FATAL_FAILURE(expect_wal_batch(&reader, &output, 
projected_columns.size(),
+                                                 expected_rows, offset));
+        offset += expected_rows;
+    }
+    for (int attempt = 0; attempt < 2; ++attempt) {
+        ASSERT_NO_FATAL_FAILURE(expect_wal_eof(&reader, &output));
+    }
+    ASSERT_TRUE(reader.close().ok());
+}
+
+INSTANTIATE_TEST_SUITE_P(WriterBacked, WalReaderV2ProjectionTest,
+                         testing::Combine(testing::Values(std::vector<int> {}, 
std::vector<int> {0},
+                                                          std::vector<int> {1},
+                                                          std::vector<int> {1, 
0}),
+                                          testing::Bool()));
+
 TEST(WalReaderV2Test, ParseColumnIdsPreservesHeaderOrder) {
     std::vector<int32_t> column_ids;
     ASSERT_TRUE(parse_wal_column_ids("17,4,99", &column_ids).ok());
diff --git a/contrib/apache-orc b/contrib/apache-orc
index be0f1b73a7a..5962bb071d6 160000
--- a/contrib/apache-orc
+++ b/contrib/apache-orc
@@ -1 +1 @@
-Subproject commit be0f1b73a7aeb78824a03e0dcb692c50a176d513
+Subproject commit 5962bb071d6345cc29c5353d64d85a203525e4d9
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java 
b/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
index 7f9f773133f..f7f6909e4a1 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/service/FrontendServiceImpl.java
@@ -345,7 +345,6 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -363,6 +362,7 @@ import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.ObjLongConsumer;
 import java.util.stream.Collectors;
 
 // Frontend service used to serve all request for this frontend through
@@ -496,7 +496,7 @@ public class FrontendServiceImpl implements 
FrontendService.Iface {
         List<Long> dbIds = Lists.newArrayList();
         List<Long> catalogIds = Lists.newArrayList();
 
-        PatternMatcher matcher = null;
+        PatternMatcher matcher;
         if (params.isSetPattern()) {
             try {
                 matcher = 
PatternMatcher.createMysqlPattern(params.getPattern(),
@@ -504,6 +504,8 @@ public class FrontendServiceImpl implements 
FrontendService.Iface {
             } catch (PatternMatcherException e) {
                 throw new TException("Pattern is in bad format: " + 
params.getPattern());
             }
+        } else {
+            matcher = null;
         }
 
         Env env = Env.getCurrentEnv();
@@ -517,55 +519,77 @@ public class FrontendServiceImpl implements 
FrontendService.Iface {
                             catalog -> new TException("Unknown catalog " + 
catalog)));
         }
 
+        boolean includeDbIds = params.isIncludeDbIds();
         for (CatalogIf catalog : catalogIfs) {
-            Collection<DatabaseIf> dbs = new HashSet<DatabaseIf>();
+            Collection<DatabaseIf> dbs = Collections.emptyList();
+            List<String> names = Collections.emptyList();
             try {
-                dbs = catalog.getAllDbs();
+                if (includeDbIds) {
+                    dbs = catalog.getAllDbs();
+                } else {
+                    // SCHEMATA only needs names; loading every database 
churns the object cache on large catalogs.
+                    names = catalog.getDbNames();
+                }
             } catch (Exception e) {
                 LOG.warn("failed to get database names for catalog {}", 
catalog.getName(), e);
                 // Some external catalog may fail to get databases due to 
wrong connection info.
             }
+            int dbCount = includeDbIds ? dbs.size() : names.size();
             if (LOG.isDebugEnabled()) {
-                LOG.debug("get db size: {}, in catalog: {}", dbs.size(), 
catalog.getName());
+                LOG.debug("get db size: {}, in catalog: {}", dbCount, 
catalog.getName());
             }
-            if (dbs.isEmpty() && params.isSetGetNullCatalog() && 
params.get_null_catalog) {
+            if (dbCount == 0 && params.isSetGetNullCatalog() && 
params.get_null_catalog) {
                 catalogNames.add(catalog.getName());
                 dbNames.add("NULL");
                 catalogIds.add(catalog.getId());
-                dbIds.add(-1L);
+                if (includeDbIds) {
+                    dbIds.add(-1L);
+                }
                 continue;
             }
-            if (dbs.isEmpty()) {
+            if (dbCount == 0) {
                 continue;
             }
-            UserIdentity currentUser = null;
+            UserIdentity currentUser;
             if (params.isSetCurrentUserIdent()) {
                 currentUser = 
UserIdentity.fromThrift(params.current_user_ident);
             } else {
                 currentUser = 
UserIdentity.createAnalyzedUserIdentWithIp(params.user, params.user_ip);
             }
-            for (DatabaseIf db : dbs) {
-                String dbName = db.getFullName();
+            ObjLongConsumer<String> addDatabase = (dbName, dbId) -> {
                 if (!env.getAccessManager()
                         .checkDbPriv(currentUser, catalog.getName(), dbName, 
PrivPredicate.SHOW)) {
-                    continue;
+                    return;
                 }
 
                 if (matcher != null && 
!matcher.match(getMysqlTableSchema(catalog.getName(), dbName))) {
-                    continue;
+                    return;
                 }
 
                 catalogNames.add(catalog.getName());
                 dbNames.add(getMysqlTableSchema(catalog.getName(), dbName));
                 catalogIds.add(catalog.getId());
-                dbIds.add(db.getId());
+                if (includeDbIds) {
+                    dbIds.add(dbId);
+                }
+            };
+            if (includeDbIds) {
+                for (DatabaseIf db : dbs) {
+                    addDatabase.accept(db.getFullName(), db.getId());
+                }
+            } else {
+                for (String dbName : names) {
+                    addDatabase.accept(dbName, -1L);
+                }
             }
         }
 
         result.setDbs(dbNames);
         result.setCatalogs(catalogNames);
         result.setCatalogIds(catalogIds);
-        result.setDbIds(dbIds);
+        if (includeDbIds) {
+            result.setDbIds(dbIds);
+        }
         return result;
     }
 
diff --git 
a/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceGetDbNamesTest.java
 
b/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceGetDbNamesTest.java
new file mode 100644
index 00000000000..236569257fd
--- /dev/null
+++ 
b/fe/fe-core/src/test/java/org/apache/doris/service/FrontendServiceGetDbNamesTest.java
@@ -0,0 +1,186 @@
+// 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.service;
+
+import org.apache.doris.analysis.UserIdentity;
+import org.apache.doris.catalog.DatabaseIf;
+import org.apache.doris.catalog.Env;
+import org.apache.doris.catalog.InfoSchemaDb;
+import org.apache.doris.datasource.CatalogIf;
+import org.apache.doris.datasource.CatalogMgr;
+import org.apache.doris.mysql.privilege.AccessControllerManager;
+import org.apache.doris.mysql.privilege.PrivPredicate;
+import org.apache.doris.qe.GlobalVariable;
+import org.apache.doris.thrift.TGetDbsParams;
+import org.apache.doris.thrift.TGetDbsResult;
+
+import org.apache.thrift.TDeserializer;
+import org.apache.thrift.TSerializer;
+import org.apache.thrift.protocol.TBinaryProtocol;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+public class FrontendServiceGetDbNamesTest {
+    private final Env env = Mockito.mock(Env.class);
+    private final CatalogMgr catalogMgr = Mockito.mock(CatalogMgr.class);
+    private final AccessControllerManager accessManager = 
Mockito.mock(AccessControllerManager.class);
+    private final CatalogIf catalog = Mockito.mock(CatalogIf.class);
+    private final FrontendServiceImpl service = new 
FrontendServiceImpl(Mockito.mock(ExecuteEnv.class));
+    private MockedStatic<Env> envMock;
+
+    @BeforeEach
+    public void setUp() {
+        envMock = Mockito.mockStatic(Env.class);
+        envMock.when(Env::getCurrentEnv).thenReturn(env);
+        Mockito.when(env.getCatalogMgr()).thenReturn(catalogMgr);
+        Mockito.when(env.getAccessManager()).thenReturn(accessManager);
+        
Mockito.when(catalogMgr.listCatalogs()).thenReturn(Collections.singletonList(catalog));
+        Mockito.when(catalog.getName()).thenReturn("example_catalog");
+        Mockito.when(catalog.getId()).thenReturn(42L);
+        
Mockito.when(accessManager.checkDbPriv(Mockito.any(UserIdentity.class), 
Mockito.anyString(),
+                Mockito.anyString(), 
Mockito.eq(PrivPredicate.SHOW))).thenReturn(true);
+    }
+
+    @AfterEach
+    public void tearDown() {
+        envMock.close();
+    }
+
+    @Test
+    public void testNamesOnlyDoesNotMaterializeDatabases() throws Exception {
+        List<String> names = new ArrayList<>();
+        for (int i = 0; i < 100_000; i++) {
+            names.add("namespace_" + i);
+        }
+        names.add(InfoSchemaDb.DATABASE_NAME);
+        names.add("mysql");
+        Mockito.when(catalog.getDbNames()).thenReturn(names);
+        Mockito.when(catalog.getAllDbs()).thenThrow(new 
AssertionError("Database objects must not be loaded"));
+
+        TGetDbsParams params = params();
+        params.setIncludeDbIds(false);
+        TGetDbsResult result = service.getDbNames(params);
+
+        Assertions.assertEquals(names.size(), result.getDbsSize());
+        Assertions.assertEquals(names.size(), result.getCatalogsSize());
+        Assertions.assertEquals(names.size(), result.getCatalogIdsSize());
+        
Assertions.assertTrue(result.getCatalogs().stream().allMatch("example_catalog"::equals));
+        Assertions.assertTrue(result.getCatalogIds().stream().allMatch(id -> 
id == 42L));
+        Assertions.assertFalse(result.isSetDbIds());
+        Mockito.verify(catalog, Mockito.times(1)).getDbNames();
+        Mockito.verify(catalog, Mockito.never()).getAllDbs();
+    }
+
+    @Test
+    public void testNamesOnlyPreservesPrivilegesPatternAndDisplayNames() 
throws Exception {
+        
Mockito.when(catalog.getDbNames()).thenReturn(Arrays.asList("visible_a", 
"visible_hidden", "other"));
+        
Mockito.when(accessManager.checkDbPriv(Mockito.any(UserIdentity.class), 
Mockito.eq("example_catalog"),
+                Mockito.eq("visible_hidden"), 
Mockito.eq(PrivPredicate.SHOW))).thenReturn(false);
+
+        boolean original = GlobalVariable.showFullDbNameInInfoSchemaDb;
+        try {
+            for (boolean showFullName : new boolean[] {false, true}) {
+                GlobalVariable.showFullDbNameInInfoSchemaDb = showFullName;
+                TGetDbsParams params = params();
+                params.setIncludeDbIds(false);
+                params.setPattern(showFullName ? "example_catalog.visible%" : 
"visible%");
+                TGetDbsResult result = service.getDbNames(params);
+                Assertions.assertEquals(Collections.singletonList(
+                        showFullName ? "example_catalog.visible_a" : 
"visible_a"), result.getDbs());
+                Assertions.assertFalse(result.isSetDbIds());
+            }
+        } finally {
+            GlobalVariable.showFullDbNameInInfoSchemaDb = original;
+        }
+    }
+
+    @Test
+    public void testExistingCallersKeepDatabaseIds() throws Exception {
+        DatabaseIf database = Mockito.mock(DatabaseIf.class);
+        Mockito.when(database.getFullName()).thenReturn("visible_a");
+        Mockito.when(database.getId()).thenReturn(123L);
+        
Mockito.when(catalog.getAllDbs()).thenReturn(Collections.singletonList(database));
+
+        for (boolean explicitlySet : new boolean[] {false, true}) {
+            TGetDbsParams params = params();
+            if (explicitlySet) {
+                params.setIncludeDbIds(true);
+            }
+            TGetDbsResult result = service.getDbNames(params);
+            Assertions.assertEquals(Collections.singletonList(123L), 
result.getDbIds());
+            Assertions.assertEquals(Collections.singletonList(42L), 
result.getCatalogIds());
+        }
+        Mockito.verify(catalog, Mockito.never()).getDbNames();
+    }
+
+    @Test
+    public void testAbsentIncludeDbIdsOnWireKeepsLegacyDefault() throws 
Exception {
+        TGetDbsParams legacy = params();
+        legacy.unsetIncludeDbIds();
+        TSerializer serializer = new TSerializer(new 
TBinaryProtocol.Factory());
+        TDeserializer deserializer = new TDeserializer(new 
TBinaryProtocol.Factory());
+        TGetDbsParams decoded = new TGetDbsParams();
+        deserializer.deserialize(decoded, serializer.serialize(legacy));
+        Assertions.assertTrue(decoded.isIncludeDbIds());
+
+        legacy.setIncludeDbIds(false);
+        deserializer.deserialize(decoded, serializer.serialize(legacy));
+        Assertions.assertFalse(decoded.isIncludeDbIds());
+    }
+
+    @Test
+    public void testNamesOnlyPreservesEmptyCatalogPlaceholder() throws 
Exception {
+        Mockito.when(catalog.getDbNames()).thenReturn(Collections.emptyList());
+        TGetDbsParams params = params();
+        params.setIncludeDbIds(false);
+        params.setGetNullCatalog(true);
+
+        TGetDbsResult result = service.getDbNames(params);
+        Assertions.assertEquals(Collections.singletonList("NULL"), 
result.getDbs());
+        Assertions.assertEquals(Collections.singletonList("example_catalog"), 
result.getCatalogs());
+        Assertions.assertEquals(Collections.singletonList(42L), 
result.getCatalogIds());
+        Assertions.assertFalse(result.isSetDbIds());
+    }
+
+    @Test
+    public void testNamesOnlySkipsUnavailableCatalog() throws Exception {
+        Mockito.when(catalog.getDbNames()).thenThrow(new 
IllegalStateException("Metadata service unavailable"));
+        TGetDbsParams params = params();
+        params.setIncludeDbIds(false);
+
+        TGetDbsResult result = service.getDbNames(params);
+        Assertions.assertTrue(result.getDbs().isEmpty());
+        Assertions.assertTrue(result.getCatalogs().isEmpty());
+        Assertions.assertFalse(result.isSetDbIds());
+    }
+
+    private TGetDbsParams params() {
+        TGetDbsParams params = new TGetDbsParams();
+        params.setCurrentUserIdent(UserIdentity.ROOT.toThrift());
+        return params;
+    }
+}
diff --git a/gensrc/thrift/FrontendService.thrift 
b/gensrc/thrift/FrontendService.thrift
index 4e753eb859b..d1624f6ac2e 100644
--- a/gensrc/thrift/FrontendService.thrift
+++ b/gensrc/thrift/FrontendService.thrift
@@ -101,6 +101,8 @@ struct TGetDbsParams {
   // Reserved for downstream field `current_roles` to keep thrift field ids
   // wire-compatible across maintained branches. Do not reuse this id.
   7: optional set<string> reserved_field_7
+  // Name-only scanners can avoid materializing database objects and omit 
db_ids in the response.
+  8: optional bool include_db_ids = true
 }
 
 // getDbNames returns a list of database names , database ids and catalog 
names ,catalog ids
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/data/insert_p0/group_commit/test_group_commit_wal_row_count.out
 
b/regression-test/data/insert_p0/group_commit/test_group_commit_wal_row_count.out
new file mode 100644
index 00000000000..0ae4de99333
--- /dev/null
+++ 
b/regression-test/data/insert_p0/group_commit/test_group_commit_wal_row_count.out
@@ -0,0 +1,17 @@
+-- This file is automatically generated. You should know what you did if you 
want to edit this
+-- !before_replay --
+0
+
+-- !counts --
+5      4       15      120
+
+-- !values --
+1      10
+2      20
+3      \N
+4      40
+5      50
+
+-- !filtered_count --
+3
+
diff --git 
a/regression-test/suites/external_table_p0/hive/test_hive_json_partition_only.groovy
 
b/regression-test/suites/external_table_p0/hive/test_hive_json_partition_only.groovy
new file mode 100644
index 00000000000..69ae572d563
--- /dev/null
+++ 
b/regression-test/suites/external_table_p0/hive/test_hive_json_partition_only.groovy
@@ -0,0 +1,85 @@
+// 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_hive_json_partition_only", "p0,external,hive") {
+    String enabled = context.config.otherConfigs.get("enableHiveTest")
+    if (enabled == null || !enabled.equalsIgnoreCase("true")) {
+        logger.info("disable Hive test")
+        return
+    }
+
+    String externalEnvIp = context.config.otherConfigs.get("externalEnvIp")
+    String hmsPort = context.config.otherConfigs.get("hive3HmsPort")
+    String catalog = "test_hive_json_partition_only"
+    def previousSettings = sql """SELECT 
@@enable_count_push_down_for_external_table,
+                                       @@enable_sql_cache"""
+    sql """DROP CATALOG IF EXISTS ${catalog}"""
+    try {
+        sql """CREATE CATALOG ${catalog} PROPERTIES (
+            "type" = "hms",
+            "hive.metastore.uris" = "thrift://${externalEnvIp}:${hmsPort}"
+        )"""
+        sql """USE `${catalog}`.`default`"""
+        sql "SET enable_sql_cache=false"
+
+        // Reading a real JSON field provides the row-count oracle for the 
same partition files.
+        def baseline = sql "SELECT id, dt FROM json_all_complex_types"
+        assertTrue(!baseline.isEmpty())
+        def partitions = baseline.groupBy { row -> row[1] }
+
+        for (boolean pushdown : [false, true]) {
+            sql "SET enable_count_push_down_for_external_table=${pushdown}"
+            partitions.each { partition, rows ->
+                assertNotNull(partition)
+                String value = partition.toString().replace("'", "''")
+                String predicate = "dt = '${value}'"
+                String countQuery = "SELECT COUNT(*) FROM 
json_all_complex_types WHERE ${predicate}"
+                explain {
+                    sql countQuery
+                    contains "pushdown agg=NONE"
+                }
+                def count = sql countQuery
+                assertEquals(rows.size(), (count[0][0] as Number).intValue())
+
+                // Partition values come from the split, so neither query 
needs a JSON file column.
+                def projected = sql "SELECT dt FROM json_all_complex_types 
WHERE ${predicate}"
+                assertEquals(rows.size(), projected.size())
+                assertTrue(projected.every { row -> row[0] == partition })
+                def partitionCount = sql "SELECT COUNT(dt) FROM 
json_all_complex_types WHERE ${predicate}"
+                assertEquals(rows.size(), (partitionCount[0][0] as 
Number).intValue())
+
+                def filtered = sql """SELECT COUNT(*) FROM 
json_all_complex_types
+                                      WHERE ${predicate} AND id IS NOT NULL"""
+                assertEquals(rows.count { row -> row[0] != null },
+                             (filtered[0][0] as Number).intValue())
+            }
+
+            def grouped = sql "SELECT dt, COUNT(*) FROM json_all_complex_types 
GROUP BY dt"
+            assertEquals(partitions.size(), grouped.size())
+            grouped.each { row ->
+                assertEquals(partitions[row[0]].size(), (row[1] as 
Number).intValue())
+            }
+            def empty = sql """SELECT COUNT(*) FROM json_all_complex_types
+                               WHERE dt = '__missing_json_partition__'"""
+            assertEquals(0, (empty[0][0] as Number).intValue())
+        }
+    } finally {
+        sql "SET 
enable_count_push_down_for_external_table=${previousSettings[0][0]}"
+        sql "SET enable_sql_cache=${previousSettings[0][1]}"
+        sql """DROP CATALOG IF EXISTS ${catalog}"""
+    }
+}
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)
+                }
+            }
+        }
+    }
+}
diff --git 
a/regression-test/suites/insert_p0/group_commit/test_group_commit_wal_row_count.groovy
 
b/regression-test/suites/insert_p0/group_commit/test_group_commit_wal_row_count.groovy
new file mode 100644
index 00000000000..ed1a449724a
--- /dev/null
+++ 
b/regression-test/suites/insert_p0/group_commit/test_group_commit_wal_row_count.groovy
@@ -0,0 +1,85 @@
+// 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 org.awaitility.Awaitility
+import static java.util.concurrent.TimeUnit.SECONDS
+
+suite("test_group_commit_wal_row_count", "nonConcurrent") {
+    sql "DROP TABLE IF EXISTS test_group_commit_wal_row_count"
+    sql """
+        CREATE TABLE test_group_commit_wal_row_count (
+            k INT NOT NULL,
+            v INT NULL
+        ) ENGINE=OLAP
+        DUPLICATE KEY(k)
+        DISTRIBUTED BY HASH(k) BUCKETS 1
+        PROPERTIES (
+            "replication_num" = "1",
+            "group_commit_interval_ms" = "2000"
+        )
+    """
+
+    String loadError = "LoadBlockQueue._finish_group_commit_load.load_error"
+    String replayStop = "WalTable.replay_wals.stop"
+    try {
+        GetDebugPoint().enableDebugPointForAllBEs(loadError)
+        GetDebugPoint().enableDebugPointForAllBEs(replayStop)
+
+        // Abort each original transaction before the next load so both WALs 
must be replayed.
+        for (String data : ["1,10\n2,20\n3,\\N\n", "4,40\n5,50\n"]) {
+            long txnId = 0
+            streamLoad {
+                table "test_group_commit_wal_row_count"
+                set "column_separator", ","
+                set "group_commit", "async_mode"
+                unset "label"
+                inputStream new ByteArrayInputStream(data.getBytes("UTF-8"))
+                time 10000
+                check { result, exception, startTime, endTime ->
+                    assertNull(exception)
+                    def response = parseJson(result)
+                    assertEquals("Success", response.Status)
+                    assertEquals(data.readLines().size(), 
response.NumberTotalRows as int)
+                    txnId = response.TxnId as long
+                    assertTrue(txnId > 0)
+                }
+            }
+            Awaitility.await().atMost(30, SECONDS).pollInterval(1, 
SECONDS).until {
+                def txn = sql_return_maparray "SHOW TRANSACTION WHERE ID = 
${txnId}"
+                return txn.size() == 1 && txn[0].TransactionStatus == "ABORTED"
+            }
+        }
+        qt_before_replay "SELECT COUNT(*) FROM test_group_commit_wal_row_count"
+
+        // Keep the original-load failure enabled: rows can become visible 
only through WAL replay.
+        GetDebugPoint().disableDebugPointForAllBEs(replayStop)
+        Awaitility.await().atMost(90, SECONDS).pollInterval(1, SECONDS).until {
+            def rows = sql "SELECT COUNT(*) FROM 
test_group_commit_wal_row_count"
+            return rows[0][0] == 5
+        }
+        qt_counts """SELECT COUNT(*), COUNT(v), SUM(k), SUM(v)
+                     FROM test_group_commit_wal_row_count"""
+        qt_values "SELECT k, v FROM test_group_commit_wal_row_count ORDER BY k"
+        qt_filtered_count "SELECT COUNT(*) FROM 
test_group_commit_wal_row_count WHERE v >= 20"
+    } finally {
+        try {
+            GetDebugPoint().disableDebugPointForAllBEs(replayStop)
+        } finally {
+            GetDebugPoint().disableDebugPointForAllBEs(loadError)
+        }
+    }
+}


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

Reply via email to