lance-format / lance-format/lance

panic when using merge_insert with an index and # of rows * embedding dimension > u32::MAX

Open
#6,195 0 comments 0 reactions 1 assignee View on GitHub

@Xuanwo is already working on this.

Since Mar 16, 2026.

A-index bug
Dominant language
Rust
Stars
7.1k
Forks
852
Avg merge
3d 18h
Merged PRs (30d)
272

Description

Lance version

4.0.0-beta.10 + 311ff1ad1b37e784dceed8d30873282ccacdc907

What happened?

See original issue for context. And one PR that adresses this https://github.com/lance-format/lance/pull/6148#issuecomment-4039498958

With an updated test that uses an index we still encounter a panic.

With the docs suggesting to use a scalar index to speed up merge insert, this should be fixed.

thread 'main' (19105945) panicked at /Users/valkum/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/arrow-data-57.3.0/src/data.rs:551:9:
assertion failed: (offset + length) <= self.len()
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::panic
   3: arrow_data::data::ArrayData::slice
   4: <arrow_array::array::fixed_size_list_array::FixedSizeListArray as core::convert::From<arrow_data::data::ArrayData>>::from
   5: arrow_select::take::take_fixed_size_list
   6: arrow_select::take::take_impl
   7: arrow_select::take::take
   8: datafusion_physical_plan::joins::utils::build_batch_from_indices
   9: datafusion_physical_plan::joins::hash_join::stream::HashJoinStream::poll_next_impl
  10: <S as futures_core::stream::TryStream>::try_poll_next
  11: <futures_util::stream::try_stream::try_flatten::TryFlatten<St> as futures_core::stream::Stream>::poll_next
  12: <futures_util::stream::try_stream::try_flatten::TryFlatten<St> as futures_core::stream::Stream>::poll_next
  13: <futures_util::stream::stream::map::Map<St,F> as futures_core::stream::Stream>::poll_next
  14: lance::dataset::write::write_fragments_internal::{{closure}}::{{closure}}
  15: lance::dataset::write::write_fragments_internal::{{closure}}
  16: lance::dataset::write::merge_insert::MergeInsertJob::execute_uncommitted_impl::{{closure}}
  17: <futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll
  18: <core::pin::Pin<P> as core::future::future::Future>::poll
  19: <futures_util::future::future::map::Map<Fut,F> as core::future::future::Future>::poll
  20: <lancedb::table::NativeTable as lancedb::table::BaseTable>::merge_insert::{{closure}}
  21: with_index::main::{{closure}}
  22: tokio::runtime::park::CachedParkThread::block_on
  23: tokio::runtime::context::runtime::enter_runtime
  24: tokio::runtime::runtime::Runtime::block_on
  25: with_index::main
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Are there known steps to reproduce?

main.rs:

//! Reproducer: `merge_insert` panics on tables with >23M rows containing a
//! `FixedSizeListArray` column (e.g. embeddings).
//!
//! Root cause: u32 overflow in `take_value_indices_from_fixed_size_list`
//! (arrow-select take.rs). `merge_insert` with `when_not_matched_by_source_delete`
//! uses a FULL OUTER JOIN whose HashJoin concatenates all build-side rows into a
//! single RecordBatch. When the FixedSizeList column has more than
//! `u32::MAX / value_length` rows, `take()` overflows internally and panics with:
//!
//!     assertion failed: (offset + length) <= self.len()
//!
//! For value_length=184, the threshold is 23,342,213 rows.
//!
//! Run:  cargo run --release          (requires ~1 GB RAM, panics)
//!       cargo run --release -- 23342213  (5k rows, passes)

use arrow_array::{
    BooleanArray, FixedSizeListArray, RecordBatch, RecordBatchIterator, UInt32Array,
};
use arrow_buffer::{BooleanBuffer, MutableBuffer};
use arrow_schema::{DataType, Field, Schema};
use std::sync::Arc;

const DIM: i32 = 184;
const NUM_ROWS: usize = 24_000_000;
const CHUNK: usize = 1_000_000;

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();
    let n: usize = std::env::args()
        .nth(1)
        .and_then(|s| s.parse().ok())
        .unwrap_or(NUM_ROWS);

    let threshold = (u32::MAX as u64) / (DIM as u64);
    eprintln!("rows={n}, dim={DIM}, overflow threshold={threshold}");

    let tmp = tempfile::tempdir().unwrap();
    let db = lancedb::connect(tmp.path().to_str().unwrap())
        .execute()
        .await
        .unwrap();

    eprintln!("creating table...");
    let table = db
        .create_table("t", batches(n).collect::<Result<Vec<_>, _>>().unwrap())
        .execute()
        .await
        .unwrap();
    eprintln!("table has {} rows", table.count_rows(None).await.unwrap());

    table
        .create_index(&["id"], lancedb::index::Index::Auto)
        .execute()
        .await
        .unwrap();

    // merge_insert with when_not_matched_by_source_delete forces a FULL OUTER JOIN.
    // DataFusion's HashJoin concat_batches all build-side rows into one RecordBatch,
    // then calls take() → u32 overflow → panic.
    eprintln!("running merge_insert...");
    let mut merge = table.merge_insert(&["id"]);
    merge
        .when_matched_update_all(None)
        .when_not_matched_by_source_delete(None);

    merge
        .execute(Box::new(RecordBatchIterator::new(batches(n), schema())))
        .await
        .unwrap();

    eprintln!("merge_insert succeeded (bug not triggered at this scale)");
}

fn schema() -> Arc<Schema> {
    Arc::new(Schema::new(vec![
        Field::new("id", DataType::UInt32, false),
        Field::new(
            "vec",
            DataType::FixedSizeList(Arc::new(Field::new("item", DataType::Boolean, true)), DIM),
            true,
        ),
    ]))
}

// In Production there will be changes between the batches,
// but the amount of removed rows is small and thus not relevant to the bug.
fn batches(
    total: usize,
) -> impl Iterator<Item = Result<RecordBatch, arrow_schema::ArrowError>> + Send {
    let mut off = 0;
    std::iter::from_fn(move || {
        if off >= total {
            return None;
        }
        let n = (total - off).min(CHUNK);
        let b = make_batch(off, n);
        off += n;
        Some(Ok(b))
    })
}

fn make_batch(start: usize, count: usize) -> RecordBatch {
    let ids: Vec<u32> = (start..start + count).map(|i| i as u32).collect();
    let n = count * DIM as usize;
    let buf: arrow_buffer::Buffer = MutableBuffer::from_len_zeroed((n + 7) / 8).into();
    let bools = BooleanArray::new(BooleanBuffer::new(buf, 0, n), None);
    let fsl = FixedSizeListArray::new(
        Arc::new(Field::new("item", DataType::Boolean, true)),
        DIM,
        Arc::new(bools),
        None,
    );
    RecordBatch::try_new(
        schema(),
        vec![Arc::new(UInt32Array::from(ids)), Arc::new(fsl)],
    )
    .unwrap()
}

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.