lxy-9602 commented on code in PR #315:
URL: https://github.com/apache/paimon-cpp/pull/315#discussion_r4002630845


##########
src/paimon/format/lance/lance_file_batch_reader.cpp:
##########
@@ -0,0 +1,304 @@
+/*
+ * 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 "arrow/c/helpers.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;
+    fields.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()));
+        }

Review Comment:
   `AlignArrayToReadType` is expected to produce exactly the requested 
`read_type`; its callers do not require the source nullability to match. 
Existing tests already cover nullability alignment for other nested types 
(`AlignArrayToReadTypeAppliesReadNullability`). `FIXED_SIZE_LIST` is the only 
gap because it currently falls through the leaf path and is rejected. Could we 
support it directly in `AlignArrayToReadType` and add a corresponding test, 
instead of keeping this Lance-specific workaround?



##########
test/inte/write_inte_test.cpp:
##########
@@ -4051,7 +4063,10 @@ TEST_P(WriteInteTest, TestNullabilityCheck) {
 
 TEST_P(WriteInteTest, TestPkSpillableMapSharedShreddingReadWrite) {
     auto file_format = GetParam();
-    if (file_format == "avro" || file_format == "mosaic" || file_format == 
"lance") {
+    if (file_format == "lance") {
+        GTEST_SKIP() << "Lance file format does not support type MAP";
+    }
+    if (file_format == "avro" || file_format == "mosaic") {
         return;

Review Comment:
   I don’t understand the reason for this change. We should either keep the 
previous behavior of skipping all of them without explanation, or explain the 
reason for each skipped case consistently. Only adding an explanation for lance 
feels a bit odd.



##########
src/paimon/format/lance/lance_format_writer.cpp:
##########
@@ -80,8 +113,13 @@ Status LanceFormatWriter::AddBatch(::ArrowArray* batch) {
     PAIMON_RETURN_NOT_OK_FROM_ARROW(arrow::ExportSchema(*schema_, 
&import_schema));
     PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> array,
                                       arrow::ImportArray(batch, 
&import_schema));
+    // Normalize sliced nested arrays before Arrow C export: Lance 0.39's 
encoder
+    // expects child buffers and offsets to describe the same logical slice.
     PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> normalized,
                            ArrowUtils::NormalizeArrayOffsets(array, 
arrow_pool_.get()));
+    for (const auto& child : 
checked_pointer_cast<arrow::StructArray>(normalized)->fields()) {
+        PAIMON_RETURN_NOT_OK(ValidateRowValues(child));
+    }

Review Comment:
   Why is the for loop needed here? Would 
`PAIMON_RETURN_NOT_OK(ValidateRowValues(normalized));` work directly instead?



##########
test/test_data/lance/GenerateLanceTable.java:
##########
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.CatalogFactory;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.BinaryVector;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.GenericArray;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.data.serializer.InternalRowSerializer;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.table.source.ReadBuilder;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.CloseableIterator;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.TimeZone;
+
+/** Generate a real Paimon table, including schemas, snapshots and manifests. 
*/
+public class GenerateLanceTable {
+    private static final RowType NESTED =
+            RowType.of(
+                    new org.apache.paimon.types.DataType[] {
+                        DataTypes.INT(), DataTypes.STRING()
+                    },
+                    new String[] {"number", "label"});
+
+    public static void main(String[] args) throws Exception {
+        if (args.length < 1 || args.length > 2
+                || (args.length == 2 && !"null-parent".equals(args[1]))) {
+            throw new IllegalArgumentException("Usage: GenerateLanceTable 
NEW_WAREHOUSE [null-parent]");
+        }
+        // Never overwrite a checked-in fixture or an existing warehouse.
+        if (Files.exists(Paths.get(args[0]))) {
+            throw new IllegalArgumentException("Output must not exist: " + 
args[0]);
+        }
+        boolean nullParent = args.length == 2;
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+        Options options = new Options();
+        options.set("warehouse", 
Paths.get(args[0]).toAbsolutePath().toUri().toString());
+        try (Catalog catalog = 
CatalogFactory.createCatalog(CatalogContext.create(options))) {
+            catalog.createDatabase("append_java_compat", false);
+            Identifier id = Identifier.create("append_java_compat", 
"append_java_compat");
+            Schema schema = Schema.newBuilder()
+                    .column("id", DataTypes.INT())
+                    .column("f_boolean", DataTypes.BOOLEAN())
+                    .column("f_tinyint", DataTypes.TINYINT())
+                    .column("f_smallint", DataTypes.SMALLINT())
+                    .column("f_bigint", DataTypes.BIGINT())
+                    .column("f_float", DataTypes.FLOAT())
+                    .column("f_double", DataTypes.DOUBLE())
+                    .column("f_char", DataTypes.CHAR(8))
+                    .column("f_string", DataTypes.STRING())
+                    .column("f_binary", DataTypes.BINARY(4))
+                    .column("f_varbinary", DataTypes.BYTES())
+                    .column("f_date", DataTypes.DATE())
+                    .column("f_ts_0", DataTypes.TIMESTAMP(0))
+                    .column("f_ts_3", DataTypes.TIMESTAMP(3))
+                    .column("f_ts_6", DataTypes.TIMESTAMP(6))
+                    .column("f_ts_9", DataTypes.TIMESTAMP(9))
+                    .column("f_decimal_1_0", DataTypes.DECIMAL(1, 0))
+                    .column("f_decimal_18_2", DataTypes.DECIMAL(18, 2))
+                    .column("f_decimal_19_2", DataTypes.DECIMAL(19, 2))
+                    .column("f_decimal_38_18", DataTypes.DECIMAL(38, 18))
+                    .column("f_array_int", DataTypes.ARRAY(DataTypes.INT()))
+                    .column("f_array_array_int", 
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT())))
+                    .column("f_struct", NESTED.notNull())
+                    .column("f_nullable_struct", NESTED)
+                    .column("f_vector", DataTypes.VECTOR(3, DataTypes.FLOAT()))
+                    .option("file.format", "lance")
+                    .option("bucket", "-1")
+                    .option("metadata.stats-mode", "none")
+                    .option("fields.id.stats-mode", "full")
+                    .build();
+            catalog.createTable(id, schema, false);
+            Table table = catalog.getTable(id);
+            InternalRowSerializer serializer = new 
InternalRowSerializer(table.rowType());
+            // Separate commits ensure a predicate can prune an entire data 
file.
+            for (int first : new int[] {1, 10}) {
+                BatchWriteBuilder builder = table.newBatchWriteBuilder();
+                try (BatchTableWrite write = builder.newWrite();
+                        BatchTableCommit commit = builder.newCommit()) {
+                    write.write(row(first, false));
+                    write.write(row(first + 1, nullParent));
+                    commit.commit(write.prepareCommit());
+                }
+            }
+            ReadBuilder readBuilder = table.newReadBuilder();
+            int count = 0;
+            try (CloseableIterator<InternalRow> rows = readBuilder.newRead()
+                    
.createReader(readBuilder.newScan().plan()).toCloseableIterator()) {
+                while (rows.hasNext()) {
+                    InternalRow actual = rows.next();
+                    int rowId = actual.getInt(0);
+                    boolean expectedNullParent = nullParent && (rowId == 2 || 
rowId == 11);
+                    if (nullParent) {
+                        System.out.println("id=" + rowId + " expected null 
parent="
+                                + expectedNullParent + " actual null parent=" 
+ actual.isNullAt(23));
+                    }
+                    if (!serializer.toBinaryRow(actual).copy()
+                            .equals(serializer.toBinaryRow(row(rowId, 
expectedNullParent)).copy())) {
+                        throw new AssertionError("Java round trip mismatch for 
id " + rowId);
+                    }
+                    count++;
+                }
+            }
+            if (count != 4) {
+                throw new AssertionError("Expected 4 rows, got " + count);
+            }
+            System.out.println("Verified Java table round trip: 4 rows, 25 
columns, 2 commits");
+        }
+    }
+
+    private static GenericRow row(int id, boolean nullParent) {
+        if (id == 2) {
+            GenericRow row = new GenericRow(25);
+            row.setField(0, id);
+            row.setField(22, GenericRow.of(null, null));
+            row.setField(23, nullParent ? null : GenericRow.of(null, null));
+            return row;
+        }
+        return GenericRow.of(
+                id, id % 2 == 1, (byte) -5, (short) -1000, 10000000000L + id,
+                1.25f, -2.5d, BinaryString.fromString("char0001"),
+                BinaryString.fromString(id == 10 ? "" : "value-" + id),
+                "bin1".getBytes(StandardCharsets.UTF_8), new byte[] {0, 1, 2, 
127},
+                id == 1 ? -1 : 20000,
+                Timestamp.fromEpochMillis(1000),
+                Timestamp.fromEpochMillis(1123),
+                Timestamp.fromEpochMillis(1123, 456000),
+                Timestamp.fromEpochMillis(1123, 456789),
+                Decimal.fromBigDecimal(new BigDecimal("-3"), 1, 0),
+                Decimal.fromBigDecimal(new BigDecimal("1.25"), 18, 2),
+                Decimal.fromBigDecimal(new BigDecimal("12345678901234567.89"), 
19, 2),
+                Decimal.fromBigDecimal(
+                        new 
BigDecimal("12345678901234567890.123456789012345678"), 38, 18),
+                new GenericArray(id == 10 ? new Integer[] {} : new Integer[] 
{id, null, -id}),
+                new GenericArray(new GenericArray[] {
+                    new GenericArray(new Integer[] {id, null}), null,
+                    new GenericArray(new Integer[] {})
+                }),
+                GenericRow.of(id, BinaryString.fromString("required")),
+                nullParent ? null : GenericRow.of(id, 
BinaryString.fromString("nullable")),
+                BinaryVector.fromPrimitiveArray(new float[] {id, -1.5f, 
0.25f}));
+    }

Review Comment:
   Please remove this code and document the data in the `README` instead. Also, 
the current `README` format is inconsistent with the others. Please rewrite it 
following the `README` in `mosaic/append_java_compat.db`.



##########
test/test_data/lance/GenerateLanceTable.java:
##########
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.apache.paimon.catalog.Catalog;
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.CatalogFactory;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.data.BinaryString;
+import org.apache.paimon.data.BinaryVector;
+import org.apache.paimon.data.Decimal;
+import org.apache.paimon.data.GenericArray;
+import org.apache.paimon.data.GenericRow;
+import org.apache.paimon.data.InternalRow;
+import org.apache.paimon.data.Timestamp;
+import org.apache.paimon.data.serializer.InternalRowSerializer;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.Table;
+import org.apache.paimon.table.sink.BatchTableCommit;
+import org.apache.paimon.table.sink.BatchTableWrite;
+import org.apache.paimon.table.sink.BatchWriteBuilder;
+import org.apache.paimon.table.source.ReadBuilder;
+import org.apache.paimon.types.DataTypes;
+import org.apache.paimon.types.RowType;
+import org.apache.paimon.utils.CloseableIterator;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.TimeZone;
+
+/** Generate a real Paimon table, including schemas, snapshots and manifests. 
*/
+public class GenerateLanceTable {
+    private static final RowType NESTED =
+            RowType.of(
+                    new org.apache.paimon.types.DataType[] {
+                        DataTypes.INT(), DataTypes.STRING()
+                    },
+                    new String[] {"number", "label"});
+
+    public static void main(String[] args) throws Exception {
+        if (args.length < 1 || args.length > 2
+                || (args.length == 2 && !"null-parent".equals(args[1]))) {
+            throw new IllegalArgumentException("Usage: GenerateLanceTable 
NEW_WAREHOUSE [null-parent]");
+        }
+        // Never overwrite a checked-in fixture or an existing warehouse.
+        if (Files.exists(Paths.get(args[0]))) {
+            throw new IllegalArgumentException("Output must not exist: " + 
args[0]);
+        }
+        boolean nullParent = args.length == 2;
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+        Options options = new Options();
+        options.set("warehouse", 
Paths.get(args[0]).toAbsolutePath().toUri().toString());
+        try (Catalog catalog = 
CatalogFactory.createCatalog(CatalogContext.create(options))) {
+            catalog.createDatabase("append_java_compat", false);
+            Identifier id = Identifier.create("append_java_compat", 
"append_java_compat");
+            Schema schema = Schema.newBuilder()
+                    .column("id", DataTypes.INT())
+                    .column("f_boolean", DataTypes.BOOLEAN())
+                    .column("f_tinyint", DataTypes.TINYINT())
+                    .column("f_smallint", DataTypes.SMALLINT())
+                    .column("f_bigint", DataTypes.BIGINT())
+                    .column("f_float", DataTypes.FLOAT())
+                    .column("f_double", DataTypes.DOUBLE())
+                    .column("f_char", DataTypes.CHAR(8))
+                    .column("f_string", DataTypes.STRING())
+                    .column("f_binary", DataTypes.BINARY(4))
+                    .column("f_varbinary", DataTypes.BYTES())
+                    .column("f_date", DataTypes.DATE())
+                    .column("f_ts_0", DataTypes.TIMESTAMP(0))
+                    .column("f_ts_3", DataTypes.TIMESTAMP(3))
+                    .column("f_ts_6", DataTypes.TIMESTAMP(6))
+                    .column("f_ts_9", DataTypes.TIMESTAMP(9))
+                    .column("f_decimal_1_0", DataTypes.DECIMAL(1, 0))
+                    .column("f_decimal_18_2", DataTypes.DECIMAL(18, 2))
+                    .column("f_decimal_19_2", DataTypes.DECIMAL(19, 2))
+                    .column("f_decimal_38_18", DataTypes.DECIMAL(38, 18))
+                    .column("f_array_int", DataTypes.ARRAY(DataTypes.INT()))
+                    .column("f_array_array_int", 
DataTypes.ARRAY(DataTypes.ARRAY(DataTypes.INT())))
+                    .column("f_struct", NESTED.notNull())
+                    .column("f_nullable_struct", NESTED)
+                    .column("f_vector", DataTypes.VECTOR(3, DataTypes.FLOAT()))
+                    .option("file.format", "lance")
+                    .option("bucket", "-1")
+                    .option("metadata.stats-mode", "none")
+                    .option("fields.id.stats-mode", "full")
+                    .build();
+            catalog.createTable(id, schema, false);
+            Table table = catalog.getTable(id);
+            InternalRowSerializer serializer = new 
InternalRowSerializer(table.rowType());
+            // Separate commits ensure a predicate can prune an entire data 
file.
+            for (int first : new int[] {1, 10}) {
+                BatchWriteBuilder builder = table.newBatchWriteBuilder();
+                try (BatchTableWrite write = builder.newWrite();
+                        BatchTableCommit commit = builder.newCommit()) {
+                    write.write(row(first, false));
+                    write.write(row(first + 1, nullParent));
+                    commit.commit(write.prepareCommit());
+                }
+            }
+            ReadBuilder readBuilder = table.newReadBuilder();
+            int count = 0;
+            try (CloseableIterator<InternalRow> rows = readBuilder.newRead()
+                    
.createReader(readBuilder.newScan().plan()).toCloseableIterator()) {
+                while (rows.hasNext()) {
+                    InternalRow actual = rows.next();
+                    int rowId = actual.getInt(0);
+                    boolean expectedNullParent = nullParent && (rowId == 2 || 
rowId == 11);
+                    if (nullParent) {
+                        System.out.println("id=" + rowId + " expected null 
parent="
+                                + expectedNullParent + " actual null parent=" 
+ actual.isNullAt(23));
+                    }
+                    if (!serializer.toBinaryRow(actual).copy()
+                            .equals(serializer.toBinaryRow(row(rowId, 
expectedNullParent)).copy())) {
+                        throw new AssertionError("Java round trip mismatch for 
id " + rowId);
+                    }
+                    count++;
+                }
+            }
+            if (count != 4) {
+                throw new AssertionError("Expected 4 rows, got " + count);
+            }
+            System.out.println("Verified Java table round trip: 4 rows, 25 
columns, 2 commits");
+        }
+    }
+
+    private static GenericRow row(int id, boolean nullParent) {
+        if (id == 2) {
+            GenericRow row = new GenericRow(25);
+            row.setField(0, id);
+            row.setField(22, GenericRow.of(null, null));
+            row.setField(23, nullParent ? null : GenericRow.of(null, null));
+            return row;
+        }
+        return GenericRow.of(
+                id, id % 2 == 1, (byte) -5, (short) -1000, 10000000000L + id,
+                1.25f, -2.5d, BinaryString.fromString("char0001"),
+                BinaryString.fromString(id == 10 ? "" : "value-" + id),
+                "bin1".getBytes(StandardCharsets.UTF_8), new byte[] {0, 1, 2, 
127},
+                id == 1 ? -1 : 20000,
+                Timestamp.fromEpochMillis(1000),
+                Timestamp.fromEpochMillis(1123),
+                Timestamp.fromEpochMillis(1123, 456000),
+                Timestamp.fromEpochMillis(1123, 456789),
+                Decimal.fromBigDecimal(new BigDecimal("-3"), 1, 0),
+                Decimal.fromBigDecimal(new BigDecimal("1.25"), 18, 2),
+                Decimal.fromBigDecimal(new BigDecimal("12345678901234567.89"), 
19, 2),
+                Decimal.fromBigDecimal(
+                        new 
BigDecimal("12345678901234567890.123456789012345678"), 38, 18),
+                new GenericArray(id == 10 ? new Integer[] {} : new Integer[] 
{id, null, -id}),
+                new GenericArray(new GenericArray[] {
+                    new GenericArray(new Integer[] {id, null}), null,
+                    new GenericArray(new Integer[] {})
+                }),
+                GenericRow.of(id, BinaryString.fromString("required")),
+                nullParent ? null : GenericRow.of(id, 
BinaryString.fromString("nullable")),
+                BinaryVector.fromPrimitiveArray(new float[] {id, -1.5f, 
0.25f}));
+    }

Review Comment:
   `java_lance_0_39_0.lance` does not seem necessary anymore since we already 
have data covering the full set of types. Please also place the `README` 
alongside the corresponding table, consistent with the other `test_data` 
entries.



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