wangyong9999 commented on code in PR #315:
URL: https://github.com/apache/paimon-cpp/pull/315#discussion_r3975654317


##########
crates/lance_ffi/build.rs:
##########
@@ -0,0 +1,38 @@
+// 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.
+
+use std::env;
+use std::path::PathBuf;
+
+fn main() {
+    let crate_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR 
must be set");
+    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR must be 
set"));
+    let header_path = out_dir.join("paimon_lance_ffi.h");
+    let config = 
cbindgen::Config::from_file(PathBuf::from(&crate_dir).join("cbindgen.toml"))
+        .expect("cbindgen.toml must exist at crate root");
+
+    cbindgen::Builder::new()
+        .with_crate(&crate_dir)
+        .with_config(config)
+        .generate()
+        .expect("generate paimon_lance_ffi.h")
+        .write_to_file(&header_path);
+
+    println!("cargo:include={}", out_dir.display());
+    println!("cargo:rerun-if-changed=src/lib.rs");

Review Comment:
   Fixed in a0305ce4. The build script now watches the entire `src` directory, 
so changes to FFI declarations in any Rust module regenerate the cbindgen 
header.



##########
src/paimon/format/lance/lance_file_batch_reader.cpp:
##########
@@ -0,0 +1,303 @@
+/*
+ * 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 "paimon/format/lance/lance_file_batch_reader.h"
+
+#include <utility>
+
+#include "arrow/api.h"
+#include "arrow/c/bridge.h"
+#include "fmt/format.h"
+#include "paimon/common/metrics/metrics_impl.h"
+#include "paimon/common/utils/arrow/arrow_utils.h"
+#include "paimon/common/utils/arrow/mem_utils.h"
+#include "paimon/common/utils/arrow/status_utils.h"
+#include "paimon/common/utils/checked_cast.h"
+#include "paimon/core/utils/nested_projection_utils.h"
+#include "paimon/format/lance/lance_utils.h"
+#include "paimon/fs/file_system.h"
+
+namespace paimon::lance {
+
+LanceFileBatchReader::LanceFileBatchReader(const std::shared_ptr<InputStream>& 
input,
+                                           int32_t batch_size, uint32_t 
batch_readahead,
+                                           PaimonLanceReader* reader,
+                                           const 
std::shared_ptr<arrow::Schema>& file_schema,
+                                           uint64_t total_rows,
+                                           const std::shared_ptr<MemoryPool>& 
pool,
+                                           const 
std::shared_ptr<arrow::MemoryPool>& arrow_pool)
+    : input_(input),
+      batch_size_(batch_size),
+      batch_readahead_(batch_readahead),
+      reader_(reader),
+      file_schema_(file_schema),
+      total_rows_(total_rows),
+      pool_(pool),
+      arrow_pool_(arrow_pool),
+      metrics_(std::make_shared<MetricsImpl>()) {}
+
+Result<std::unique_ptr<LanceFileBatchReader>> LanceFileBatchReader::Create(
+    const std::shared_ptr<InputStream>& input, int32_t batch_size, uint32_t 
batch_readahead,
+    const std::map<std::string, std::string>& options, const 
std::shared_ptr<MemoryPool>& pool,
+    const std::shared_ptr<arrow::MemoryPool>& arrow_pool) {
+    if (input == nullptr || pool == nullptr || arrow_pool == nullptr || 
batch_size <= 0 ||
+        batch_readahead == 0) {
+        return Status::Invalid(
+            "Lance reader requires non-null input and memory pools, positive "
+            "batch size, and positive batch readahead");
+    }
+    PAIMON_ASSIGN_OR_RAISE(std::string uri, input->GetUri());
+    if (uri.empty()) {
+        return Status::Invalid("Lance reader input URI is empty");
+    }
+    LanceStorageOptions storage_options = GetLanceStorageOptions(options, uri);
+    std::vector<const char*> keys;
+    std::vector<const char*> values;
+    keys.reserve(storage_options.size());
+    values.reserve(storage_options.size());
+    for (const auto& [key, value] : storage_options) {
+        keys.push_back(key.c_str());
+        values.push_back(value.c_str());
+    }
+    PaimonLanceReader* reader = nullptr;
+    if (paimon_lance_reader_open(uri.c_str(), keys.data(), values.data(), 
keys.size(), &reader) !=
+        0) {
+        return LanceFfiError("open Lance reader");
+    }
+
+    ::ArrowSchema ffi_schema = {};
+    if (paimon_lance_reader_export_schema(reader, &ffi_schema) != 0) {
+        paimon_lance_reader_free(reader);
+        return LanceFfiError("read Lance schema");
+    }
+    arrow::Result<std::shared_ptr<arrow::Schema>> schema_result = 
arrow::ImportSchema(&ffi_schema);
+    if (!schema_result.ok()) {
+        paimon_lance_reader_free(reader);
+        return ToPaimonStatus(schema_result.status());
+    }
+    uint64_t total_rows = 0;
+    if (paimon_lance_reader_num_rows(reader, &total_rows) != 0) {
+        paimon_lance_reader_free(reader);
+        return LanceFfiError("read Lance row count");
+    }
+    return std::unique_ptr<LanceFileBatchReader>(new LanceFileBatchReader(
+        input, batch_size, batch_readahead, reader, 
std::move(schema_result).MoveValueUnsafe(),
+        total_rows, pool, arrow_pool));
+}
+
+LanceFileBatchReader::~LanceFileBatchReader() {
+    CloseInternal();
+}
+
+Status LanceFileBatchReader::OpenStream() {
+    if (batch_reader_ != nullptr || (has_selection_ && 
selection_row_ids_.empty())) {
+        return Status::OK();
+    }
+    std::vector<const char*> projection_pointers;
+    projection_pointers.reserve(projection_names_.size());
+    for (const std::string& name : projection_names_) {
+        projection_pointers.push_back(name.c_str());
+    }
+    if (paimon_lance_reader_open_stream(reader_, 
static_cast<uint32_t>(batch_size_),
+                                        batch_readahead_, 
projection_pointers.data(),
+                                        projection_pointers.size(), 
has_selection_,
+                                        selection_starts_.data(), 
selection_ends_.data(),
+                                        selection_starts_.size(), 
&batch_reader_) != 0) {
+        return LanceFfiError("open Lance batch reader");
+    }
+    return Status::OK();
+}
+
+Result<BatchReader::ReadBatch> LanceFileBatchReader::AlignBatch(ReadBatch 
batch) const {
+    if (read_schema_ == nullptr) {
+        return batch;
+    }
+    auto& [ffi_array, ffi_schema] = batch;
+    PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
+                                      arrow::ImportArray(ffi_array.get(), 
ffi_schema.get()));
+    if (array->type_id() != arrow::Type::STRUCT) {
+        return Status::Invalid("Lance reader returned a non-struct Arrow 
batch");
+    }
+    auto struct_array = checked_pointer_cast<arrow::StructArray>(array);
+    arrow::ArrayVector fields;
+    std::vector<std::string> names;
+    fields.reserve(read_schema_->num_fields());
+    names.reserve(read_schema_->num_fields());
+    for (const std::shared_ptr<arrow::Field>& field : read_schema_->fields()) {
+        std::shared_ptr<arrow::Array> child = 
struct_array->GetFieldByName(field->name());
+        if (child == nullptr) {
+            PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
+                child,
+                arrow::MakeArrayOfNull(field->type(), struct_array->length(), 
arrow_pool_.get()));
+        } else if (!child->type()->Equals(field->type()) &&
+                   ArrowUtils::EqualsIgnoreNullable(child->type(), 
field->type())) {
+            std::shared_ptr<arrow::ArrayData> data = child->data()->Copy();
+            data->type = field->type();
+            child = arrow::MakeArray(std::move(data));
+        } else if (!child->type()->Equals(field->type())) {
+            PAIMON_ASSIGN_OR_RAISE(child, 
NestedProjectionUtils::AlignArrayToReadType(
+                                              child, field->type(), 
arrow_pool_.get()));
+        }
+        fields.push_back(std::move(child));
+        names.push_back(field->name());
+    }
+    std::shared_ptr<arrow::Array> aligned;
+    if (fields.empty()) {
+        aligned = std::make_shared<arrow::StructArray>(arrow::struct_({}), 
array->length(), fields);
+    } else {
+        PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(aligned, 
arrow::StructArray::Make(fields, names));
+    }
+    auto out_array = std::make_unique<::ArrowArray>();
+    auto out_schema = std::make_unique<::ArrowSchema>();
+    PAIMON_RETURN_NOT_OK_FROM_ARROW(
+        arrow::ExportArray(*aligned, out_array.get(), out_schema.get()));
+    PAIMON_RETURN_NOT_OK(AddArrowArrayLifetime(out_array.get(), 
out_schema.get(), arrow_pool_));
+    return std::make_pair(std::move(out_array), std::move(out_schema));
+}
+
+Result<BatchReader::ReadBatch> LanceFileBatchReader::NextBatch() {
+    if (closed_) {
+        return Status::Invalid("Lance reader is closed");
+    }
+    if (has_selection_ && selection_row_ids_.empty()) {
+        previous_batch_row_count_ = 0;
+        return BatchReader::MakeEofBatch();
+    }
+    PAIMON_RETURN_NOT_OK(OpenStream());
+    auto ffi_array = std::make_unique<::ArrowArray>();
+    auto ffi_schema = std::make_unique<::ArrowSchema>();
+    bool eof = false;
+    if (paimon_lance_batch_reader_next(batch_reader_, ffi_array.get(), 
ffi_schema.get(), &eof) !=
+        0) {
+        return LanceFfiError("read Lance batch");
+    }
+    if (eof) {
+        previous_batch_row_count_ = 0;
+        return BatchReader::MakeEofBatch();
+    }
+    previous_row_offset_ = next_row_offset_;
+    previous_batch_row_count_ = static_cast<uint64_t>(ffi_array->length);
+    next_row_offset_ += previous_batch_row_count_;
+    if (has_selection_ && next_row_offset_ > selection_row_ids_.size()) {
+        return Status::Invalid("Lance reader returned more selected rows than 
requested");

Review Comment:
   Fixed in a0305ce4. The overflow guard now releases both exported Arrow C 
structures before returning the error.



##########
src/paimon/common/reader/data_file_reader_factory.cpp:
##########
@@ -33,14 +33,11 @@ namespace paimon {
 
 namespace {
 
-/// Formats the prefetching reader cannot drive: `blob` is read whole rather 
than in batches and
-/// `avro` is row-oriented, so neither has the batch boundaries it reads ahead 
to. `mosaic` has
-/// never been tried under one.
-// TODO(xinyu.lxy): test mosaic under prefetch. A format table cannot hold 
mosaic files, so this
-// only concerns the managed table path.
+/// Formats the prefetching reader cannot drive: `blob` is read whole rather 
than in batches,
+/// `avro` is row-oriented, and the Rust-backed formats manage their own 
read-ahead.

Review Comment:
   Fixed in a0305ce4. The original Mosaic rationale and TODO are restored; 
Lance has a separate statement about its Rust-side batch readahead.



##########
src/paimon/core/schema/schema_validation.cpp:
##########
@@ -802,6 +804,73 @@ Status SchemaValidation::ValidateMosaicDataFields(const 
TableSchema& schema,
     return Status::OK();
 }
 
+Status SchemaValidation::ValidateLanceDataField(const 
std::shared_ptr<arrow::Field>& field) {
+    if (VariantTypeUtils::IsVariantField(field)) {
+        return Status::Invalid("Lance file format does not support type 
VARIANT");
+    }
+    if (BlobUtils::IsBlobField(field)) {
+        return Status::Invalid("Lance file format does not support type BLOB");
+    }
+
+    const std::shared_ptr<arrow::DataType>& type = field->type();
+    switch (type->id()) {
+        case arrow::Type::BOOL:
+        case arrow::Type::INT8:
+        case arrow::Type::INT16:
+        case arrow::Type::INT32:
+        case arrow::Type::INT64:
+        case arrow::Type::FLOAT:
+        case arrow::Type::DOUBLE:
+        case arrow::Type::DATE32:
+        case arrow::Type::STRING:
+        case arrow::Type::BINARY:
+        case arrow::Type::TIME32:
+        case arrow::Type::DECIMAL128:
+        case arrow::Type::FIXED_SIZE_LIST:
+            return Status::OK();
+        case arrow::Type::TIMESTAMP: {
+            const auto& timestamp_type = checked_cast<const 
arrow::TimestampType&>(*type);
+            if (!timestamp_type.timezone().empty()) {
+                return Status::Invalid(
+                    "Lance file format does not support type 
LOCAL_ZONED_TIMESTAMP");
+            }
+            return Status::OK();
+        }
+        case arrow::Type::LIST:
+            return ValidateLanceDataField(type->field(0));
+        case arrow::Type::STRUCT:
+            for (const std::shared_ptr<arrow::Field>& child : type->fields()) {
+                PAIMON_RETURN_NOT_OK(ValidateLanceDataField(child));
+            }
+            return Status::OK();

Review Comment:
   Fixed in a0305ce4. Schema validation now rejects nullable ROW fields 
recursively, with coverage for both top-level and nested nullable ROWs. 
Non-null ROWs remain supported.



##########
src/paimon/core/schema/schema_validation.cpp:
##########
@@ -802,6 +804,73 @@ Status SchemaValidation::ValidateMosaicDataFields(const 
TableSchema& schema,
     return Status::OK();
 }
 
+Status SchemaValidation::ValidateLanceDataField(const 
std::shared_ptr<arrow::Field>& field) {
+    if (VariantTypeUtils::IsVariantField(field)) {
+        return Status::Invalid("Lance file format does not support type 
VARIANT");
+    }
+    if (BlobUtils::IsBlobField(field)) {
+        return Status::Invalid("Lance file format does not support type BLOB");
+    }
+
+    const std::shared_ptr<arrow::DataType>& type = field->type();
+    switch (type->id()) {
+        case arrow::Type::BOOL:
+        case arrow::Type::INT8:
+        case arrow::Type::INT16:
+        case arrow::Type::INT32:
+        case arrow::Type::INT64:
+        case arrow::Type::FLOAT:
+        case arrow::Type::DOUBLE:
+        case arrow::Type::DATE32:
+        case arrow::Type::STRING:
+        case arrow::Type::BINARY:
+        case arrow::Type::TIME32:
+        case arrow::Type::DECIMAL128:
+        case arrow::Type::FIXED_SIZE_LIST:
+            return Status::OK();
+        case arrow::Type::TIMESTAMP: {
+            const auto& timestamp_type = checked_cast<const 
arrow::TimestampType&>(*type);
+            if (!timestamp_type.timezone().empty()) {
+                return Status::Invalid(
+                    "Lance file format does not support type 
LOCAL_ZONED_TIMESTAMP");
+            }
+            return Status::OK();
+        }
+        case arrow::Type::LIST:
+            return ValidateLanceDataField(type->field(0));
+        case arrow::Type::STRUCT:
+            for (const std::shared_ptr<arrow::Field>& child : type->fields()) {
+                PAIMON_RETURN_NOT_OK(ValidateLanceDataField(child));
+            }
+            return Status::OK();
+        case arrow::Type::MAP:
+            return Status::Invalid("Lance file format does not support type 
MAP");
+        default:
+            break;
+    }
+    return Status::Invalid(
+        fmt::format("Lance file format does not support type {}", 
type->ToString()));
+}
+
+Status SchemaValidation::ValidateLanceDataFields(const TableSchema& schema,
+                                                 const CoreOptions& options) {
+    if (StringUtils::ToLowerCase(options.GetFileFormat()->Identifier()) != 
"lance") {

Review Comment:
   Fixed in a0305ce4. Lance type validation now applies to the default format, 
every `file.format.per.level` entry, and `changelog-file.format`, with tests 
for both alternate-format paths.



-- 
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]

Reply via email to