Skip to content

feat(format): support Vortex file format - #376

Merged
lxy-9602 merged 14 commits into
apache:mainfrom
zhouyc-ali:feat/vortex-file-format
Oct 15, 2026
Merged

lxy-9602 merged 14 commits into
apache:mainfrom
zhouyc-ali:feat/vortex-file-format

Conversation

@zhouyc-ali

@zhouyc-ali zhouyc-ali commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

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.

  • Add the Vortex file reader, writer, format factory, stats extractor, and Arrow C Data Interface
    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.
  • Pin the vendored Vortex to tag 0.75.0, which interoperates bidirectionally with paimon-java's
    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.
  • Add a callback-based IO bridge so Vortex reads and writes directly through paimon's
    InputStream/OutputStream (works with any FileSystem: local, OSS, S3, Jindo), instead of
    buffering 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-ffi
    as an injected Rust module (crates/vortex_callback_io/) plus a one-line patch
    (cmake_modules/vortex.diff).
  • Support projection (including nested fields) and configurable output batch sizes; reset the reader
    to the beginning when SetReadSchema is called.
  • Read the file row count for FileInfo; Vortex exposes no per-column statistics to Paimon
    (consistent with the Java implementation).
  • Validate Vortex-incompatible schema types (MAP, MULTISET, VARIANT, BLOB).
  • Register Vortex in the file-format factory and bypass the generic PrefetchFileBatchReader
    (Vortex performs random-access reads through its own callbacks).
  • Scan in storage order (ordered=true, matching paimon-java) so the physical row positions the
    read 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.
  • Enable Vortex in CI: add -DPAIMON_ENABLE_VORTEX=ON to build_paimon.sh, install the stable
    Rust toolchain (Vortex MSRV 1.95), and build flatc 25.12.19 (ci/scripts/setup_flatc.sh), which
    Vortex 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

  • Clang Debug build with PAIMON_ENABLE_VORTEX=ON
  • full CTest suite: 33/33 targets passed
  • Vortex unit tests (paimon-vortex-format-test): 12/12 passed, including round-trips for
    list<utf8>, fixed_size_list<utf8> and list<struct> (view normalization), nested-struct
    projection (r.b from r:ROW<a,b>), row order across batches (ordered scan), and sliced
    top-level struct write/read
  • Rust cargo test -p vortex-ffi callback_io: 3/3 passed
  • Java 0.73.0 -> C++ fixture read (TestVortexJavaCompatibility)
  • C++ 0.75.0 -> Java 0.73.0 direct read (verified out-of-band; the two directions are reciprocal)
  • clang-format and git diff --check clean; cpplint/cmake-format/rustfmt/pre-commit and Apache RAT
    are enforced by CI

API and Format

Adds the optional Vortex file-format factory and the PAIMON_ENABLE_VORTEX CMake option. It reads
and 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 flatc 25.12.19; both are set up in CI.

Documentation

Updated docs/source/user_guide/format_table.rst to list Vortex among the formats a format table
does not reach, and added crates/vortex_callback_io/README.md describing the injected module and
its build wiring.

Generative AI tooling

Generated-by: Qoder

@zhouyc-ali
zhouyc-ali force-pushed the feat/vortex-file-format branch 3 times, most recently from 36b8af1 to 0d74468 Compare September 22, 2026 15:09
Comment thread src/paimon/core/table/format/format_table.cpp
Comment thread src/paimon/format/vortex/vortex_file_batch_reader.cpp
Comment thread src/paimon/format/vortex/vortex_file_batch_reader.cpp Outdated
Comment thread src/paimon/format/vortex/vortex_file_batch_reader.cpp
Comment thread src/paimon/format/vortex/vortex_file_batch_reader.cpp Outdated
Comment thread test/inte/scan_and_read_inte_test.cpp
Comment thread test/test_data/vortex/append_java_compat.db/append_java_compat/README.md Outdated
Comment thread test/inte/scan_and_read_inte_test.cpp
Comment thread src/paimon/format/vortex/vortex_file_format_test.cpp
Comment thread src/paimon/format/vortex/vortex_file_format_test.cpp
Comment thread src/paimon/format/vortex/vortex_stats_extractor.cpp Outdated
Comment thread src/paimon/format/vortex/vortex_file_batch_reader.cpp Outdated
Comment thread src/paimon/format/vortex/vortex_file_batch_reader.cpp Outdated
Comment thread src/paimon/format/vortex/vortex_format_writer.cpp
Comment thread crates/vortex_callback_io/callback_io.rs
Comment thread src/paimon/format/vortex/vortex_format_writer.cpp Outdated
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).
…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.
Report one unknown statistic per schema field, validate the schema at the
writer boundary, close the input stream with its last owner, align scanned
batches to the read schema, make Flush() a no-op and shrink the write channel.
@zhouyc-ali
zhouyc-ali force-pushed the feat/vortex-file-format branch from 1c814ff to 64d5ed4 Compare October 10, 2026 07:38
Comment thread src/paimon/format/vortex/vortex_file_batch_reader.cpp
@lxy-9602
lxy-9602 requested a review from SteNicholas October 13, 2026 03:09

@SteNicholas SteNicholas left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhouyc-ali, thanks for updates. I have left some comments from reviewing 64d5ed4: write-callback shutdown ordering, TIME statistics extraction, and TIMESTAMP_LTZ reads across timezones.

Comment thread crates/vortex_callback_io/callback_io.rs
Comment thread src/paimon/format/vortex/vortex_stats_extractor.cpp
Comment thread src/paimon/format/vortex/vortex_file_batch_reader.cpp Outdated
…stamp zones

Destroy the format writer before closing the stream in SingleFileWriter::Abort,
and reject writes on a writer that was already aborted, so a format writer with
a background task quiesces while its stream is still usable. The Vortex writer
marks its callbacks aborted, drops the sink and waits for the in-flight one.
Report an unknown INT statistic for TIME columns, and retype timezone-bearing
timestamps to the read schema's zone before alignment.
cpplint's runtime/int check rejects the C type long long in the PAIMON_LOG_WARN
argument, which failed the pre-commit job.

@SteNicholas SteNicholas left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhouyc-ali, thanks for updates. I left two correctness comments in the Vortex writer. PTAL.

Comment thread src/paimon/format/vortex/vortex_format_writer.cpp Outdated
Comment thread src/paimon/format/vortex/vortex_format_writer.cpp Outdated
gcc rejects binding a const std::string& loop variable to an initializer_list
of const char* under -Werror=range-loop-construct.
arrow-rs rejects sliced arrays at any nesting level with an aborting
assertion, and a sliced child hides inside an offset-zero batch, so normalize
offsets unconditionally before handing the batch to Vortex. Bind the writer's
Arrow pool to the exported batch so buffers rebased from it outlive the writer.

@SteNicholas SteNicholas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zhouyc-ali, thanks for updates. LGTM.

@lxy-9602 lxy-9602 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@lxy-9602
lxy-9602 merged commit 242decf into apache:main Oct 15, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Support Vortex file format

3 participants