fix: preserve non-stable row IDs from later fragments - #240
fix: preserve non-stable row IDs from later fragments#240lance-gatefixer[bot] wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
❌ Gate recommendation: request changes.
The per-fragment address model is the right direction, but validation must use Lance-authoritative physical row counts for legacy manifests; cached manifest counts are intentionally absent or untrusted there.
A viable revision should resolve only referenced fragments through the Lance FileFragment::physical_rows() fallback (and can binary-search the sorted manifest rather than allocating a full-fragment map), then preserve the current downstream deletion filtering.
| .iter() | ||
| .map(|fragment| fragment.num_rows().unwrap_or_default() as u64) | ||
| .sum::<u64>() | ||
| .map(|fragment| (fragment.id, fragment.physical_rows.unwrap_or_default())) |
There was a problem hiding this comment.
unwrap_or_default() silently drops every valid row address in a legacy fragment whose physical_rows is absent, and trusting the cached value is also unsafe when manifest.writer_version is absent. Lance v9's FileFragment::physical_rows() deliberately opens file metadata in these cases because early manifests can contain missing or incorrect counts; the downstream take_rows path can otherwise read these rows correctly.
Please validate the referenced fragments with that authoritative fallback and propagate metadata errors. Since Manifest::fragments is sorted by ID, this can also avoid building a HashMap for every fragment on every point lookup.
Reproducer run against this head
I added this isolated unit test in a disposable worktree:
#[cfg(test)]
mod tests {
use std::sync::Arc;
use arrow::array::{Int64Array, RecordBatch, RecordBatchIterator};
use arrow::datatypes::{DataType, Field, Schema};
use lance::dataset::{Dataset, WriteParams};
use super::*;
use crate::ffi::types::DatasetHandle;
#[test]
fn legacy_unknown_physical_rows_remain_addressable() {
let schema = Arc::new(Schema::new(vec![Field::new(
"id",
DataType::Int64,
false,
)]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![Arc::new(Int64Array::from(vec![10_i64, 20]))],
)
.unwrap();
let batches = RecordBatchIterator::new([Ok(batch)], schema);
let params = WriteParams {
max_rows_per_file: 1,
..Default::default()
};
let mut dataset = runtime::block_on(Dataset::write(
batches,
"memory://pr240-legacy-unknown-rows",
Some(params),
))
.unwrap()
.unwrap();
let manifest = Arc::make_mut(&mut dataset.manifest);
manifest.writer_version = None;
for fragment in Arc::make_mut(&mut manifest.fragments) {
fragment.physical_rows = None;
}
let handle = Box::new(DatasetHandle::new(Arc::new(dataset)));
let handle_ptr = Box::into_raw(handle) as *mut c_void;
let row_ids = [1_u64 << 32];
let stream = create_dataset_take_stream_inner(
handle_ptr,
row_ids.as_ptr(),
row_ids.len(),
std::ptr::null(),
0,
true,
)
.unwrap();
unsafe {
drop(Box::from_raw(handle_ptr as *mut DatasetHandle));
}
let StreamHandle::Batches(mut batches) = stream else {
panic!("expected batches stream");
};
let result = batches.next().unwrap();
assert_eq!(result.num_rows(), 1, "valid legacy row address was dropped");
}
}Executed:
CARGO_TARGET_DIR=/home/agent/tmp/pr240-cargo-target cargo +1.97.1-x86_64-unknown-linux-gnu test --manifest-path Cargo.toml ffi::take::tests::legacy_unknown_physical_rows_remain_addressable -- --exact --nocapture
Observed on 9b7c6606ce8d9548d6355292c14d41c93ed9caa3: the assertion failed with left: 0, right: 1.
There was a problem hiding this comment.
Addressed in 9a7ad06: referenced fragments are resolved by sorted-ID binary search and validated with Lance’s authoritative FileFragment::physical_rows() fallback; metadata errors propagate as dataset-take failures, and the legacy-manifest reproducer now passes.
There was a problem hiding this comment.
The revision resolves the legacy-manifest correctness issue by obtaining Lance-authoritative physical row counts only for referenced fragments; the added regression now passes, while missing fragments, out-of-range offsets, stable IDs, deletion filtering, ordering, and error propagation preserve their contracts.
One non-blocking performance risk remains for cold legacy datasets spanning many referenced fragments: metadata reads are awaited serially. If large remote _rowid IN (...) lists are expected on such datasets, buffering those reads to the object-store I/O limit would avoid serial round trips.
|
Addressed in 6d4a15d: referenced-fragment |
There was a problem hiding this comment.
✅ Gate recommendation: approve.
The revision preserves the authoritative legacy-manifest validation while buffering referenced-fragment metadata reads at Lance's object-store I/O limit. Ordered buffering keeps binary-search lookup and error behavior deterministic, so the earlier correctness and performance concerns are both addressed.
Summary
Root cause
The take path treated non-stable row addresses as contiguous dataset offsets and compared them with the dataset's total live row count. Because later-fragment addresses encode the fragment ID in their high 32 bits, valid addresses were filtered out before reaching Lance.
Validation
uv run make format-checkcargo check --manifest-path Cargo.tomlcargo clippy --manifest-path Cargo.toml --all-targets./build/debug/test/unittest "test/sql/scan_rowid_in.test"./build/debug/test/unittest "test/sql/scan_rowid_native.test"GEN=ninja make test_debug(all 4,152 SQL assertions passed; six environment-gated tests skipped; the debug harness subsequently reported repository-wide LeakSanitizer leaks at process exit)Fixes #239