Repository navigation
feat(vortex): add predicate pushdown and optimize selective reads - #471
zhouyc-ali wants to merge 2 commits into
Conversation
Retain the smaller side of bitmap selections and preserve physical row mapping. Add regression coverage for dense, sparse, and reconfigured selections. Use callback I/O in native-scan test fixtures to avoid the Rust async filesystem path reported by TSAN, without adding suppressions. Add Apache license headers to both Vortex patch files.
| + assert_eq!(source.request_count.load(Ordering::SeqCst), ids.len()); | ||
| + Ok(()) | ||
| + } | ||
| + |
There was a problem hiding this comment.
Could you please describe the real benefits this change will bring? After all, diffs are hard to maintain.
| +++ b/vortex-ffi/src/dtype.rs | ||
| @@ -344,7 +344,9 @@ | ||
| ) -> c_int { | ||
| try_or(err, 1, || { |
There was a problem hiding this comment.
[P2] Bound native batches before converting view arrays to Utf8/Binary
Normalizing the target schema here makes execute_arrow materialize ordinary Utf8/Binary arrays for the entire native chunk, before VortexFileBatchReader::NextBatch() slices it to the configured batch_size. Previously, the view-to-standard conversion happened after that slicing.
For dictionary-encoded long strings, this can expand a small encoded chunk into a large contiguous allocation. If a column’s expanded payload exceeds the approximately 2 GiB limit of 32-bit offsets, conversion can fail even though each requested C++ batch would fit. Reducing the C++ batch_size cannot prevent this because conversion has already happened.
Could we split the native output before execute_arrow, or retain view arrays across the FFI and convert after slicing? This would preserve bounded materialization and avoid introducing a larger conversion boundary.
|
|
||
| let data_source = if local { | ||
| vx_data_source_new_callback_with_coalesce( | ||
| session, |
There was a problem hiding this comment.
It seems we only support the local and object store modes right now. We have HDFS scenarios that need to support configurable max_gap and max_size, so we can’t just hard-code it based on the mode.
| : batch_size_(batch_size), pool_(GetDefaultPool()), arrow_pool_(GetArrowPool(pool_)) {} | ||
| explicit VortexReaderBuilder( | ||
| int32_t batch_size, const VortexPredicateOptions& predicate_options = {}, | ||
| VortexIoCoalesceMode coalesce_mode = VortexIoCoalesceMode::OBJECT_STORAGE) |
There was a problem hiding this comment.
Please avoid default parameters in the production code.
| results, retain the full residual predicate: enable ``EnablePredicateFilter(true)`` | ||
| with ``TableRead``, or apply equivalent filtering in the caller. ``force`` does not | ||
| bypass this requirement. | ||
|
|
There was a problem hiding this comment.
Please put these parameters into a vortex_defs.h file.
There was a problem hiding this comment.
Can you confirm whether this configuration is the default for Java? If it isn’t, please use a more flexible configuration instead of an enum that limits choices to a fixed set of categories.
There was a problem hiding this comment.
Also, why is this documentation placed in read.rst? Please check where it should live—it seems intended for external users (the user guide) rather than format-configuration documentation. Please adjust the doc location accordingly.
| return VortexCallbackError("create Vortex scan", error, | ||
| input_context_->GetCallbackStatus()); | ||
| } | ||
| scan_ = std::move(scan); |
There was a problem hiding this comment.
Could you please refactor this part of the code? For example, the creation of scan_ could be extracted into a separate function. The current function is a bit too long.
| selected_row_count_ = selected_row_count; | ||
| selection_indices_ = std::move(selection_indices); | ||
| metrics_->SetCounter("vortex.selection-index-count", selection_indices_.size()); | ||
| ReleaseStream(); |
There was a problem hiding this comment.
Please define the metric name in defs.h first. Also, could we rename it to vortex.selection-row-count? “index” is a bit ambiguous since it could be interpreted as an index.
| return std::make_unique<VortexReaderBuilder>(batch_size); | ||
| VortexPredicateOptions predicate_options; | ||
| const auto membership_mode = options_.find("vortex.read.membership-pushdown"); | ||
| if (membership_mode != options_.end()) { |
There was a problem hiding this comment.
Similar changes needed: please adjust the parameter naming and their positions as well.
| .EnablePrefetch(true) | ||
| .SetPrefetchBatchCount(2) | ||
| .SetPrefetchMaxParallelNum(2) | ||
| .AddOption(Options::READ_BATCH_SIZE, "2"); |
There was a problem hiding this comment.
I’m curious whether vortex supports prefetch. If it doesn’t, why are the EnablePrefetch and related options being configured?
| // A non-exact reader may prune candidates, but it must retain every matching row. | ||
| ASSERT_NO_FATAL_FAILURE( | ||
| AssertVortexSdkRows(actual, result_type, expected, exact_filter)); | ||
| } |
There was a problem hiding this comment.
Please tighten your validation. Enabling either late_materializing or exact_filter is sufficient to guarantee correct query results and predicate accuracy.
| @@ -0,0 +1,131 @@ | |||
| # Licensed to the Apache Software Foundation (ASF) under one | |||
There was a problem hiding this comment.
The "fmm" is from the db name? It's not a public name, maybe need rename it.
| /// @return An owned expression, or nullptr if no condition can be pushed. | ||
| static VxExpressionPtr Convert(const std::shared_ptr<Predicate>& predicate, | ||
| const std::shared_ptr<arrow::Schema>& file_schema, | ||
| const VortexPredicateOptions& options = {}); |
There was a problem hiding this comment.
Similar to before: in formal code, please avoid using default parameters as much as possible.
| if (!timestamp_type.timezone().empty() && timestamp_type.timezone() != "UTC") { | ||
| return false; | ||
| } | ||
| const auto timestamp = literal.GetValue<Timestamp>(); |
There was a problem hiding this comment.
Please carefully review the read/write paths and predicates related to tz, and ensure the behavior is consistent with Java.
| *result = value + fraction; | ||
| *result_unit = unit; | ||
| return true; | ||
| } |
There was a problem hiding this comment.
This function seems to be primarily handling timestamp precision conversion. Could you please try to use the utility functions from DateTimeUtils instead?
| case FieldType::FLOAT: | ||
| case FieldType::DOUBLE: | ||
| // Native totalOrder distinguishes NaN signs and payloads; Paimon treats all NaNs alike. | ||
| return scalar; |
There was a problem hiding this comment.
I checked the Java logic— it looks like they push down floating-point values. For correctness around NaN, could you please open a PR to fix it in Java and keep both sides consistent?
| if (literal.IsNull() || literal.GetType() != field_type || arrow_type == nullptr) { | ||
| return false; | ||
| } | ||
| switch (field_type) { |
There was a problem hiding this comment.
Similarly, you can assume the predicate types match the corresponding field types in the file. If there’s a mismatch, just error out—there’s no need for this kind of fallback.
| nodes += lower == upper ? 1 : 3; | ||
| } | ||
| return nodes; | ||
| } |
There was a problem hiding this comment.
I think these IN-expression optimizations should be handled by the query engine itself. Do we really need to introduce these code changes specifically for this? It seems to add a lot of complexity, and other formats don’t appear to benefit from the same optimization.
There was a problem hiding this comment.
Also, pushing down deduplication seems more appropriate to be done in the upstream layer.
| const auto& decimal_type = checked_cast<const arrow::Decimal128Type&>(*arrow_type); | ||
| const auto decimal = literal.GetValue<Decimal>(); | ||
| return decimal.Precision() == decimal_type.precision() && | ||
| decimal.Scale() == decimal_type.scale(); |
There was a problem hiding this comment.
If the decimal precisions don’t match, can we do a precision conversion? Or does vortex actually support this kind of pushdown?
| if (type == Function::Type::AND || type == Function::Type::OR) { | ||
| const auto compound = std::dynamic_pointer_cast<CompoundPredicate>(predicate); | ||
| if (compound == nullptr) { | ||
| return NullExpr(); |
There was a problem hiding this comment.
For compound, besides and/or, are there any other type? It looks like most predicate logic is swallowed and falls back to a “don’t push down” behavior on error. We should distinguish between cases where we silently avoid pushdown because it’s unsupported, versus cases where there’s a real issue that should result in an error.
Purpose
Linked issue: #319.
Push supported predicates, projections and bitmap selections into native Vortex scans while preserving physical file row IDs and Paimon's residual-filter semantics.
Tests
Added and updated tests covering:
IN and NOT IN, including duplicates, integer boundaries, range compression,
and combined AND/OR predicates.
timestamps, floating-point semantics, and membership policy limits.
Fixed-seed differential tests check native results before SDK residual filtering.
NULL filling, schema metadata preservation, and incompatible-type rejection.
and projection, preserving physical row IDs across batches. The single-deletion
regression verifies one exclusion index instead of expanding all surviving rows.
active scan and returned batches remain valid after reset or reader destruction.
waiting for in-flight callbacks on abort, and deferred stream closure.
primary-key updates/deletes with merging before value filtering, and schema
evolution with missing columns and a user column named
_row_idx.Local validation: 89 Vortex tests passed in normal and targeted TSAN runs.
The TSAN run instrumented Vortex C++ code and tests, reusing core/Rust dependencies.
Apache RAT and changed-file lint checks passed.
API and Format
No changes to public C++ API signatures, stored file formats or the table protocol. Add the reader options
vortex.read.membership-pushdownandvortex.read.io-coalesce.The private Vortex FFI is extended; its patched header and native library must be rebuilt together. Readers continue to require SDK residual filtering (
EnablePredicateFilter(true)) or equivalent filtering by the caller for exact results.Documentation
Document the two Vortex reader options, their defaults and the residual-filter requirement in the existing reading guide (
docs/source/user_guide/read.rst).Generative AI tooling
Generated-by: OpenAI Codex 0.163.0