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

yiguolei 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 ccc81563d3d [opt](lance) reduce redundant work in LanceTableReader 
(#68335)
ccc81563d3d is described below

commit ccc81563d3d7511f551065eaf86e3a35be25f795
Author: zhangstar333 <[email protected]>
AuthorDate: Tue Sep 22 10:07:34 2026 +0800

    [opt](lance) reduce redundant work in LanceTableReader (#68335)
    
    ### What problem does this PR solve?
    Problem Summary:
    
    optimize LanceTableReader by lazily initializing schemas and scanner
    metrics, reusing the record batch converter, caching Arrow normalization
    plans, and avoiding unnecessary full-column reads. Also improve code
    structure by extracting schema import and record batch conversion logic.
    
    ### Release note
    
    None
    
    ### Check List (For Author)
    
    - Test <!-- At least one of them must be included. -->
        - [ ] Regression test
        - [ ] Unit Test
        - [ ] Manual test (add detailed scripts or steps below)
        - [ ] No need to test or manual test. Explain why:
    - [x] This is a refactor/code format and no logic has been changed.
            - [ ] Previous test can cover this change.
            - [ ] No code files have been changed.
            - [ ] Other reason <!-- Add your reason?  -->
    
    - Behavior changed:
        - [ ] No.
        - [ ] Yes. <!-- Explain the behavior change -->
    
    - Does this need documentation?
        - [ ] No.
    - [ ] Yes. <!-- Add document PR link here. eg:
    https://github.com/apache/doris-website/pull/1214 -->
    
    ### Check List (For Reviewer who merge this PR)
    
    - [ ] Confirm the release note
    - [ ] Confirm test cases
    - [ ] Confirm document
    - [ ] Add branch pick label <!-- Add branch pick label that this PR
    should merge into -->
---
 be/src/format_v2/lance/lance_reader_helper.cpp     | 251 +++++--------
 be/src/format_v2/lance/lance_reader_helper.h       |  31 +-
 .../lance/lance_record_batch_converter.cpp         | 261 ++++++++++++++
 .../format_v2/lance/lance_record_batch_converter.h |  91 +++++
 .../lance/lance_runtime_filter_helper.cpp          |   6 +
 .../format_v2/lance/lance_runtime_filter_helper.h  |   4 +
 be/src/format_v2/table/lance_reader.cpp            | 391 ++++++---------------
 be/src/format_v2/table/lance_reader.h              |  19 +-
 be/test/format_v2/lance/lance_nested_null_test.cpp |   5 +-
 be/test/format_v2/table/lance_reader_test.cpp      |  20 +-
 10 files changed, 606 insertions(+), 473 deletions(-)

diff --git a/be/src/format_v2/lance/lance_reader_helper.cpp 
b/be/src/format_v2/lance/lance_reader_helper.cpp
index 8599b7c3850..2b14381437e 100644
--- a/be/src/format_v2/lance/lance_reader_helper.cpp
+++ b/be/src/format_v2/lance/lance_reader_helper.cpp
@@ -19,6 +19,7 @@
 
 #include <arrow/array.h>
 #include <arrow/builder.h>
+#include <arrow/c/bridge.h>
 #include <arrow/extension_type.h>
 #include <arrow/type.h>
 #include <arrow/util/key_value_metadata.h>
@@ -272,26 +273,6 @@ Status arrow_field_to_doris_type(const 
std::shared_ptr<arrow::Field>& field,
     }
 }
 
-// Determine whether a field subtree contains values that require Lance 
normalization.
-Status field_requires_lance_normalization(const std::shared_ptr<arrow::Field>& 
field,
-                                          bool* requires_normalization) {
-    DORIS_CHECK(field != nullptr);
-    DORIS_CHECK(requires_normalization != nullptr);
-
-    LanceExtensionKind extension_kind;
-    std::shared_ptr<arrow::DataType> storage_type;
-    RETURN_IF_ERROR(get_lance_extension(field, &extension_kind, 
&storage_type));
-    bool required = extension_kind == LanceExtensionKind::BFLOAT16 ||
-                    field->type()->id() == arrow::Type::EXTENSION;
-    for (const auto& child : storage_type->fields()) {
-        bool child_required = false;
-        RETURN_IF_ERROR(field_requires_lance_normalization(child, 
&child_required));
-        required |= child_required;
-    }
-    *requires_normalization = required;
-    return Status::OK();
-}
-
 // Widen little-endian Lance BFloat16 values to Arrow Float32 without 
precision loss.
 Status convert_bfloat16_array(const std::shared_ptr<arrow::Array>& array,
                               arrow::MemoryPool* memory_pool,
@@ -373,89 +354,6 @@ Status set_lance_nested_type(std::string_view field_name,
     return Status::OK();
 }
 
-// Check whether an Arrow type tree contains a registered extension wrapper.
-bool type_contains_registered_extension(const 
std::shared_ptr<arrow::DataType>& type) {
-    if (type->id() == arrow::Type::EXTENSION) {
-        return true;
-    }
-    for (const auto& field : type->fields()) {
-        if (type_contains_registered_extension(field->type())) {
-            return true;
-        }
-    }
-    return false;
-}
-
-// Remove registered ExtensionArray wrappers only along extension-bearing 
branches.
-Status unwrap_lance_extension_arrays(const std::shared_ptr<arrow::DataType>& 
expected_type,
-                                     const std::shared_ptr<arrow::Array>& 
array,
-                                     std::shared_ptr<arrow::Array>* unwrapped) 
{
-    DORIS_CHECK(expected_type != nullptr);
-    DORIS_CHECK(array != nullptr);
-    DORIS_CHECK(unwrapped != nullptr);
-
-    auto storage_array = array;
-    auto expected_storage_type = expected_type;
-    if (expected_type->id() == arrow::Type::EXTENSION) {
-        const auto extension_type = 
std::dynamic_pointer_cast<arrow::ExtensionType>(expected_type);
-        if (extension_type == nullptr) {
-            return Status::InvalidArgument("invalid expected Arrow extension 
type {}",
-                                           expected_type->ToString());
-        }
-        expected_storage_type = extension_type->storage_type();
-    }
-    if (array->type_id() == arrow::Type::EXTENSION) {
-        const auto extension_array = 
std::dynamic_pointer_cast<arrow::ExtensionArray>(array);
-        if (extension_array == nullptr) {
-            return Status::InvalidArgument("invalid Arrow extension array: {}",
-                                           array->type()->ToString());
-        }
-        storage_array = extension_array->storage();
-    }
-
-    const auto& child_data = storage_array->data()->child_data;
-    const auto& child_fields = expected_storage_type->fields();
-    if (child_data.empty()) {
-        *unwrapped = std::move(storage_array);
-        return Status::OK();
-    }
-    if (child_fields.size() != child_data.size()) {
-        return Status::InvalidArgument(
-                "Arrow array type {} has {} child fields but its data has {} 
children",
-                storage_array->type()->ToString(), child_fields.size(), 
child_data.size());
-    }
-
-    std::shared_ptr<arrow::ArrayData> unwrapped_data;
-    arrow::FieldVector unwrapped_fields;
-    for (size_t child_idx = 0; child_idx < child_data.size(); ++child_idx) {
-        if 
(!type_contains_registered_extension(child_fields[child_idx]->type())) {
-            continue;
-        }
-        auto child_array = arrow::MakeArray(child_data[child_idx]);
-        std::shared_ptr<arrow::Array> unwrapped_child;
-        
RETURN_IF_ERROR(unwrap_lance_extension_arrays(child_fields[child_idx]->type(), 
child_array,
-                                                      &unwrapped_child));
-        if (unwrapped_child.get() == child_array.get()) {
-            continue;
-        }
-        if (unwrapped_data == nullptr) {
-            unwrapped_data = storage_array->data()->Copy();
-            unwrapped_fields = storage_array->type()->fields();
-        }
-        unwrapped_data->child_data[child_idx] = unwrapped_child->data();
-        unwrapped_fields[child_idx] =
-                unwrapped_fields[child_idx]->WithType(unwrapped_child->type());
-    }
-    if (unwrapped_data == nullptr) {
-        *unwrapped = std::move(storage_array);
-        return Status::OK();
-    }
-    RETURN_IF_ERROR(
-            set_lance_nested_type("", storage_array->type(), unwrapped_fields, 
&unwrapped_data));
-    *unwrapped = arrow::MakeArray(std::move(unwrapped_data));
-    return Status::OK();
-}
-
 // Materialize the visible range into an offset-zero Arrow array for Doris 
SerDes.
 Status compact_lance_array(const std::shared_ptr<arrow::Array>& array,
                            arrow::MemoryPool* memory_pool,
@@ -575,78 +473,86 @@ Status compact_lance_array_if_needed(const 
std::shared_ptr<arrow::Array>& array,
 
 } // namespace
 
-// Normalize Lance extensions and materialize sliced arrays for Doris Arrow 
SerDes.
-Status normalize_lance_arrow_array(const std::shared_ptr<arrow::Field>& field,
-                                   const std::shared_ptr<arrow::Array>& array,
-                                   arrow::MemoryPool* memory_pool,
-                                   std::shared_ptr<arrow::Array>* normalized) {
+Status LanceArrowArrayNormalizer::create(const std::shared_ptr<arrow::Field>& 
field,
+                                         LanceArrowArrayNormalizer* 
normalizer) {
     DORIS_CHECK(field != nullptr);
-    DORIS_CHECK(array != nullptr);
-    DORIS_CHECK(memory_pool != nullptr);
-    DORIS_CHECK(normalized != nullptr);
+    DORIS_CHECK(normalizer != nullptr);
 
     LanceExtensionKind extension_kind;
     std::shared_ptr<arrow::DataType> storage_type;
     RETURN_IF_ERROR(get_lance_extension(field, &extension_kind, 
&storage_type));
 
+    LanceArrowArrayNormalizer result;
+    result._field_name = field->name();
+    result._storage_type = std::move(storage_type);
+    result._unwrap_registered_extension = field->type()->id() == 
arrow::Type::EXTENSION;
+    result._convert_bfloat16 = extension_kind == LanceExtensionKind::BFLOAT16;
+    result._requires_special_handling =
+            result._unwrap_registered_extension || result._convert_bfloat16;
+    result._child_normalizers.reserve(result._storage_type->num_fields());
+    for (const auto& child_field : result._storage_type->fields()) {
+        LanceArrowArrayNormalizer child_normalizer;
+        RETURN_IF_ERROR(create(child_field, &child_normalizer));
+        result._requires_special_handling |= 
child_normalizer._requires_special_handling;
+        result._child_normalizers.emplace_back(std::move(child_normalizer));
+    }
+    *normalizer = std::move(result);
+    return Status::OK();
+}
+
+Status LanceArrowArrayNormalizer::normalize_for_doris(
+        const std::shared_ptr<arrow::Array>& array, arrow::MemoryPool* 
memory_pool,
+        std::shared_ptr<arrow::Array>* normalized) const {
+    DORIS_CHECK(array != nullptr);
+    DORIS_CHECK(memory_pool != nullptr);
+    DORIS_CHECK(normalized != nullptr);
+
+    // The schema binding already resolved extension metadata and nested 
special types. For common
+    // arrays, runtime work is limited to checking whether the visible slice 
needs compaction.
+    if (!_requires_special_handling) {
+        return compact_lance_array_if_needed(array, memory_pool, normalized);
+    }
+
     auto storage_array = array;
-    if (type_contains_registered_extension(field->type())) {
-        std::shared_ptr<arrow::Array> unwrapped_array;
-        RETURN_IF_ERROR(unwrap_lance_extension_arrays(field->type(), array, 
&unwrapped_array));
-        storage_array = std::move(unwrapped_array);
+    if (_unwrap_registered_extension && array->type_id() == 
arrow::Type::EXTENSION) {
+        const auto extension_array = 
std::dynamic_pointer_cast<arrow::ExtensionArray>(array);
+        if (extension_array == nullptr) {
+            return Status::InvalidArgument("invalid Arrow extension array: {}",
+                                           array->type()->ToString());
+        }
+        storage_array = extension_array->storage();
     }
-    if (storage_array->type_id() != storage_type->id()) {
+    // Extension and BFloat16 arrays are first converted to their physical 
storage representation.
+    if (storage_array->type_id() != _storage_type->id()) {
         return Status::InvalidArgument(
-                "Lance field '{}' storage type {} does not match array type 
{}", field->name(),
-                storage_type->ToString(), storage_array->type()->ToString());
+                "Lance field '{}' storage type {} does not match array type 
{}", _field_name,
+                _storage_type->ToString(), storage_array->type()->ToString());
     }
-    if (extension_kind == LanceExtensionKind::BFLOAT16) {
+    if (_convert_bfloat16) {
         return convert_bfloat16_array(storage_array, memory_pool, normalized);
     }
 
-    std::shared_ptr<arrow::Array> compacted_array;
-    RETURN_IF_ERROR(compact_lance_array_if_needed(storage_array, memory_pool, 
&compacted_array));
-    storage_array = std::move(compacted_array);
-
-    const auto& child_fields = storage_type->fields();
+    // Normalize extension children before compacting the parent. Arrow cannot 
create a builder
+    // for list/struct types whose child is still an ExtensionType (Arrow 24 
returns
+    // NotImplemented), while the rebuilt physical type can be compacted 
normally.
     const auto& child_data = storage_array->data()->child_data;
-    if (child_fields.empty()) {
-        *normalized = std::move(storage_array);
-        return Status::OK();
-    }
-    if (child_fields.size() != child_data.size()) {
+    if (_child_normalizers.size() != child_data.size()) {
         return Status::InvalidArgument(
                 "Lance field '{}' has {} child fields but its Arrow array has 
{} children",
-                field->name(), child_fields.size(), child_data.size());
-    }
-
-    bool requires_normalization = false;
-    for (const auto& child_field : child_fields) {
-        bool child_required = false;
-        RETURN_IF_ERROR(field_requires_lance_normalization(child_field, 
&child_required));
-        if (child_required) {
-            requires_normalization = true;
-            break;
-        }
-    }
-    if (!requires_normalization) {
-        *normalized = std::move(storage_array);
-        return Status::OK();
+                _field_name, _child_normalizers.size(), child_data.size());
     }
 
     arrow::FieldVector normalized_fields;
     std::shared_ptr<arrow::ArrayData> normalized_data;
-    for (size_t child_idx = 0; child_idx < child_fields.size(); ++child_idx) {
-        bool child_required = false;
-        RETURN_IF_ERROR(
-                field_requires_lance_normalization(child_fields[child_idx], 
&child_required));
-        if (!child_required) {
+    for (size_t child_idx = 0; child_idx < _child_normalizers.size(); 
++child_idx) {
+        const auto& child_normalizer = _child_normalizers[child_idx];
+        if (!child_normalizer._requires_special_handling) {
             continue;
         }
-        auto child_array = 
arrow::MakeArray(storage_array->data()->child_data[child_idx]);
+        auto child_array = arrow::MakeArray(child_data[child_idx]);
         std::shared_ptr<arrow::Array> normalized_child;
-        RETURN_IF_ERROR(normalize_lance_arrow_array(child_fields[child_idx], 
child_array,
-                                                    memory_pool, 
&normalized_child));
+        RETURN_IF_ERROR(
+                child_normalizer.normalize_for_doris(child_array, memory_pool, 
&normalized_child));
         if (normalized_child.get() == child_array.get()) {
             continue;
         }
@@ -658,15 +564,15 @@ Status normalize_lance_arrow_array(const 
std::shared_ptr<arrow::Field>& field,
         normalized_fields[child_idx] =
                 
normalized_fields[child_idx]->WithType(normalized_child->type());
     }
-    if (normalized_data == nullptr) {
-        *normalized = std::move(storage_array);
-        return Status::OK();
+    if (normalized_data != nullptr) {
+        RETURN_IF_ERROR(set_lance_nested_type(_field_name, 
storage_array->type(), normalized_fields,
+                                              &normalized_data));
+        storage_array = arrow::MakeArray(std::move(normalized_data));
     }
 
-    RETURN_IF_ERROR(set_lance_nested_type(field->name(), 
storage_array->type(), normalized_fields,
-                                          &normalized_data));
-    *normalized = arrow::MakeArray(std::move(normalized_data));
-    return Status::OK();
+    // Compact only after child types are physical, so MakeBuilder never sees 
a nested
+    // ExtensionType. This also preserves the existing zero-copy path when no 
slice is present.
+    return compact_lance_array_if_needed(storage_array, memory_pool, 
normalized);
 }
 
 #ifdef BE_TEST
@@ -675,12 +581,33 @@ Status normalize_lance_arrow_array_for_test(const 
std::shared_ptr<arrow::Field>&
                                             const 
std::shared_ptr<arrow::Array>& array,
                                             std::shared_ptr<arrow::Array>* 
normalized,
                                             arrow::MemoryPool* memory_pool) {
-    return normalize_lance_arrow_array(
-            field, array, memory_pool != nullptr ? memory_pool : 
arrow::default_memory_pool(),
-            normalized);
+    LanceArrowArrayNormalizer normalizer;
+    RETURN_IF_ERROR(LanceArrowArrayNormalizer::create(field, &normalizer));
+    return normalizer.normalize_for_doris(
+            array, memory_pool != nullptr ? memory_pool : 
arrow::default_memory_pool(), normalized);
 }
 #endif
 
+Status import_lance_dataset_schema(LanceDataset* dataset, 
std::shared_ptr<arrow::Schema>* schema) {
+    DORIS_CHECK(dataset != nullptr);
+    DORIS_CHECK(schema != nullptr);
+
+    ArrowSchema arrow_schema {};
+    if (lance_dataset_schema(dataset, &arrow_schema) != 0) {
+        return lance_error("get Lance dataset schema");
+    }
+    auto imported_schema = arrow::ImportSchema(&arrow_schema);
+    if (!imported_schema.ok()) {
+        if (arrow_schema.release != nullptr) {
+            arrow_schema.release(&arrow_schema);
+        }
+        return Status::InternalError("import Lance Arrow schema failed: {}",
+                                     imported_schema.status().message());
+    }
+    *schema = std::move(imported_schema).ValueUnsafe();
+    return Status::OK();
+}
+
 void LanceDatasetDeleter::operator()(LanceDataset* dataset) const {
     lance_dataset_close(dataset);
 }
diff --git a/be/src/format_v2/lance/lance_reader_helper.h 
b/be/src/format_v2/lance/lance_reader_helper.h
index 8a8fb5a4f59..c2f67e201be 100644
--- a/be/src/format_v2/lance/lance_reader_helper.h
+++ b/be/src/format_v2/lance/lance_reader_helper.h
@@ -34,6 +34,7 @@ struct LanceScanner;
 
 namespace arrow {
 class Array;
+class DataType;
 class Field;
 class MemoryPool;
 class Schema;
@@ -60,17 +61,37 @@ struct LanceBatchDeleter {
 
 size_t lance_vector_element_width(TVectorElementType::type type);
 
+// Import the physical Arrow schema owned by a Lance dataset. The caller owns 
the resulting
+// shared schema and may cache it for operations that need physical Arrow 
types.
+Status import_lance_dataset_schema(LanceDataset* dataset, 
std::shared_ptr<arrow::Schema>* schema);
+
 // Validate and convert the fragment and index-segment identifiers carried by 
the FE into the
 // unsigned and packed representations expected by lance-c.
 Status parse_fragment_ids(const TLanceFileDesc& lance_params, 
std::vector<uint64_t>* fragment_ids);
 Status parse_index_segment_uuids(const TLanceFileDesc& lance_params,
                                  std::vector<uint8_t>* segment_uuids, size_t* 
segment_count);
 
-// Normalize Lance extension arrays into Arrow arrays supported by Doris.
-Status normalize_lance_arrow_array(const std::shared_ptr<arrow::Field>& field,
-                                   const std::shared_ptr<arrow::Array>& array,
-                                   arrow::MemoryPool* memory_pool,
-                                   std::shared_ptr<arrow::Array>* normalized);
+// Resolves one Arrow field once when a stream schema is bound. Runtime 
conversion then reuses this
+// plan instead of rescanning extension metadata and nested fields for every 
array.
+class LanceArrowArrayNormalizer {
+public:
+    static Status create(const std::shared_ptr<arrow::Field>& field,
+                         LanceArrowArrayNormalizer* normalizer);
+
+    // Return an Arrow array whose physical layout and type can be consumed by 
Doris SerDes. The
+    // input is returned unchanged when no compaction or type adaptation is 
required.
+    Status normalize_for_doris(const std::shared_ptr<arrow::Array>& array,
+                               arrow::MemoryPool* memory_pool,
+                               std::shared_ptr<arrow::Array>* normalized) 
const;
+
+private:
+    std::string _field_name;
+    std::shared_ptr<arrow::DataType> _storage_type;
+    std::vector<LanceArrowArrayNormalizer> _child_normalizers;
+    bool _unwrap_registered_extension = false;
+    bool _convert_bfloat16 = false;
+    bool _requires_special_handling = false;
+};
 
 #ifdef BE_TEST
 // Expose Lance Arrow normalization for allocation-sensitive unit tests.
diff --git a/be/src/format_v2/lance/lance_record_batch_converter.cpp 
b/be/src/format_v2/lance/lance_record_batch_converter.cpp
new file mode 100644
index 00000000000..7f0bf848a08
--- /dev/null
+++ b/be/src/format_v2/lance/lance_record_batch_converter.cpp
@@ -0,0 +1,261 @@
+// 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 "format_v2/lance/lance_record_batch_converter.h"
+
+#include <arrow/array.h>
+#include <arrow/memory_pool.h>
+#include <arrow/record_batch.h>
+#include <arrow/type.h>
+
+#include <limits>
+
+#include "common/consts.h"
+#include "common/exception.h"
+#include "core/block/block.h"
+#include "core/column/column_nullable.h"
+#include "core/column/column_string.h"
+#include "core/data_type/data_type_nullable.h"
+#include "format_v2/lance/lance_reader_helper.h"
+#include "runtime/exec_env.h"
+#include "runtime/runtime_state.h"
+#include "storage/utils.h"
+
+namespace doris::format::lance {
+namespace {
+
+constexpr size_t IGNORED_OUTPUT_INDEX = std::numeric_limits<size_t>::max();
+
+} // namespace
+
+arrow::MemoryPool* LanceRecordBatchConverter::_query_arrow_memory_pool(
+        RuntimeState* runtime_state) {
+    DORIS_CHECK(runtime_state != nullptr);
+    if (runtime_state->exec_env() != nullptr) {
+        if (auto* memory_pool = runtime_state->exec_env()->arrow_memory_pool();
+            memory_pool != nullptr) {
+            return memory_pool;
+        }
+    }
+    return arrow::default_memory_pool();
+}
+
+Status LanceRecordBatchConverter::init(RuntimeState* runtime_state,
+                                       const std::vector<ColumnDefinition>& 
projected_columns,
+                                       SearchKind search_kind) {
+    DORIS_CHECK(runtime_state != nullptr);
+    _search_kind = search_kind;
+    _timezone = runtime_state->timezone_obj();
+    _memory_pool = _query_arrow_memory_pool(runtime_state);
+    _output_columns.clear();
+    _output_columns.reserve(projected_columns.size());
+    _output_index_by_input_name.clear();
+    _output_index_by_input_name.reserve(projected_columns.size());
+    _global_rowid_output_index.reset();
+    reset_schema();
+
+    for (const auto& column : projected_columns) {
+        if (column.type == nullptr) {
+            return Status::InvalidArgument("Lance projected column '{}' has no 
type", column.name);
+        }
+        if (column.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) {
+            if (_search_kind == SearchKind::NORMAL) {
+                return Status::NotSupported(
+                        "Lance global row id is currently supported only for 
external search");
+            }
+            if (_global_rowid_output_index.has_value()) {
+                return Status::InvalidArgument("duplicate Lance global row id 
projected column: {}",
+                                               column.name);
+            }
+            if (remove_nullable(column.type)->get_primitive_type() != 
TYPE_STRING) {
+                return Status::InvalidArgument(
+                        "Lance global row id column '{}' must have Doris 
STRING type, but was {}",
+                        column.name, column.type->get_name());
+            }
+            _global_rowid_output_index = _output_columns.size();
+            // Lance returns the native `_rowid`, while Doris exposes it 
through a generated
+            // global-rowid column. One input column cannot also satisfy a 
regular `_rowid`
+            // projection, so reject an existing binding instead of silently 
replacing it.
+            if (!_output_index_by_input_name
+                         .emplace(LANCE_ROW_ID_COLUMN, 
*_global_rowid_output_index)
+                         .second) {
+                return Status::InvalidArgument(
+                        "Lance global row id cannot be combined with projected 
column '{}'",
+                        LANCE_ROW_ID_COLUMN);
+            }
+            _output_columns.push_back({column.name, nullptr});
+            continue;
+        }
+        const auto output_index = _output_columns.size();
+        if (!_output_index_by_input_name.emplace(column.name, 
output_index).second) {
+            return Status::InvalidArgument("duplicate Lance projected column: 
{}", column.name);
+        }
+        if (_search_kind == SearchKind::VECTOR && column.name == 
LANCE_DISTANCE_COLUMN &&
+            remove_nullable(column.type)->get_primitive_type() != TYPE_FLOAT) {
+            return Status::InvalidArgument(
+                    "Lance vector search column '{}' must have Doris FLOAT 
type, but was {}",
+                    LANCE_DISTANCE_COLUMN, column.type->get_name());
+        }
+        if (_search_kind == SearchKind::FULL_TEXT && column.name == 
LANCE_SCORE_COLUMN &&
+            remove_nullable(column.type)->get_primitive_type() != TYPE_FLOAT) {
+            return Status::InvalidArgument(
+                    "Lance full-text search column '{}' must have Doris FLOAT 
type, but was {}",
+                    LANCE_SCORE_COLUMN, column.type->get_name());
+        }
+        _output_columns.push_back({column.name, column.type->get_serde()});
+    }
+    return Status::OK();
+}
+
+Status LanceRecordBatchConverter::_bind_schema(const 
std::shared_ptr<arrow::Schema>& schema) {
+    DORIS_CHECK(schema != nullptr);
+    if (_bound_schema == schema ||
+        (_bound_schema != nullptr && _bound_schema->Equals(*schema, true))) {
+        return Status::OK();
+    }
+
+    std::vector<size_t> 
arrow_column_to_output(static_cast<size_t>(schema->num_fields()),
+                                               IGNORED_OUTPUT_INDEX);
+    std::vector<LanceArrowArrayNormalizer> array_normalizer_for_arrow_column(
+            static_cast<size_t>(schema->num_fields()));
+    std::vector<bool> output_has_source_column(_output_columns.size(), false);
+    for (int arrow_index = 0; arrow_index < schema->num_fields(); 
++arrow_index) {
+        const auto& field = schema->field(arrow_index);
+        const auto output = _output_index_by_input_name.find(field->name());
+        if (output == _output_index_by_input_name.end()) {
+            const bool is_omitted_search_result =
+                    (_search_kind == SearchKind::VECTOR &&
+                     field->name() == LANCE_DISTANCE_COLUMN) ||
+                    (_search_kind == SearchKind::FULL_TEXT && field->name() == 
LANCE_SCORE_COLUMN);
+            // Lance may return this generated result even when Doris did not 
project it.
+            if (is_omitted_search_result) {
+                continue;
+            }
+            return Status::InternalError("Lance returned unknown column '{}'", 
field->name());
+        }
+        const auto output_index = output->second;
+        if (output_has_source_column[output_index]) {
+            return Status::InternalError("Lance returned duplicate column 
'{}'", field->name());
+        }
+        output_has_source_column[output_index] = true;
+        arrow_column_to_output[arrow_index] = output_index;
+        RETURN_IF_ERROR(LanceArrowArrayNormalizer::create(
+                field, 
&array_normalizer_for_arrow_column[static_cast<size_t>(arrow_index)]));
+    }
+    for (size_t output_index = 0; output_index < _output_columns.size(); 
++output_index) {
+        if (!output_has_source_column[output_index]) {
+            return Status::InternalError("Lance did not return requested 
column '{}'",
+                                         _output_columns[output_index].name);
+        }
+    }
+    _bound_schema = schema;
+    _arrow_column_to_output_index = std::move(arrow_column_to_output);
+    _array_normalizer_for_arrow_column = 
std::move(array_normalizer_for_arrow_column);
+    return Status::OK();
+}
+
+Status LanceRecordBatchConverter::convert_record_batch_to_block(
+        const std::shared_ptr<arrow::RecordBatch>& record_batch, Block* block,
+        const std::optional<GlobalRowIdContext>& global_rowid_context, size_t* 
rows) {
+    DORIS_CHECK(record_batch != nullptr);
+    DORIS_CHECK(block != nullptr);
+    DORIS_CHECK(rows != nullptr);
+    RETURN_IF_ERROR(_bind_schema(record_batch->schema()));
+
+    const auto row_count = static_cast<size_t>(record_batch->num_rows());
+    auto columns_guard = block->mutate_columns_scoped();
+    auto& columns = columns_guard.mutable_columns();
+    for (int arrow_index = 0; arrow_index < record_batch->num_columns(); 
++arrow_index) {
+        const auto output_index = 
_arrow_column_to_output_index[static_cast<size_t>(arrow_index)];
+        if (output_index == IGNORED_OUTPUT_INDEX) {
+            continue;
+        }
+        const auto& output = _output_columns[output_index];
+        const auto& arrow_column = record_batch->column(arrow_index);
+        if (is_global_rowid_output(output_index)) {
+            if (!global_rowid_context.has_value()) {
+                return Status::InvalidArgument(
+                        "Lance global row id requested without global row id 
context");
+            }
+            RETURN_IF_ERROR(_append_global_row_ids(arrow_column, 
columns[output_index],
+                                                   *global_rowid_context));
+            continue;
+        }
+        const auto& field = record_batch->schema()->field(arrow_index);
+        try {
+            if (arrow_column->type_id() == arrow::Type::NA) {
+                columns[output_index]->insert_many_defaults(row_count);
+                continue;
+            }
+            std::shared_ptr<arrow::Array> normalized_column;
+            RETURN_IF_ERROR(
+                    
_array_normalizer_for_arrow_column[static_cast<size_t>(arrow_index)]
+                            .normalize_for_doris(arrow_column, _memory_pool, 
&normalized_column));
+            RETURN_IF_ERROR(output.serde->read_column_from_arrow(
+                    *columns[output_index], normalized_column.get(), 0, 
row_count, _timezone));
+        } catch (const Exception& e) {
+            return Status::InternalError("convert Lance Arrow column '{}' 
failed: {}",
+                                         field->name(), e.what());
+        }
+    }
+    *rows = row_count;
+    return Status::OK();
+}
+
+void LanceRecordBatchConverter::reset_schema() {
+    _bound_schema.reset();
+    _arrow_column_to_output_index.clear();
+    _array_normalizer_for_arrow_column.clear();
+}
+
+Status LanceRecordBatchConverter::_append_global_row_ids(
+        const std::shared_ptr<arrow::Array>& row_ids, MutableColumnPtr& 
output_column,
+        const GlobalRowIdContext& context) const {
+    if (row_ids->type_id() != arrow::Type::UINT64) {
+        return Status::InternalError("Lance row id column must be Arrow 
UINT64, but was {}",
+                                     row_ids->type()->ToString());
+    }
+
+    ColumnString* data_column = nullptr;
+    ColumnUInt8::Container* null_map = nullptr;
+    if (auto* nullable = check_and_get_column<ColumnNullable>(*output_column)) 
{
+        data_column = 
check_and_get_column<ColumnString>(nullable->get_nested_column());
+        null_map = &nullable->get_null_map_data();
+    } else {
+        data_column = check_and_get_column<ColumnString>(*output_column);
+    }
+    if (data_column == nullptr) {
+        return Status::InternalError("Lance global row id output column must 
be STRING");
+    }
+
+    const auto typed_row_ids = 
std::static_pointer_cast<arrow::UInt64Array>(row_ids);
+    if (typed_row_ids->null_count() != 0) {
+        return Status::InternalError("Lance returned null row id");
+    }
+    const auto row_count = static_cast<size_t>(typed_row_ids->length());
+    if (null_map != nullptr) {
+        null_map->resize_fill(null_map->size() + row_count, 0);
+    }
+    for (size_t row = 0; row < row_count; ++row) {
+        const GlobalRowLoacationV2 location(ROW_VERSION::LANCE_DATASET_ROW_ID, 
context.backend_id,
+                                            context.file_id, 
typed_row_ids->Value(row));
+        data_column->insert_data(reinterpret_cast<const char*>(&location), 
sizeof(location));
+    }
+    return Status::OK();
+}
+
+} // namespace doris::format::lance
diff --git a/be/src/format_v2/lance/lance_record_batch_converter.h 
b/be/src/format_v2/lance/lance_record_batch_converter.h
new file mode 100644
index 00000000000..69ebd4618a9
--- /dev/null
+++ b/be/src/format_v2/lance/lance_record_batch_converter.h
@@ -0,0 +1,91 @@
+// 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.
+
+#pragma once
+
+#include <cctz/time_zone.h>
+
+#include <cstddef>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "common/status.h"
+#include "core/data_type_serde/data_type_serde.h"
+#include "format_v2/lance/lance_reader_helper.h"
+#include "format_v2/table_reader.h"
+
+namespace arrow {
+class Array;
+class MemoryPool;
+class RecordBatch;
+class Schema;
+} // namespace arrow
+
+namespace doris {
+class RuntimeState;
+}
+
+namespace doris::format::lance {
+
+enum class SearchKind { NORMAL, VECTOR, FULL_TEXT };
+
+// Bridges one Lance Arrow record-batch stream into an existing Doris Block. 
It owns the stable
+// output-column metadata and lazily binds an incoming Arrow schema once, so 
ordinary batches only
+// perform positional conversion. Each Arrow stream has an independent schema 
binding.
+class LanceRecordBatchConverter {
+public:
+    Status init(RuntimeState* runtime_state, const 
std::vector<ColumnDefinition>& projected_columns,
+                SearchKind search_kind);
+
+    Status convert_record_batch_to_block(
+            const std::shared_ptr<arrow::RecordBatch>& record_batch, Block* 
block,
+            const std::optional<GlobalRowIdContext>& global_rowid_context, 
size_t* rows);
+
+    bool requires_global_rowid() const { return 
_global_rowid_output_index.has_value(); }
+    bool is_global_rowid_output(size_t output_index) const {
+        return _global_rowid_output_index.has_value() &&
+               *_global_rowid_output_index == output_index;
+    }
+    void reset_schema();
+
+private:
+    struct OutputColumn {
+        std::string name;
+        DataTypeSerDeSPtr serde;
+    };
+
+    static arrow::MemoryPool* _query_arrow_memory_pool(RuntimeState* 
runtime_state);
+    Status _bind_schema(const std::shared_ptr<arrow::Schema>& schema);
+    Status _append_global_row_ids(const std::shared_ptr<arrow::Array>& row_ids,
+                                  MutableColumnPtr& output_column,
+                                  const GlobalRowIdContext& context) const;
+
+    SearchKind _search_kind = SearchKind::NORMAL;
+    cctz::time_zone _timezone;
+    arrow::MemoryPool* _memory_pool = nullptr;
+    std::vector<OutputColumn> _output_columns;
+    std::unordered_map<std::string, size_t> _output_index_by_input_name;
+    std::optional<size_t> _global_rowid_output_index;
+    std::shared_ptr<arrow::Schema> _bound_schema;
+    std::vector<size_t> _arrow_column_to_output_index;
+    std::vector<LanceArrowArrayNormalizer> _array_normalizer_for_arrow_column;
+};
+
+} // namespace doris::format::lance
diff --git a/be/src/format_v2/lance/lance_runtime_filter_helper.cpp 
b/be/src/format_v2/lance/lance_runtime_filter_helper.cpp
index 0072f5f81e7..d72a98bb8d9 100644
--- a/be/src/format_v2/lance/lance_runtime_filter_helper.cpp
+++ b/be/src/format_v2/lance/lance_runtime_filter_helper.cpp
@@ -493,6 +493,12 @@ std::optional<std::string> build_cache_key(const 
VExprContextSPtrs& conjuncts) {
 
 } // namespace
 
+bool has_lance_runtime_filters(const VExprContextSPtrs& conjuncts) {
+    return std::ranges::any_of(conjuncts, [](const auto& conjunct) {
+        return get_runtime_filter(conjunct) != nullptr;
+    });
+}
+
 std::shared_ptr<const LanceRuntimeFilterSql> 
get_or_create_lance_runtime_filter_sql(
         const VExprContextSPtrs& conjuncts, const arrow::Schema& 
physical_schema,
         ShardedKVCache* cache) {
diff --git a/be/src/format_v2/lance/lance_runtime_filter_helper.h 
b/be/src/format_v2/lance/lance_runtime_filter_helper.h
index 4dbf43e68da..40529951ccc 100644
--- a/be/src/format_v2/lance/lance_runtime_filter_helper.h
+++ b/be/src/format_v2/lance/lance_runtime_filter_helper.h
@@ -40,6 +40,10 @@ struct LanceRuntimeFilterSql {
     std::vector<int> skipped_filter_ids;
 };
 
+// Return whether the conjunct list contains at least one Doris runtime 
filter. This lets callers
+// avoid importing Lance's physical Arrow schema when no runtime-filter SQL 
can be produced.
+bool has_lance_runtime_filters(const VExprContextSPtrs& conjuncts);
+
 // Build one immutable SQL snapshot for all supported Doris runtime filters. 
When cache is non-null,
 // equivalent RF snapshots from parallel Lance readers in the same 
FileScanLocalState share the
 // conversion result. The returned snapshot also identifies RFs that cannot be 
represented exactly
diff --git a/be/src/format_v2/table/lance_reader.cpp 
b/be/src/format_v2/table/lance_reader.cpp
index dd431206e62..840338e4b33 100644
--- a/be/src/format_v2/table/lance_reader.cpp
+++ b/be/src/format_v2/table/lance_reader.cpp
@@ -28,57 +28,17 @@
 #include <cstring>
 #include <limits>
 #include <memory>
-#include <unordered_set>
 
 #include "common/config.h"
-#include "common/consts.h"
 #include "common/logging.h"
-#include "core/column/column_nullable.h"
-#include "core/column/column_string.h"
 #include "exec/common/endian.h"
 #include "format_v2/lance/lance_reader_helper.h"
 #include "format_v2/lance/lance_runtime_filter_helper.h"
 #include "format_v2/lance/lance_session_manager.h"
-#include "runtime/exec_env.h"
 #include "runtime/file_scan_profile.h"
 #include "runtime/runtime_state.h"
-#include "storage/utils.h"
 
 namespace doris::format::lance {
-namespace {
-
-Status import_dataset_schema(LanceDataset* dataset, 
std::shared_ptr<arrow::Schema>* schema) {
-    DORIS_CHECK(dataset != nullptr);
-    DORIS_CHECK(schema != nullptr);
-
-    ArrowSchema arrow_schema {};
-    if (lance_dataset_schema(dataset, &arrow_schema) != 0) {
-        return lance_error("get Lance dataset schema");
-    }
-    auto imported_schema = arrow::ImportSchema(&arrow_schema);
-    if (!imported_schema.ok()) {
-        if (arrow_schema.release != nullptr) {
-            arrow_schema.release(&arrow_schema);
-        }
-        return Status::InternalError("import Lance Arrow schema failed: {}",
-                                     imported_schema.status().message());
-    }
-    *schema = std::move(imported_schema).ValueUnsafe();
-    return Status::OK();
-}
-
-// Return the query-tracked Arrow pool, falling back only for standalone 
unit-test states.
-arrow::MemoryPool* get_lance_arrow_memory_pool(RuntimeState* runtime_state) {
-    if (runtime_state != nullptr && runtime_state->exec_env() != nullptr) {
-        auto* memory_pool = runtime_state->exec_env()->arrow_memory_pool();
-        if (memory_pool != nullptr) {
-            return memory_pool;
-        }
-    }
-    return arrow::default_memory_pool();
-}
-
-} // namespace
 
 LanceTableReader::~LanceTableReader() {
     static_cast<void>(close());
@@ -110,7 +70,7 @@ Status LanceTableReader::fetch_schema(const TFileRangeDesc& 
range,
     }
 
     std::shared_ptr<arrow::Schema> schema;
-    RETURN_IF_ERROR(import_dataset_schema(dataset.get(), &schema));
+    RETURN_IF_ERROR(import_lance_dataset_schema(dataset.get(), &schema));
     return convert_arrow_schema_to_doris(schema, column_names, column_types);
 }
 
@@ -121,86 +81,19 @@ Status LanceTableReader::init(TableReadOptions&& options) {
     DORIS_CHECK(_scan_params != nullptr);
     RETURN_IF_ERROR(_resolve_search_kind());
 
-    _ctz = _runtime_state->timezone_obj();
     const auto& lance_scan_params = _scan_params->lance_scan_params;
     ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, LANCE_READER_PROFILE,
                                file_scan_profile::TABLE_READER, 1);
     _dataset_open_time = ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceDatasetOpenTime",
                                                     LANCE_READER_PROFILE, 1);
-    _scanner_configure_time = ADD_CHILD_TIMER_WITH_LEVEL(
-            _scanner_profile, "LanceScannerConfigureTime", 
LANCE_READER_PROFILE, 1);
-    _runtime_filter_sql_time = ADD_CHILD_TIMER_WITH_LEVEL(
-            _scanner_profile, "LanceRuntimeFilterSqlTime", 
LANCE_READER_PROFILE, 1);
-    _scanner_read_time = ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceScannerReadTime",
-                                                    LANCE_READER_PROFILE, 1);
     _arrow_to_doris_block_time = ADD_CHILD_TIMER_WITH_LEVEL(
             _scanner_profile, "LanceArrowToDorisBlockTime", 
LANCE_READER_PROFILE, 1);
-    _execution_iops = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceExecutionIOOps",
-                                                   TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
-    _execution_requests = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceExecutionIORequests",
-                                                       TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
-    _execution_bytes_read = ADD_CHILD_COUNTER_WITH_LEVEL(
-            _scanner_profile, "LanceExecutionIOBytesRead", TUnit::BYTES, 
LANCE_READER_PROFILE, 1);
     _data_cache_bytes_read_from_cache =
             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceDataCacheBytesReadFromCache",
                                          TUnit::BYTES, LANCE_READER_PROFILE, 
1);
     _data_cache_bytes_read_from_remote =
             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceDataCacheBytesReadFromRemote",
                                          TUnit::BYTES, LANCE_READER_PROFILE, 
1);
-    _index_partition_cache_miss_loads =
-            ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceIndexPartitionCacheMissLoads",
-                                         TUnit::UNIT, LANCE_READER_PROFILE, 1);
-    _index_comparisons = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceIndexComparisons",
-                                                      TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
-    // These scan counts are emitted by Lance's FilteredRead execution node. 
For vector searches
-    // with an explicit fragment set, they normally describe the fragments, 
ranges, and rows read
-    // while applying the row-id prefilter. They are scan input counts, not 
ANN result counts.
-    _lance_count_metrics = {
-            {"fragments_scanned",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceFragmentsScanned", TUnit::UNIT,
-                                          LANCE_READER_PROFILE, 1)},
-            {"ranges_scanned",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceRowOffsetRangesScanned",
-                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
-            {"rows_scanned", ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceRowsScanned",
-                                                          TUnit::UNIT, 
LANCE_READER_PROFILE, 1)},
-            {"partitions_ranked",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceIVFPartitionsRanked", TUnit::UNIT,
-                                          LANCE_READER_PROFILE, 1)},
-            {"partitions_searched",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceIVFPartitionsSearched",
-                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
-            {"deltas_searched",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceVectorIndexSegmentsSearched",
-                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
-            {"scalar_segments_requested",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentsRequested",
-                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
-            {"scalar_segments_searched",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentsSearched",
-                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
-            {"scalar_segment_fallbacks",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentFallbacks",
-                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
-            {"scalar_segment_candidate_rows",
-             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexCandidateRows",
-                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
-    };
-    _lance_time_metrics = {
-            // This is wait time reported by the same Lance scan execution 
node described above,
-            // rather than Doris scanner scheduling wait time.
-            {"task_wait_time", ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceTaskWaitTime",
-                                                          
LANCE_READER_PROFILE, 1)},
-            {"find_partitions_elapsed",
-             ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceIVFPartitionRankingTime",
-                                        LANCE_READER_PROFILE, 1)},
-            {"scalar_segment_prepare_time",
-             ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentPrepareTime",
-                                        LANCE_READER_PROFILE, 1)},
-            {"scalar_segment_search_time",
-             ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentSearchTime",
-                                        LANCE_READER_PROFILE, 1)},
-    };
     if (_search_kind != SearchKind::NORMAL) {
         RETURN_IF_ERROR(_validate_external_search_request());
         const auto& request = lance_scan_params.external_search_request;
@@ -236,15 +129,6 @@ Status LanceTableReader::init(TableReadOptions&& options) {
         _scanner_profile->add_info_string("LanceTopK", std::to_string(top_k));
         _scanner_profile->add_info_string("LanceOffset", 
std::to_string(offset));
         _scanner_profile->add_info_string("LanceTopKPlusOffset", 
std::to_string(top_k + offset));
-        _planned_index_segment_count =
-                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LancePlannedIndexSegmentCount",
-                                             TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
-        _planned_indexed_fragment_count =
-                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LancePlannedIndexedFragmentCount",
-                                             TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
-        _planned_flat_search_fragment_count = ADD_CHILD_COUNTER_WITH_LEVEL(
-                _scanner_profile, "LancePlannedFlatSearchFragmentCount", 
TUnit::UNIT,
-                LANCE_READER_PROFILE, 1);
     }
     if (_scan_params->__isset.lance_scan_params &&
         lance_scan_params.__isset.lance_substrait_filter) {
@@ -254,51 +138,7 @@ Status LanceTableReader::init(TableReadOptions&& options) {
                 
std::to_string(lance_scan_params.lance_substrait_filter.size()));
     }
 
-    _output_name_to_idx.clear();
-    _output_name_to_idx.reserve(_projected_columns.size());
-    _global_rowid_output_idx.reset();
-    for (size_t idx = 0; idx < _projected_columns.size(); ++idx) {
-        const auto& column = _projected_columns[idx];
-        if (column.type == nullptr) {
-            return Status::InvalidArgument("Lance projected column '{}' has no 
type", column.name);
-        }
-        if (column.name.starts_with(BeConsts::GLOBAL_ROWID_COL)) {
-            if (_search_kind == SearchKind::NORMAL) {
-                return Status::NotSupported(
-                        "Lance global row id is currently supported only for 
external search");
-            }
-            if (_global_rowid_output_idx.has_value()) {
-                return Status::InvalidArgument("duplicate Lance global row id 
projected column: {}",
-                                               column.name);
-            }
-            if (remove_nullable(column.type)->get_primitive_type() != 
TYPE_STRING) {
-                return Status::InvalidArgument(
-                        "Lance global row id column '{}' must have Doris 
STRING type, but was {}",
-                        column.name, column.type->get_name());
-            }
-            _global_rowid_output_idx = idx;
-            continue;
-        }
-        if (!_output_name_to_idx.emplace(column.name, idx).second) {
-            return Status::InvalidArgument("duplicate Lance projected column: 
{}", column.name);
-        }
-        if (_search_kind == SearchKind::VECTOR && column.name == 
LANCE_DISTANCE_COLUMN) {
-            const auto distance_type = remove_nullable(column.type);
-            if (distance_type->get_primitive_type() != TYPE_FLOAT) {
-                return Status::InvalidArgument(
-                        "Lance vector search column '{}' must have Doris FLOAT 
type, but was {}",
-                        LANCE_DISTANCE_COLUMN, column.type->get_name());
-            }
-        } else if (_search_kind == SearchKind::FULL_TEXT && column.name == 
LANCE_SCORE_COLUMN) {
-            const auto score_type = remove_nullable(column.type);
-            if (score_type->get_primitive_type() != TYPE_FLOAT) {
-                return Status::InvalidArgument(
-                        "Lance full-text search column '{}' must have Doris 
FLOAT type, but was {}",
-                        LANCE_SCORE_COLUMN, column.type->get_name());
-            }
-        }
-    }
-    return Status::OK();
+    return _record_batch_converter.init(_runtime_state, _projected_columns, 
_search_kind);
 }
 
 Status LanceTableReader::prepare_split(const SplitReadOptions& options) {
@@ -316,7 +156,7 @@ Status LanceTableReader::prepare_split(const 
SplitReadOptions& options) {
     if (_is_table_level_count_active()) {
         return Status::OK();
     }
-    if (_global_rowid_output_idx.has_value() && 
!_global_rowid_context.has_value()) {
+    if (_record_batch_converter.requires_global_rowid() && 
!_global_rowid_context.has_value()) {
         return Status::InvalidArgument(
                 "Lance global row id requested without global row id context");
     }
@@ -469,7 +309,8 @@ Status LanceTableReader::read_by_row_ids(const 
TFileRangeDesc& range,
         size_t rows = 0;
         {
             SCOPED_TIMER(_arrow_to_doris_block_time);
-            RETURN_IF_ERROR(_fill_block_from_record_batch(record_batch, block, 
&rows));
+            
RETURN_IF_ERROR(_record_batch_converter.convert_record_batch_to_block(
+                    record_batch, block, _global_rowid_context, &rows));
         }
         fetched_rows += rows;
     }
@@ -748,10 +589,7 @@ Status LanceTableReader::_open_dataset(const DatasetKey& 
key) {
                 static_cast<uint64_t>(key.version), &raw_dataset));
         dataset.reset(raw_dataset);
     }
-    std::shared_ptr<arrow::Schema> schema;
-    RETURN_IF_ERROR(import_dataset_schema(dataset.get(), &schema));
     _dataset = dataset.release();
-    _dataset_schema = std::move(schema);
     return Status::OK();
 }
 
@@ -790,12 +628,98 @@ Status LanceTableReader::_prepare_fts_query_context() {
     return Status::OK();
 }
 
+void LanceTableReader::_init_scanner_profile() {
+    if (_scanner_configure_time != nullptr) {
+        return;
+    }
+
+    _scanner_configure_time = ADD_CHILD_TIMER_WITH_LEVEL(
+            _scanner_profile, "LanceScannerConfigureTime", 
LANCE_READER_PROFILE, 1);
+    _runtime_filter_sql_time = ADD_CHILD_TIMER_WITH_LEVEL(
+            _scanner_profile, "LanceRuntimeFilterSqlTime", 
LANCE_READER_PROFILE, 1);
+    _scanner_read_time = ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceScannerReadTime",
+                                                    LANCE_READER_PROFILE, 1);
+    _execution_iops = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceExecutionIOOps",
+                                                   TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
+    _execution_requests = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceExecutionIORequests",
+                                                       TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
+    _execution_bytes_read = ADD_CHILD_COUNTER_WITH_LEVEL(
+            _scanner_profile, "LanceExecutionIOBytesRead", TUnit::BYTES, 
LANCE_READER_PROFILE, 1);
+    _index_partition_cache_miss_loads =
+            ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceIndexPartitionCacheMissLoads",
+                                         TUnit::UNIT, LANCE_READER_PROFILE, 1);
+    _index_comparisons = ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceIndexComparisons",
+                                                      TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
+
+    // These scan counts are emitted by Lance's FilteredRead execution node. 
For vector searches
+    // with an explicit fragment set, they normally describe the fragments, 
ranges, and rows read
+    // while applying the row-id prefilter. They are scan input counts, not 
ANN result counts.
+    _lance_count_metrics = {
+            {"fragments_scanned",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceFragmentsScanned", TUnit::UNIT,
+                                          LANCE_READER_PROFILE, 1)},
+            {"ranges_scanned",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceRowOffsetRangesScanned",
+                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
+            {"rows_scanned", ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceRowsScanned",
+                                                          TUnit::UNIT, 
LANCE_READER_PROFILE, 1)},
+            {"partitions_ranked",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceIVFPartitionsRanked", TUnit::UNIT,
+                                          LANCE_READER_PROFILE, 1)},
+            {"partitions_searched",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceIVFPartitionsSearched",
+                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
+            {"deltas_searched",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceVectorIndexSegmentsSearched",
+                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
+            {"scalar_segments_requested",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentsRequested",
+                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
+            {"scalar_segments_searched",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentsSearched",
+                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
+            {"scalar_segment_fallbacks",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentFallbacks",
+                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
+            {"scalar_segment_candidate_rows",
+             ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexCandidateRows",
+                                          TUnit::UNIT, LANCE_READER_PROFILE, 
1)},
+    };
+    _lance_time_metrics = {
+            // This is wait time reported by the same Lance scan execution 
node described above,
+            // rather than Doris scanner scheduling wait time.
+            {"task_wait_time", ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceTaskWaitTime",
+                                                          
LANCE_READER_PROFILE, 1)},
+            {"find_partitions_elapsed",
+             ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceIVFPartitionRankingTime",
+                                        LANCE_READER_PROFILE, 1)},
+            {"scalar_segment_prepare_time",
+             ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentPrepareTime",
+                                        LANCE_READER_PROFILE, 1)},
+            {"scalar_segment_search_time",
+             ADD_CHILD_TIMER_WITH_LEVEL(_scanner_profile, 
"LanceScalarIndexSegmentSearchTime",
+                                        LANCE_READER_PROFILE, 1)},
+    };
+    if (_search_kind != SearchKind::NORMAL) {
+        _planned_index_segment_count =
+                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LancePlannedIndexSegmentCount",
+                                             TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
+        _planned_indexed_fragment_count =
+                ADD_CHILD_COUNTER_WITH_LEVEL(_scanner_profile, 
"LancePlannedIndexedFragmentCount",
+                                             TUnit::UNIT, 
LANCE_READER_PROFILE, 1);
+        _planned_flat_search_fragment_count = ADD_CHILD_COUNTER_WITH_LEVEL(
+                _scanner_profile, "LancePlannedFlatSearchFragmentCount", 
TUnit::UNIT,
+                LANCE_READER_PROFILE, 1);
+    }
+}
+
 Status LanceTableReader::_open_scanner(const TFileRangeDesc& range) {
+    _init_scanner_profile();
     SCOPED_TIMER(_scanner_configure_time);
     std::vector<const char*> columns;
     columns.reserve(_projected_columns.size() + 1);
     for (size_t idx = 0; idx < _projected_columns.size(); ++idx) {
-        if (_global_rowid_output_idx == idx) {
+        if (_record_batch_converter.is_global_rowid_output(idx)) {
             continue;
         }
         const auto& column = _projected_columns[idx];
@@ -813,10 +737,14 @@ Status LanceTableReader::_open_scanner(const 
TFileRangeDesc& range) {
     std::string sql_filter;
     std::shared_ptr<const LanceRuntimeFilterSql> runtime_filter_sql;
     if (_search_kind == SearchKind::NORMAL) {
-        DORIS_CHECK(_dataset_schema != nullptr);
-        SCOPED_TIMER(_runtime_filter_sql_time);
-        runtime_filter_sql = 
get_or_create_lance_runtime_filter_sql(_conjuncts, *_dataset_schema,
-                                                                    
_runtime_filter_cache);
+        if (has_lance_runtime_filters(_conjuncts)) {
+            if (_dataset_schema == nullptr) {
+                RETURN_IF_ERROR(import_lance_dataset_schema(_dataset, 
&_dataset_schema));
+            }
+            SCOPED_TIMER(_runtime_filter_sql_time);
+            runtime_filter_sql = get_or_create_lance_runtime_filter_sql(
+                    _conjuncts, *_dataset_schema, _runtime_filter_cache);
+        }
     } else {
         const auto& request = lance_scan_params.external_search_request;
         if (request.__isset.search_filter &&
@@ -824,9 +752,8 @@ Status LanceTableReader::_open_scanner(const 
TFileRangeDesc& range) {
             sql_filter = request.search_filter.payload;
         }
     }
-    LanceScanner* scanner =
-            lance_scanner_new(_dataset, columns.size() == 1 ? nullptr : 
columns.data(),
-                              sql_filter.empty() ? nullptr : 
sql_filter.c_str());
+    LanceScanner* scanner = lance_scanner_new(_dataset, columns.data(),
+                                              sql_filter.empty() ? nullptr : 
sql_filter.c_str());
     if (scanner == nullptr) {
         return lance_error("create Lance scanner");
     }
@@ -839,7 +766,8 @@ Status LanceTableReader::_open_scanner(const 
TFileRangeDesc& range) {
         return lance_error("set Lance scanner statistics callback");
     }
 
-    if (_global_rowid_output_idx.has_value() && 
lance_scanner_with_row_id(scanner, true) != 0) {
+    if (_record_batch_converter.requires_global_rowid() &&
+        lance_scanner_with_row_id(scanner, true) != 0) {
         return lance_error("enable Lance row id output");
     }
 
@@ -1238,10 +1166,12 @@ void LanceTableReader::_close_dataset() {
         _dataset = nullptr;
     }
     _dataset_schema.reset();
+    _record_batch_converter.reset_schema();
 }
 
 void LanceTableReader::_collect_data_cache_statistics() {
-    if (_dataset == nullptr) {
+    if (_dataset == nullptr || (_data_cache_bytes_read_from_cache == nullptr &&
+                                _data_cache_bytes_read_from_remote == 
nullptr)) {
         return;
     }
 
@@ -1292,111 +1222,8 @@ Status 
LanceTableReader::_fill_block_from_lance_batch(LanceBatch* batch, Block*
                                      result.status().message());
     }
 
-    return _fill_block_from_record_batch(std::move(result).ValueUnsafe(), 
block, rows);
-}
-
-Status LanceTableReader::_append_global_row_ids(const 
std::shared_ptr<arrow::Array>& row_ids,
-                                                MutableColumnPtr& 
output_column) const {
-    DORIS_CHECK(row_ids != nullptr);
-    DORIS_CHECK(_global_rowid_context.has_value());
-    if (row_ids->type_id() != arrow::Type::UINT64) {
-        return Status::InternalError("Lance row id column must be Arrow 
UINT64, but was {}",
-                                     row_ids->type()->ToString());
-    }
-
-    ColumnString* data_column = nullptr;
-    ColumnUInt8::Container* null_map = nullptr;
-    if (auto* nullable = check_and_get_column<ColumnNullable>(*output_column)) 
{
-        data_column = 
check_and_get_column<ColumnString>(nullable->get_nested_column());
-        null_map = &nullable->get_null_map_data();
-    } else {
-        data_column = check_and_get_column<ColumnString>(*output_column);
-    }
-    if (data_column == nullptr) {
-        return Status::InternalError("Lance global row id output column must 
be STRING");
-    }
-
-    const auto typed_row_ids = 
std::static_pointer_cast<arrow::UInt64Array>(row_ids);
-    if (typed_row_ids->null_count() != 0) {
-        return Status::InternalError("Lance returned null row id");
-    }
-    const auto row_count = static_cast<size_t>(typed_row_ids->length());
-    if (null_map != nullptr) {
-        null_map->resize_fill(null_map->size() + row_count, 0);
-    }
-    const auto& context = *_global_rowid_context;
-    for (size_t row = 0; row < row_count; ++row) {
-        const GlobalRowLoacationV2 location(ROW_VERSION::LANCE_DATASET_ROW_ID, 
context.backend_id,
-                                            context.file_id, 
typed_row_ids->Value(row));
-        data_column->insert_data(reinterpret_cast<const char*>(&location), 
sizeof(location));
-    }
-    return Status::OK();
-}
-
-Status LanceTableReader::_fill_block_from_record_batch(
-        const std::shared_ptr<arrow::RecordBatch>& record_batch, Block* block, 
size_t* rows) {
-    DORIS_CHECK(record_batch != nullptr);
-    DORIS_CHECK(block != nullptr);
-    DORIS_CHECK(rows != nullptr);
-    const auto row_count = static_cast<size_t>(record_batch->num_rows());
-    std::unordered_set<std::string> materialized_columns;
-    materialized_columns.reserve(record_batch->num_columns());
-    auto columns_guard = block->mutate_columns_scoped();
-    auto& columns = columns_guard.mutable_columns();
-    for (int arrow_idx = 0; arrow_idx < record_batch->num_columns(); 
++arrow_idx) {
-        const auto& field = record_batch->schema()->field(arrow_idx);
-        if (field->name() == LANCE_ROW_ID_COLUMN && 
_global_rowid_output_idx.has_value()) {
-            const auto output_idx = *_global_rowid_output_idx;
-            const auto& output_name = _projected_columns[output_idx].name;
-            if (!materialized_columns.emplace(output_name).second) {
-                return Status::InternalError("Lance returned duplicate column 
'{}'",
-                                             LANCE_ROW_ID_COLUMN);
-            }
-            RETURN_IF_ERROR(
-                    _append_global_row_ids(record_batch->column(arrow_idx), 
columns[output_idx]));
-            continue;
-        }
-        const auto output_it = _output_name_to_idx.find(field->name());
-        if (output_it == _output_name_to_idx.end()) {
-            if ((_search_kind == SearchKind::VECTOR && field->name() == 
LANCE_DISTANCE_COLUMN) ||
-                (_search_kind == SearchKind::FULL_TEXT && field->name() == 
LANCE_SCORE_COLUMN)) {
-                // Lance auto-projects the generated search result column. It 
is valid for Doris
-                // slot pruning to omit that optional result column.
-                continue;
-            }
-            return Status::InternalError("Lance returned unknown column '{}'", 
field->name());
-        }
-        if (!materialized_columns.emplace(field->name()).second) {
-            return Status::InternalError("Lance returned duplicate column 
'{}'", field->name());
-        }
-        const auto output_idx = output_it->second;
-        try {
-            const auto& arrow_column = record_batch->column(arrow_idx);
-            if (arrow_column->type_id() == arrow::Type::NA) {
-                columns[output_idx]->insert_many_defaults(row_count);
-                continue;
-            }
-            std::shared_ptr<arrow::Array> normalized_column;
-            RETURN_IF_ERROR(normalize_lance_arrow_array(field, arrow_column,
-                                                        
get_lance_arrow_memory_pool(_runtime_state),
-                                                        &normalized_column));
-            RETURN_IF_ERROR(columns_guard.get_datatype_by_position(output_idx)
-                                    ->get_serde()
-                                    
->read_column_from_arrow(*columns[output_idx],
-                                                             
normalized_column.get(), 0, row_count,
-                                                             _ctz));
-        } catch (const Exception& e) {
-            return Status::InternalError("convert Lance Arrow column '{}' 
failed: {}",
-                                         field->name(), e.what());
-        }
-    }
-    for (const auto& column : _projected_columns) {
-        if (!materialized_columns.contains(column.name)) {
-            return Status::InternalError("Lance did not return requested 
column '{}'", column.name);
-        }
-    }
-    *rows = row_count;
-    return Status::OK();
+    return _record_batch_converter.convert_record_batch_to_block(
+            std::move(result).ValueUnsafe(), block, _global_rowid_context, 
rows);
 }
 
 Status LanceTableReader::_dataset_key(const TFileRangeDesc& range, DatasetKey* 
key) const {
diff --git a/be/src/format_v2/table/lance_reader.h 
b/be/src/format_v2/table/lance_reader.h
index 944041bed3d..88deea610b3 100644
--- a/be/src/format_v2/table/lance_reader.h
+++ b/be/src/format_v2/table/lance_reader.h
@@ -17,8 +17,6 @@
 
 #pragma once
 
-#include <cctz/time_zone.h>
-
 #include <cstddef>
 #include <cstdint>
 #include <memory>
@@ -29,6 +27,7 @@
 #include <vector>
 
 #include "common/status.h"
+#include "format_v2/lance/lance_record_batch_converter.h"
 #include "format_v2/table_reader.h"
 #include "runtime/runtime_profile.h"
 
@@ -41,12 +40,6 @@ namespace doris {
 class ShardedKVCache;
 }
 
-namespace arrow {
-class Array;
-class RecordBatch;
-class Schema;
-} // namespace arrow
-
 namespace doris::format::lance {
 
 // A FORMAT_LANCE table reader. Unlike file formats such as Parquet, a Lance 
split is not a
@@ -87,6 +80,7 @@ private:
     Status _open_dataset(const DatasetKey& key);
     Status _prepare_fts_query_context();
     Status _open_scanner(const TFileRangeDesc& range);
+    void _init_scanner_profile();
     Status _configure_scan_options(LanceScanner* scanner) const;
     Status _configure_normal_scan(LanceScanner* scanner, const TLanceFileDesc& 
lance_params) const;
     Status _configure_vector_search(LanceScanner* scanner,
@@ -102,10 +96,6 @@ private:
     void _close_scanner();
     void _close_dataset();
     Status _fill_block_from_lance_batch(LanceBatch* batch, Block* block, 
size_t* rows);
-    Status _fill_block_from_record_batch(const 
std::shared_ptr<arrow::RecordBatch>& record_batch,
-                                         Block* block, size_t* rows);
-    Status _append_global_row_ids(const std::shared_ptr<arrow::Array>& row_ids,
-                                  MutableColumnPtr& output_column) const;
     Status _dataset_key(const TFileRangeDesc& range, DatasetKey* key) const;
 
     LanceDataset* _dataset = nullptr;
@@ -113,9 +103,7 @@ private:
     LanceScanner* _scanner = nullptr;
     ShardedKVCache* _runtime_filter_cache = nullptr;
     std::optional<DatasetKey> _opened_dataset_key;
-    std::unordered_map<std::string, size_t> _output_name_to_idx;
-    std::optional<size_t> _global_rowid_output_idx;
-    cctz::time_zone _ctz;
+    LanceRecordBatchConverter _record_batch_converter;
     size_t _scanner_batch_size = 0;
     RuntimeProfile::Counter* _planned_index_segment_count = nullptr;
     RuntimeProfile::Counter* _planned_indexed_fragment_count = nullptr;
@@ -137,7 +125,6 @@ private:
     std::unordered_map<std::string_view, RuntimeProfile::Counter*> 
_lance_count_metrics;
     std::unordered_map<std::string_view, RuntimeProfile::Counter*> 
_lance_time_metrics;
     LanceFtsQueryContext* _fts_query_context = nullptr;
-    enum class SearchKind { NORMAL, VECTOR, FULL_TEXT };
     SearchKind _search_kind = SearchKind::NORMAL;
     bool _eof = false;
 };
diff --git a/be/test/format_v2/lance/lance_nested_null_test.cpp 
b/be/test/format_v2/lance/lance_nested_null_test.cpp
index 8d1115687df..c36b936de65 100644
--- a/be/test/format_v2/lance/lance_nested_null_test.cpp
+++ b/be/test/format_v2/lance/lance_nested_null_test.cpp
@@ -57,8 +57,9 @@ protected:
     void read(const std::shared_ptr<arrow::Array>& array, const DataTypePtr& 
type,
               MutableColumnPtr* column) {
         std::shared_ptr<arrow::Array> normalized;
-        ASSERT_TRUE(normalize_lance_arrow_array(arrow::field("value", 
array->type()), array,
-                                                arrow::default_memory_pool(), 
&normalized)
+        ASSERT_TRUE(normalize_lance_arrow_array_for_test(arrow::field("value", 
array->type()),
+                                                         array, &normalized,
+                                                         
arrow::default_memory_pool())
                             .ok());
         *column = type->create_column();
         ASSERT_TRUE(type->get_serde()
diff --git a/be/test/format_v2/table/lance_reader_test.cpp 
b/be/test/format_v2/table/lance_reader_test.cpp
index 6baccb3bd9d..b0e1b2bb831 100644
--- a/be/test/format_v2/table/lance_reader_test.cpp
+++ b/be/test/format_v2/table/lance_reader_test.cpp
@@ -299,6 +299,14 @@ Status init_reader(LanceTableReader* reader, const 
Columns& projected_columns,
     });
 }
 
+Status convert_record_batch_for_test(RuntimeState* runtime_state, const 
Columns& projected_columns,
+                                     const 
std::shared_ptr<arrow::RecordBatch>& record_batch,
+                                     Block* block, size_t* rows) {
+    LanceRecordBatchConverter converter;
+    RETURN_IF_ERROR(converter.init(runtime_state, projected_columns, 
SearchKind::NORMAL));
+    return converter.convert_record_batch_to_block(record_batch, block, 
std::nullopt, rows);
+}
+
 Status prepare_range(LanceTableReader* reader, TFileRangeDesc range,
                      std::optional<GlobalRowIdContext> global_rowid_context = 
std::nullopt) {
     // Assign after value initialization so adding optional split state cannot 
break this fixture's
@@ -2302,7 +2310,7 @@ TEST(LanceTableReaderTypeTest, 
ReadsAdditionalArrowAndLanceTypes) {
     Block block;
     add_output_columns(&block, columns);
     size_t rows = 0;
-    ASSERT_TRUE(reader._fill_block_from_record_batch(record_batch, &block, 
&rows).ok());
+    ASSERT_TRUE(convert_record_batch_for_test(&state, columns, record_batch, 
&block, &rows).ok());
     ASSERT_EQ(2, rows);
 
     const auto& null_values = assert_cast<const 
ColumnNullable&>(*block.get_by_position(0).column);
@@ -2377,7 +2385,7 @@ TEST(LanceTableReaderTypeTest, 
ReadsSlicedDurationAndJsonValues) {
     Block block;
     add_output_columns(&block, columns);
     size_t rows = 0;
-    ASSERT_TRUE(reader._fill_block_from_record_batch(record_batch, &block, 
&rows).ok());
+    ASSERT_TRUE(convert_record_batch_for_test(&state, columns, record_batch, 
&block, &rows).ok());
     ASSERT_EQ(2, rows);
 
     const auto& duration = assert_cast<const 
ColumnNullable&>(*block.get_by_position(0).column);
@@ -2444,7 +2452,7 @@ TEST(LanceTableReaderTypeTest, 
ReadsRegisteredJsonNestedInSlicedList) {
     Block block;
     add_output_columns(&block, columns);
     size_t rows = 0;
-    ASSERT_TRUE(reader._fill_block_from_record_batch(record_batch, &block, 
&rows).ok());
+    ASSERT_TRUE(convert_record_batch_for_test(&state, columns, record_batch, 
&block, &rows).ok());
     ASSERT_EQ(1, rows);
 
     const auto& nullable_list =
@@ -2693,7 +2701,7 @@ TEST(LanceTableReaderTypeTest, 
NormalizesVisibleBFloat16ValuesInSlicedMap) {
     Block block;
     add_output_columns(&block, columns);
     size_t rows = 0;
-    ASSERT_TRUE(reader._fill_block_from_record_batch(record_batch, &block, 
&rows).ok());
+    ASSERT_TRUE(convert_record_batch_for_test(&state, columns, record_batch, 
&block, &rows).ok());
     ASSERT_EQ(1, rows);
 
     const auto& nullable_map = assert_cast<const 
ColumnNullable&>(*block.get_by_position(0).column);
@@ -2740,7 +2748,7 @@ TEST(LanceTableReaderTypeTest, 
ReadsLanceJsonLargeBinaryValues) {
     Block block;
     add_output_columns(&block, columns);
     size_t rows = 0;
-    ASSERT_TRUE(reader._fill_block_from_record_batch(record_batch, &block, 
&rows).ok());
+    ASSERT_TRUE(convert_record_batch_for_test(&state, columns, record_batch, 
&block, &rows).ok());
     ASSERT_EQ(2, rows);
     const auto& json_values = assert_cast<const 
ColumnNullable&>(*block.get_by_position(0).column);
     EXPECT_EQ((ColumnUInt8::Container {0, 1}), 
json_values.get_null_map_data());
@@ -2772,7 +2780,7 @@ TEST(LanceTableReaderTypeTest, 
ReadsDurationBoundaryValues) {
     Block block;
     add_output_columns(&block, columns);
     size_t rows = 0;
-    ASSERT_TRUE(reader._fill_block_from_record_batch(record_batch, &block, 
&rows).ok());
+    ASSERT_TRUE(convert_record_batch_for_test(&state, columns, record_batch, 
&block, &rows).ok());
     ASSERT_EQ(5, rows);
     const auto& durations = assert_cast<const 
ColumnNullable&>(*block.get_by_position(0).column);
     const auto& duration_values = assert_cast<const 
ColumnInt64&>(durations.get_nested_column());


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

Reply via email to