Support the Vortex file format - #112950
Conversation
Add `Vortex` input and output formats for reading and writing Vortex files (https://github.com/vortex-data/vortex), an extensible columnar file format for compressed Apache Arrow-compatible data. The implementation uses the Rust `vortex` crate through a new C FFI crate `rust/workspace/vortex` (`_ch_rust_vortex`). Data crosses the FFI boundary through the Arrow C Data Interface and is converted with the same `ArrowColumnToCHColumn`/`CHColumnToArrowColumn` code as the `Arrow` format. IO is delegated back to ClickHouse through callbacks, so reads go through ClickHouse's own buffers (with range reads for seekable inputs and whole-file buffering otherwise), and writes stream into the output buffer. All work is driven by a single-threaded runtime on the calling thread: the library spawns no threads. The reader supports projection pushdown (only the requested columns are read), count-only queries from file metadata, and schema inference. The writer uses the library's default adaptive compression (BtrBlocks-style cascading encodings plus zstd). Closes #87327 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Preview deployment for your docs. Learn more about Mintlify Previews.
|
|
Workflow [PR], commit [aa8fba4] Summary: ❌
AI ReviewSummaryThis PR adds native Findings
Final Verdict
|
# Conflicts: # src/Formats/registerFormats.cpp
Address the AI review of #112950 and the `arm_tidy` build failure. - `VortexBlockInputFormat::prepareReader` pruned the scanned columns to the exact top-level names of the requested header. A header column addressing a subcolumn, such as `t.a`, dropped the parent field `t` from the scan, and `ArrowColumnToCHColumn` then silently filled the column with default values. Keep `Nested::extractTableName` of every header column, the same way the `ArrowIPC` reader does. Covered by a new case in `04669_vortex_format`. - `VortexBlockOutputFormat` did not override `resetFormatterImpl`, so a formatter reused by `MessageQueueSink` kept the finished Rust writer and the next message failed with `writer is already finished`. Free the writer and the conversion state on reset, like `Arrow`, `ORC`, and `Parquet` do. - Value-initialize the `ArrowArray` and `ArrowSchema` C structures, fixing the `cppcoreguidelines-pro-type-member-init` errors in `Build (arm_tidy)`: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=112950&sha=d4debff97a3088dde960cc32e1d0ce8af1c997b6&name_0=PR&name_1=Build%20%28arm_tidy%29
The MSan build failed to link unit_tests_dbms with relocation R_X86_64_PC32 out of range errors: the MSan-instrumented Vortex Rust library (with origin tracking) adds so much code that the binary exceeds the 2 GiB range of the small code model. Disable Vortex under MSan, following the precedent of wasmtime and delta-kernel-rs. Consequently, exclude Vortex from the format enumeration in 02187_async_inserts_all_formats (its availability now varies by build, so it cannot appear in the static reference; this also fixes the reference mismatch this PR introduced there), and tag 04669_vortex_format with no-msan. CI: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=112950&sha=645a6771def9e396863d6401ca67230109b220dd Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Master removed the legacy Docusaurus tree (docs/en), which conflicted with the Vortex doc added there. The Vortex format page is ported to the Mintlify tree: docs/reference/formats/Vortex.mdx (body matches the embedded documentation registered via FormatFactory::setDocumentation), a row in docs/reference/formats/index.mdx, and an entry in docs/reference/navigation.json.
|
🕵 @groeneai, investigate the two
Both hangs are unrelated to this PR: the hung queries do not involve the |
|
Both hangs are the same defect, and it is not the one the
and its stack ( The So both rows are Cost vs interruptibility are two separate things, and only the first has a fix. Cost: your #113140 is the right fix for it, and the Interruptibility: #113140 does not address it. Its 87 added lines contain no cancellation check (0 occurrences of The phase counters confirm the hang is in the still-running phase rather than anything already finished: recorded phases sum to 6.6 s of 1757 s ( I am authoring the cancellation-checkpoint fix and will link the PR here when it opens. #112203 is a different mechanism (uninterruptible evaluation of one large value); this is uninterruptible plan construction, closer to #113003 and covered in general by #47272. Unrelated to this PR, confirmed rather than assumed. Your MSan argument holds: |
The `Vortex` output format inherited the generic `CHColumnToArrowColumn` mapping that serializes `DateTime` as plain Arrow `UINT32`, so the temporal type was silently lost on round-trip: `DESC file(...)` of a written file inferred `UInt32`. Add an `output_datetime_as_timestamp` setting to `CHColumnToArrowColumn` (following the existing `output_date_as_uint16` pattern) that exports `DateTime` as Arrow `TIMESTAMP` with second precision and the column's time zone, and enable it for `Vortex`. Such files are read back as `DateTime64(0)`. `Arrow` and `Parquet` keep the old behavior. Update the format documentation accordingly and add a test with schema-inferred readback of `DateTime`, including `Nullable` and `Array` nesting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Fixing PR for the I had a fix for this on a local branch, but @alexey-milovidov opened #113415 with the identical |
…ex` format Vortex has no type for IP addresses, so `IPv4` columns are written through the generic Arrow `UINT32` carrier (the same as in `Parquet` and `Arrow`), and schema inference reads them back as `UInt32`. Document this explicitly, including the explicit-schema way to read the column back as `IPv4`, and add a stateless test covering inference, both round trips, and the documented `IPv6` write rejection. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
🕵 CI triage for the reds on
|
|
The AST fuzzer STID 2270-2caa failure on I confirmed that on your exact query rather than by inference. A pre-fix binary reproduces your log line byte for byte ( Mechanism: The crash is the visible half. When no constant survives there is no error and rows are silently dropped instead: over two shards of 1000 rows,
113469 covers this carrier; I am not claiming it clears the whole message class. Agreed that 96656 and 103695 are the same family with different stack IDs. One caveat on keying: within this family a single STID pools more than one query shape, so the shape identifies a carrier more reliably than the STID does. |
…merged ClickHouse/rust_vendor#79 is merged, so the vendored 0.84.0 crates are reachable from `main` and the submodule no longer has to point into a pull request branch. The tree of the merge commit is identical to the branch tip it replaces - `git diff ab24d7175 810b5603c` is empty - so nothing about the vendored crates changes; only the commit the submodule names does. Related: ClickHouse/rust_vendor#79
|
🕵 Rebased onto Conflict with The 0.84 bump had never been through CI — it was pushed after the last report, and the pull request was
One thing worth recording about the new Vendoring. ClickHouse/rust_vendor#79 is merged, so Remaining review threads are both agreed follow-ups, not blockers: extension dtypes for the currently unwritable types ( |
… observability Signed-off-by: Dergousov Maksim <dergousovmaxim99@gmail.com>
|
@alexey-milovidov i believe that once we merge #116021 we’ll be on par with parquet on clickbench, though we still need to do some work on missing pushdowns and late materialization. soon i’ll write a comment here about all the follow ups that need to be done after we merge this to create separate issue for that |
after we merge this, let's also run clickbench on parquet vs vortex. from what i see on my vm, geomean on 3 hot runs is 1.1 (vortex is faster) and we win in 21/43 queries |
|
🕵 Merged |
|
@alexey-milovidov hey, is it possible for me to get commit rights to this branch so i can prepare this pr for merge? except for #116021, only small cosmetic things are left |
# Conflicts: # tests/queries/0_stateless/03251_insert_sparse_all_formats.sh
…preserve_order` records to the 26.9 block of `SettingsChangesHistory.cpp` Master rolled over to 26.9 while the PR still recorded the new settings under the 26.8 block, which the `settings_changes_history` style check rejects: https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=112950&sha=2e9870581b89ba9c65bf95679c23434724d4e57d&name_0=PR&name_1=Style%20check
|
🕵 Merged |
|
@m7kss1, it's not possible to do it, as the branch is in ClickHouse/ClickHouse. But please send fixes in the other PR, and I will merge them ASAP. |
|
@alexey-milovidov oh, i see. but what do you think about #116021? translating filter expressions directly from the |
Rework the `Vortex` filter pushdown: an `ActionsDAG` converter, support for temporal types, and observability.
`05030` and `05031` are already taken in `tests/queries/0_stateless`.
…e scan `clang-tidy` failed the build with `cppcoreguidelines-init-variables` on `bool_value` in `VortexExpressionConverter::makeLiteral`; initialize it. The AI review reported that a real `Bool` column can never be pushed into the scan, because `typesMatchForFilterPushdown` accepts `which.isUInt8()` for `arrow::Type::BOOL`. That premise does not hold: `Bool` is `TypeIndex::UInt8` with a custom name, which is exactly why the `UINT8` case next to it has to spell out `which.isUInt8() && !isBool(cmp_type)`. Verified against a local build - a `Bool` predicate reports `VortexFilterPushdownConjunctsPushed = 1` and drops whole splits. The review also asked for a scan-observable regression for `Bool`, since `05045_vortex_pushdown_types` only compares results and ClickHouse reapplies the `WHERE` either way. `05053_vortex_pushdown_bool` asserts the `ProfileEvents` of `b = true`, a bare `b` and `NOT b` over a `Bool` column whose true values sit in a single split.
|
🕵 Merged
AI review Major ( AI review test request was valid and is addressed: All 12 Vortex stateless tests pass locally (aarch64 Unrelated finding while verifying, worth a separate look: a local build dir configured with |
| /// The callbacks a scan reports to. Both run on the caller's own threads, possibly several at a | ||
| /// time. The only calls back into the library they may make are `vortex_ffi_scan_cancel`, which is | ||
| /// allowed from either of them, and `vortex_ffi_scan_release`, which is not allowed from | ||
| /// `on_chunk`. | ||
| struct FFI_VortexScanCallbacks | ||
| { | ||
| void * context; | ||
| /// Delivers one chunk: an Arrow struct array in the scan's schema, together with the position | ||
| /// of its split in the file. The array is borrowed for the duration of the call - the callback | ||
| /// takes the data out of it (or releases it) before returning, and must not keep the pointer. | ||
| /// A null array means the split matched no rows; it is still reported so that the caller can | ||
| /// restore the file order. Returning non-zero stops the scan and surfaces from `on_finish` as | ||
| /// an error. | ||
| int32_t (*on_chunk)(void * context, struct ArrowArray * array, uint64_t split_index); | ||
| /// Reports the end of the scan, exactly once: nullptr if every split was delivered, otherwise | ||
| /// a message that is only valid for the duration of the call. Never called for a scan that was | ||
| /// cancelled. After a failure a split task already in flight can still reach `on_chunk`, so the |
There was a problem hiding this comment.
VortexBlockInputFormat already relies on onChunk being callable before vortex_ffi_scan_create returns (src/Processors/Formats/Impl/Vortex/VortexBlockInputFormat.cpp:465), so the first callbacks do not have an FFI_VortexScan * yet and cannot actually call vortex_ffi_scan_cancel even though this contract says that is allowed from on_chunk.
Once the handle is published, the current C++ failure path calls vortex_ffi_scan_cancel before returning non-zero (src/Processors/Formats/Impl/Vortex/VortexBlockInputFormat.cpp:276), which suppresses on_finish even though the next lines say a non-zero return surfaces there. So the terminal callback sequence depends on timing.
We need either to publish the scan handle before any on_chunk can fire, or to narrow/specify this API so on_chunk does not promise cancellation and the cancel + non-zero path has defined semantics.
| /// The day numbers are copied 1:1 - except under `Saturate`, where out-of-range days are | ||
| /// clamped onto the bounds and an equality on a bound would match rows it should not. | ||
| case arrow::Type::DATE32: | ||
| return which.isDate32() && format_settings.date_time_overflow_behavior != FormatSettings::DateTimeOverflowBehavior::Saturate; |
There was a problem hiding this comment.
The public contract now says input_format_vortex_filter_push_down supports Date, but the real vortex.date / Arrow DATE32 carrier still lands here and rejects a Date header outright.
That is observable on normal Vortex files, not just on synthetic schemas: CHColumnToArrowColumn writes ClickHouse Date as Arrow DATE32 (src/Processors/Formats/Impl/CHColumnToArrowColumn.cpp:137), so file(..., 'Vortex', 'd Date') silently loses pushdown on actual Vortex date columns. The only current coverage in 05045_vortex_pushdown_types.sh:151 is the special Date-over-raw-UInt16 case.
Either teach this DATE32 branch how to preserve the Date overflow semantics for Date headers, or narrow the docs/settings/tests back to Date32.
Closes: #87327
Related: ClickHouse/rust_vendor#74
Related: ClickHouse/rust_vendor#79
Related: #115096
Related: #114373
Related: #116021
Changelog category (leave one):
Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):
Added support for reading and writing the Vortex columnar file format (the
Vortexinput and output format). This closes #87327.Documentation entry for user-facing changes
The implementation uses the Rust
vortexcrate (v0.84.0) through a new C FFI craterust/workspace/vortex(_ch_rust_vortex), following the same pattern asprqlandpolyglot. Data crosses the FFI boundary through the Arrow C Data Interface and is converted with the sameArrowColumnToCHColumn/CHColumnToArrowColumncode as theArrowformat. IO is delegated back to ClickHouse through callbacks: reads go through ClickHouse's own read buffers (range reads for seekable inputs, whole-file buffering otherwise), and the produced file is streamed into the output buffer. The library spawns no threads of its own: a scan is split into tasks that ClickHouse runs on its own thread pools (max_parsing_threadsfor decoding,max_download_threadsfor reads), writing is driven on the calling thread, and Rust panics are caught at the FFI boundary and turned into exceptions.Features:
input_format_vortex_preserve_orderis set.count()-only queries answered from file metadata without reading data.Limitations (documented in
docs/reference/formats/Vortex.mdx):Map,Int128/UInt128/Int256/UInt256,IPv6, andIntervalcolumns cannot be written (no corresponding Vortex type).StringandFixedStringare written as VortexBinary(ClickHouse strings are arbitrary bytes, while Vortex requiresUtf8to be valid UTF-8).unit_tests_dbmsoverflows the 2 GiBR_X86_64_PC32relocation range (same approach aswasmtimeanddelta-kernel-rs).Parquet— parallelism can be added later. Reading is parallel since feat: parallel reading for vortex #115096 was merged into this PR: on ClickBench, reads went from geomean ~26.8x slower thanParquetto ~1.2x, winning 9 of 43 queries (see the benchmark results). Filter pushdown (added in Enhancement for Vortex format #114373,input_format_vortex_filter_push_down, on by default) lets the scan skip the statistics zones the predicate rules out and decode only the matching rows of the rest: on a file of 1 000 000 sorted values,WHERE n = 500000reads 66 KiB with the pushdown against 7.5 MiB without it. The most selective ClickBench queries are nevertheless still 1.5-5.5x slower thanParquet. The pushdown was reworked in ref: rework filter pushdown: ActionsDAG converter, temporal types and… #116021 by @m7kss1 (merged into this PR): the filter is translated directly from the query'sActionsDAGinstead of going through aKeyCondition, which addsBool,Date,Date32andDateTime64columns andLIKE/NOT LIKE/startsWithprefix ranges, and reports what reached the scan through theVortexFilterPushdownConjunctsPushed,VortexFilterPushdownConjunctsDropped,VortexScanSplitsandVortexScanEmptySplitsprofile events.The 127 new vendored Rust crates were added in ClickHouse/rust_vendor#74, and ClickHouse/rust_vendor#79 moved them to 0.84.0. Both are merged, so
contrib/rust_vendorpoints atmainrather than at a pull request branch.Workflow [PR]
Sync PR [sync-upstream/pr/112950]