This is an automated email from the ASF dual-hosted git repository.
SteNicholas pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-cpp.git
The following commit(s) were added to refs/heads/main by this push:
new 05d6497b fix(parquet): support nullable fixed-size lists for vector
(#231)
05d6497b is described below
commit 05d6497bdd8111fbec45eba731ea5b2c06ce0d0b
Author: lxy <[email protected]>
AuthorDate: Fri Aug 21 19:41:52 2026 +0800
fix(parquet): support nullable fixed-size lists for vector (#231)
---
cmake_modules/arrow.diff | 194 ++++++++++++++++++++-
docs/source/user_guide/data_types.rst | 8 +-
src/paimon/core/io/vector_file_batch_reader.cpp | 5 +
src/paimon/format/parquet/CMakeLists.txt | 2 -
.../format/parquet/parquet_format_writer.cpp | 30 +---
src/paimon/format/parquet/parquet_format_writer.h | 2 -
.../format/parquet/parquet_vector_converter.cpp | 174 ------------------
.../format/parquet/parquet_vector_converter.h | 46 -----
.../parquet/parquet_vector_converter_test.cpp | 95 ----------
.../format/parquet/parquet_vector_io_test.cpp | 72 +++++---
.../parquet/vector_compatibility/README.md | 8 +-
11 files changed, 251 insertions(+), 385 deletions(-)
diff --git a/cmake_modules/arrow.diff b/cmake_modules/arrow.diff
index 75e3bb51..b8b83517 100644
--- a/cmake_modules/arrow.diff
+++ b/cmake_modules/arrow.diff
@@ -48,10 +48,71 @@ index b36c38c6d4..f974a33073 100644
/// \brief Return zero-copy string_view to upcoming bytes.
///
+diff --git a/cpp/src/arrow/util/bit_run_reader.h
b/cpp/src/arrow/util/bit_run_reader.h
+index a436a503a0..27d483978c 100644
+--- a/cpp/src/arrow/util/bit_run_reader.h
++++ b/cpp/src/arrow/util/bit_run_reader.h
+@@ -168,6 +168,26 @@ class ARROW_EXPORT BitRunReader {
+ using BitRunReader = BitRunReaderLinear;
+ #endif
+
++template <typename Visit>
++inline Status VisitBitRuns(const uint8_t* bitmap, int64_t offset, int64_t
length,
++ Visit&& visit) {
++ if (bitmap == NULLPTR) {
++ // Assuming all set (as in a null bitmap)
++ return visit(static_cast<int64_t>(0), length, true);
++ }
++ BitRunReader reader(bitmap, offset, length);
++ int64_t position = 0;
++ while (true) {
++ const auto run = reader.NextRun();
++ if (run.length == 0) {
++ break;
++ }
++ ARROW_RETURN_NOT_OK(visit(position, run.length, run.set));
++ position += run.length;
++ }
++ return Status::OK();
++}
++
+ struct SetBitRun {
+ int64_t position;
+ int64_t length;
diff --git a/cpp/src/parquet/arrow/reader.cc b/cpp/src/parquet/arrow/reader.cc
-index 285e2a5973..db919d7ef8 100644
+index 285e2a5973..52f42cf5b3 100644
--- a/cpp/src/parquet/arrow/reader.cc
+++ b/cpp/src/parquet/arrow/reader.cc
+@@ -19,12 +19,14 @@
+
+ #include <algorithm>
+ #include <cstring>
++#include <iterator>
+ #include <memory>
+ #include <unordered_set>
+ #include <utility>
+ #include <vector>
+
+ #include "arrow/array.h"
++#include "arrow/array/concatenate.h"
+ #include "arrow/buffer.h"
+ #include "arrow/extension_type.h"
+ #include "arrow/io/memory.h"
+@@ -32,12 +34,14 @@
+ #include "arrow/table.h"
+ #include "arrow/type.h"
+ #include "arrow/util/async_generator.h"
++#include "arrow/util/bit_run_reader.h"
+ #include "arrow/util/bit_util.h"
+ #include "arrow/util/future.h"
+ #include "arrow/util/iterator.h"
+ #include "arrow/util/logging.h"
+ #include "arrow/util/parallel.h"
+ #include "arrow/util/range.h"
++#include "arrow/util/span.h"
+ #include "arrow/util/tracing_internal.h"
+ #include "parquet/arrow/reader_internal.h"
+ #include "parquet/column_reader.h"
@@ -254,6 +254,11 @@ class FileReaderImpl : public FileReader {
return GetColumn(i, AllRowGroupsFactory(), out);
}
@@ -151,7 +212,87 @@ index 285e2a5973..db919d7ef8 100644
virtual ::arrow::Result<std::shared_ptr<ChunkedArray>> AssembleArray(
std::shared_ptr<ArrayData> data) {
if (field_->type()->id() == ::arrow::Type::MAP) {
-@@ -709,6 +776,39 @@ class PARQUET_NO_EXPORT StructReader : public
ColumnReaderImpl {
+@@ -642,8 +713,10 @@ class ListReader : public ColumnReaderImpl {
+
+ const std::shared_ptr<Field> field() override { return field_; }
+
+- private:
++ protected:
+ std::shared_ptr<ReaderContext> ctx_;
++
++ private:
+ std::shared_ptr<Field> field_;
+ ::parquet::internal::LevelInfo level_info_;
+ std::unique_ptr<ColumnReaderImpl> item_reader_;
+@@ -662,12 +735,62 @@ class PARQUET_NO_EXPORT FixedSizeListReader : public
ListReader<int32_t> {
+ DCHECK_EQ(field()->type()->id(), ::arrow::Type::FIXED_SIZE_LIST);
+ const auto& type =
checked_cast<::arrow::FixedSizeListType&>(*field()->type());
+ const int32_t* offsets = reinterpret_cast<const
int32_t*>(data->buffers[1]->data());
+- for (int x = 1; x <= data->length; x++) {
+- int32_t size = offsets[x] - offsets[x - 1];
+- if (size != type.list_size()) {
+- return Status::Invalid("Expected all lists to be of size=",
type.list_size(),
+- " but index ", x, " had size=", size);
++ const int32_t list_size = type.list_size();
++ auto validate_offsets = [&](int64_t start, int64_t length,
++ bool has_elements) -> Status {
++ const int32_t expected_size = has_elements ? list_size : 0;
++ ::arrow::util::span<const int32_t> run_offsets(
++ offsets + start, static_cast<size_t>(length + 1));
++ const auto first_invalid_offset = std::adjacent_find(
++ run_offsets.begin(), run_offsets.end(),
++ [&](int32_t left, int32_t right) { return right - left !=
expected_size; });
++ if (first_invalid_offset != run_offsets.end()) {
++ const int64_t x =
++ start + std::distance(run_offsets.begin(), first_invalid_offset);
++ const int32_t size = offsets[x + 1] - offsets[x];
++ if (has_elements) {
++ return Status::Invalid("Expected all lists to be of size=",
list_size,
++ " but index ", x + 1, " had size=", size);
++ }
++ return Status::Invalid("Expected null fixed-size list at index ", x +
1,
++ " to have no child values but had size=",
size);
+ }
++ return Status::OK();
++ };
++ if (data->GetNullCount() != 0) {
++ // Rebuild the child array run-by-run so null fixed-size list slots
still
++ // contribute list_size child values in the final layout.
++ ::arrow::ArrayVector child_arrays;
++
++ auto visit_run = [&](int64_t start, int64_t length, bool has_elements)
-> Status {
++ RETURN_NOT_OK(validate_offsets(start, length, has_elements));
++
++ const int64_t child_length = length * list_size;
++ // Valid runs reuse the decoded child slice; null runs materialize
null
++ // children to preserve the fixed-size list shape.
++ if (!has_elements) {
++ ARROW_ASSIGN_OR_RAISE(
++ auto null_array,
++ ::arrow::MakeArrayOfNull(type.value_type(), child_length,
ctx_->pool));
++ child_arrays.push_back(std::move(null_array));
++ return Status::OK();
++ }
++ child_arrays.push_back(
++ ::arrow::MakeArray(data->child_data[0]->Slice(offsets[start],
child_length)));
++ return Status::OK();
++ };
++
++ DCHECK_NE(data->buffers[0], nullptr);
++ RETURN_NOT_OK(::arrow::internal::VisitBitRuns(
++ data->buffers[0]->data(), data->offset, data->length, visit_run));
++
++ // TODO(GH-50271): Build one padded child array directly instead of
creating
++ // one temporary Array/ArrayData per validity run and concatenating
them.
++ ARROW_ASSIGN_OR_RAISE(auto child_array_with_padding,
++ ::arrow::Concatenate(child_arrays, ctx_->pool));
++ data->child_data[0] = child_array_with_padding->data();
++ } else {
++ RETURN_NOT_OK(validate_offsets(/*start=*/0, data->length,
/*valid=*/true));
+ }
+ data->buffers.resize(1);
+ std::shared_ptr<Array> result = ::arrow::MakeArray(data);
+@@ -709,6 +832,39 @@ class PARQUET_NO_EXPORT StructReader : public
ColumnReaderImpl {
}
return Status::OK();
}
@@ -191,7 +332,7 @@ index 285e2a5973..db919d7ef8 100644
Status BuildArray(int64_t length_upper_bound,
std::shared_ptr<ChunkedArray>* out) override;
Status GetDefLevels(const int16_t** data, int64_t* length) override;
-@@ -1013,25 +1113,32 @@ Status FileReaderImpl::GetRecordBatchReader(const
std::vector<int>& row_groups,
+@@ -1013,25 +1169,32 @@ Status FileReaderImpl::GetRecordBatchReader(const
std::vector<int>& row_groups,
return Status::OK();
}
@@ -230,7 +371,7 @@ index 285e2a5973..db919d7ef8 100644
RETURN_NOT_OK(::arrow::internal::OptionalParallelFor(
reader_properties_.use_threads(),
static_cast<int>(readers.size()),
-@@ -1224,6 +1331,23 @@ Status FileReaderImpl::GetColumn(int i,
FileColumnIteratorFactory iterator_facto
+@@ -1224,6 +1387,23 @@ Status FileReaderImpl::GetColumn(int i,
FileColumnIteratorFactory iterator_facto
return Status::OK();
}
@@ -400,10 +541,49 @@ index ec3890a41f..943f69bb6c 100644
return Status::OK();
}
diff --git a/cpp/src/parquet/arrow/writer.cc b/cpp/src/parquet/arrow/writer.cc
-index 4fd7ef1b47..87326a54f1 100644
+index 4fd7ef1b47..feff99c99b 100644
--- a/cpp/src/parquet/arrow/writer.cc
+++ b/cpp/src/parquet/arrow/writer.cc
-@@ -314,6 +314,14 @@ class FileWriterImpl : public FileWriter {
+@@ -26,6 +26,7 @@
+ #include <vector>
+
+ #include "arrow/array.h"
++#include "arrow/array/concatenate.h"
+ #include "arrow/extension_type.h"
+ #include "arrow/ipc/writer.h"
+ #include "arrow/record_batch.h"
+@@ -142,13 +143,24 @@ class ArrowColumnWriterV2 {
+ leaf_idx, ctx, [&](const MultipathLevelBuilderResult& result) {
+ size_t visited_component_size =
result.post_list_visited_elements.size();
+ DCHECK_GT(visited_component_size, 0);
+- if (visited_component_size != 1) {
+- return Status::NotImplemented(
+- "Lists with non-zero length null components are not
supported");
++ std::shared_ptr<Array> values_array;
++ if (visited_component_size == 1) {
++ const ElementRange& range =
result.post_list_visited_elements[0];
++ values_array = result.leaf_array->Slice(range.start,
range.Size());
++ } else {
++ // Multiple leaf ranges can be produced when child values are
++ // skipped, such as null fixed-size-list slots, or when
++ // list-view ranges are non-contiguous. Concatenate the slices
++ // in logical write order.
++ ::arrow::ArrayVector arrays;
++ arrays.reserve(visited_component_size);
++ for (const auto& range : result.post_list_visited_elements) {
++ DCHECK(!range.Empty());
++ arrays.push_back(result.leaf_array->Slice(range.start,
range.Size()));
++ }
++ ARROW_ASSIGN_OR_RAISE(values_array,
++ ::arrow::Concatenate(arrays,
ctx->memory_pool));
+ }
+- const ElementRange& range =
result.post_list_visited_elements[0];
+- std::shared_ptr<Array> values_array =
+- result.leaf_array->Slice(range.start, range.Size());
+
+ return column_writer->WriteArrow(result.def_levels,
result.rep_levels,
+ result.def_rep_level_count,
*values_array,
+@@ -314,6 +326,14 @@ class FileWriterImpl : public FileWriter {
return Status::OK();
}
@@ -418,7 +598,7 @@ index 4fd7ef1b47..87326a54f1 100644
Status Close() override {
if (!closed_) {
// Make idempotent
-@@ -418,10 +426,13 @@ class FileWriterImpl : public FileWriter {
+@@ -418,10 +438,13 @@ class FileWriterImpl : public FileWriter {
// Max number of rows allowed in a row group.
const int64_t max_row_group_length =
this->properties().max_row_group_length();
diff --git a/docs/source/user_guide/data_types.rst
b/docs/source/user_guide/data_types.rst
index 9fdecf6e..add537ad 100644
--- a/docs/source/user_guide/data_types.rst
+++ b/docs/source/user_guide/data_types.rst
@@ -201,11 +201,9 @@ and `Arrow DataTypes
<https://arrow.apache.org/docs/format/Columnar.html#data-ty
VECTOR fields are rejected. VECTOR columns also cannot be partition or
bucket keys. Dedicated vector storage is not included yet.
- **Note:** A data file written by another engine that records the column
as
- Arrow ``FixedSizeList`` instead of ``LIST``, such as Paimon Rust or
Python,
- can only be read while it holds no NULL vector. Parquet stores a NULL
list
- slot with no values, which the Arrow 17 Parquet reader rejects for a
- ``FixedSizeList`` column.
+ Paimon C++ also reads Parquet files written by Paimon Rust or Python
whose
+ embedded Arrow schema restores VECTOR columns as ``FixedSizeList``,
+ including NULL vector values.
* - ``MAP<kt, vt>``
- Map
diff --git a/src/paimon/core/io/vector_file_batch_reader.cpp
b/src/paimon/core/io/vector_file_batch_reader.cpp
index a4573eef..f16025c4 100644
--- a/src/paimon/core/io/vector_file_batch_reader.cpp
+++ b/src/paimon/core/io/vector_file_batch_reader.cpp
@@ -119,6 +119,11 @@ Result<std::shared_ptr<arrow::Array>> CastListToVector(
fmt::format("Cannot restore VECTOR from type {}",
array->type()->ToString()));
}
PAIMON_RETURN_NOT_OK(VectorUtils::ValidateVectorElements(*array));
+ if (array->null_count() == array->length()) {
+ PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Array> result,
+ arrow::MakeArrayOfNull(read_type,
array->length(), pool));
+ return result;
+ }
arrow::compute::ExecContext exec_context(pool);
arrow::TypeHolder type_holder(read_type.get());
arrow::compute::CastOptions options = arrow::compute::CastOptions::Safe();
diff --git a/src/paimon/format/parquet/CMakeLists.txt
b/src/paimon/format/parquet/CMakeLists.txt
index c31e3cc3..96854658 100644
--- a/src/paimon/format/parquet/CMakeLists.txt
+++ b/src/paimon/format/parquet/CMakeLists.txt
@@ -20,7 +20,6 @@ set(PAIMON_PARQUET_FILE_FORMAT
file_reader_wrapper.cpp
page_filtered_row_group_reader.cpp
parquet_timestamp_converter.cpp
- parquet_vector_converter.cpp
parquet_file_batch_reader.cpp
parquet_file_format_factory.cpp
parquet_format_writer.cpp
@@ -56,7 +55,6 @@ if(PAIMON_BUILD_TESTS)
file_reader_wrapper_test.cpp
page_filtered_row_group_reader_test.cpp
parquet_timestamp_converter_test.cpp
- parquet_vector_converter_test.cpp
parquet_vector_io_test.cpp
parquet_field_id_converter_test.cpp
parquet_file_batch_reader_test.cpp
diff --git a/src/paimon/format/parquet/parquet_format_writer.cpp
b/src/paimon/format/parquet/parquet_format_writer.cpp
index 6e69e694..0a8e38b4 100644
--- a/src/paimon/format/parquet/parquet_format_writer.cpp
+++ b/src/paimon/format/parquet/parquet_format_writer.cpp
@@ -23,7 +23,6 @@
#include <string_view>
#include <utility>
-#include "arrow/array/array_nested.h"
#include "arrow/c/bridge.h"
#include "arrow/memory_pool.h"
#include "arrow/record_batch.h"
@@ -32,9 +31,7 @@
#include "paimon/common/metrics/metrics_impl.h"
#include "paimon/common/utils/arrow/arrow_output_stream_adapter.h"
#include "paimon/common/utils/arrow/status_utils.h"
-#include "paimon/common/utils/checked_cast.h"
#include "paimon/format/parquet/parquet_format_defs.h"
-#include "paimon/format/parquet/parquet_vector_converter.h"
#include "parquet/arrow/writer.h"
#include "parquet/properties.h"
@@ -58,33 +55,17 @@ Result<std::unique_ptr<ParquetFormatWriter>>
ParquetFormatWriter::Create(
::parquet::ArrowWriterProperties::Builder arrow_properties_builder;
auto arrow_writer_properties =
arrow_properties_builder.enable_deprecated_int96_timestamps()->build();
- auto logical_type = arrow::struct_(schema->fields());
- auto write_type =
-
checked_pointer_cast<arrow::StructType>(ParquetVectorConverter::GetWriteType(logical_type));
- auto write_schema = arrow::schema(write_type->fields(),
schema->metadata());
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
std::unique_ptr<::parquet::arrow::FileWriter> file_writer,
- ::parquet::arrow::FileWriter::Open(*write_schema, pool.get(), out,
writer_properties,
+ ::parquet::arrow::FileWriter::Open(*schema, pool.get(), out,
writer_properties,
arrow_writer_properties));
- return std::unique_ptr<ParquetFormatWriter>(new ParquetFormatWriter(
- std::move(file_writer), out, schema, max_memory_use,
- /*needs_vector_conversion=*/!logical_type->Equals(write_type), pool));
+ return std::unique_ptr<ParquetFormatWriter>(
+ new ParquetFormatWriter(std::move(file_writer), out, schema,
max_memory_use, pool));
}
Status ParquetFormatWriter::AddBatch(ArrowArray* batch) {
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<::arrow::RecordBatch>
record_batch,
arrow::ImportRecordBatch(batch,
schema_));
- if (needs_vector_conversion_) {
- // TODO(ChaomingZhangCN): Remove this conversion after upgrading
Arrow. Arrow 17
- // mishandles nullable FixedSizeList values when writing them as
Parquet LIST.
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::StructArray>
struct_array,
- record_batch->ToStructArray());
- std::shared_ptr<arrow::Array> array = struct_array;
- PAIMON_ASSIGN_OR_RAISE(array,
-
ParquetVectorConverter::ConvertToWriteType(array, pool_.get()));
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(record_batch,
-
arrow::RecordBatch::FromStructArray(array, pool_.get()));
- }
if (static_cast<uint64_t>(pool_->bytes_allocated()) > max_memory_use_) {
PAIMON_RETURN_NOT_OK_FROM_ARROW(writer_->NewBufferedRowGroup());
}
@@ -132,14 +113,13 @@ Result<uint64_t> ParquetFormatWriter::GetEstimateLength()
const {
ParquetFormatWriter::ParquetFormatWriter(std::unique_ptr<::parquet::arrow::FileWriter>
writer,
const
std::shared_ptr<ArrowOutputStreamAdapter>& out,
const std::shared_ptr<arrow::Schema>&
schema,
- uint64_t max_memory_use, bool
needs_vector_conversion,
+ uint64_t max_memory_use,
const
std::shared_ptr<arrow::MemoryPool>& pool)
: pool_(pool),
out_(out),
writer_(std::move(writer)),
schema_(schema),
metrics_(std::make_shared<MetricsImpl>()),
- max_memory_use_(max_memory_use),
- needs_vector_conversion_(needs_vector_conversion) {}
+ max_memory_use_(max_memory_use) {}
} // namespace paimon::parquet
diff --git a/src/paimon/format/parquet/parquet_format_writer.h
b/src/paimon/format/parquet/parquet_format_writer.h
index f8f44119..4ab58d73 100644
--- a/src/paimon/format/parquet/parquet_format_writer.h
+++ b/src/paimon/format/parquet/parquet_format_writer.h
@@ -72,7 +72,6 @@ class ParquetFormatWriter : public FormatWriter {
ParquetFormatWriter(std::unique_ptr<::parquet::arrow::FileWriter> writer,
const std::shared_ptr<ArrowOutputStreamAdapter>& out,
const std::shared_ptr<arrow::Schema>& schema, uint64_t
max_memory_use,
- bool needs_vector_conversion,
const std::shared_ptr<arrow::MemoryPool>& pool);
Result<uint64_t> GetEstimateLength() const;
@@ -84,7 +83,6 @@ class ParquetFormatWriter : public FormatWriter {
std::shared_ptr<Metrics> metrics_;
int64_t total_records_written_ = 0;
uint64_t max_memory_use_;
- bool needs_vector_conversion_;
};
} // namespace paimon::parquet
diff --git a/src/paimon/format/parquet/parquet_vector_converter.cpp
b/src/paimon/format/parquet/parquet_vector_converter.cpp
deleted file mode 100644
index 5b6446d2..00000000
--- a/src/paimon/format/parquet/parquet_vector_converter.cpp
+++ /dev/null
@@ -1,174 +0,0 @@
-/*
- * 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/parquet/parquet_vector_converter.h"
-
-#include <cstdint>
-#include <limits>
-#include <memory>
-#include <vector>
-
-#include "arrow/array.h"
-#include "arrow/array/array_nested.h"
-#include "arrow/array/builder_primitive.h"
-#include "arrow/compute/api.h"
-#include "arrow/type.h"
-#include "paimon/common/utils/arrow/status_utils.h"
-#include "paimon/common/utils/arrow/vector_utils.h"
-#include "paimon/common/utils/checked_cast.h"
-#include "paimon/status.h"
-
-namespace paimon::parquet {
-namespace {
-
-Result<std::shared_ptr<arrow::Array>> CastToListType(
- const std::shared_ptr<arrow::Array>& array, const
std::shared_ptr<arrow::DataType>& write_type,
- arrow::MemoryPool* pool) {
- arrow::compute::ExecContext exec_context(pool);
- arrow::TypeHolder type_holder(write_type.get());
- arrow::compute::CastOptions options = arrow::compute::CastOptions::Safe();
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
- std::shared_ptr<arrow::Array> result,
- arrow::compute::Cast(*array, type_holder, options, &exec_context));
- return result;
-}
-
-/// Rebuilds a nullable VECTOR as a LIST whose null slots have a zero length,
dropping the
-/// values Arrow keeps for them.
-///
-/// TODO(ChaomingZhangCN): Cast the whole array once Arrow is upgraded. Arrow
17 casts a null
-/// FixedSizeList row to a null LIST slot spanning `list_size` values, and the
Parquet writer
-/// rejects a LIST with non-zero length null slots.
-Result<std::shared_ptr<arrow::Array>> CompactNullVectorsToList(
- const arrow::FixedSizeListArray& vector_array,
- const std::shared_ptr<arrow::DataType>& write_type, arrow::MemoryPool*
pool) {
- const auto& vector_type = checked_cast<const
arrow::FixedSizeListType&>(*vector_array.type());
- const int32_t vector_length = vector_type.list_size();
- if (vector_array.length() > std::numeric_limits<int32_t>::max() /
vector_length) {
- return Status::Invalid("VECTOR values exceed the maximum Parquet LIST
offset");
- }
-
- arrow::Int32Builder offsets_builder(pool);
- arrow::Int64Builder indices_builder(pool);
- arrow::BooleanBuilder validity_builder(pool);
-
PAIMON_RETURN_NOT_OK_FROM_ARROW(offsets_builder.Reserve(vector_array.length() +
1));
-
PAIMON_RETURN_NOT_OK_FROM_ARROW(indices_builder.Reserve(vector_array.length() *
vector_length));
-
PAIMON_RETURN_NOT_OK_FROM_ARROW(validity_builder.Reserve(vector_array.length()));
- PAIMON_RETURN_NOT_OK_FROM_ARROW(offsets_builder.Append(0));
-
- int32_t offset = 0;
- for (int64_t i = 0; i < vector_array.length(); ++i) {
- bool valid = !vector_array.IsNull(i);
- PAIMON_RETURN_NOT_OK_FROM_ARROW(validity_builder.Append(valid));
- if (valid) {
- int64_t value_offset = (vector_array.offset() + i) * vector_length;
- for (int32_t j = 0; j < vector_length; ++j) {
-
PAIMON_RETURN_NOT_OK_FROM_ARROW(indices_builder.Append(value_offset + j));
- }
- offset += vector_length;
- }
- PAIMON_RETURN_NOT_OK_FROM_ARROW(offsets_builder.Append(offset));
- }
-
- std::shared_ptr<arrow::Array> offsets;
- std::shared_ptr<arrow::Array> indices;
- std::shared_ptr<arrow::Array> validity;
- PAIMON_RETURN_NOT_OK_FROM_ARROW(offsets_builder.Finish(&offsets));
- PAIMON_RETURN_NOT_OK_FROM_ARROW(indices_builder.Finish(&indices));
- PAIMON_RETURN_NOT_OK_FROM_ARROW(validity_builder.Finish(&validity));
-
- arrow::compute::ExecContext exec_context(pool);
- PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(
- arrow::Datum values,
- arrow::compute::Take(arrow::Datum(vector_array.values()),
arrow::Datum(indices),
- arrow::compute::TakeOptions::NoBoundsCheck(),
&exec_context));
- return std::make_shared<arrow::ListArray>(
- write_type, vector_array.length(), offsets->data()->buffers[1],
values.make_array(),
- validity->data()->buffers[1], vector_array.null_count());
-}
-
-} // namespace
-
-std::shared_ptr<arrow::DataType> ParquetVectorConverter::GetWriteType(
- const std::shared_ptr<arrow::DataType>& logical_type) {
- switch (logical_type->id()) {
- case arrow::Type::FIXED_SIZE_LIST: {
- const auto& vector_type = checked_cast<const
arrow::FixedSizeListType&>(*logical_type);
- return arrow::list(
-
vector_type.value_field()->WithType(GetWriteType(vector_type.value_type())));
- }
- case arrow::Type::STRUCT: {
- arrow::FieldVector fields;
- fields.reserve(logical_type->num_fields());
- for (const auto& field : logical_type->fields()) {
- fields.push_back(field->WithType(GetWriteType(field->type())));
- }
- return arrow::struct_(fields);
- }
- case arrow::Type::LIST:
- return arrow::list(
-
logical_type->field(0)->WithType(GetWriteType(logical_type->field(0)->type())));
- case arrow::Type::MAP: {
- const auto& map_type = checked_cast<const
arrow::MapType&>(*logical_type);
- return std::make_shared<arrow::MapType>(
- map_type.value_field()->WithType(arrow::struct_(
-
{map_type.key_field()->WithType(GetWriteType(map_type.key_type())),
-
map_type.item_field()->WithType(GetWriteType(map_type.item_type()))})),
- map_type.keys_sorted());
- }
- default:
- return logical_type;
- }
-}
-
-Result<std::shared_ptr<arrow::Array>>
ParquetVectorConverter::ConvertToWriteType(
- const std::shared_ptr<arrow::Array>& array, arrow::MemoryPool* pool) {
- if (!VectorUtils::ContainsVectorType(array->type())) {
- return array;
- }
- std::shared_ptr<arrow::DataType> write_type = GetWriteType(array->type());
- switch (array->type_id()) {
- case arrow::Type::FIXED_SIZE_LIST: {
- PAIMON_RETURN_NOT_OK(VectorUtils::ValidateVectorElements(*array));
- const auto& vector_array = checked_cast<const
arrow::FixedSizeListArray&>(*array);
- if (vector_array.null_count() == 0) {
- return CastToListType(array, write_type, pool);
- }
- return CompactNullVectorsToList(vector_array, write_type, pool);
- }
- case arrow::Type::STRUCT:
- case arrow::Type::LIST:
- case arrow::Type::MAP: {
- std::vector<std::shared_ptr<arrow::ArrayData>> children;
- children.reserve(array->data()->child_data.size());
- for (const auto& child_data : array->data()->child_data) {
- PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Array> child,
-
ConvertToWriteType(arrow::MakeArray(child_data), pool));
- children.push_back(child->data());
- }
- std::shared_ptr<arrow::ArrayData> data = array->data()->Copy();
- data->child_data = std::move(children);
- data->type = write_type;
- return arrow::MakeArray(data);
- }
- default:
- return array;
- }
-}
-
-} // namespace paimon::parquet
diff --git a/src/paimon/format/parquet/parquet_vector_converter.h
b/src/paimon/format/parquet/parquet_vector_converter.h
deleted file mode 100644
index a265e2d1..00000000
--- a/src/paimon/format/parquet/parquet_vector_converter.h
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#pragma once
-
-#include <memory>
-
-#include "arrow/memory_pool.h"
-#include "paimon/result.h"
-
-namespace arrow {
-class Array;
-class DataType;
-} // namespace arrow
-
-namespace paimon::parquet {
-
-/// Converts logical FixedSizeList VECTOR arrays to Parquet LIST arrays.
-class ParquetVectorConverter {
- public:
- ParquetVectorConverter() = delete;
- ~ParquetVectorConverter() = delete;
-
- static Result<std::shared_ptr<arrow::Array>> ConvertToWriteType(
- const std::shared_ptr<arrow::Array>& array, arrow::MemoryPool* pool);
-
- static std::shared_ptr<arrow::DataType> GetWriteType(
- const std::shared_ptr<arrow::DataType>& logical_type);
-};
-
-} // namespace paimon::parquet
diff --git a/src/paimon/format/parquet/parquet_vector_converter_test.cpp
b/src/paimon/format/parquet/parquet_vector_converter_test.cpp
deleted file mode 100644
index 6e1c0b0d..00000000
--- a/src/paimon/format/parquet/parquet_vector_converter_test.cpp
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * 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/parquet/parquet_vector_converter.h"
-
-#include <memory>
-
-#include "arrow/api.h"
-#include "arrow/ipc/json_simple.h"
-#include "gtest/gtest.h"
-#include "paimon/common/utils/checked_cast.h"
-#include "paimon/testing/utils/testharness.h"
-
-namespace paimon::parquet::test {
-
-TEST(ParquetVectorConverterTest, ConvertNullableVectorToList) {
- auto vector_type = arrow::fixed_size_list(arrow::float32(), 3);
- auto vector_array = arrow::ipc::internal::json::ArrayFromJSON(
- vector_type, R"([[1.0, 2.0, 3.0], null, [4.0, 5.0,
6.0]])")
- .ValueOrDie();
-
- ASSERT_OK_AND_ASSIGN(
- std::shared_ptr<arrow::Array> converted,
- ParquetVectorConverter::ConvertToWriteType(vector_array,
arrow::default_memory_pool()));
- ASSERT_EQ(converted->type()->id(), arrow::Type::LIST);
- auto list_array = checked_pointer_cast<arrow::ListArray>(converted);
- ASSERT_EQ(list_array->value_length(0), 3);
- ASSERT_TRUE(list_array->IsNull(1));
- // The Parquet writer rejects a null LIST slot spanning values, so the
values Arrow keeps for
- // a null VECTOR row are dropped.
- ASSERT_EQ(list_array->value_length(1), 0);
- ASSERT_EQ(list_array->value_length(2), 3);
- ASSERT_EQ(list_array->values()->length(), 6);
- auto values =
checked_pointer_cast<arrow::FloatArray>(list_array->values());
- ASSERT_FLOAT_EQ(values->Value(3), 4.0f);
-}
-
-TEST(ParquetVectorConverterTest, ConvertNestedVectorsToList) {
- auto vector_type =
- arrow::fixed_size_list(arrow::field("item", arrow::float32(),
/*nullable=*/false), 2);
- auto nested_type = arrow::struct_({
- arrow::field("vectors", arrow::list(vector_type)),
- arrow::field("by_name", arrow::map(arrow::utf8(), vector_type)),
- });
- auto nested_array =
- arrow::ipc::internal::json::ArrayFromJSON(nested_type,
- R"([[[[1.0, 2.0], null],
[["a", [3.0, 4.0]]]],
- [null, [["b", null]]]])")
- .ValueOrDie();
-
- ASSERT_OK_AND_ASSIGN(
- std::shared_ptr<arrow::Array> physical_array,
- ParquetVectorConverter::ConvertToWriteType(nested_array,
arrow::default_memory_pool()));
- auto physical_type =
checked_pointer_cast<arrow::StructType>(physical_array->type());
- auto physical_list =
checked_pointer_cast<arrow::ListType>(physical_type->field(0)->type());
- auto physical_map =
checked_pointer_cast<arrow::MapType>(physical_type->field(1)->type());
- ASSERT_EQ(physical_list->value_type()->id(), arrow::Type::LIST);
- ASSERT_EQ(physical_map->item_type()->id(), arrow::Type::LIST);
-}
-
-TEST(ParquetVectorConverterTest, ConvertSlicedVectorToList) {
- auto vector_type = arrow::fixed_size_list(arrow::float64(), 2);
- auto vector_array =
- arrow::ipc::internal::json::ArrayFromJSON(vector_type, R"([[1.0, 2.0],
[3.0, 4.0], null])")
- .ValueOrDie()
- ->Slice(1, 2);
-
- ASSERT_OK_AND_ASSIGN(
- std::shared_ptr<arrow::Array> converted,
- ParquetVectorConverter::ConvertToWriteType(vector_array,
arrow::default_memory_pool()));
- auto list_array = checked_pointer_cast<arrow::ListArray>(converted);
- ASSERT_EQ(list_array->length(), 2);
- ASSERT_EQ(list_array->value_length(0), 2);
- ASSERT_TRUE(list_array->IsNull(1));
- auto values =
checked_pointer_cast<arrow::DoubleArray>(list_array->values());
- ASSERT_DOUBLE_EQ(values->Value(0), 3.0);
- ASSERT_DOUBLE_EQ(values->Value(1), 4.0);
-}
-
-} // namespace paimon::parquet::test
diff --git a/src/paimon/format/parquet/parquet_vector_io_test.cpp
b/src/paimon/format/parquet/parquet_vector_io_test.cpp
index f45dad1e..ad60caef 100644
--- a/src/paimon/format/parquet/parquet_vector_io_test.cpp
+++ b/src/paimon/format/parquet/parquet_vector_io_test.cpp
@@ -258,6 +258,45 @@ TEST_F(ParquetVectorIoTest, WriteAndReadVector) {
R"([[1, [1.0, 2.0, 3.0]], [2, null], [3, [4.0, 5.0,
6.0]]])");
}
+TEST_F(ParquetVectorIoTest, WriteAndReadAllNullVector) {
+ auto vector_type =
+ arrow::fixed_size_list(arrow::field("item", arrow::float32(),
/*nullable=*/false), 3);
+ auto struct_type = checked_pointer_cast<arrow::StructType>(arrow::struct_(
+ {arrow::field("id", arrow::int32()), arrow::field("embedding",
vector_type)}));
+ WriteAndCheck("all-null-vector-list.parquet", struct_type, struct_type,
+ R"([[1, null], [2, null], [3, null]])");
+}
+
+TEST_F(ParquetVectorIoTest, WriteAndReadAllNullFixedSizeListWithArrowSchema) {
+ auto vector_type =
+ arrow::fixed_size_list(arrow::field("item", arrow::float32(),
/*nullable=*/false), 3);
+ auto logical_type =
checked_pointer_cast<arrow::StructType>(arrow::struct_({
+ arrow::field("id", arrow::int32()),
+ arrow::field("embedding", vector_type),
+ }));
+ const std::string json = R"([[1, null], [2, null], [3, null]])";
+ std::string file_path = dir_->Str() + "/all-null-vector.parquet";
+ WriteWithArrowWriter(file_path, logical_type, json);
+
+ std::shared_ptr<arrow::StructType> file_type;
+ ReadFileType(file_path, &file_type);
+ std::shared_ptr<arrow::Field> file_vector_field =
file_type->GetFieldByName("embedding");
+ ASSERT_TRUE(file_vector_field);
+ ASSERT_EQ(file_vector_field->type()->id(), arrow::Type::FIXED_SIZE_LIST);
+
+ std::unique_ptr<FileBatchReader> reader;
+ CreateVectorReader(file_path, arrow::schema(logical_type->fields()),
/*predicate=*/nullptr,
+ /*options=*/{}, /*batch_size=*/10, &reader);
+ ASSERT_OK_AND_ASSIGN(std::shared_ptr<arrow::ChunkedArray> actual,
+
paimon::test::ReadResultCollector::CollectResult(reader.get()));
+ arrow::Result<std::shared_ptr<arrow::Array>> expected_result =
+ arrow::ipc::internal::json::ArrayFromJSON(logical_type, json);
+ ASSERT_TRUE(expected_result.ok()) << expected_result.status().ToString();
+
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(std::move(expected_result).ValueOrDie())
+ ->Equals(actual))
+ << actual->ToString();
+}
+
TEST_F(ParquetVectorIoTest, ReadOrdinaryParquetListAsVector) {
auto physical_type = checked_pointer_cast<arrow::StructType>(
arrow::struct_({arrow::field("id", arrow::int32()),
@@ -370,6 +409,13 @@ TEST_F(ParquetVectorIoTest, ReadNullableJavaFixture) {
{{{1.0f, 2.0f, 3.0f}}, std::nullopt, {{4.0f, 5.0f,
6.0f}}});
}
+TEST_F(ParquetVectorIoTest, ReadNullableRustFixture) {
+ ReadFixtureAndCheck("rust_vector_nullable.parquet",
arrow::Type::FIXED_SIZE_LIST,
+ /*vector_length=*/3, /*expected_ids=*/{1, 2, 3},
+ /*expected_vectors=*/
+ {{{1.0f, 2.0f, 3.0f}}, std::nullopt, {{4.0f, 5.0f,
6.0f}}});
+}
+
// A table can hold files from several writers, and Paimon Java stores VECTOR
as Parquet LIST
// while Paimon Rust stores it as FixedSizeList. Reading both with the table
schema must produce
// batches of one Arrow type, otherwise they cannot be combined into a single
result.
@@ -386,7 +432,7 @@ TEST_F(ParquetVectorIoTest,
ReadMixedListAndFixedSizeListFixtures) {
// the whole result has been consumed.
std::vector<std::unique_ptr<FileBatchReader>> readers;
arrow::ArrayVector chunks;
- for (const char* file_name : {"java_vector_nullable.parquet",
"rust_vector.parquet"}) {
+ for (const char* file_name : {"java_vector_nullable.parquet",
"rust_vector_nullable.parquet"}) {
std::string file_path =
paimon::test::GetDataDir() + "/parquet/vector_compatibility/" +
file_name;
std::unique_ptr<FileBatchReader> reader;
@@ -406,7 +452,7 @@ TEST_F(ParquetVectorIoTest,
ReadMixedListAndFixedSizeListFixtures) {
arrow::Result<std::shared_ptr<arrow::Array>> expected_result =
arrow::ipc::internal::json::ArrayFromJSON(
logical_type, R"([[1, [1.0, 2.0, 3.0]], [2, null], [3, [4.0, 5.0,
6.0]],
- [1, [1.0, 2.0, 3.0]], [2, [7.0, 8.0, 9.0]], [3,
[4.0, 5.0, 6.0]]])");
+ [1, [1.0, 2.0, 3.0]], [2, null], [3, [4.0, 5.0,
6.0]]])");
ASSERT_TRUE(expected_result.ok()) << expected_result.status().ToString();
std::shared_ptr<arrow::ChunkedArray> merged =
std::move(merged_result).ValueOrDie();
ASSERT_TRUE(std::make_shared<arrow::ChunkedArray>(std::move(expected_result).ValueOrDie())
@@ -414,26 +460,4 @@ TEST_F(ParquetVectorIoTest,
ReadMixedListAndFixedSizeListFixtures) {
<< merged->ToString();
}
-// A writer that stores the Arrow schema, such as Paimon Rust or Python,
exposes the VECTOR column
-// as FixedSizeList. Arrow 17 cannot read a null value from such a column:
Parquet stores a null
-// list slot with no values, while FixedSizeListReader::AssembleArray in
-// parquet/arrow/reader.cc requires every slot to span exactly `list_size`
values.
-//
-// TODO(ChaomingZhangCN): Turn this into a read check once Arrow is upgraded.
-TEST_F(ParquetVectorIoTest, ReadNullableRustFixtureIsUnsupported) {
- std::string file_path =
- paimon::test::GetDataDir() +
"/parquet/vector_compatibility/rust_vector_nullable.parquet";
- std::shared_ptr<arrow::StructType> file_type;
- ReadFileType(file_path, &file_type);
- std::shared_ptr<arrow::Field> file_vector_field =
file_type->GetFieldByName("embedding");
- ASSERT_TRUE(file_vector_field);
- ASSERT_EQ(file_vector_field->type()->id(), arrow::Type::FIXED_SIZE_LIST);
-
- std::unique_ptr<FileBatchReader> reader;
- CreateVectorReader(file_path, arrow::schema(file_type->fields()),
/*predicate=*/nullptr,
- /*options=*/{}, /*batch_size=*/10, &reader);
-
ASSERT_NOK_WITH_MSG(paimon::test::ReadResultCollector::CollectResult(reader.get()),
- "Expected all lists to be of size=3");
-}
-
} // namespace paimon::parquet::test
diff --git a/test/test_data/parquet/vector_compatibility/README.md
b/test/test_data/parquet/vector_compatibility/README.md
index 15eb2ef3..8a2fe25c 100644
--- a/test/test_data/parquet/vector_compatibility/README.md
+++ b/test/test_data/parquet/vector_compatibility/README.md
@@ -22,11 +22,9 @@ VECTOR columns, with and without null vectors.
`(2, null)` and `(3, [4, 5, 6])`.
A file that stores the Arrow schema, as the Rust writer does, is read back as
-`fixed_size_list`. Arrow 17 cannot read a null value from such a column,
because Parquet stores a
-null list slot with no values while `FixedSizeListReader::AssembleArray` in
-`parquet/arrow/reader.cc` requires every slot to span exactly `list_size`
values. Reading
-`rust_vector_nullable.parquet` therefore fails until Arrow is upgraded, which
-`ParquetVectorIoTest.ReadNullableRustFixtureIsUnsupported` pins.
+`fixed_size_list`. The bundled Arrow 17 patch backports the Arrow community
fix that pads the
+decoded child array for null fixed-size-list slots, so
`rust_vector_nullable.parquet` is readable
+as a nullable VECTOR.
SHA-256 checksums: