feat(format): support Vortex file format - #376
zhouyc-ali wants to merge 9 commits into
Conversation
eecbd85 to
ab83648
Compare
Pin the vendored Vortex from commit d82de0e to tag 0.75.0 so paimon-cpp interoperates bidirectionally with paimon-java's vortex-jni 0.73.0: 0.75 reads 0.73-written files and, verified end-to-end, 0.73 reads 0.75-written files. Newer pins (>=0.77) break the cpp->java direction with "Encoding not found in registry". Adapt the FFI call sites to the 0.75 signatures (error message accessor, the arrow/dtype converters drop their session argument, VortexFile::data_source) and fix a latent ownership bug uncovered by the downgrade: at 0.75 vx_data_source_dtype returns a BORROWED pointer (arc_wrapper new_ref, no refcount bump), so it must not be released with vx_dtype_free. Freeing it spuriously decremented the data source's DType Arc, a use-after-free that surfaced later as a segfault in the scan path. The pointer is now treated as borrowed; it stays valid for the schema conversion, which the data source outlives.
…Array NormalizeViewArray only recursed through STRING_VIEW/BINARY_VIEW/STRUCT/LIST, so a FIXED_SIZE_LIST (or LARGE_LIST) with view-typed elements fell into the default branch and was returned unchanged. NormalizeViewType already normalizes those element types, so the read schema declared e.g. fixed_size_list<utf8> while the data stayed fixed_size_list<utf8view> (Vortex hardcodes Utf8 -> Utf8View on export). The ArrowSchema mismatch made callers reinterpret the view buffers as standard strings, silently corrupting data on read. Add LARGE_LIST and FIXED_SIZE_LIST branches symmetric to the LIST branch, including the unchanged-values fast path, reusing the source null bitmap, null count, offset, length and (for LARGE_LIST) value offsets. Covered by WriteThenReadListOfString, WriteThenReadFixedSizeListOfString and WriteThenReadListOfStruct, whose data includes nulls, empty strings and strings long enough to exceed the 12-byte inline view threshold.
Address four review findings plus a related write-path abort: - Scan in storage order. The scan used Vortex's default ordered=false, which may emit chunks out of order via buffer_unordered; NextBatch assigns physical row positions by batch order, so unordered chunks would misalign deletion vectors and primary-key merge. Set ScanOptions.ordered(true), matching paimon-java's VortexRecordsReader. - Recurse into nested fields when projecting to the read schema. ProjectToReadSchema only selected top-level columns, so reading a pruned nested field (e.g. r.b from r:ROW<a,b>) kept the full child under a pruned parent type, mismatching the exported Arrow schema and silently corrupting the output. Project struct/list/fixed-size-list children recursively. - Fix a use-after-free on the stream error path. get_last_error returns a pointer owned by the ArrowArrayStream; it was read after ReleaseStream() invalidated it. Copy the message before releasing. - Rebase sliced batches to offset 0 before vx_array_from_arrow. arrow-rs cannot import a sliced (offset > 0) top-level struct (the parent offset is re-applied to already-offset children, tripping an arrow-data slice assertion that aborts). Offset-0 batches pass through untouched, so the hot path is unchanged. - Attribute the adapted sink entry points in callback_io.rs to vortex-ffi's sink.rs (Apache-2.0, Copyright the Vortex contributors) per Apache-2.0 section 4. Adds regression tests: nested-struct projection, row order across batches, and sliced top-level struct write/read.
…x TSAN races The Debug/TSAN matrix jobs failed the check-clang-tidy gate and the TSAN race gate on the Vortex files: - clang-tidy (modernize-use-using / modernize-use-auto): convert the hand-written C-ABI typedefs in vortex_ffi.h to `using` aliases / plain structs, and use `auto` for the cast-initialized locals in vortex_io_callbacks.cpp and vortex_stats_extractor.cpp. - TSAN: Vortex deserializes its footer/dtype and drives segment reads from internal runtime threads inside the un-instrumented Rust staticlib, so TSAN cannot see the happens-before edges and reports false races whose only non-paimon frames are the vortex_array/vortex_error/vortex_file/vortex_layout/vortex_mask Rust modules. Suppress those module-scoped patterns (they match Rust frames only, so races confined to paimon C++ are still reported), following the existing vortex_buffer/vortex_io/CallbackReadAt precedent.
…validation, CI deps) - Serialize host OutputStream access between the Vortex writer task and a caller-thread Flush(): VortexOutputContext gains a stream mutex and a FlushStream() entry, and VortexFormatWriter::Flush() plus the write/flush callbacks all take it, so Write and Flush can no longer interleave on a stream with no concurrent-access contract. The remaining internally buffered data is drained when the sink closes in Finish(). - Thread the caller's Arrow memory pool into VortexFormatWriter (Create now takes it and the builder passes GetArrowPool(pool_)), so sliced-batch rebasing allocates from the Paimon pool instead of Arrow's default pool. - Validate Vortex-incompatible schema types wherever Vortex can be selected (default file format, file.format-per-level, changelog-file.format), mirroring the Lance validation, instead of only the default format. - setup_flatc.sh: install libclang before the flatc early-exit, so an environment that already has a matching flatc but lacks libclang no longer silently skips the bindgen build dependency. - Tests: FlushBetweenBatchesThenRead (flush interleaved with background writes) and SchemaValidationTest.TestVortexDataTypes (per-level/changelog validation).
36b8af1 to
0d74468
Compare
…Pool fwd decl The Debug build's check-clang-tidy gate analyses every changed translation unit. Any TU that includes paimon/type_fwd.h and also uses arrow::MemoryPool (as the Vortex writer now does for its rebasing pool) trips bugprone-forward-declaration-namespace on the paimon::MemoryPool forward declaration, even though paimon::MemoryPool is a real public type defined in paimon/memory/. Annotate the forward declaration with NOLINT plus a comment explaining that it is intentional.
| if (normalized == "csv" || normalized == "text" || normalized == "json" || | ||
| normalized == "mosaic") { | ||
| normalized == "mosaic" || normalized == "vortex") { | ||
| return Status::NotImplemented( |
There was a problem hiding this comment.
Can we double-check if Lance supports this? If it doesn't, we should add this NotImplemented here.
| // offset-normalized (offset 0) so the rebuilt parents and children stay consistent. | ||
| Result<std::shared_ptr<arrow::Array>> NormalizeViewArray(const std::shared_ptr<arrow::Array>& array, | ||
| arrow::MemoryPool* pool) { | ||
| switch (array->type_id()) { |
There was a problem hiding this comment.
Please add a TODO here to track this potential performance hotspot for future optimization.
| return std::make_shared<arrow::LargeListArray>( | ||
| arrow::large_list(array->type()->field(0)->WithType(values->type())), list.length(), | ||
| list.value_offsets(), values, list.null_bitmap(), list.null_count(), list.offset()); | ||
| } |
There was a problem hiding this comment.
It seems that LARGE_LIST is not supported?
| total_rows_(total_rows), | ||
| pool_(pool), | ||
| arrow_pool_(arrow_pool), | ||
| metrics_(std::make_shared<MetricsImpl>()) {} |
There was a problem hiding this comment.
I'm curious about the reason for the Paimon pool here. Looks like this pool is unused?
| // already matches. Leaf types are expected to match after view normalization; an unexpected leaf | ||
| // mismatch is returned as-is for the caller's schema check to surface. | ||
| Result<std::shared_ptr<arrow::Array>> ProjectArrayToType( | ||
| const std::shared_ptr<arrow::Array>& array, |
There was a problem hiding this comment.
This function can be replaced with NestedProjectionUtils::AlignArrayToReadType. Also, does Vortex not support column pruning? Do we need to read everything back and then prune it?
There was a problem hiding this comment.
If you plan to support this in the next PR, please add a TODO to indicate that this issue is known and will be addressed later. If not supoort, there’s no need to add this kind of fallback; we can simply avoid testing scenarios like nested subcolumn pushdown.
| // paimon-cpp reads back a Vortex table produced by the Java implementation. | ||
| arrow::FieldVector fields = { | ||
| arrow::field("_VALUE_KIND", arrow::int8()), | ||
| arrow::field("id", arrow::int32()), |
There was a problem hiding this comment.
Please add complete compatibility tests for Vortex supporting all field types, similar to test_data/mosaic.
| (BatchWriteBuilder / BatchTableWrite / BatchTableCommit) on JDK 17 with | ||
| --add-opens=java.base/java.nio=ALL-UNNAMED (required by Arrow Java memory). | ||
| Read back and verified by paimon-cpp in | ||
| ScanAndReadInteTest.TestVortexJavaCompatibility. |
There was a problem hiding this comment.
Please remove this. This test data may be used by other tests in the future, so it shouldn’t be coupled to TestVortexJavaCompatibility.
| }; | ||
| std::shared_ptr<arrow::DataType> data_type = arrow::struct_(fields); | ||
| std::shared_ptr<arrow::Array> expected_array = | ||
| arrow::ipc::internal::json::ArrayFromJSON(data_type, R"([ |
There was a problem hiding this comment.
Also, I see write support has already been added. Are you planning to leave the combined read/write TEST_P cases from the other integration tests for a follow-up commit?
| CHECK_HOOK_STATUS(batch.status(), i); | ||
| eof = BatchReader::IsEofBatch(batch.value()); | ||
| } | ||
| run_complete = true; |
There was a problem hiding this comment.
If the read eventually succeeds, please verify that the final read result matches the expectation. You can use ReadResultCollector for this.
| run_complete = true; | ||
| break; | ||
| } | ||
| ASSERT_TRUE(run_complete); |
There was a problem hiding this comment.
Similarly, when the write eventually succeeds, please also verify that the written data can be read back and matches the expectation.
Purpose
Linked issue: close #319
Adds optional read and write support for the Apache Paimon Vortex file format, integrated through
the upstream Vortex Rust FFI and gated by
PAIMON_ENABLE_VORTEX.conversion. Vortex exports strings and binaries as Arrow view types; the reader normalizes them
back to the standard types declared in the Paimon schema, recursing through structs, lists and
fixed-size lists.
vortex-jni 0.73.0 (each implementation reads the other's files, verified end-to-end). Newer Vortex
(>= 0.77) emits encodings that vortex-jni 0.73.0 cannot read, breaking the C++ -> Java direction.
InputStream/OutputStream(works with anyFileSystem: local, OSS, S3, Jindo), instead ofbuffering whole files in memory on read or staging a local temporary file on write. The stock
Vortex C API cannot route IO through paimon's filesystem, so the bridge is added to
vortex-ffias an injected Rust module (
crates/vortex_callback_io/) plus a one-line patch(
cmake_modules/vortex.diff).to the beginning when
SetReadSchemais called.FileInfo; Vortex exposes no per-column statistics to Paimon(consistent with the Java implementation).
PrefetchFileBatchReader(Vortex performs random-access reads through its own callbacks).
ordered=true, matching paimon-java) so the physical row positions theread path assigns by batch order stay aligned for deletion vectors and primary-key merge; rebase
sliced batches to offset 0 before handing them to Vortex, and copy the Arrow stream error message
before releasing the stream.
-DPAIMON_ENABLE_VORTEX=ONtobuild_paimon.sh, install thestableRust toolchain (Vortex MSRV 1.95), and build
flatc25.12.19 (ci/scripts/setup_flatc.sh), whichVortex needs to compile its FlatBuffers schemas at build time.
Predicate/projection pushdown into Vortex is not implemented; all rows and columns are scanned and
projected to the read schema.
Tests
PAIMON_ENABLE_VORTEX=ONpaimon-vortex-format-test): 12/12 passed, including round-trips forlist<utf8>,fixed_size_list<utf8>andlist<struct>(view normalization), nested-structprojection (
r.bfromr:ROW<a,b>), row order across batches (ordered scan), and slicedtop-level struct write/read
cargo test -p vortex-ffi callback_io: 3/3 passedTestVortexJavaCompatibility)git diff --checkclean; cpplint/cmake-format/rustfmt/pre-commit and Apache RATare enforced by CI
API and Format
Adds the optional Vortex file-format factory and the
PAIMON_ENABLE_VORTEXCMake option. It readsand writes the existing Apache Paimon Vortex file format through the upstream Vortex Rust FFI (pinned
to 0.75.0) and does not change the Vortex binary format or any existing public API under
include/.Building Vortex requires a Rust toolchain (>= 1.95) and
flatc25.12.19; both are set up in CI.Documentation
Updated
docs/source/user_guide/format_table.rstto list Vortex among the formats a format tabledoes not reach, and added
crates/vortex_callback_io/README.mddescribing the injected module andits build wiring.
Generative AI tooling
Generated-by: Qoder