Skip to content

fix: preserve non-stable row IDs from later fragments - #240

Open
lance-gatefixer[bot] wants to merge 3 commits into
mainfrom
gatekeeper/fix-239-1
Open

fix: preserve non-stable row IDs from later fragments#240
lance-gatefixer[bot] wants to merge 3 commits into
mainfrom
gatekeeper/fix-239-1

Conversation

@lance-gatefixer

Copy link
Copy Markdown

Summary

  • decode non-stable row IDs into fragment IDs and row offsets before range validation
  • validate offsets against each fragment's physical row count
  • cover point lookups into later fragments with an end-to-end SQL regression test

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-check
  • cargo check --manifest-path Cargo.toml
  • cargo 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

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Comment thread rust/ffi/take.rs Outdated
.iter()
.map(|fragment| fragment.num_rows().unwrap_or_default() as u64)
.sum::<u64>()
.map(|fragment| (fragment.id, fragment.physical_rows.unwrap_or_default()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Gate recommendation: approve with a non-blocking risk.

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.

@lance-gatefixer

Copy link
Copy Markdown
Author

Addressed in 6d4a15d: referenced-fragment physical_rows() futures are now buffered at Lance’s dataset object-store I/O parallelism limit while preserving sorted result order for binary-search validation.

@lance-gatekeeper lance-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

@Xuanwo Xuanwo added the K-approved Latest Gatekeeper recommendation permits acceptance. label Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

K-approved Latest Gatekeeper recommendation permits acceptance.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_rowid point lookup drops valid non-stable row IDs from fragments

1 participant