This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/paimon-rust.git
The following commit(s) were added to refs/heads/main by this push:
new fce3890c fix(table): check batch arity instead of asserting it in
debug builds (#801)
fce3890c is described below
commit fce3890c646caa3f7974c6d080cd9dfa2e269777
Author: Junrui Lee <[email protected]>
AuthorDate: Thu Sep 10 15:21:15 2026 +0800
fix(table): check batch arity instead of asserting it in debug builds (#801)
---
crates/paimon/src/table/vector_search_builder.rs | 58 ++++++++++++++++++++----
1 file changed, 49 insertions(+), 9 deletions(-)
diff --git a/crates/paimon/src/table/vector_search_builder.rs
b/crates/paimon/src/table/vector_search_builder.rs
index 447d6995..ea4cd0b3 100644
--- a/crates/paimon/src/table/vector_search_builder.rs
+++ b/crates/paimon/src/table/vector_search_builder.rs
@@ -226,6 +226,33 @@ fn same_vector_search_table(left: &Table, right: &Table)
-> bool {
&& left.branch() == right.branch()
}
+/// Unwrap a single-query result from a batch entry point that must return
exactly
+/// one element per input query.
+///
+/// The batch terminals below are handed one query, so their result vector
holds
+/// exactly one entry. A `debug_assert_eq!(len, 1)` followed by `remove(0)`
checked
+/// that only in debug builds, where a release build would instead panic on an
index
+/// out of bounds for an empty vector -- or SILENTLY return the first of
several,
+/// pairing the caller's single query with another query's result. A length
that is
+/// wrong means the batch ran the wrong number of searches, which is a
programming
+/// error in this crate rather than bad input, so it is reported as one.
+fn take_only_result<T>(results: Vec<T>, operation: &str) -> crate::Result<T> {
+ let mut results = results.into_iter();
+ let result = results
+ .next()
+ .ok_or_else(|| crate::Error::UnexpectedError {
+ message: format!("{operation} returned no result for one query"),
+ source: None,
+ })?;
+ if results.next().is_some() {
+ return Err(crate::Error::UnexpectedError {
+ message: format!("{operation} returned more than one result for
one query"),
+ source: None,
+ });
+ }
+ Ok(result)
+}
+
/// The primary-key vector route's search output plus the source context a
later
/// materialization (or a hybrid fusion across routes) needs. `candidates` are
the
/// best-first hits; `splits` are the per-bucket source splits their
`split_index`
@@ -360,10 +387,9 @@ impl<'a> VectorSearchBuilder<'a> {
if let Some(filter) = &self.filter {
batch_builder.with_filter(filter.clone());
}
- let mut results = batch_builder.execute().await?;
+ let results = batch_builder.execute().await?;
- debug_assert_eq!(results.len(), 1);
- Ok(results.remove(0))
+ take_only_result(results, "vector search")
}
/// Run the vector search and materialize the matching rows as Arrow
batches,
@@ -528,7 +554,7 @@ impl<'a> VectorSearchBuilder<'a> {
query_vector: &[f32],
limit: usize,
) -> crate::Result<(Vec<PkVectorCandidate>, PkVectorScanPlan,
VectorSearchMetric)> {
- let (mut candidates, plan, metric) =
plan_and_search_pk_candidates_batch(
+ let (candidates, plan, metric) = plan_and_search_pk_candidates_batch(
self.table,
&self.options,
self.filter.as_ref(),
@@ -538,8 +564,11 @@ impl<'a> VectorSearchBuilder<'a> {
limit,
)
.await?;
- debug_assert_eq!(candidates.len(), 1);
- Ok((candidates.remove(0), plan, metric))
+ Ok((
+ take_only_result(candidates, "planned vector search")?,
+ plan,
+ metric,
+ ))
}
/// Plan + search the primary-key vector route and return the best-first
@@ -1957,14 +1986,13 @@ async fn evaluate_vector_search(
index_entries: &[IndexManifestEntry],
vector_search: &VectorSearch,
) -> crate::Result<Vec<RowRange>> {
- let mut results = evaluate_batch_vector_search(
+ let results = evaluate_batch_vector_search(
evaluation,
index_entries,
std::slice::from_ref(vector_search),
)
.await?;
- debug_assert_eq!(results.len(), 1);
- results.remove(0).to_row_ranges()
+ take_only_result(results, "vector search")?.to_row_ranges()
}
async fn evaluate_batch_vector_search(
@@ -8330,6 +8358,18 @@ mod residual_positions_tests {
assert!(!combined[0].contains_key("d1"));
}
+ /// The batch terminals here are handed exactly one query, so a result
vector of any
+ /// other length means the batch ran the wrong number of searches. The
+ /// `debug_assert_eq!` this replaced was compiled out of release builds,
where an
+ /// empty vector panicked on `remove(0)` and a longer one silently
returned another
+ /// query's result.
+ #[test]
+ fn take_only_result_rejects_bad_batch_arity() {
+ assert_eq!(take_only_result(vec![7], "test").unwrap(), 7);
+ assert!(take_only_result::<i32>(Vec::new(), "test").is_err());
+ assert!(take_only_result(vec![1, 2], "test").is_err());
+ }
+
#[test]
fn rejects_allow_lists_that_do_not_cover_every_split() {
let physical = vec![range_allow_list(&[("d0", &[1])])];