github-actions[bot] commented on code in PR #65730:
URL: https://github.com/apache/doris/pull/65730#discussion_r3689417093
##########
fe/fe-core/src/main/java/org/apache/doris/common/util/PrintableMap.java:
##########
@@ -61,6 +61,8 @@ public class PrintableMap<K, V> {
SENSITIVE_KEY.add("bos_secret_accesskey");
SENSITIVE_KEY.add("jdbc.password");
SENSITIVE_KEY.add("elasticsearch.password");
+ SENSITIVE_KEY.add("lance.rest.bearer-token");
+ SENSITIVE_KEY.add("lance.rest.api-key");
Review Comment:
[P1] Mask credential-bearing custom REST headers
The catalog accepts arbitrary `lance.rest.header.*` properties and forwards
their values as HTTP headers, but these exact-key additions leave every custom
header printable. For example, a service authenticated with
`lance.rest.header.X-Auth-Token=secret` will expose `secret` through catalog
properties and `SHOW CREATE CATALOG`. Please mask this prefix (or restrict
custom headers to an explicit non-secret allowlist) and add a custom-token
regression.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/lance/source/LanceScanNode.java:
##########
@@ -0,0 +1,272 @@
+// 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.datasource.lance.source;
+
+import org.apache.doris.analysis.TupleDescriptor;
+import org.apache.doris.catalog.TableIf;
+import org.apache.doris.common.UserException;
+import org.apache.doris.datasource.ExternalUtil;
+import org.apache.doris.datasource.FileQueryScanNode;
+import org.apache.doris.datasource.TableFormatType;
+import org.apache.doris.datasource.lance.LanceExternalCatalog;
+import org.apache.doris.datasource.lance.LanceExternalTable;
+import org.apache.doris.datasource.lance.LanceTableMetadata;
+import org.apache.doris.planner.PlanNodeId;
+import org.apache.doris.planner.ScanContext;
+import org.apache.doris.qe.SessionVariable;
+import org.apache.doris.spi.Split;
+import org.apache.doris.statistics.StatisticalType;
+import org.apache.doris.thrift.TExplainLevel;
+import org.apache.doris.thrift.TExternalSearchRequest;
+import org.apache.doris.thrift.TFileFormatType;
+import org.apache.doris.thrift.TFileRangeDesc;
+import org.apache.doris.thrift.TLanceFileDesc;
+import org.apache.doris.thrift.TTableFormatFileDesc;
+import org.apache.doris.thrift.TVectorMetric;
+import org.apache.doris.thrift.TVectorSearchParams;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Scan node for both ordinary Lance table scans and Lance external-search
scans.
+ *
+ * <p>These modes share dataset metadata, storage properties, and BE
scan-range serialization.
+ * Keeping them in one node prevents those common parts from drifting apart.
The search request is
+ * also an explicit mode marker: ordinary scans are split by fragment, while
the first version of
+ * vector search deliberately sends one whole-snapshot split to one scanner so
Lance can compute a
+ * global TopK result.
+ */
+public class LanceScanNode extends FileQueryScanNode {
+ private LanceExternalTable lanceTable;
+ private LanceTableMetadata plannedMetadata;
+ private TExternalSearchRequest externalSearchRequest;
+ private byte[] lanceSubstraitFilter = new byte[0];
+ private String lancePushdownPredicate = "";
+ private long plannedVersion = -1;
+ private int plannedFragments;
+
+ public LanceScanNode(PlanNodeId id, TupleDescriptor desc, boolean
needCheckColumnPriv,
+ SessionVariable sessionVariable, ScanContext scanContext) {
+ super(id, desc, "LANCE_SCAN_NODE", StatisticalType.LANCE_SCAN_NODE,
+ scanContext, needCheckColumnPriv, sessionVariable);
+ }
+
+ /**
+ * Creates the search mode of this node.
+ *
+ * <p>The tuple descriptor belongs to a FunctionGenTable and contains
generated columns such as
+ * {@code _distance}. Therefore the real Lance table and the metadata
snapshot selected while
+ * analyzing the TVF must be passed separately.
+ */
+ public LanceScanNode(PlanNodeId id, TupleDescriptor desc,
LanceExternalTable lanceTable,
+ LanceTableMetadata plannedMetadata, TExternalSearchRequest
externalSearchRequest,
+ SessionVariable sessionVariable) {
+ super(id, desc, "LANCE_SCAN_NODE", StatisticalType.LANCE_SCAN_NODE,
+
ScanContext.builder().clusterName(sessionVariable.resolveCloudClusterName()).build(),
+ false, sessionVariable);
+ this.lanceTable = lanceTable;
+ this.plannedMetadata = plannedMetadata;
+ this.externalSearchRequest = externalSearchRequest.deepCopy();
+ }
+
+ @Override
+ protected void doInitialize() throws UserException {
+ super.doInitialize();
+
+ if (isExternalSearch()) {
+ // Search output comes from the FunctionGenTable because it adds
generated columns such
+ // as _distance. The real Lance table is still retained for
storage and metadata access.
+ ExternalUtil.initSchemaInfo(params, -1L,
desc.getTable().getColumns());
+ params.setExternalSearchRequest(externalSearchRequest.deepCopy());
+ } else {
+ lanceTable = (LanceExternalTable) desc.getTable();
+ ExternalUtil.initSchemaInfo(params, -1L, lanceTable.getColumns());
+ }
+ }
+
+ @Override
+ protected void convertPredicate() {
+ if (isExternalSearch()) {
+ // The TVF "filter" property is already serialized in
externalSearchRequest and is
+ // evaluated by Lance before vector search. Outer WHERE conjuncts
have different
+ // semantics: Doris must keep them here and evaluate them after
Lance returns TopK.
+ } else {
+ plannedMetadata = lanceTable.loadMetadata();
Review Comment:
[P1] Do not silently ignore the requested Lance snapshot
The planner preserves `FOR VERSION/TIME AS OF` in `queryTableSnapshot`, but
this path always loads the latest Lance metadata and never reads that snapshot.
The resulting splits are pinned consistently—just to the wrong version—so a
historical query silently returns current rows. Please resolve and load the
requested Lance version/time here, or reject snapshot-qualified Lance relations
until time travel is supported.
##########
be/src/core/data_type_serde/data_type_array_serde.cpp:
##########
@@ -326,32 +326,59 @@ Status
DataTypeArraySerDe::read_column_from_arrow(IColumn& column, const arrow::
const cctz::time_zone& ctz)
const {
auto& column_array = static_cast<ColumnArray&>(column);
auto& offsets_data = column_array.get_offsets();
- const auto* concrete_array = dynamic_cast<const
arrow::ListArray*>(arrow_array);
- auto arrow_offsets_array = concrete_array->offsets();
- auto* arrow_offsets =
dynamic_cast<arrow::Int32Array*>(arrow_offsets_array.get());
- if (config::enable_arrow_input_validation) {
- check_arrow_list_offsets(*concrete_array, start, end);
- }
- auto prev_size = offsets_data.back();
- const auto* base_offsets_ptr = reinterpret_cast<const
uint8_t*>(arrow_offsets->raw_values());
- const size_t offset_element_size = sizeof(int32_t);
- int32_t arrow_nested_start_offset = 0;
- int32_t arrow_nested_end_offset = 0;
- const uint8_t* start_offset_ptr = base_offsets_ptr + start *
offset_element_size;
- const uint8_t* end_offset_ptr = base_offsets_ptr + end *
offset_element_size;
- memcpy(&arrow_nested_start_offset, start_offset_ptr, offset_element_size);
- memcpy(&arrow_nested_end_offset, end_offset_ptr, offset_element_size);
-
- for (auto i = start + 1; i < end + 1; ++i) {
- int32_t current_offset = 0;
- const uint8_t* current_offset_ptr = base_offsets_ptr + i *
offset_element_size;
- memcpy(¤t_offset, current_offset_ptr, offset_element_size);
- // convert to doris offset, start from offsets.back()
- offsets_data.emplace_back(prev_size + current_offset -
arrow_nested_start_offset);
- }
- return nested_serde->read_column_from_arrow(
- column_array.get_data(), concrete_array->values().get(),
arrow_nested_start_offset,
- arrow_nested_end_offset, ctz);
+
+ const auto read_list = [&](const auto* concrete_array) -> Status {
+ const int64_t arrow_nested_start_offset =
concrete_array->value_offset(start);
+ const int64_t arrow_nested_end_offset =
concrete_array->value_offset(end);
+ const auto prev_size = offsets_data.back();
+ for (int64_t i = start + 1; i <= end; ++i) {
+ // Convert Arrow offsets to Doris offsets, starting at
offsets.back().
+ offsets_data.emplace_back(prev_size +
concrete_array->value_offset(i) -
+ arrow_nested_start_offset);
+ }
+ return nested_serde->read_column_from_arrow(
+ column_array.get_data(), concrete_array->values().get(),
arrow_nested_start_offset,
+ arrow_nested_end_offset, ctz);
+ };
+
+ switch (arrow_array->type_id()) {
+ case arrow::Type::LIST: {
+ const auto* concrete_array = dynamic_cast<const
arrow::ListArray*>(arrow_array);
+ if (concrete_array == nullptr) {
+ return Status::InvalidArgument("Expected Arrow ListArray, got {}",
+ arrow_array->type()->name());
+ }
+ if (config::enable_arrow_input_validation) {
+ check_arrow_list_offsets(*concrete_array, start, end);
+ }
+ return read_list(concrete_array);
+ }
+ case arrow::Type::LARGE_LIST: {
+ const auto* concrete_array = dynamic_cast<const
arrow::LargeListArray*>(arrow_array);
+ if (concrete_array == nullptr) {
+ return Status::InvalidArgument("Expected Arrow LargeListArray, got
{}",
+ arrow_array->type()->name());
+ }
+ if (config::enable_arrow_input_validation) {
Review Comment:
[P1] Validate LargeList offsets before reading them
Unlike the existing List branch, this only validates the requested row range
and then `read_list` immediately calls `value_offset()`. A short, negative,
non-monotonic, or child-overrunning 64-bit offsets buffer from an Arrow FFI
producer can therefore be read out of bounds or passed as an invalid nested
range even with input validation enabled. Please add a LargeList offset
validator equivalent to `check_arrow_list_offsets` and malformed-buffer tests.
##########
be/src/core/data_type_serde/data_type_time_serde.cpp:
##########
@@ -281,6 +284,86 @@ Status
DataTypeTimeV2SerDe::from_string_strict_mode(StringRef& str, IColumn& col
return Status::OK();
}
+Status DataTypeTimeV2SerDe::read_column_from_arrow(IColumn& column, const
arrow::Array* arrow_array,
Review Comment:
[P1] Validate temporal Arrow buffers before access
This new TIME32/64 reader calls `IsNull`/`Value` without validating the
requested range, validity bitmap, or fixed-width values buffer. The new
TIMESTAMPTZ path has the same gap, and Date64 only calls
`check_arrow_no_offset`, which validates none of those buffers. A malformed
external Arrow array can therefore read out of bounds even with input
validation enabled; the nullable wrapper still leaves the values buffer
unchecked. Please apply `check_arrow_array_range` plus the appropriate 4/8-byte
fixed-width check to all three temporal paths and add malformed-buffer tests.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceExternalTable.java:
##########
@@ -0,0 +1,81 @@
+// 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.datasource.lance;
+
+import org.apache.doris.catalog.Column;
+import org.apache.doris.datasource.ExternalTable;
+import org.apache.doris.datasource.SchemaCacheValue;
+import org.apache.doris.statistics.AnalysisInfo;
+import org.apache.doris.statistics.BaseAnalysisTask;
+import org.apache.doris.statistics.ExternalAnalysisTask;
+import org.apache.doris.thrift.THiveTable;
+import org.apache.doris.thrift.TTableDescriptor;
+import org.apache.doris.thrift.TTableType;
+
+import org.apache.arrow.vector.types.pojo.Field;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+
+public class LanceExternalTable extends ExternalTable {
+ public LanceExternalTable(long id, String name, String remoteName,
LanceExternalCatalog catalog,
+ LanceExternalDatabase db) {
+ super(id, name, remoteName, catalog, db,
TableType.LANCE_EXTERNAL_TABLE);
+ }
+
+ @Override
+ public Optional<SchemaCacheValue> initSchema() {
+ LanceTableMetadata metadata = loadMetadata();
+ List<Column> columns = new
ArrayList<>(metadata.getSchema().getFields().size());
+ int position = 0;
+ for (Field field : metadata.getSchema().getFields()) {
+ String comment = field.getMetadata() == null ? null :
field.getMetadata().get("comment");
+ columns.add(new Column(field.getName(),
LanceTypeConverter.toDorisType(field), false,
+ null, field.isNullable(), comment, true, position++));
+ }
+ return Optional.of(new SchemaCacheValue(columns));
Review Comment:
[P1] Keep the bound schema and scan on one Lance snapshot
This loads a fixed Lance metadata snapshot but caches only its converted
columns. `LanceScanNode` later builds BE slots from those cached columns and
independently loads the current latest metadata for predicates and splits.
After schema evolution, an INT slot can therefore read a newer Float32 Arrow
array; the numeric SerDe copies same-width bytes without checking the physical
Arrow type, so `1.0f` becomes `1065353216` instead of failing. Please retain
the version/fragments/credentials with this schema and plan from that same
snapshot, or verify and rebind before creating splits.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/tvf/source/TVFScanNode.java:
##########
@@ -169,6 +176,29 @@ public List<Split> getSplits(int numBackends) throws
UserException {
return splits;
}
+ private List<Split> getLanceSplits() throws UserException {
+ if (!sessionVariable.enableFileScannerV2) {
+ throw new UserException("Lance TVF requires
enable_file_scanner_v2=true");
+ }
+ if (tableValuedFunction.getTFileType() == TFileType.FILE_LOCAL) {
+ // A local dataset is visible to its selected BE, not to FE. Keep
exactly one
+ // whole-dataset split; BE resolves version zero to latest when it
opens the dataset.
+ return Collections.singletonList(
+
LanceSplit.wholeDatasetAtLatest(tableValuedFunction.getFilePath()));
Review Comment:
[P1] Pin local TVF execution to the discovered version
Local schema discovery already opens this dataset at version zero, but it
returns only columns and discards the resolved version; this creates a second
version-zero open for execution. If the dataset evolves between analysis and
scan, Doris can bind version-N types and decode version-N+1 values (including
silent same-width bit reinterpretation). Please return the resolved version
with the schema and use that positive version here, as the S3 TVF path does.
##########
be/src/core/data_type_serde/data_type_number_serde.cpp:
##########
@@ -840,6 +856,47 @@ Status
DataTypeNumberSerDe<T>::read_column_from_arrow(IColumn& column,
return Status::OK();
}
+ if constexpr (T == TYPE_FLOAT) {
+ if (arrow_array->type_id() == arrow::Type::HALF_FLOAT) {
Review Comment:
[P1] Validate new numeric buffers before the early returns
The range check above does not validate the values or validity buffers. This
HalfFloat branch and the new unsigned-widening branches below call Arrow's raw
`IsNull`/`Value` accessors and return before the common
`check_arrow_fixed_width_buffer` path, so a missing or short external buffer is
still read out of bounds with validation enabled. Please validate the matching
physical width and bitmap before each early return, and add malformed-buffer
coverage.
##########
be/src/core/data_type_serde/data_type_varbinary_serde.cpp:
##########
@@ -184,6 +186,63 @@ Status DataTypeVarbinarySerDe::write_column_to_arrow(const
IColumn& column, cons
return Status::OK();
}
+Status DataTypeVarbinarySerDe::read_column_from_arrow(IColumn& column,
Review Comment:
[P1] Validate Arrow buffers before VARBINARY access
This shared reader dereferences validity, offset, and value buffers without
any of the checks enabled by `enable_arrow_input_validation`. In the Binary
branch, for example, a short offsets buffer makes the `offset_i + 1` memcpy
read out of bounds, while negative or non-monotonic offsets can form an invalid
pointer or huge length. The adjacent String reader already applies the required
range, bitmap, offset-buffer, per-value, and fixed-width checks. Please mirror
those checks for every VARBINARY branch and cover malformed inputs.
##########
be/src/format_v2/table/lance_reader.cpp:
##########
@@ -0,0 +1,912 @@
+// 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/table/lance_reader.h"
+
+#include <arrow/c/bridge.h>
+#include <arrow/record_batch.h>
+#include <arrow/type.h>
+#include <lance/lance.h>
+
+#include <bit>
+#include <cstring>
+#include <limits>
+#include <memory>
+
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_struct.h"
+#include "exec/common/endian.h"
+
+namespace doris::format::lance {
+namespace {
+
+struct LanceDatasetDeleter {
+ void operator()(LanceDataset* dataset) const {
lance_dataset_close(dataset); }
+};
+
+struct LanceScannerDeleter {
+ void operator()(LanceScanner* scanner) const {
lance_scanner_close(scanner); }
+};
+
+struct LanceBatchDeleter {
+ void operator()(LanceBatch* batch) const { lance_batch_free(batch); }
+};
+
+constexpr std::string_view DISTANCE_COLUMN = "_distance";
+
+size_t vector_element_width(TVectorElementType::type type) {
+ switch (type) {
+ case TVectorElementType::FLOAT16:
+ return sizeof(uint16_t);
+ case TVectorElementType::FLOAT32:
+ return sizeof(float);
+ case TVectorElementType::FLOAT64:
+ return sizeof(double);
+ case TVectorElementType::UINT8:
+ case TVectorElementType::INT8:
+ return sizeof(uint8_t);
+ }
+ return 0;
+}
+
+std::string vector_metric_name(TVectorMetric::type metric) {
+ switch (metric) {
+ case TVectorMetric::DEFAULT:
+ return "default";
+ case TVectorMetric::L2:
+ return "l2";
+ case TVectorMetric::COSINE:
+ return "cosine";
+ case TVectorMetric::DOT_PRODUCT:
+ return "dot";
+ case TVectorMetric::HAMMING:
+ return "hamming";
+ }
+ return "unknown";
+}
+
+int arrow_time_precision(arrow::TimeUnit::type unit) {
+ switch (unit) {
+ case arrow::TimeUnit::SECOND:
+ return 0;
+ case arrow::TimeUnit::MILLI:
+ return 3;
+ case arrow::TimeUnit::MICRO:
+ case arrow::TimeUnit::NANO:
+ return 6;
+ }
+ return 6;
+}
+
+Status arrow_type_to_doris_type(const std::shared_ptr<arrow::DataType>&
arrow_type,
+ DataTypePtr* doris_type) {
+ const auto nullable_primitive = [&](PrimitiveType type, int precision = 0,
int scale = 0,
+ int len = -1) {
+ *doris_type =
+ DataTypeFactory::instance().create_data_type(type, true,
precision, scale, len);
+ return Status::OK();
+ };
+
+ switch (arrow_type->id()) {
+ case arrow::Type::BOOL:
+ return nullable_primitive(TYPE_BOOLEAN);
+ case arrow::Type::INT8:
+ return nullable_primitive(TYPE_TINYINT);
+ case arrow::Type::UINT8:
+ case arrow::Type::INT16:
+ return nullable_primitive(TYPE_SMALLINT);
+ case arrow::Type::UINT16:
+ case arrow::Type::INT32:
+ return nullable_primitive(TYPE_INT);
+ case arrow::Type::UINT32:
+ case arrow::Type::INT64:
+ return nullable_primitive(TYPE_BIGINT);
+ case arrow::Type::UINT64:
+ return nullable_primitive(TYPE_LARGEINT);
+ case arrow::Type::HALF_FLOAT:
+ case arrow::Type::FLOAT:
+ return nullable_primitive(TYPE_FLOAT);
+ case arrow::Type::DOUBLE:
+ return nullable_primitive(TYPE_DOUBLE);
+ case arrow::Type::STRING:
+ case arrow::Type::LARGE_STRING:
+ return nullable_primitive(TYPE_STRING);
+ case arrow::Type::BINARY:
+ case arrow::Type::LARGE_BINARY:
+ return nullable_primitive(TYPE_VARBINARY, 0, 0,
std::numeric_limits<int32_t>::max());
+ case arrow::Type::FIXED_SIZE_BINARY: {
+ const auto binary =
std::static_pointer_cast<arrow::FixedSizeBinaryType>(arrow_type);
+ return nullable_primitive(TYPE_VARBINARY, 0, 0, binary->byte_width());
+ }
+ case arrow::Type::DATE32:
+ case arrow::Type::DATE64:
+ return nullable_primitive(TYPE_DATEV2);
+ case arrow::Type::TIME32:
+ case arrow::Type::TIME64: {
+ const auto time =
std::static_pointer_cast<arrow::TimeType>(arrow_type);
+ return nullable_primitive(TYPE_TIMEV2, 0,
arrow_time_precision(time->unit()));
+ }
+ case arrow::Type::TIMESTAMP: {
+ const auto timestamp =
std::static_pointer_cast<arrow::TimestampType>(arrow_type);
+ const auto doris_type = timestamp->timezone().empty() ?
TYPE_DATETIMEV2 : TYPE_TIMESTAMPTZ;
+ return nullable_primitive(doris_type, 0,
arrow_time_precision(timestamp->unit()));
+ }
+ case arrow::Type::DECIMAL128:
+ case arrow::Type::DECIMAL256: {
+ const auto decimal =
std::static_pointer_cast<arrow::DecimalType>(arrow_type);
+ const int precision = decimal->precision();
+ const PrimitiveType doris_decimal_type = precision <= 9 ?
TYPE_DECIMAL32
+ : precision <= 18 ?
TYPE_DECIMAL64
+ : precision <= 38 ?
TYPE_DECIMAL128I
+ :
TYPE_DECIMAL256;
+ return nullable_primitive(doris_decimal_type, precision,
decimal->scale());
+ }
+ case arrow::Type::LIST:
+ case arrow::Type::LARGE_LIST:
+ case arrow::Type::FIXED_SIZE_LIST: {
+ const auto list =
std::static_pointer_cast<arrow::BaseListType>(arrow_type);
+ DataTypePtr value_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(list->value_type(),
&value_type));
+ *doris_type =
make_nullable(std::make_shared<DataTypeArray>(value_type));
+ return Status::OK();
+ }
+ case arrow::Type::MAP: {
+ const auto map = std::static_pointer_cast<arrow::MapType>(arrow_type);
+ DataTypePtr key_type;
+ DataTypePtr item_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(map->key_type(), &key_type));
+ RETURN_IF_ERROR(arrow_type_to_doris_type(map->item_type(),
&item_type));
+ *doris_type = make_nullable(std::make_shared<DataTypeMap>(key_type,
item_type));
+ return Status::OK();
+ }
+ case arrow::Type::STRUCT: {
+ const auto struct_type =
std::static_pointer_cast<arrow::StructType>(arrow_type);
+ DataTypes field_types;
+ Strings field_names;
+ field_types.reserve(struct_type->num_fields());
+ field_names.reserve(struct_type->num_fields());
+ for (const auto& field : struct_type->fields()) {
+ DataTypePtr field_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(field->type(),
&field_type));
+ field_types.emplace_back(std::move(field_type));
+ field_names.emplace_back(field->name());
+ }
+ *doris_type =
make_nullable(std::make_shared<DataTypeStruct>(field_types, field_names));
+ return Status::OK();
+ }
+ default:
+ return Status::NotSupported("unsupported Lance Arrow type: {}",
arrow_type->ToString());
+ }
+}
+
+} // namespace
+
+LanceTableReader::~LanceTableReader() {
+ static_cast<void>(close());
+}
+
+Status LanceTableReader::fetch_schema(const TFileRangeDesc& range,
+ const TFileScanRangeParams& scan_params,
+ std::vector<std::string>* column_names,
+ std::vector<DataTypePtr>* column_types)
const {
+ if (column_names == nullptr || column_types == nullptr) {
+ return Status::InvalidArgument("Lance schema output must not be null");
+ }
+ RETURN_IF_ERROR(_validate_range(range));
+
+ const auto& params = range.table_format_params.lance_params;
+ const auto storage_options = _storage_options(&scan_params);
+ std::vector<const char*> storage_option_ptrs;
+ storage_option_ptrs.reserve(storage_options.size() + 1);
+ for (const auto& option : storage_options) {
+ storage_option_ptrs.emplace_back(option.c_str());
+ }
+ storage_option_ptrs.emplace_back(nullptr);
+
+ std::unique_ptr<LanceDataset, LanceDatasetDeleter> dataset(
+ lance_dataset_open(params.dataset_uri.c_str(),
+ storage_options.empty() ? nullptr :
storage_option_ptrs.data(),
+ static_cast<uint64_t>(params.version)));
+ if (dataset == nullptr) {
+ return _lance_error("open Lance dataset for schema");
+ }
+
+ ArrowSchema arrow_schema {};
+ if (lance_dataset_schema(dataset.get(), &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());
+ }
+
+ const auto schema = std::move(imported_schema).ValueUnsafe();
+ std::vector<std::string> parsed_names;
+ std::vector<DataTypePtr> parsed_types;
+ parsed_names.reserve(schema->num_fields());
+ parsed_types.reserve(schema->num_fields());
+ std::unordered_set<std::string> unique_names;
+ unique_names.reserve(schema->num_fields());
+ for (const auto& field : schema->fields()) {
+ if (!unique_names.emplace(field->name()).second) {
+ return Status::InvalidArgument("duplicate Lance schema column:
{}", field->name());
+ }
+ DataTypePtr doris_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(field->type(), &doris_type));
+ parsed_names.emplace_back(field->name());
+ parsed_types.emplace_back(std::move(doris_type));
+ }
+ *column_names = std::move(parsed_names);
+ *column_types = std::move(parsed_types);
+ return Status::OK();
+}
+
+Status LanceTableReader::init(TableReadOptions&& options) {
+ RETURN_IF_ERROR(TableReader::init(std::move(options)));
+ DORIS_CHECK(_runtime_state != nullptr);
+ DORIS_CHECK(_scanner_profile != nullptr);
+ DORIS_CHECK(_scan_params != nullptr);
+
+ _ctz = _runtime_state->timezone_obj();
+ _vector_search = _scan_params->__isset.external_search_request;
+ _search_split_prepared = false;
+ if (_vector_search) {
+ RETURN_IF_ERROR(_validate_external_search_request());
+ const auto& vector =
_scan_params->external_search_request.query.vector;
+ _scanner_profile->add_info_string("ExternalSearchType", "VECTOR");
+ _scanner_profile->add_info_string("LanceVectorColumn", vector.column);
+ _scanner_profile->add_info_string("LanceTopK",
std::to_string(vector.top_k));
+ _scanner_profile->add_info_string("LanceOffset",
std::to_string(vector.offset));
+ _scanner_profile->add_info_string("LanceMetric", vector.__isset.metric
+ ?
vector_metric_name(vector.metric)
+ : "default");
+ _scanner_profile->add_info_string("LanceVectorDimension",
+
std::to_string(vector.query_vector.dimension));
+ }
+ if (_scan_params->__isset.lance_substrait_filter) {
+ _scanner_profile->add_info_string("LancePushdownFormat", "SUBSTRAIT");
+ _scanner_profile->add_info_string(
+ "LanceSubstraitFilterBytes",
+ std::to_string(_scan_params->lance_substrait_filter.size()));
+ }
+
+ _output_name_to_idx.clear();
+ _output_name_to_idx.reserve(_projected_columns.size());
+ 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 (!_output_name_to_idx.emplace(column.name, idx).second) {
+ return Status::InvalidArgument("duplicate Lance projected column:
{}", column.name);
+ }
+ if (_vector_search && column.name == 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 {}",
+ DISTANCE_COLUMN, column.type->get_name());
+ }
+ }
+ }
+ return Status::OK();
+}
+
+Status LanceTableReader::prepare_split(const SplitReadOptions& options) {
+ RETURN_IF_ERROR(_validate_range(options.current_range));
+ _close_scanner();
+ _eof = false;
+
+ RETURN_IF_ERROR(TableReader::prepare_split(options));
+ // Lance does not currently provide metadata aggregate pushdown. Do not
let a generic
+ // table-level count supplied by a future planner bypass fragment reads.
+ _remaining_table_level_count = -1;
+ if (current_split_pruned()) {
+ return Status::OK();
+ }
+
+ if (_vector_search) {
+ const auto& lance_params =
options.current_range.table_format_params.lance_params;
+ if (lance_params.version <= 0) {
+ return Status::InvalidArgument(
+ "Lance vector search requires a fixed positive dataset
version");
+ }
+ if (lance_params.__isset.fragment_ids) {
+ return Status::InvalidArgument("Lance vector search split must not
set fragment ids");
+ }
+ if (_search_split_prepared) {
+ return Status::InvalidArgument(
+ "Lance vector search supports exactly one whole-dataset
split");
+ }
+ }
+
+ const auto key = _dataset_key(options.current_range);
+ if (_dataset == nullptr) {
+ RETURN_IF_ERROR(_open_dataset(key));
+ _opened_dataset_key = key;
+ } else if (!_opened_dataset_key.has_value() || *_opened_dataset_key !=
key) {
+ return Status::InvalidArgument(
+ "Lance reader cannot mix dataset snapshots or storage options
in one scan");
+ }
+
+ RETURN_IF_ERROR(_prepare_conjuncts());
+ RETURN_IF_ERROR(_open_scanner(options.current_range));
+ _search_split_prepared = _vector_search;
+ return Status::OK();
+}
+
+Status LanceTableReader::get_block(Block* block, bool* eos) {
+ DORIS_CHECK(block != nullptr);
+ DORIS_CHECK(eos != nullptr);
+ DORIS_CHECK(block->columns() == _projected_columns.size());
+ *eos = false;
+
+ if (_eof) {
+ *eos = true;
+ return Status::OK();
+ }
+ if (_scanner == nullptr) {
+ return Status::InternalError("Lance scanner is not initialized for the
current split");
+ }
+
+ const auto target_rows = std::max<size_t>(1, _scanner_batch_size);
+ while (true) {
+ block->clear_column_data(_projected_columns.size());
+ size_t raw_rows = 0;
+ while (raw_rows < target_rows) {
+ if (_io_ctx != nullptr && _io_ctx->should_stop) {
+ _eof = true;
+ _close_scanner();
+ *eos = true;
+ return Status::OK();
+ }
+
+ LanceBatch* raw_batch = nullptr;
+ const int32_t scan_status = lance_scanner_next(_scanner,
&raw_batch);
+ if (scan_status == 1) {
+ _eof = true;
+ _close_scanner();
+ break;
+ }
+ if (scan_status != 0 || raw_batch == nullptr) {
+ return _lance_error("read next Lance batch");
+ }
+
+ std::unique_ptr<LanceBatch, LanceBatchDeleter> batch(raw_batch);
+ size_t rows = 0;
+ RETURN_IF_ERROR(_fill_block_from_arrow(batch.get(), block, &rows));
+ _record_scan_rows(rows);
+ raw_rows += rows;
+ }
+
+ if (raw_rows == 0) {
+ DORIS_CHECK(_eof);
+ *eos = true;
+ return Status::OK();
+ }
+
+ if (!_conjuncts.empty()) {
Review Comment:
[P1] Do not evaluate residual predicates twice
`FileScannerV2` gives this reader a clone of every scanner conjunct, but
`Scanner::get_block()` always filters the returned block with the originals
afterward. Applying all clones here bypasses the generic TableReader safety
gate: a legal residual such as `rand() < 0.5` is evaluated by two independently
seeded contexts, so roughly 25% of rows survive instead of 50%. Please leave
residual evaluation to `Scanner`, or localize only predicates proven safe while
preserving a single semantic owner.
##########
be/src/format_v2/table/lance_reader.cpp:
##########
@@ -0,0 +1,912 @@
+// 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/table/lance_reader.h"
+
+#include <arrow/c/bridge.h>
+#include <arrow/record_batch.h>
+#include <arrow/type.h>
+#include <lance/lance.h>
+
+#include <bit>
+#include <cstring>
+#include <limits>
+#include <memory>
+
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_struct.h"
+#include "exec/common/endian.h"
+
+namespace doris::format::lance {
+namespace {
+
+struct LanceDatasetDeleter {
+ void operator()(LanceDataset* dataset) const {
lance_dataset_close(dataset); }
+};
+
+struct LanceScannerDeleter {
+ void operator()(LanceScanner* scanner) const {
lance_scanner_close(scanner); }
+};
+
+struct LanceBatchDeleter {
+ void operator()(LanceBatch* batch) const { lance_batch_free(batch); }
+};
+
+constexpr std::string_view DISTANCE_COLUMN = "_distance";
+
+size_t vector_element_width(TVectorElementType::type type) {
+ switch (type) {
+ case TVectorElementType::FLOAT16:
+ return sizeof(uint16_t);
+ case TVectorElementType::FLOAT32:
+ return sizeof(float);
+ case TVectorElementType::FLOAT64:
+ return sizeof(double);
+ case TVectorElementType::UINT8:
+ case TVectorElementType::INT8:
+ return sizeof(uint8_t);
+ }
+ return 0;
+}
+
+std::string vector_metric_name(TVectorMetric::type metric) {
+ switch (metric) {
+ case TVectorMetric::DEFAULT:
+ return "default";
+ case TVectorMetric::L2:
+ return "l2";
+ case TVectorMetric::COSINE:
+ return "cosine";
+ case TVectorMetric::DOT_PRODUCT:
+ return "dot";
+ case TVectorMetric::HAMMING:
+ return "hamming";
+ }
+ return "unknown";
+}
+
+int arrow_time_precision(arrow::TimeUnit::type unit) {
+ switch (unit) {
+ case arrow::TimeUnit::SECOND:
+ return 0;
+ case arrow::TimeUnit::MILLI:
+ return 3;
+ case arrow::TimeUnit::MICRO:
+ case arrow::TimeUnit::NANO:
+ return 6;
+ }
+ return 6;
+}
+
+Status arrow_type_to_doris_type(const std::shared_ptr<arrow::DataType>&
arrow_type,
+ DataTypePtr* doris_type) {
+ const auto nullable_primitive = [&](PrimitiveType type, int precision = 0,
int scale = 0,
+ int len = -1) {
+ *doris_type =
+ DataTypeFactory::instance().create_data_type(type, true,
precision, scale, len);
+ return Status::OK();
+ };
+
+ switch (arrow_type->id()) {
+ case arrow::Type::BOOL:
+ return nullable_primitive(TYPE_BOOLEAN);
+ case arrow::Type::INT8:
+ return nullable_primitive(TYPE_TINYINT);
+ case arrow::Type::UINT8:
+ case arrow::Type::INT16:
+ return nullable_primitive(TYPE_SMALLINT);
+ case arrow::Type::UINT16:
+ case arrow::Type::INT32:
+ return nullable_primitive(TYPE_INT);
+ case arrow::Type::UINT32:
+ case arrow::Type::INT64:
+ return nullable_primitive(TYPE_BIGINT);
+ case arrow::Type::UINT64:
+ return nullable_primitive(TYPE_LARGEINT);
+ case arrow::Type::HALF_FLOAT:
+ case arrow::Type::FLOAT:
+ return nullable_primitive(TYPE_FLOAT);
+ case arrow::Type::DOUBLE:
+ return nullable_primitive(TYPE_DOUBLE);
+ case arrow::Type::STRING:
+ case arrow::Type::LARGE_STRING:
+ return nullable_primitive(TYPE_STRING);
+ case arrow::Type::BINARY:
+ case arrow::Type::LARGE_BINARY:
+ return nullable_primitive(TYPE_VARBINARY, 0, 0,
std::numeric_limits<int32_t>::max());
+ case arrow::Type::FIXED_SIZE_BINARY: {
+ const auto binary =
std::static_pointer_cast<arrow::FixedSizeBinaryType>(arrow_type);
+ return nullable_primitive(TYPE_VARBINARY, 0, 0, binary->byte_width());
+ }
+ case arrow::Type::DATE32:
+ case arrow::Type::DATE64:
+ return nullable_primitive(TYPE_DATEV2);
+ case arrow::Type::TIME32:
+ case arrow::Type::TIME64: {
+ const auto time =
std::static_pointer_cast<arrow::TimeType>(arrow_type);
+ return nullable_primitive(TYPE_TIMEV2, 0,
arrow_time_precision(time->unit()));
+ }
+ case arrow::Type::TIMESTAMP: {
+ const auto timestamp =
std::static_pointer_cast<arrow::TimestampType>(arrow_type);
+ const auto doris_type = timestamp->timezone().empty() ?
TYPE_DATETIMEV2 : TYPE_TIMESTAMPTZ;
+ return nullable_primitive(doris_type, 0,
arrow_time_precision(timestamp->unit()));
+ }
+ case arrow::Type::DECIMAL128:
+ case arrow::Type::DECIMAL256: {
+ const auto decimal =
std::static_pointer_cast<arrow::DecimalType>(arrow_type);
+ const int precision = decimal->precision();
+ const PrimitiveType doris_decimal_type = precision <= 9 ?
TYPE_DECIMAL32
+ : precision <= 18 ?
TYPE_DECIMAL64
+ : precision <= 38 ?
TYPE_DECIMAL128I
+ :
TYPE_DECIMAL256;
+ return nullable_primitive(doris_decimal_type, precision,
decimal->scale());
Review Comment:
[P1] Reject negative-scale decimals before schema construction
Arrow permits negative decimal scales, but this passes the signed scale
directly into `DataTypeFactory`; conversion to the factory's unsigned scale
makes `create_decimal` throw. `fetch_table_schema` runs this path in a
heavy-work-pool task with no exception guard, so a local Lance dataset such as
`decimal(10,-2)` can terminate the BE instead of returning an
unsupported-schema error. Please validate precision/scale and return
`Status::NotSupported` before the factory call (matching `LanceTypeConverter`),
and add negative-scale local schema-fetch coverage.
##########
be/src/format_v2/table/lance_reader.cpp:
##########
@@ -0,0 +1,912 @@
+// 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/table/lance_reader.h"
+
+#include <arrow/c/bridge.h>
+#include <arrow/record_batch.h>
+#include <arrow/type.h>
+#include <lance/lance.h>
+
+#include <bit>
+#include <cstring>
+#include <limits>
+#include <memory>
+
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_struct.h"
+#include "exec/common/endian.h"
+
+namespace doris::format::lance {
+namespace {
+
+struct LanceDatasetDeleter {
+ void operator()(LanceDataset* dataset) const {
lance_dataset_close(dataset); }
+};
+
+struct LanceScannerDeleter {
+ void operator()(LanceScanner* scanner) const {
lance_scanner_close(scanner); }
+};
+
+struct LanceBatchDeleter {
+ void operator()(LanceBatch* batch) const { lance_batch_free(batch); }
+};
+
+constexpr std::string_view DISTANCE_COLUMN = "_distance";
+
+size_t vector_element_width(TVectorElementType::type type) {
+ switch (type) {
+ case TVectorElementType::FLOAT16:
+ return sizeof(uint16_t);
+ case TVectorElementType::FLOAT32:
+ return sizeof(float);
+ case TVectorElementType::FLOAT64:
+ return sizeof(double);
+ case TVectorElementType::UINT8:
+ case TVectorElementType::INT8:
+ return sizeof(uint8_t);
+ }
+ return 0;
+}
+
+std::string vector_metric_name(TVectorMetric::type metric) {
+ switch (metric) {
+ case TVectorMetric::DEFAULT:
+ return "default";
+ case TVectorMetric::L2:
+ return "l2";
+ case TVectorMetric::COSINE:
+ return "cosine";
+ case TVectorMetric::DOT_PRODUCT:
+ return "dot";
+ case TVectorMetric::HAMMING:
+ return "hamming";
+ }
+ return "unknown";
+}
+
+int arrow_time_precision(arrow::TimeUnit::type unit) {
+ switch (unit) {
+ case arrow::TimeUnit::SECOND:
+ return 0;
+ case arrow::TimeUnit::MILLI:
+ return 3;
+ case arrow::TimeUnit::MICRO:
+ case arrow::TimeUnit::NANO:
+ return 6;
+ }
+ return 6;
+}
+
+Status arrow_type_to_doris_type(const std::shared_ptr<arrow::DataType>&
arrow_type,
+ DataTypePtr* doris_type) {
+ const auto nullable_primitive = [&](PrimitiveType type, int precision = 0,
int scale = 0,
+ int len = -1) {
+ *doris_type =
+ DataTypeFactory::instance().create_data_type(type, true,
precision, scale, len);
+ return Status::OK();
+ };
+
+ switch (arrow_type->id()) {
+ case arrow::Type::BOOL:
+ return nullable_primitive(TYPE_BOOLEAN);
+ case arrow::Type::INT8:
+ return nullable_primitive(TYPE_TINYINT);
+ case arrow::Type::UINT8:
+ case arrow::Type::INT16:
+ return nullable_primitive(TYPE_SMALLINT);
+ case arrow::Type::UINT16:
+ case arrow::Type::INT32:
+ return nullable_primitive(TYPE_INT);
+ case arrow::Type::UINT32:
+ case arrow::Type::INT64:
+ return nullable_primitive(TYPE_BIGINT);
+ case arrow::Type::UINT64:
+ return nullable_primitive(TYPE_LARGEINT);
+ case arrow::Type::HALF_FLOAT:
+ case arrow::Type::FLOAT:
+ return nullable_primitive(TYPE_FLOAT);
+ case arrow::Type::DOUBLE:
+ return nullable_primitive(TYPE_DOUBLE);
+ case arrow::Type::STRING:
+ case arrow::Type::LARGE_STRING:
+ return nullable_primitive(TYPE_STRING);
+ case arrow::Type::BINARY:
+ case arrow::Type::LARGE_BINARY:
+ return nullable_primitive(TYPE_VARBINARY, 0, 0,
std::numeric_limits<int32_t>::max());
+ case arrow::Type::FIXED_SIZE_BINARY: {
+ const auto binary =
std::static_pointer_cast<arrow::FixedSizeBinaryType>(arrow_type);
+ return nullable_primitive(TYPE_VARBINARY, 0, 0, binary->byte_width());
+ }
+ case arrow::Type::DATE32:
+ case arrow::Type::DATE64:
+ return nullable_primitive(TYPE_DATEV2);
+ case arrow::Type::TIME32:
+ case arrow::Type::TIME64: {
+ const auto time =
std::static_pointer_cast<arrow::TimeType>(arrow_type);
+ return nullable_primitive(TYPE_TIMEV2, 0,
arrow_time_precision(time->unit()));
+ }
+ case arrow::Type::TIMESTAMP: {
+ const auto timestamp =
std::static_pointer_cast<arrow::TimestampType>(arrow_type);
+ const auto doris_type = timestamp->timezone().empty() ?
TYPE_DATETIMEV2 : TYPE_TIMESTAMPTZ;
+ return nullable_primitive(doris_type, 0,
arrow_time_precision(timestamp->unit()));
+ }
+ case arrow::Type::DECIMAL128:
+ case arrow::Type::DECIMAL256: {
+ const auto decimal =
std::static_pointer_cast<arrow::DecimalType>(arrow_type);
+ const int precision = decimal->precision();
+ const PrimitiveType doris_decimal_type = precision <= 9 ?
TYPE_DECIMAL32
+ : precision <= 18 ?
TYPE_DECIMAL64
+ : precision <= 38 ?
TYPE_DECIMAL128I
+ :
TYPE_DECIMAL256;
+ return nullable_primitive(doris_decimal_type, precision,
decimal->scale());
+ }
+ case arrow::Type::LIST:
+ case arrow::Type::LARGE_LIST:
+ case arrow::Type::FIXED_SIZE_LIST: {
+ const auto list =
std::static_pointer_cast<arrow::BaseListType>(arrow_type);
+ DataTypePtr value_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(list->value_type(),
&value_type));
+ *doris_type =
make_nullable(std::make_shared<DataTypeArray>(value_type));
+ return Status::OK();
+ }
+ case arrow::Type::MAP: {
+ const auto map = std::static_pointer_cast<arrow::MapType>(arrow_type);
+ DataTypePtr key_type;
+ DataTypePtr item_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(map->key_type(), &key_type));
+ RETURN_IF_ERROR(arrow_type_to_doris_type(map->item_type(),
&item_type));
+ *doris_type = make_nullable(std::make_shared<DataTypeMap>(key_type,
item_type));
+ return Status::OK();
+ }
+ case arrow::Type::STRUCT: {
+ const auto struct_type =
std::static_pointer_cast<arrow::StructType>(arrow_type);
+ DataTypes field_types;
+ Strings field_names;
+ field_types.reserve(struct_type->num_fields());
+ field_names.reserve(struct_type->num_fields());
+ for (const auto& field : struct_type->fields()) {
+ DataTypePtr field_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(field->type(),
&field_type));
+ field_types.emplace_back(std::move(field_type));
+ field_names.emplace_back(field->name());
+ }
+ *doris_type =
make_nullable(std::make_shared<DataTypeStruct>(field_types, field_names));
+ return Status::OK();
+ }
+ default:
+ return Status::NotSupported("unsupported Lance Arrow type: {}",
arrow_type->ToString());
+ }
+}
+
+} // namespace
+
+LanceTableReader::~LanceTableReader() {
+ static_cast<void>(close());
+}
+
+Status LanceTableReader::fetch_schema(const TFileRangeDesc& range,
+ const TFileScanRangeParams& scan_params,
+ std::vector<std::string>* column_names,
+ std::vector<DataTypePtr>* column_types)
const {
+ if (column_names == nullptr || column_types == nullptr) {
+ return Status::InvalidArgument("Lance schema output must not be null");
+ }
+ RETURN_IF_ERROR(_validate_range(range));
+
+ const auto& params = range.table_format_params.lance_params;
+ const auto storage_options = _storage_options(&scan_params);
+ std::vector<const char*> storage_option_ptrs;
+ storage_option_ptrs.reserve(storage_options.size() + 1);
+ for (const auto& option : storage_options) {
+ storage_option_ptrs.emplace_back(option.c_str());
+ }
+ storage_option_ptrs.emplace_back(nullptr);
+
+ std::unique_ptr<LanceDataset, LanceDatasetDeleter> dataset(
+ lance_dataset_open(params.dataset_uri.c_str(),
+ storage_options.empty() ? nullptr :
storage_option_ptrs.data(),
+ static_cast<uint64_t>(params.version)));
+ if (dataset == nullptr) {
+ return _lance_error("open Lance dataset for schema");
+ }
+
+ ArrowSchema arrow_schema {};
+ if (lance_dataset_schema(dataset.get(), &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());
+ }
+
+ const auto schema = std::move(imported_schema).ValueUnsafe();
+ std::vector<std::string> parsed_names;
+ std::vector<DataTypePtr> parsed_types;
+ parsed_names.reserve(schema->num_fields());
+ parsed_types.reserve(schema->num_fields());
+ std::unordered_set<std::string> unique_names;
+ unique_names.reserve(schema->num_fields());
+ for (const auto& field : schema->fields()) {
+ if (!unique_names.emplace(field->name()).second) {
+ return Status::InvalidArgument("duplicate Lance schema column:
{}", field->name());
+ }
+ DataTypePtr doris_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(field->type(), &doris_type));
+ parsed_names.emplace_back(field->name());
+ parsed_types.emplace_back(std::move(doris_type));
+ }
+ *column_names = std::move(parsed_names);
+ *column_types = std::move(parsed_types);
+ return Status::OK();
+}
+
+Status LanceTableReader::init(TableReadOptions&& options) {
+ RETURN_IF_ERROR(TableReader::init(std::move(options)));
+ DORIS_CHECK(_runtime_state != nullptr);
+ DORIS_CHECK(_scanner_profile != nullptr);
+ DORIS_CHECK(_scan_params != nullptr);
+
+ _ctz = _runtime_state->timezone_obj();
+ _vector_search = _scan_params->__isset.external_search_request;
+ _search_split_prepared = false;
+ if (_vector_search) {
+ RETURN_IF_ERROR(_validate_external_search_request());
+ const auto& vector =
_scan_params->external_search_request.query.vector;
+ _scanner_profile->add_info_string("ExternalSearchType", "VECTOR");
+ _scanner_profile->add_info_string("LanceVectorColumn", vector.column);
+ _scanner_profile->add_info_string("LanceTopK",
std::to_string(vector.top_k));
+ _scanner_profile->add_info_string("LanceOffset",
std::to_string(vector.offset));
+ _scanner_profile->add_info_string("LanceMetric", vector.__isset.metric
+ ?
vector_metric_name(vector.metric)
+ : "default");
+ _scanner_profile->add_info_string("LanceVectorDimension",
+
std::to_string(vector.query_vector.dimension));
+ }
+ if (_scan_params->__isset.lance_substrait_filter) {
+ _scanner_profile->add_info_string("LancePushdownFormat", "SUBSTRAIT");
+ _scanner_profile->add_info_string(
+ "LanceSubstraitFilterBytes",
+ std::to_string(_scan_params->lance_substrait_filter.size()));
+ }
+
+ _output_name_to_idx.clear();
+ _output_name_to_idx.reserve(_projected_columns.size());
+ 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 (!_output_name_to_idx.emplace(column.name, idx).second) {
+ return Status::InvalidArgument("duplicate Lance projected column:
{}", column.name);
+ }
+ if (_vector_search && column.name == 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 {}",
+ DISTANCE_COLUMN, column.type->get_name());
+ }
+ }
+ }
+ return Status::OK();
+}
+
+Status LanceTableReader::prepare_split(const SplitReadOptions& options) {
+ RETURN_IF_ERROR(_validate_range(options.current_range));
+ _close_scanner();
+ _eof = false;
+
+ RETURN_IF_ERROR(TableReader::prepare_split(options));
+ // Lance does not currently provide metadata aggregate pushdown. Do not
let a generic
+ // table-level count supplied by a future planner bypass fragment reads.
+ _remaining_table_level_count = -1;
+ if (current_split_pruned()) {
+ return Status::OK();
+ }
+
+ if (_vector_search) {
+ const auto& lance_params =
options.current_range.table_format_params.lance_params;
+ if (lance_params.version <= 0) {
+ return Status::InvalidArgument(
+ "Lance vector search requires a fixed positive dataset
version");
+ }
+ if (lance_params.__isset.fragment_ids) {
+ return Status::InvalidArgument("Lance vector search split must not
set fragment ids");
+ }
+ if (_search_split_prepared) {
+ return Status::InvalidArgument(
+ "Lance vector search supports exactly one whole-dataset
split");
+ }
+ }
+
+ const auto key = _dataset_key(options.current_range);
+ if (_dataset == nullptr) {
+ RETURN_IF_ERROR(_open_dataset(key));
+ _opened_dataset_key = key;
+ } else if (!_opened_dataset_key.has_value() || *_opened_dataset_key !=
key) {
+ return Status::InvalidArgument(
+ "Lance reader cannot mix dataset snapshots or storage options
in one scan");
+ }
+
+ RETURN_IF_ERROR(_prepare_conjuncts());
+ RETURN_IF_ERROR(_open_scanner(options.current_range));
+ _search_split_prepared = _vector_search;
+ return Status::OK();
+}
+
+Status LanceTableReader::get_block(Block* block, bool* eos) {
+ DORIS_CHECK(block != nullptr);
+ DORIS_CHECK(eos != nullptr);
+ DORIS_CHECK(block->columns() == _projected_columns.size());
+ *eos = false;
+
+ if (_eof) {
+ *eos = true;
+ return Status::OK();
+ }
+ if (_scanner == nullptr) {
+ return Status::InternalError("Lance scanner is not initialized for the
current split");
+ }
+
+ const auto target_rows = std::max<size_t>(1, _scanner_batch_size);
+ while (true) {
+ block->clear_column_data(_projected_columns.size());
+ size_t raw_rows = 0;
+ while (raw_rows < target_rows) {
+ if (_io_ctx != nullptr && _io_ctx->should_stop) {
+ _eof = true;
+ _close_scanner();
+ *eos = true;
+ return Status::OK();
+ }
+
+ LanceBatch* raw_batch = nullptr;
+ const int32_t scan_status = lance_scanner_next(_scanner,
&raw_batch);
+ if (scan_status == 1) {
+ _eof = true;
+ _close_scanner();
+ break;
+ }
+ if (scan_status != 0 || raw_batch == nullptr) {
+ return _lance_error("read next Lance batch");
+ }
+
+ std::unique_ptr<LanceBatch, LanceBatchDeleter> batch(raw_batch);
+ size_t rows = 0;
+ RETURN_IF_ERROR(_fill_block_from_arrow(batch.get(), block, &rows));
+ _record_scan_rows(rows);
+ raw_rows += rows;
+ }
+
+ if (raw_rows == 0) {
+ DORIS_CHECK(_eof);
+ *eos = true;
+ return Status::OK();
+ }
+
+ if (!_conjuncts.empty()) {
+ RETURN_IF_ERROR(VExprContext::filter_block(_conjuncts, block,
block->columns()));
+ }
+ if (block->rows() > 0) {
+ // Preserve a non-empty final block. The next get_block() observes
`_eof` and reports
+ // split EOF, matching the generic reader contract.
+ return Status::OK();
+ }
+ if (_eof) {
+ *eos = true;
+ return Status::OK();
+ }
+ }
+}
+
+Status LanceTableReader::abort_split() {
+ _close_scanner();
+ _eof = true;
+ return TableReader::abort_split();
+}
+
+Status LanceTableReader::close() {
+ _close_scanner();
+ _close_dataset();
+ _opened_dataset_key.reset();
+ _eof = true;
+ return TableReader::close();
+}
+
+Status LanceTableReader::_validate_range(const TFileRangeDesc& range) const {
+ if (!range.__isset.table_format_params ||
!range.table_format_params.__isset.lance_params) {
+ return Status::InvalidArgument("Lance split requires lance_params in
table format params");
+ }
+ const auto& params = range.table_format_params.lance_params;
+ if (!params.__isset.dataset_uri || params.dataset_uri.empty()) {
+ return Status::InvalidArgument("Lance split requires a non-empty
dataset_uri");
+ }
+ if (!params.__isset.version || params.version < 0) {
+ return Status::InvalidArgument("Lance split requires a non-negative
dataset version");
+ }
+ std::unordered_set<int64_t> unique_ids;
+ for (const auto fragment_id : params.fragment_ids) {
+ if (fragment_id < 0) {
+ return Status::InvalidArgument("Lance fragment id must be
non-negative: {}",
+ fragment_id);
+ }
+ if (!unique_ids.emplace(fragment_id).second) {
+ return Status::InvalidArgument("Lance split contains duplicate
fragment id: {}",
+ fragment_id);
+ }
+ }
+ return Status::OK();
+}
+
+Status LanceTableReader::_validate_external_search_request() const {
+ DORIS_CHECK(_scan_params != nullptr);
+ DORIS_CHECK(_scan_params->__isset.external_search_request);
+ if (_scan_params->__isset.lance_substrait_filter) {
+ return Status::InvalidArgument(
+ "Lance vector search cannot combine its pre-search filter with
"
+ "lance_substrait_filter");
+ }
+ const auto& request = _scan_params->external_search_request;
+ if (request.__isset.schema_version && request.schema_version != 1) {
+ return Status::NotSupported("unsupported external search schema
version: {}",
+ request.schema_version);
+ }
+ if (!request.__isset.query) {
+ return Status::InvalidArgument("external search request requires
query");
+ }
+
+ const bool has_vector = request.query.__isset.vector;
+ const bool has_full_text = request.query.__isset.full_text;
+ if (has_vector == has_full_text) {
+ return Status::InvalidArgument("external search query must set exactly
one search kind");
+ }
+ if (has_full_text) {
+ return Status::NotSupported("Lance Format V2 reader does not yet
support full-text search");
+ }
+
+ const auto& vector = request.query.vector;
+ if (!vector.__isset.column || vector.column.empty() ||
+ vector.column.find('\0') != std::string::npos) {
+ return Status::InvalidArgument("Lance vector search requires a
non-empty column");
+ }
+ if (!vector.__isset.query_vector) {
+ return Status::InvalidArgument("Lance vector search requires a query
vector");
+ }
+ const auto& query_vector = vector.query_vector;
+ if (!query_vector.__isset.element_type || !query_vector.__isset.dimension
||
+ !query_vector.__isset.values) {
+ return Status::InvalidArgument(
+ "Lance query vector requires element_type, dimension, and
values");
+ }
+ if (query_vector.dimension <= 0) {
+ return Status::InvalidArgument("Lance query vector dimension must be
positive: {}",
+ query_vector.dimension);
+ }
+ const auto element_width = vector_element_width(query_vector.element_type);
+ if (element_width == 0) {
+ return Status::NotSupported("unsupported Lance query vector element
type: {}",
+
static_cast<int>(query_vector.element_type));
+ }
+ const auto dimension = static_cast<size_t>(query_vector.dimension);
+ if (dimension > std::numeric_limits<size_t>::max() / element_width ||
+ query_vector.values.size() != dimension * element_width) {
+ return Status::InvalidArgument(
+ "Lance query vector byte size {} does not match dimension {}
and element width {}",
+ query_vector.values.size(), dimension, element_width);
+ }
+ if (!vector.__isset.top_k || vector.top_k <= 0) {
+ return Status::InvalidArgument("Lance vector search top_k must be
positive");
+ }
+ if (!vector.__isset.offset || vector.offset < 0) {
+ return Status::InvalidArgument("Lance vector search offset must be
non-negative");
+ }
+ constexpr auto UINT32_MAX_VALUE =
static_cast<int64_t>(std::numeric_limits<uint32_t>::max());
+ if (vector.offset > UINT32_MAX_VALUE || vector.top_k > UINT32_MAX_VALUE -
vector.offset) {
+ return Status::InvalidArgument("Lance vector search top_k + offset
exceeds uint32 range");
+ }
+
+ if (request.__isset.filter) {
+ const auto& filter = request.filter;
+ if (!filter.__isset.format || !filter.__isset.payload ||
filter.payload.empty()) {
+ return Status::InvalidArgument(
+ "external search filter requires format and non-empty
payload");
+ }
+ switch (filter.format) {
+ case TSearchFilterFormat::SQL:
+ if (filter.payload.find('\0') != std::string::npos) {
+ return Status::InvalidArgument(
+ "Lance SQL search filter contains an embedded NUL
byte");
+ }
+ break;
+ case TSearchFilterFormat::SUBSTRAIT:
+ break;
+ default:
+ return Status::NotSupported("unsupported external search filter
format: {}",
+ static_cast<int>(filter.format));
+ }
+ }
+
+ if (request.__isset.lance_options) {
+ const auto& options = request.lance_options;
+ if (options.__isset.nprobes && options.nprobes <= 0) {
+ return Status::InvalidArgument("Lance nprobes must be positive");
+ }
+ if (options.__isset.refine_factor && options.refine_factor <= 0) {
+ return Status::InvalidArgument("Lance refine_factor must be
positive");
+ }
+ if (options.__isset.ef && options.ef <= 0) {
+ return Status::InvalidArgument("Lance ef must be positive");
+ }
+ }
+ return Status::OK();
+}
+
+Status LanceTableReader::_open_dataset(const DatasetKey& key) {
+ std::vector<const char*> storage_option_ptrs;
+ storage_option_ptrs.reserve(key.storage_options.size() + 1);
+ for (const auto& option : key.storage_options) {
+ storage_option_ptrs.emplace_back(option.c_str());
+ }
+ storage_option_ptrs.emplace_back(nullptr);
+
+ _dataset = lance_dataset_open(
+ key.uri.c_str(), key.storage_options.empty() ? nullptr :
storage_option_ptrs.data(),
+ static_cast<uint64_t>(key.version));
+ if (_dataset == nullptr) {
+ return _lance_error("open Lance dataset");
+ }
+ return Status::OK();
+}
+
+Status LanceTableReader::_open_scanner(const TFileRangeDesc& range) {
+ std::vector<const char*> columns;
+ columns.reserve(_projected_columns.size() + 1);
+ for (const auto& column : _projected_columns) {
+ columns.emplace_back(column.name.c_str());
+ }
+ if (_vector_search && _projected_columns.empty()) {
+ columns.emplace_back(DISTANCE_COLUMN.data());
+ }
+ columns.emplace_back(nullptr);
+
+ const char* sql_filter = nullptr;
+ if (_vector_search) {
+ const auto& request = _scan_params->external_search_request;
+ if (request.__isset.filter && request.filter.format ==
TSearchFilterFormat::SQL) {
+ sql_filter = request.filter.payload.c_str();
+ }
+ }
+ LanceScanner* scanner =
+ lance_scanner_new(_dataset, columns.size() == 1 ? nullptr :
columns.data(), sql_filter);
+ if (scanner == nullptr) {
+ return _lance_error("create Lance scanner");
+ }
+ std::unique_ptr<LanceScanner, LanceScannerDeleter> scanner_guard(scanner);
+
+ if (_scan_params->__isset.lance_substrait_filter &&
+ !_scan_params->lance_substrait_filter.empty()) {
+ const auto& filter = _scan_params->lance_substrait_filter;
+ if (lance_scanner_set_substrait_filter(
+ scanner, reinterpret_cast<const uint8_t*>(filter.data()),
filter.size()) != 0) {
+ return _lance_error("set Lance Substrait filter");
+ }
+ }
+ if (_vector_search) {
+ const auto& request = _scan_params->external_search_request;
+ if (request.__isset.filter && request.filter.format ==
TSearchFilterFormat::SUBSTRAIT) {
+ const auto& filter = request.filter.payload;
+ if (lance_scanner_set_substrait_filter(scanner,
+ reinterpret_cast<const
uint8_t*>(filter.data()),
+ filter.size()) != 0) {
+ return _lance_error("set Lance vector search Substrait
filter");
+ }
+ }
+ }
+
+ const auto batch_size = _batch_size > 0 ? _batch_size :
_runtime_state->batch_size();
Review Comment:
[P2] Keep Lance batches within the adaptive byte budget
`FORMAT_LANCE` is absent from FileScannerV2's adaptive-batch list, so this
normally configures thousands of rows immediately instead of the 32-row probe
used to learn wide/nested row size. The size is also copied into
`_scanner_batch_size`, so later `set_batch_size()` updates cannot affect the
open scanner. A Lance table with large strings, arrays, maps, or vectors can
therefore materialize an enormous Arrow batch plus its Doris copy despite
`preferred_block_size_bytes`. Please enable the probe and make subsequent
predicted sizes reach lance-c, or enforce an equivalent byte bound.
##########
fe/fe-core/src/main/java/org/apache/doris/datasource/lance/LanceExternalCatalog.java:
##########
@@ -0,0 +1,551 @@
+// 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.datasource.lance;
+
+import org.apache.doris.common.DdlException;
+import org.apache.doris.datasource.CatalogProperty;
+import org.apache.doris.datasource.ExternalCatalog;
+import org.apache.doris.datasource.InitCatalogLog;
+import org.apache.doris.datasource.SessionContext;
+
+import org.apache.arrow.memory.BufferAllocator;
+import org.apache.arrow.memory.RootAllocator;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.lance.namespace.LanceNamespace;
+import org.lance.namespace.model.DescribeTableRequest;
+import org.lance.namespace.model.DescribeTableResponse;
+import org.lance.namespace.model.ListNamespacesRequest;
+import org.lance.namespace.model.ListNamespacesResponse;
+import org.lance.namespace.model.ListTablesRequest;
+import org.lance.namespace.model.ListTablesResponse;
+
+import java.net.URI;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.regex.Pattern;
+
+/** Read-only Lance Directory or REST Namespace catalog. */
+public class LanceExternalCatalog extends ExternalCatalog {
+ public static final String LANCE_CATALOG_TYPE = "lance.catalog.type";
+ public static final String LANCE_FILESYSTEM = "filesystem";
+ public static final String LANCE_REST = "rest";
+ public static final String WAREHOUSE = "warehouse";
+ public static final String NAMESPACE_PARENT = "lance.namespace.parent";
+ public static final String NAMESPACE_DELIMITER =
"lance.namespace.delimiter";
+ public static final String ROOT_DATABASE = "lance.namespace.root_database";
+ public static final String REST_URI = "lance.rest.uri";
+ public static final String REST_SECURITY_TYPE = "lance.rest.security.type";
+ public static final String REST_BEARER_TOKEN = "lance.rest.bearer-token";
+ public static final String REST_API_KEY = "lance.rest.api-key";
+ public static final String REST_HEADER_PREFIX = "lance.rest.header.";
+
+ private static final String DEFAULT_DELIMITER = "$";
+ private static final String DEFAULT_ROOT_DATABASE = "default";
+ private static final String DATABASE_NAMESPACE_DELIMITER = ".";
+ private static final String REST_SECURITY_NONE = "none";
+ private static final String REST_SECURITY_BEARER = "bearer";
+ private static final String REST_SECURITY_API_KEY = "api_key";
+ private static final Pattern HTTP_HEADER_NAME =
Pattern.compile("^[!#$%&'*+.^_`|~0-9A-Za-z-]+$");
+ private static final int PAGE_SIZE = 1000;
+ private static final long ALLOCATOR_LIMIT = 256L * 1024 * 1024;
+
+ private transient LanceNamespace namespace;
+ private transient BufferAllocator allocator;
+ private transient List<String> parentNamespace = Collections.emptyList();
+ private transient String catalogType;
+ private transient String namespaceDelimiter;
+ private transient String rootDatabase;
+ private transient Map<String, String> javaStorageOptions =
Collections.emptyMap();
+ private transient Map<String, String> backendStorageOptions =
Collections.emptyMap();
+ private transient Object namespaceLock = new Object();
+
+ public LanceExternalCatalog(long catalogId, String name, String resource,
Map<String, String> props,
+ String comment) {
+ super(catalogId, name, InitCatalogLog.Type.LANCE, comment);
+ catalogProperty = new CatalogProperty(resource, props);
+ }
+
+ @Override
+ protected void initLocalObjectsImpl() {
+ try {
+ namespaceLock = new Object();
+ catalogType = normalizedCatalogType();
+ namespaceDelimiter =
catalogProperty.getOrDefault(NAMESPACE_DELIMITER, DEFAULT_DELIMITER);
+ rootDatabase = catalogProperty.getOrDefault(ROOT_DATABASE,
DEFAULT_ROOT_DATABASE);
+ parentNamespace = LanceNamespaceName.parseParent(
+ catalogProperty.getOrDefault(NAMESPACE_PARENT, ""),
namespaceDelimiter);
+ backendStorageOptions =
catalogProperty.getBackendStorageProperties();
+ javaStorageOptions =
LanceStorageOptions.forJavaSdk(backendStorageOptions);
+
+ allocator = new RootAllocator(ALLOCATOR_LIMIT);
+ namespace = connectNamespace(catalogType, allocator,
javaStorageOptions);
+ } catch (Exception e) {
+ closeLanceObjects();
+ throw new RuntimeException("Failed to initialize Lance " +
normalizedCatalogType() + " catalog '"
+ + getName()
+ + "': " + sanitizedRootCauseMessage(e), safeCause(e));
+ }
+ }
+
+ @Override
+ public void checkWhenCreating() throws DdlException {
+ checkProperties();
+ boolean testConnection = Boolean.parseBoolean(
+ catalogProperty.getOrDefault(TEST_CONNECTION,
String.valueOf(DEFAULT_TEST_CONNECTION)));
+ if (!testConnection) {
+ return;
+ }
+
+ Map<String, String> storageOptions = LanceStorageOptions.forJavaSdk(
+ catalogProperty.getBackendStorageProperties());
+ String delimiter = catalogProperty.getOrDefault(NAMESPACE_DELIMITER,
DEFAULT_DELIMITER);
+ List<String> parent = LanceNamespaceName.parseParent(
+ catalogProperty.getOrDefault(NAMESPACE_PARENT, ""), delimiter);
+ String type = normalizedCatalogType();
+ try (BufferAllocator testAllocator = new
RootAllocator(ALLOCATOR_LIMIT)) {
+ LanceNamespace testNamespace = connectNamespace(type,
testAllocator, storageOptions);
+ try {
+ testNamespace.listTables(new
ListTablesRequest().id(parent).limit(1));
+ testNamespace.listNamespaces(new
ListNamespacesRequest().id(parent).limit(1));
+ } finally {
+ closeNamespace(testNamespace);
+ }
+ } catch (Exception e) {
+ throw new DdlException("Lance " + type + " catalog connectivity
test failed: "
+ + sanitizedRootCauseMessage(e), safeCause(e));
+ }
+ }
+
+ private LanceNamespace connectNamespace(String type, BufferAllocator
namespaceAllocator,
+ Map<String, String> storageOptions) {
+ if (LANCE_FILESYSTEM.equals(type)) {
+ return connectDirectoryNamespace(namespaceAllocator,
storageOptions);
+ }
+ if (LANCE_REST.equals(type)) {
+ return connectRestNamespace(namespaceAllocator);
+ }
+ throw new IllegalArgumentException("Unsupported Lance catalog type '"
+ type + "'");
+ }
+
+ private LanceNamespace connectDirectoryNamespace(BufferAllocator
namespaceAllocator,
+ Map<String, String> storageOptions) {
+ Map<String, String> namespaceProperties = new HashMap<>();
+ namespaceProperties.put("root",
catalogProperty.getOrDefault(WAREHOUSE, ""));
+ storageOptions.forEach((key, value) ->
namespaceProperties.put("storage." + key, value));
+ return LanceNamespace.connect("dir", namespaceProperties,
namespaceAllocator);
+ }
+
+ private LanceNamespace connectRestNamespace(BufferAllocator
namespaceAllocator) {
+ Map<String, String> namespaceProperties = new HashMap<>();
+ namespaceProperties.put("uri", normalizedRestUri());
+ namespaceProperties.put("delimiter",
+ catalogProperty.getOrDefault(NAMESPACE_DELIMITER,
DEFAULT_DELIMITER));
+
+ Map<String, String> properties = catalogProperty.getProperties();
+ properties.forEach((key, value) -> {
+ if (key.startsWith(REST_HEADER_PREFIX)) {
+ namespaceProperties.put("header." +
key.substring(REST_HEADER_PREFIX.length()), value);
+ }
+ });
+ String securityType = properties.getOrDefault(REST_SECURITY_TYPE,
REST_SECURITY_NONE)
+ .trim().toLowerCase(Locale.ROOT);
+ if (REST_SECURITY_BEARER.equals(securityType)) {
+ namespaceProperties.put("header.Authorization", "Bearer " +
properties.get(REST_BEARER_TOKEN));
+ } else if (REST_SECURITY_API_KEY.equals(securityType)) {
+ namespaceProperties.put("header.x-api-key",
properties.get(REST_API_KEY));
+ }
+ return LanceNamespace.connect("rest", namespaceProperties,
namespaceAllocator);
+ }
+
+ @Override
+ public void checkProperties() throws DdlException {
+ super.checkProperties();
+ Map<String, String> properties = catalogProperty.getProperties();
+ String type = normalizedCatalogType();
+ if (!LANCE_FILESYSTEM.equals(type) && !LANCE_REST.equals(type)) {
+ throw new DdlException("Property '" + LANCE_CATALOG_TYPE
+ + "' must be 'filesystem' or 'rest', but was '" + type +
"'");
+ }
+ validateCommonProperties(properties);
+ if (LANCE_FILESYSTEM.equals(type)) {
+ validateFilesystemProperties(properties);
+ } else {
+ validateRestProperties(properties);
+ }
+ }
+
+ private void validateCommonProperties(Map<String, String> properties)
throws DdlException {
+ String delimiter = properties.getOrDefault(NAMESPACE_DELIMITER,
DEFAULT_DELIMITER);
+ if (delimiter.isEmpty() || delimiter.indexOf('\\') >= 0) {
+ throw new DdlException("Property '" + NAMESPACE_DELIMITER
+ + "' cannot be empty or contain the escape character
'\\'");
+ }
+ String rootDb = properties.getOrDefault(ROOT_DATABASE,
DEFAULT_ROOT_DATABASE);
+ if (StringUtils.isBlank(rootDb)) {
+ throw new DdlException("Property '" + ROOT_DATABASE
+ + "' must be non-empty");
+ }
+
LanceNamespaceName.parseParent(properties.getOrDefault(NAMESPACE_PARENT, ""),
delimiter);
+ }
+
+ private void validateFilesystemProperties(Map<String, String> properties)
throws DdlException {
+ String warehouse = properties.get(WAREHOUSE);
+ if (StringUtils.isBlank(warehouse)) {
+ throw new DdlException("Missing required property 'warehouse' for
Lance filesystem catalog");
+ }
+ validateWarehouse(warehouse);
+ for (String key : properties.keySet()) {
+ if (key.startsWith("lance.rest.")) {
+ throw new DdlException("Property '" + key + "' is not valid
for Lance filesystem catalog");
+ }
+ }
+ }
+
+ private void validateRestProperties(Map<String, String> properties) throws
DdlException {
+ if (properties.containsKey(WAREHOUSE)) {
+ throw new DdlException("Property 'warehouse' is not valid for
Lance REST catalog");
+ }
+ String restUri = properties.get(REST_URI);
+ if (StringUtils.isBlank(restUri)) {
+ throw new DdlException("Missing required property '" + REST_URI +
"' for Lance REST catalog");
+ }
+ validateRestUri(restUri);
+
+ String securityType = properties.getOrDefault(REST_SECURITY_TYPE,
REST_SECURITY_NONE)
+ .trim().toLowerCase(Locale.ROOT);
+ boolean bearerTokenConfigured =
properties.containsKey(REST_BEARER_TOKEN);
+ boolean apiKeyConfigured = properties.containsKey(REST_API_KEY);
+ boolean hasBearerToken =
StringUtils.isNotBlank(properties.get(REST_BEARER_TOKEN));
+ boolean hasApiKey =
StringUtils.isNotBlank(properties.get(REST_API_KEY));
+ switch (securityType) {
+ case REST_SECURITY_NONE:
+ if (bearerTokenConfigured || apiKeyConfigured) {
+ throw new DdlException("Lance REST security type 'none'
cannot configure '"
+ + REST_BEARER_TOKEN + "' or '" + REST_API_KEY +
"'");
+ }
+ break;
+ case REST_SECURITY_BEARER:
+ if (!hasBearerToken || apiKeyConfigured) {
+ throw new DdlException("Lance REST security type 'bearer'
requires only '"
+ + REST_BEARER_TOKEN + "'");
+ }
+ validateCredentialHeaderValue(REST_BEARER_TOKEN,
properties.get(REST_BEARER_TOKEN));
+ break;
+ case REST_SECURITY_API_KEY:
+ if (!hasApiKey || bearerTokenConfigured) {
+ throw new DdlException("Lance REST security type 'api_key'
requires only '"
+ + REST_API_KEY + "'");
+ }
+ validateCredentialHeaderValue(REST_API_KEY,
properties.get(REST_API_KEY));
+ break;
+ default:
+ throw new DdlException("Property '" + REST_SECURITY_TYPE
+ + "' must be 'none', 'bearer', or 'api_key'");
+ }
+
+ Set<String> supportedKeys = new HashSet<>();
+ Collections.addAll(supportedKeys, REST_URI, REST_SECURITY_TYPE,
REST_BEARER_TOKEN, REST_API_KEY);
+ for (Map.Entry<String, String> entry : properties.entrySet()) {
+ String key = entry.getKey();
+ if (!key.startsWith("lance.rest.") || supportedKeys.contains(key))
{
+ continue;
+ }
+ if (!key.startsWith(REST_HEADER_PREFIX)) {
+ throw new DdlException("Unsupported Lance REST property '" +
key + "'");
+ }
+ validateRestHeader(key.substring(REST_HEADER_PREFIX.length()),
entry.getValue());
+ }
+ }
+
+ private void validateRestHeader(String headerName, String headerValue)
throws DdlException {
+ if (StringUtils.isBlank(headerName) ||
!HTTP_HEADER_NAME.matcher(headerName).matches()) {
+ throw new DdlException("Invalid HTTP header name in property '" +
REST_HEADER_PREFIX
+ + headerName + "'");
+ }
+ if ("authorization".equalsIgnoreCase(headerName) ||
"x-api-key".equalsIgnoreCase(headerName)) {
+ throw new DdlException("Authentication header '" + headerName + "'
must be configured through '"
+ + REST_SECURITY_TYPE + "'");
+ }
+ if (headerValue == null || headerValue.indexOf('\r') >= 0 ||
headerValue.indexOf('\n') >= 0) {
+ throw new DdlException("Invalid HTTP header value in property '" +
REST_HEADER_PREFIX
+ + headerName + "'");
+ }
+ }
+
+ private void validateCredentialHeaderValue(String propertyName, String
value) throws DdlException {
+ if (value.indexOf('\r') >= 0 || value.indexOf('\n') >= 0) {
+ throw new DdlException("Invalid HTTP credential value in property
'" + propertyName + "'");
+ }
+ }
+
+ private void validateRestUri(String value) throws DdlException {
+ URI uri;
+ try {
+ uri = URI.create(value.trim());
+ } catch (IllegalArgumentException e) {
+ throw new DdlException("Invalid Lance REST URI in property '" +
REST_URI + "'", e);
+ }
+ String scheme = uri.getScheme();
+ if (scheme == null || (!("http".equalsIgnoreCase(scheme)) &&
!("https".equalsIgnoreCase(scheme)))) {
+ throw new DdlException("Property '" + REST_URI + "' must use http
or https");
+ }
+ if (StringUtils.isBlank(uri.getRawAuthority()) || uri.getRawUserInfo()
!= null
+ || uri.getRawQuery() != null || uri.getRawFragment() != null) {
+ throw new DdlException("Property '" + REST_URI
+ + "' must contain an authority and cannot contain
user-info, query, or fragment");
+ }
+ }
+
+ private String normalizedCatalogType() {
+ return catalogProperty.getOrDefault(LANCE_CATALOG_TYPE,
LANCE_FILESYSTEM)
+ .trim().toLowerCase(Locale.ROOT);
+ }
+
+ private String normalizedRestUri() {
+ String uri = catalogProperty.getOrDefault(REST_URI, "").trim();
+ while (uri.endsWith("/")) {
+ uri = uri.substring(0, uri.length() - 1);
+ }
+ return uri;
+ }
+
+ private void validateWarehouse(String warehouse) throws DdlException {
+ URI uri;
+ try {
+ uri = URI.create(warehouse);
+ } catch (IllegalArgumentException e) {
+ throw new DdlException("Invalid Lance warehouse URI: " +
warehouse, e);
+ }
+ if (uri.getScheme() == null) {
+ Path path = Paths.get(warehouse);
+ if (!path.isAbsolute()) {
+ throw new DdlException("Local Lance warehouse must be an
absolute path: " + warehouse);
+ }
+ return;
+ }
+ String scheme = uri.getScheme().toLowerCase();
+ if (!"file".equals(scheme) && !"s3".equals(scheme)) {
+ throw new DdlException("Unsupported Lance filesystem warehouse
scheme '" + scheme
+ + "'; first phase supports local/file and s3");
Review Comment:
[P1] Do not advertise FE-local warehouses without a placement contract
This explicitly accepts bare and `file:` warehouses, which the FE opens
successfully for namespace and metadata work. The resulting `LanceSplit` has no
host locality, however, so ordinary scan planning assigns its local URI to
arbitrary BEs; on a normal multi-host deployment those BEs see a different or
nonexistent filesystem and queries fail after catalog validation succeeded.
Please reject local/file catalog warehouses, or require an explicit
shared-mount contract and constrain placement to BEs known to see that path.
##########
be/src/format_v2/table/lance_reader.cpp:
##########
@@ -0,0 +1,912 @@
+// 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/table/lance_reader.h"
+
+#include <arrow/c/bridge.h>
+#include <arrow/record_batch.h>
+#include <arrow/type.h>
+#include <lance/lance.h>
+
+#include <bit>
+#include <cstring>
+#include <limits>
+#include <memory>
+
+#include "core/data_type/data_type_array.h"
+#include "core/data_type/data_type_factory.hpp"
+#include "core/data_type/data_type_map.h"
+#include "core/data_type/data_type_struct.h"
+#include "exec/common/endian.h"
+
+namespace doris::format::lance {
+namespace {
+
+struct LanceDatasetDeleter {
+ void operator()(LanceDataset* dataset) const {
lance_dataset_close(dataset); }
+};
+
+struct LanceScannerDeleter {
+ void operator()(LanceScanner* scanner) const {
lance_scanner_close(scanner); }
+};
+
+struct LanceBatchDeleter {
+ void operator()(LanceBatch* batch) const { lance_batch_free(batch); }
+};
+
+constexpr std::string_view DISTANCE_COLUMN = "_distance";
+
+size_t vector_element_width(TVectorElementType::type type) {
+ switch (type) {
+ case TVectorElementType::FLOAT16:
+ return sizeof(uint16_t);
+ case TVectorElementType::FLOAT32:
+ return sizeof(float);
+ case TVectorElementType::FLOAT64:
+ return sizeof(double);
+ case TVectorElementType::UINT8:
+ case TVectorElementType::INT8:
+ return sizeof(uint8_t);
+ }
+ return 0;
+}
+
+std::string vector_metric_name(TVectorMetric::type metric) {
+ switch (metric) {
+ case TVectorMetric::DEFAULT:
+ return "default";
+ case TVectorMetric::L2:
+ return "l2";
+ case TVectorMetric::COSINE:
+ return "cosine";
+ case TVectorMetric::DOT_PRODUCT:
+ return "dot";
+ case TVectorMetric::HAMMING:
+ return "hamming";
+ }
+ return "unknown";
+}
+
+int arrow_time_precision(arrow::TimeUnit::type unit) {
+ switch (unit) {
+ case arrow::TimeUnit::SECOND:
+ return 0;
+ case arrow::TimeUnit::MILLI:
+ return 3;
+ case arrow::TimeUnit::MICRO:
+ case arrow::TimeUnit::NANO:
+ return 6;
+ }
+ return 6;
+}
+
+Status arrow_type_to_doris_type(const std::shared_ptr<arrow::DataType>&
arrow_type,
+ DataTypePtr* doris_type) {
+ const auto nullable_primitive = [&](PrimitiveType type, int precision = 0,
int scale = 0,
+ int len = -1) {
+ *doris_type =
+ DataTypeFactory::instance().create_data_type(type, true,
precision, scale, len);
+ return Status::OK();
+ };
+
+ switch (arrow_type->id()) {
+ case arrow::Type::BOOL:
+ return nullable_primitive(TYPE_BOOLEAN);
+ case arrow::Type::INT8:
+ return nullable_primitive(TYPE_TINYINT);
+ case arrow::Type::UINT8:
+ case arrow::Type::INT16:
+ return nullable_primitive(TYPE_SMALLINT);
+ case arrow::Type::UINT16:
+ case arrow::Type::INT32:
+ return nullable_primitive(TYPE_INT);
+ case arrow::Type::UINT32:
+ case arrow::Type::INT64:
+ return nullable_primitive(TYPE_BIGINT);
+ case arrow::Type::UINT64:
+ return nullable_primitive(TYPE_LARGEINT);
+ case arrow::Type::HALF_FLOAT:
+ case arrow::Type::FLOAT:
+ return nullable_primitive(TYPE_FLOAT);
+ case arrow::Type::DOUBLE:
+ return nullable_primitive(TYPE_DOUBLE);
+ case arrow::Type::STRING:
+ case arrow::Type::LARGE_STRING:
+ return nullable_primitive(TYPE_STRING);
+ case arrow::Type::BINARY:
+ case arrow::Type::LARGE_BINARY:
+ return nullable_primitive(TYPE_VARBINARY, 0, 0,
std::numeric_limits<int32_t>::max());
+ case arrow::Type::FIXED_SIZE_BINARY: {
+ const auto binary =
std::static_pointer_cast<arrow::FixedSizeBinaryType>(arrow_type);
+ return nullable_primitive(TYPE_VARBINARY, 0, 0, binary->byte_width());
+ }
+ case arrow::Type::DATE32:
+ case arrow::Type::DATE64:
+ return nullable_primitive(TYPE_DATEV2);
+ case arrow::Type::TIME32:
+ case arrow::Type::TIME64: {
+ const auto time =
std::static_pointer_cast<arrow::TimeType>(arrow_type);
+ return nullable_primitive(TYPE_TIMEV2, 0,
arrow_time_precision(time->unit()));
+ }
+ case arrow::Type::TIMESTAMP: {
+ const auto timestamp =
std::static_pointer_cast<arrow::TimestampType>(arrow_type);
+ const auto doris_type = timestamp->timezone().empty() ?
TYPE_DATETIMEV2 : TYPE_TIMESTAMPTZ;
+ return nullable_primitive(doris_type, 0,
arrow_time_precision(timestamp->unit()));
+ }
+ case arrow::Type::DECIMAL128:
+ case arrow::Type::DECIMAL256: {
+ const auto decimal =
std::static_pointer_cast<arrow::DecimalType>(arrow_type);
+ const int precision = decimal->precision();
+ const PrimitiveType doris_decimal_type = precision <= 9 ?
TYPE_DECIMAL32
+ : precision <= 18 ?
TYPE_DECIMAL64
+ : precision <= 38 ?
TYPE_DECIMAL128I
+ :
TYPE_DECIMAL256;
+ return nullable_primitive(doris_decimal_type, precision,
decimal->scale());
+ }
+ case arrow::Type::LIST:
+ case arrow::Type::LARGE_LIST:
+ case arrow::Type::FIXED_SIZE_LIST: {
+ const auto list =
std::static_pointer_cast<arrow::BaseListType>(arrow_type);
+ DataTypePtr value_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(list->value_type(),
&value_type));
+ *doris_type =
make_nullable(std::make_shared<DataTypeArray>(value_type));
+ return Status::OK();
+ }
+ case arrow::Type::MAP: {
+ const auto map = std::static_pointer_cast<arrow::MapType>(arrow_type);
+ DataTypePtr key_type;
+ DataTypePtr item_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(map->key_type(), &key_type));
+ RETURN_IF_ERROR(arrow_type_to_doris_type(map->item_type(),
&item_type));
+ *doris_type = make_nullable(std::make_shared<DataTypeMap>(key_type,
item_type));
+ return Status::OK();
+ }
+ case arrow::Type::STRUCT: {
+ const auto struct_type =
std::static_pointer_cast<arrow::StructType>(arrow_type);
+ DataTypes field_types;
+ Strings field_names;
+ field_types.reserve(struct_type->num_fields());
+ field_names.reserve(struct_type->num_fields());
+ for (const auto& field : struct_type->fields()) {
+ DataTypePtr field_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(field->type(),
&field_type));
+ field_types.emplace_back(std::move(field_type));
+ field_names.emplace_back(field->name());
+ }
+ *doris_type =
make_nullable(std::make_shared<DataTypeStruct>(field_types, field_names));
+ return Status::OK();
+ }
+ default:
+ return Status::NotSupported("unsupported Lance Arrow type: {}",
arrow_type->ToString());
+ }
+}
+
+} // namespace
+
+LanceTableReader::~LanceTableReader() {
+ static_cast<void>(close());
+}
+
+Status LanceTableReader::fetch_schema(const TFileRangeDesc& range,
+ const TFileScanRangeParams& scan_params,
+ std::vector<std::string>* column_names,
+ std::vector<DataTypePtr>* column_types)
const {
+ if (column_names == nullptr || column_types == nullptr) {
+ return Status::InvalidArgument("Lance schema output must not be null");
+ }
+ RETURN_IF_ERROR(_validate_range(range));
+
+ const auto& params = range.table_format_params.lance_params;
+ const auto storage_options = _storage_options(&scan_params);
+ std::vector<const char*> storage_option_ptrs;
+ storage_option_ptrs.reserve(storage_options.size() + 1);
+ for (const auto& option : storage_options) {
+ storage_option_ptrs.emplace_back(option.c_str());
+ }
+ storage_option_ptrs.emplace_back(nullptr);
+
+ std::unique_ptr<LanceDataset, LanceDatasetDeleter> dataset(
+ lance_dataset_open(params.dataset_uri.c_str(),
+ storage_options.empty() ? nullptr :
storage_option_ptrs.data(),
+ static_cast<uint64_t>(params.version)));
+ if (dataset == nullptr) {
+ return _lance_error("open Lance dataset for schema");
+ }
+
+ ArrowSchema arrow_schema {};
+ if (lance_dataset_schema(dataset.get(), &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());
+ }
+
+ const auto schema = std::move(imported_schema).ValueUnsafe();
+ std::vector<std::string> parsed_names;
+ std::vector<DataTypePtr> parsed_types;
+ parsed_names.reserve(schema->num_fields());
+ parsed_types.reserve(schema->num_fields());
+ std::unordered_set<std::string> unique_names;
+ unique_names.reserve(schema->num_fields());
+ for (const auto& field : schema->fields()) {
+ if (!unique_names.emplace(field->name()).second) {
+ return Status::InvalidArgument("duplicate Lance schema column:
{}", field->name());
+ }
+ DataTypePtr doris_type;
+ RETURN_IF_ERROR(arrow_type_to_doris_type(field->type(), &doris_type));
+ parsed_names.emplace_back(field->name());
+ parsed_types.emplace_back(std::move(doris_type));
+ }
+ *column_names = std::move(parsed_names);
+ *column_types = std::move(parsed_types);
+ return Status::OK();
+}
+
+Status LanceTableReader::init(TableReadOptions&& options) {
+ RETURN_IF_ERROR(TableReader::init(std::move(options)));
+ DORIS_CHECK(_runtime_state != nullptr);
+ DORIS_CHECK(_scanner_profile != nullptr);
+ DORIS_CHECK(_scan_params != nullptr);
+
+ _ctz = _runtime_state->timezone_obj();
+ _vector_search = _scan_params->__isset.external_search_request;
+ _search_split_prepared = false;
+ if (_vector_search) {
+ RETURN_IF_ERROR(_validate_external_search_request());
+ const auto& vector =
_scan_params->external_search_request.query.vector;
+ _scanner_profile->add_info_string("ExternalSearchType", "VECTOR");
+ _scanner_profile->add_info_string("LanceVectorColumn", vector.column);
+ _scanner_profile->add_info_string("LanceTopK",
std::to_string(vector.top_k));
+ _scanner_profile->add_info_string("LanceOffset",
std::to_string(vector.offset));
+ _scanner_profile->add_info_string("LanceMetric", vector.__isset.metric
+ ?
vector_metric_name(vector.metric)
+ : "default");
+ _scanner_profile->add_info_string("LanceVectorDimension",
+
std::to_string(vector.query_vector.dimension));
+ }
+ if (_scan_params->__isset.lance_substrait_filter) {
+ _scanner_profile->add_info_string("LancePushdownFormat", "SUBSTRAIT");
+ _scanner_profile->add_info_string(
+ "LanceSubstraitFilterBytes",
+ std::to_string(_scan_params->lance_substrait_filter.size()));
+ }
+
+ _output_name_to_idx.clear();
+ _output_name_to_idx.reserve(_projected_columns.size());
+ 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 (!_output_name_to_idx.emplace(column.name, idx).second) {
+ return Status::InvalidArgument("duplicate Lance projected column:
{}", column.name);
+ }
+ if (_vector_search && column.name == 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 {}",
+ DISTANCE_COLUMN, column.type->get_name());
+ }
+ }
+ }
+ return Status::OK();
+}
+
+Status LanceTableReader::prepare_split(const SplitReadOptions& options) {
+ RETURN_IF_ERROR(_validate_range(options.current_range));
+ _close_scanner();
+ _eof = false;
+
+ RETURN_IF_ERROR(TableReader::prepare_split(options));
+ // Lance does not currently provide metadata aggregate pushdown. Do not
let a generic
+ // table-level count supplied by a future planner bypass fragment reads.
+ _remaining_table_level_count = -1;
+ if (current_split_pruned()) {
+ return Status::OK();
+ }
+
+ if (_vector_search) {
+ const auto& lance_params =
options.current_range.table_format_params.lance_params;
+ if (lance_params.version <= 0) {
+ return Status::InvalidArgument(
+ "Lance vector search requires a fixed positive dataset
version");
+ }
+ if (lance_params.__isset.fragment_ids) {
+ return Status::InvalidArgument("Lance vector search split must not
set fragment ids");
+ }
+ if (_search_split_prepared) {
+ return Status::InvalidArgument(
+ "Lance vector search supports exactly one whole-dataset
split");
+ }
+ }
+
+ const auto key = _dataset_key(options.current_range);
+ if (_dataset == nullptr) {
+ RETURN_IF_ERROR(_open_dataset(key));
+ _opened_dataset_key = key;
+ } else if (!_opened_dataset_key.has_value() || *_opened_dataset_key !=
key) {
+ return Status::InvalidArgument(
+ "Lance reader cannot mix dataset snapshots or storage options
in one scan");
+ }
+
+ RETURN_IF_ERROR(_prepare_conjuncts());
+ RETURN_IF_ERROR(_open_scanner(options.current_range));
+ _search_split_prepared = _vector_search;
+ return Status::OK();
+}
+
+Status LanceTableReader::get_block(Block* block, bool* eos) {
+ DORIS_CHECK(block != nullptr);
+ DORIS_CHECK(eos != nullptr);
+ DORIS_CHECK(block->columns() == _projected_columns.size());
+ *eos = false;
+
+ if (_eof) {
+ *eos = true;
+ return Status::OK();
+ }
+ if (_scanner == nullptr) {
+ return Status::InternalError("Lance scanner is not initialized for the
current split");
+ }
+
+ const auto target_rows = std::max<size_t>(1, _scanner_batch_size);
+ while (true) {
+ block->clear_column_data(_projected_columns.size());
+ size_t raw_rows = 0;
+ while (raw_rows < target_rows) {
+ if (_io_ctx != nullptr && _io_ctx->should_stop) {
+ _eof = true;
+ _close_scanner();
+ *eos = true;
+ return Status::OK();
+ }
+
+ LanceBatch* raw_batch = nullptr;
+ const int32_t scan_status = lance_scanner_next(_scanner,
&raw_batch);
Review Comment:
[P1] Integrate Lance I/O with Doris resource governance
This synchronous lance-c call owns the physical local/S3 reads, but it has
no Doris `FileReader`/`IOContext`, byte/call/time reporting, workload-group
throttle, or in-flight cancellation handle. Consequently Lance scans report
zero scan bytes and remote/local attribution, bypass the S3 remote-scan
limiter, and cannot stop a slow object-store request until it returns. Please
route physical I/O through the common layer or add callbacks that provide
accounting, throttling, and cancellation before enabling this scan path.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]