From 47600248165637bb7db6e2feae92e691176bd399 Mon Sep 17 00:00:00 2001 From: Dan Rammer Date: Thu, 25 Jun 2026 23:14:56 -0500 Subject: [PATCH 001/177] feat(mem-wal): add ShardWriter::abort + Sealed manifest fence for drop-table (#7361) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What The mem-wal primitives sophon's drop-table two-phase commit needs: - **`ShardWriter::abort(&self)`** — shut down the background flush tasks (`task_executor.shutdown_all()`) *without* flushing, discarding buffered memtable state. Unlike `close(self)` it takes `&self` (so it's callable through the `Arc` callers hold) and does no object-store IO. The caller must quiesce writes first (documented). Idempotent. Acked data is **not** lost — it's durable in the WAL log and replays on the next claim, which is what makes the drop's prepare phase reversible. - **`ShardStatus { Active | Sealed }` on `ShardManifest`** — a durable, reversible lifecycle marker (proto + struct + serde). `claim_epoch` refuses a `Sealed` manifest with a **distinguishable** error instead of minting a new epoch, so a shard mid-drop can't be re-claimed — even by a caller that skips its own status check — and a reader can tell an in-doubt drop apart from an ordinary epoch fence. Set/cleared through the existing epoch-guarded `commit_update` CAS; carried across claims via `..base`, so only the genuinely-fresh constructions default it to `Active`. ## Why A WAL-enabled table's drop spans two durable resources — the owning pod's fresh-tier state and the catalog/object-store data — so sophon's teardown is a two-phase commit. Before the dataset directory is removed, the owning pod must: - `abort` the writer so its background flush task can't re-create `_mem_wal/` under the just-deleted directory (a graceful `close()` would flush it back), and - durably mark the shard `Sealed` so the drop is in-doubt across a pod crash or a Maglev rehome — which the in-memory fence flag cannot survive. The seal is reversible (rollback clears it back to `Active`), making the prepare phase abortable without data loss. Both build on existing machinery (`shutdown_all`, the manifest CAS) — thin exposure, not new infrastructure. > **Changed since first draft:** this PR previously also added `Session::invalidate_dataset`; it has been **dropped**. Base-table read freshness rides the recreate's new object-store `e_tag` (the QN→PE and lance metadata caches are `e_tag`-keyed and miss the stale entry), so cache invalidation isn't load-bearing — only `abort` and the `Sealed` marker remain. ## Tests - `test_abort_discards_without_flushing_and_is_idempotent` — `abort` leaves no new L0 generation (contrast with `close`), idempotent on a second call. - `test_claim_epoch_refuses_sealed_manifest` — a `Sealed` manifest is refused with the distinguishable error and left untouched (no epoch minted); rolling the status back to `Active` makes the shard claimable again (reversibility). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- protos/table.proto | 14 ++++ rust/lance-table/src/system_index/mem_wal.rs | 38 +++++++++ rust/lance/src/dataset/mem_wal/manifest.rs | 65 ++++++++++++++- rust/lance/src/dataset/mem_wal/write.rs | 88 ++++++++++++++++++++ 4 files changed, 204 insertions(+), 1 deletion(-) diff --git a/protos/table.proto b/protos/table.proto index d298809d5d8..3ca04e0ed26 100644 --- a/protos/table.proto +++ b/protos/table.proto @@ -521,6 +521,16 @@ message FragmentReuseIndexDetails { // MemWAL Index Types // ============================================================================ +// Lifecycle status of a WAL shard. Drives drop-table two-phase commit: +// a SEALED shard refuses new writer claims (reversible) until the drop +// commits (the shard dir is deleted) or rolls back (status -> ACTIVE). +enum ShardStatus { + // Normal: the shard accepts writer claims. + ACTIVE = 0; + // A drop is in flight: claims are refused. Reversible to ACTIVE. + SEALED = 1; +} + // Shard manifest containing epoch-based fencing and WAL state. // Each shard has exactly one active writer at any time. message ShardManifest { @@ -566,6 +576,10 @@ message ShardManifest { // List of flushed MemTable generations and their directory paths. repeated FlushedGeneration flushed_generations = 8; + + // Lifecycle status. Default ACTIVE; SEALED marks an in-flight drop + // (drop-table 2PC). A SEALED manifest refuses claims at claim_epoch. + ShardStatus status = 15; } // A shard field value stored as raw Arrow scalar bytes. diff --git a/rust/lance-table/src/system_index/mem_wal.rs b/rust/lance-table/src/system_index/mem_wal.rs index 3bf279df062..1d82fd9e44f 100644 --- a/rust/lance-table/src/system_index/mem_wal.rs +++ b/rust/lance-table/src/system_index/mem_wal.rs @@ -141,6 +141,39 @@ impl TryFrom for IndexCatchupProgress { } } +/// Lifecycle status of a WAL shard, persisted in [`ShardManifest`]. +/// +/// `Sealed` is the durable in-doubt record for drop-table two-phase +/// commit: a sealed shard refuses new writer claims (enforced in +/// `claim_epoch`) but is reversible back to `Active` on rollback. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum ShardStatus { + /// Normal: the shard accepts writer claims. + #[default] + Active, + /// A drop is in flight: claims are refused. Reversible. + Sealed, +} + +impl ShardStatus { + /// Map to the protobuf enum discriminant (`pb::ShardStatus`). + fn to_i32(self) -> i32 { + match self { + Self::Active => 0, + Self::Sealed => 1, + } + } + + /// Map from the protobuf enum discriminant; unknown values decode as + /// `Active` (forward-compatible default). + fn from_i32(v: i32) -> Self { + match v { + 1 => Self::Sealed, + _ => Self::Active, + } + } +} + /// Shard manifest containing epoch-based fencing and WAL state. /// Each shard has exactly one active writer at any time. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -166,6 +199,9 @@ pub struct ShardManifest { pub wal_entry_position_last_seen: u64, pub current_generation: u64, pub flushed_generations: Vec, + /// Lifecycle status (drop-table 2PC). Defaults to `Active`; preserved + /// across claims via `..base` so only fresh constructions set it. + pub status: ShardStatus, } impl DeepSizeOf for ShardManifest { @@ -194,6 +230,7 @@ impl From<&ShardManifest> for pb::ShardManifest { wal_entry_position_last_seen: rm.wal_entry_position_last_seen, current_generation: rm.current_generation, flushed_generations: rm.flushed_generations.iter().map(|fg| fg.into()).collect(), + status: rm.status.to_i32(), } } } @@ -226,6 +263,7 @@ impl TryFrom for ShardManifest { .into_iter() .map(FlushedGeneration::from) .collect(), + status: ShardStatus::from_i32(rm.status), }) } } diff --git a/rust/lance/src/dataset/mem_wal/manifest.rs b/rust/lance/src/dataset/mem_wal/manifest.rs index 48df22dca88..b9d04afa23e 100644 --- a/rust/lance/src/dataset/mem_wal/manifest.rs +++ b/rust/lance/src/dataset/mem_wal/manifest.rs @@ -35,7 +35,7 @@ use bytes::Bytes; use futures::StreamExt; use futures::stream::FuturesUnordered; use lance_core::{Error, Result}; -use lance_index::mem_wal::ShardManifest; +use lance_index::mem_wal::{ShardManifest, ShardStatus}; use lance_io::object_store::ObjectStore; use lance_table::format::pb; use log::{info, warn}; @@ -147,6 +147,7 @@ impl ShardManifestStore { wal_entry_position_last_seen: 0, current_generation: 1, flushed_generations: vec![], + status: ShardStatus::Active, }; match self.write(&manifest).await { @@ -425,6 +426,21 @@ impl ShardManifestStore { for _ in 0..MAX_CLAIM_RETRIES { let current = self.read_latest().await?; + // A sealed shard is mid-drop (drop-table 2PC). Refuse the claim + // with a distinguishable error rather than minting a new epoch, + // so a caller that skips its own status check still cannot + // resurrect a shard being dropped. Sophon's reconcile keys on + // the "sealed" marker in this message to tell it apart from an + // ordinary epoch fence. + if let Some(m) = ¤t + && m.status == ShardStatus::Sealed + { + return Err(Error::invalid_input(format!( + "shard {} is sealed; refusing claim (drop in flight)", + self.shard_id + ))); + } + let (next_version, next_epoch, base_manifest) = match current { Some(m) => (m.version + 1, m.writer_epoch + 1, Some(m)), None => (1, 1, None), @@ -447,6 +463,7 @@ impl ShardManifestStore { wal_entry_position_last_seen: 0, current_generation: 1, flushed_generations: vec![], + status: ShardStatus::Active, } }; @@ -603,6 +620,7 @@ mod tests { wal_entry_position_last_seen: 0, current_generation: 1, flushed_generations: vec![], + status: ShardStatus::Active, } } @@ -752,6 +770,51 @@ mod tests { assert_eq!(second.wal_entry_position_last_seen, 42); } + #[tokio::test] + async fn test_claim_epoch_refuses_sealed_manifest() { + // A `Sealed` manifest is the drop-table 2PC in-doubt marker: + // `claim_epoch` must refuse it with a distinguishable error rather + // than mint a new epoch, so a sealed shard can't be resurrected even + // by a caller that skips its own status check. Rolling the status + // back to `Active` makes the shard claimable again (reversible). + let (store, base_path, _temp_dir) = create_local_store().await; + let shard_id = Uuid::new_v4(); + let manifest_store = ShardManifestStore::new(store, &base_path, shard_id, 2); + + let (epoch, claimed) = manifest_store.claim_epoch(0).await.unwrap(); + assert_eq!(claimed.status, ShardStatus::Active); + + // Seal it (drop-table prepare). + let sealed = ShardManifest { + version: claimed.version + 1, + status: ShardStatus::Sealed, + ..claimed + }; + manifest_store.write(&sealed).await.unwrap(); + + // The claim is refused with the distinguishable "sealed" error — + // and the manifest is left untouched (no new epoch minted). + let err = manifest_store.claim_epoch(0).await.unwrap_err(); + assert!( + err.to_string().contains("sealed"), + "expected a distinguishable sealed-refusal error, got: {err}" + ); + let after = manifest_store.read_latest().await.unwrap().unwrap(); + assert_eq!(after.writer_epoch, sealed.writer_epoch, "no epoch minted"); + assert_eq!(after.status, ShardStatus::Sealed); + + // Roll back to Active (drop-table abort) → re-claimable. + let active = ShardManifest { + version: sealed.version + 1, + status: ShardStatus::Active, + ..sealed + }; + manifest_store.write(&active).await.unwrap(); + let (next_epoch, reclaimed) = manifest_store.claim_epoch(0).await.unwrap(); + assert!(next_epoch > epoch, "rolled-back shard mints the next epoch"); + assert_eq!(reclaimed.status, ShardStatus::Active); + } + #[tokio::test] async fn test_initialize_shard_rejects_conflict_with_mismatch() { let (store, base_path, _temp_dir) = create_local_store().await; diff --git a/rust/lance/src/dataset/mem_wal/write.rs b/rust/lance/src/dataset/mem_wal/write.rs index 8403452f16d..af96e2bfa98 100644 --- a/rust/lance/src/dataset/mem_wal/write.rs +++ b/rust/lance/src/dataset/mem_wal/write.rs @@ -2173,6 +2173,37 @@ impl ShardWriter { } } + /// Abort the writer without flushing. + /// + /// Shuts down the background flush tasks and leaves all buffered + /// memtable state to be dropped with the writer. Unlike + /// [`Self::close`], no WAL/MemTable flush is issued: pending in-memory + /// rows are discarded, not made durable, and no object-store IO is + /// performed. Used on drop-table, where the dataset directory is about + /// to be removed and a flush would only race fresh files back into a + /// doomed path. + /// + /// Caller-quiesce contract: `abort` takes `&self` (so it can be called + /// through the `Arc` callers hold) and therefore cannot + /// structurally bar a concurrent or subsequent `put` the way consuming + /// `close(self)` does. After abort the dispatchers are gone, so a later + /// `put` would buffer data that never flushes. Callers MUST stop + /// issuing writes before calling abort. + /// + /// Blocks until any flush already mid-`handle()` settles — + /// cancellation only fires between messages — so no flush task lingers + /// after abort returns. Idempotent: a second call re-cancels an + /// already-cancelled token and joins an already-emptied task set. + #[instrument(name = "sw_abort", level = "info", skip_all, fields(shard_id = %self.config.shard_id, epoch = self.epoch))] + pub async fn abort(&self) -> Result<()> { + info!( + "Aborting ShardWriter for shard {} (no flush)", + self.config.shard_id + ); + self.task_executor.shutdown_all().await?; + Ok(()) + } + /// Close the writer gracefully. /// /// Flushes pending data and shuts down background tasks. @@ -5108,6 +5139,63 @@ mod tests { writer.close().await.unwrap(); } + /// `abort` tears down the background flush tasks WITHOUT flushing — + /// buffered memtable rows are discarded, not sealed into an L0 + /// generation the way `close` would. Idempotent on a second call. + #[tokio::test] + async fn test_abort_discards_without_flushing_and_is_idempotent() { + let (store, base_path, base_uri, _temp_dir) = create_local_store().await; + let schema = create_test_schema(); + + // Thresholds high enough that nothing auto-flushes; the rows stay + // in the active memtable until abort discards them. + let config = ShardWriterConfig { + shard_id: Uuid::new_v4(), + shard_spec_id: 0, + durable_write: false, + sync_indexed_write: false, + max_wal_buffer_size: 64 * 1024 * 1024, + max_wal_flush_interval: None, + max_memtable_size: 64 * 1024 * 1024, + manifest_scan_batch_size: 2, + ..Default::default() + }; + + let writer = ShardWriter::open(store, base_path, base_uri, config, schema.clone(), vec![]) + .await + .unwrap(); + + writer + .put(vec![create_test_batch(&schema, 0, 10)]) + .await + .unwrap(); + let flushed_before = writer + .manifest() + .await + .unwrap() + .map(|m| m.flushed_generations.len()) + .unwrap_or(0); + + writer.abort().await.unwrap(); + + // No generation was sealed — contrast with `close`, which flushes + // the 10 buffered rows into a new L0 generation. + let flushed_after = writer + .manifest() + .await + .unwrap() + .map(|m| m.flushed_generations.len()) + .unwrap_or(0); + assert_eq!( + flushed_after, flushed_before, + "abort must not flush a new L0 generation" + ); + + // Idempotent: re-cancels the already-cancelled token, joins an + // already-emptied task set. + writer.abort().await.unwrap(); + } + /// On a successful flush commit the sealed generation's rows land in the /// manifest immediately, but the in-memory handle is NOT dropped — it /// lingers for `frozen_memtable_grace` (so in-flight as-of reads keep From b2673c8f3366007d5ad70ef656be16ed09592e31 Mon Sep 17 00:00:00 2001 From: Dan Rammer Date: Thu, 25 Jun 2026 23:44:39 -0500 Subject: [PATCH 002/177] feat(mem_wal): tombstone-preserving point lookup (#7482) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds `LsmPointLookupPlanner::lookup_keep_tombstone` and `lookup_many_keep_tombstone` — point-lookup variants that carry the `_tombstone` marker through instead of filtering deleted keys out. ## Why The existing `lookup` / `lookup_many` collapse two distinct states — **deleted in the fresh tier** and **never written** — into a single `None`. That's correct for a normal reader, but a caller doing a read-on-write merge needs to tell them apart: a fresh-deleted PK must be treated as *absent* (so a later partial update resurrects it from carried columns + NULLs), while a never-written PK falls back to the base row. These variants return the deleted row with `_tombstone = true`; absent keys still return `None`. Implemented by refactoring `plan_lookup` to share a `plan_lookup_coalesced` helper and skipping the post-coalesce tombstone filter, so the hot read path is unchanged. ## Tests - `test_lookup_keep_tombstone_returns_deleted_row_with_marker` - `test_lookup_many_keep_tombstone_includes_tombstoned_keys` ## Context Prerequisite for a downstream WAL partial-column-update resurrection fix: a partial update arriving after a delete must resurrect carried columns + NULL untouched, never stale pre-delete base data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) --- .../dataset/mem_wal/scanner/point_lookup.rs | 169 ++++++++++++++++-- 1 file changed, 159 insertions(+), 10 deletions(-) diff --git a/rust/lance/src/dataset/mem_wal/scanner/point_lookup.rs b/rust/lance/src/dataset/mem_wal/scanner/point_lookup.rs index f247ef12cb0..ef4aec5db3d 100644 --- a/rust/lance/src/dataset/mem_wal/scanner/point_lookup.rs +++ b/rust/lance/src/dataset/mem_wal/scanner/point_lookup.rs @@ -189,6 +189,33 @@ impl LsmPointLookupPlanner { pk_values: &[ScalarValue], projection: Option<&[String]>, ) -> Result> { + match self.plan_lookup_coalesced(pk_values, projection).await? { + // Tombstones are dropped AFTER the coalesce, never per-source: the + // tombstone wins the newest-first coalesce (its source is the newest + // non-empty arm), so filtering it here yields "not found". Filtering + // per-source would empty the newest arm and let `CoalesceFirstExec` + // fall through to an older arm — resurrecting the deleted row. Then + // the carried `_tombstone` column is projected away. + Some(coalesced) => { + let canonical = + canonical_output_schema(projection, &self.base_schema, &self.pk_columns, false); + filter_tombstones_after_coalesce(coalesced, &canonical) + } + None => self.empty_plan(projection), + } + } + + /// Build the coalesced point-lookup plan: each source scanned newest-first, + /// unioned under `CoalesceFirstExec`, output in the *carry* schema (canonical + /// output + the `_tombstone` marker). `None` when there are no sources at + /// all. [`Self::plan_lookup`] drops the tombstone on top of this; + /// [`Self::lookup_keep_tombstone`] keeps it so partial-update merge can tell + /// a fresh-deleted key from an absent one. + async fn plan_lookup_coalesced( + &self, + pk_values: &[ScalarValue], + projection: Option<&[String]>, + ) -> Result>> { if pk_values.len() != self.pk_columns.len() { return Err(lance_core::Error::invalid_input(format!( "Expected {} primary key values, got {}", @@ -202,7 +229,7 @@ impl LsmPointLookupPlanner { let sources = self.collector.collect()?; if sources.is_empty() { - return self.empty_plan(projection); + return Ok(None); } // Sort by generation DESC (newest first) @@ -242,17 +269,60 @@ impl LsmPointLookupPlanner { // needs (the in-memory mem_wal execs report empty column statistics, and // datafusion's projection statistics would index out of bounds without // this normalization). - let coalesced: Arc = Arc::new(CoalesceFirstExec::new(source_plans)); - - // Tombstones must be dropped AFTER the coalesce, never per-source: the - // tombstone wins the newest-first coalesce (its source is the newest - // non-empty arm), so filtering it here yields "not found". Filtering - // per-source would empty the newest arm and let `CoalesceFirstExec` - // fall through to an older arm — resurrecting the deleted row. Then the - // carried `_tombstone` column is projected away. + Ok(Some(Arc::new(CoalesceFirstExec::new(source_plans)))) + } + + /// Like [`Self::lookup`] but does NOT drop a tombstone: the returned 1-row + /// batch carries the `_tombstone` marker (`true` ⇒ the key's newest fresh + /// version is a delete); `None` still means the key is absent from every + /// source. Partial-update merge uses this to treat a fresh-deleted PK as + /// absent, so it never resurrects stale, not-yet-compacted base columns. + /// Always plans (no in-memory fast path) — used off the hot read path, on + /// small partial-update batches. + pub async fn lookup_keep_tombstone( + &self, + pk_values: &[ScalarValue], + projection: Option<&[String]>, + ) -> Result> { + let Some(plan) = self.plan_lookup_coalesced(pk_values, projection).await? else { + return Ok(None); + }; + let batches: Vec = plan + .execute(0, self.task_ctx.clone())? + .try_collect() + .await?; + for batch in batches { + if batch.num_rows() > 0 { + return Ok(Some(batch.slice(0, 1))); + } + } + Ok(None) + } + + /// Tombstone-preserving [`Self::lookup_many`] for single- or multi-column + /// keys: resolves each key with [`Self::lookup_keep_tombstone`] and + /// concatenates the hits in the carry schema (canonical output + + /// `_tombstone`); keys absent from every source are omitted. Per-key (no + /// batched fast path) — the partial-update batches that use it are small. + pub async fn lookup_many_keep_tombstone( + &self, + keys: &[Vec], + projection: Option<&[String]>, + ) -> Result { let canonical = canonical_output_schema(projection, &self.base_schema, &self.pk_columns, false); - filter_tombstones_after_coalesce(coalesced, &canonical) + let target = carry_schema(&canonical); + let mut out: Vec = Vec::with_capacity(keys.len()); + for key in keys { + if let Some(b) = self.lookup_keep_tombstone(key, projection).await? { + out.push(b); + } + } + match out.len() { + 0 => Ok(RecordBatch::new_empty(target)), + 1 => Ok(out.pop().unwrap()), + _ => Ok(arrow_select::concat::concat_batches(&target, &out)?), + } } /// Resolve a single-row point lookup, returning the newest matching row (a @@ -1984,6 +2054,85 @@ mod tests { LsmPointLookupPlanner::new(collector, vec!["id".to_string()], base_schema) } + /// Read the `_tombstone` marker from row 0 of a keep-tombstone result. + fn tombstone_at(b: &RecordBatch) -> bool { + let idx = b.schema().index_of(TOMBSTONE).expect("_tombstone column"); + b.column(idx) + .as_any() + .downcast_ref::() + .expect("_tombstone is Boolean") + .value(0) + } + + #[tokio::test] + async fn test_lookup_keep_tombstone_returns_deleted_row_with_marker() { + // The tombstone-preserving variant keeps a deleted key that the filtered + // `lookup` would drop: id=1 deleted, id=2 live, id=99 absent. + let schema = pk_ts_schema(); + let planner = active_ts_planner(&[ts_real(&schema, &[1, 2], "v"), ts_tomb(&schema, &[1])]); + + // Deleted key: present with `_tombstone = true` (vs `None` from `lookup`). + let deleted = planner + .lookup_keep_tombstone(&[ScalarValue::Int32(Some(1))], None) + .await + .unwrap() + .expect("deleted key kept by lookup_keep_tombstone"); + assert_eq!(id_at(&deleted), 1); + assert!( + tombstone_at(&deleted), + "deleted key carries _tombstone = true" + ); + + // Live key: present with `_tombstone = false`. + let live = planner + .lookup_keep_tombstone(&[ScalarValue::Int32(Some(2))], None) + .await + .unwrap() + .expect("live key found"); + assert_eq!(id_at(&live), 2); + assert!(!tombstone_at(&live), "live key carries _tombstone = false"); + + // Absent key: still `None` — no fresh entry at all to distinguish. + assert!( + planner + .lookup_keep_tombstone(&[ScalarValue::Int32(Some(99))], None) + .await + .unwrap() + .is_none(), + "absent key has no fresh entry" + ); + } + + #[tokio::test] + async fn test_lookup_many_keep_tombstone_includes_tombstoned_keys() { + // Batched variant: the tombstoned key is INCLUDED (carrying its marker), + // unlike `lookup_many` which omits it. id=2 deleted; 1 and 3 live. + let schema = pk_ts_schema(); + let planner = + active_ts_planner(&[ts_real(&schema, &[1, 2, 3], "v"), ts_tomb(&schema, &[2])]); + let keys = vec![ + vec![ScalarValue::Int32(Some(1))], + vec![ScalarValue::Int32(Some(2))], + vec![ScalarValue::Int32(Some(3))], + ]; + let batch = planner + .lookup_many_keep_tombstone(&keys, None) + .await + .unwrap(); + assert_eq!( + batch.num_rows(), + 3, + "all three keys kept, including the tombstone" + ); + // Exactly one row (id=2) is marked deleted. + let deleted: Vec = (0..batch.num_rows()) + .map(|r| batch.slice(r, 1)) + .filter(tombstone_at) + .map(|b| id_at(&b)) + .collect(); + assert_eq!(deleted, vec![2], "only id=2 is marked deleted"); + } + #[tokio::test] async fn test_lookup_tombstone_within_active_not_found() { // Fast path: id=1 written then tombstoned (newer); id=2 is a control. From 621da36da951038ce2a6693de27faec8a78d5eeb Mon Sep 17 00:00:00 2001 From: YangJie Date: Fri, 26 Jun 2026 16:00:04 +0800 Subject: [PATCH 003/177] fix(index): implement with_io_priority for FailNewFileStore test store (#7495) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What The `lance-index` **test build** is currently broken on `main`: `error[E0046]: not all trait items implemented, missing: with_io_priority` at `rust/lance-index/src/scalar/fmindex.rs`. `with_io_priority` was added to the `IndexStore` trait in #7449. The `FailNewFileStore` test fixture was added later in #7422 (which branched before #7449 landed), so it never implemented the new method. Each PR was green on its own; together on `main` the test build fails — a semantic merge conflict. It only surfaces in the `Build tests` step because the fixture is `#[cfg(test)]`. ## Fix Implement `with_io_priority` on `FailNewFileStore` by delegating to the inner store and re-wrapping, mirroring how the fixture already delegates its other read methods while keeping the injected `new_index_file` failure. ## Verification - `cargo check -p lance-index --tests` — compiles (was failing on `main`). - `cargo test -p lance-index --lib fmindex` — 24 passed. - `cargo fmt -p lance-index -- --check` and `cargo clippy -p lance-index --tests` — clean. --- rust/lance-index/src/scalar/fmindex.rs | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/rust/lance-index/src/scalar/fmindex.rs b/rust/lance-index/src/scalar/fmindex.rs index 2aa59b5c192..dde9bcff2a9 100644 --- a/rust/lance-index/src/scalar/fmindex.rs +++ b/rust/lance-index/src/scalar/fmindex.rs @@ -2002,6 +2002,12 @@ mod tests { self.inner.io_parallelism() } + fn with_io_priority(&self, io_priority: u64) -> Arc { + Arc::new(Self { + inner: self.inner.with_io_priority(io_priority), + }) + } + async fn new_index_file( &self, _name: &str, @@ -2598,6 +2604,34 @@ mod tests { assert!(format!("{err}").contains("injected FMIndex write failure")); } + #[tokio::test] + async fn test_fail_new_file_store_with_io_priority_preserves_failure() { + // `with_io_priority` re-wraps in `FailNewFileStore`, so the reprioritized + // store must keep injecting the `new_index_file` failure. + let tempdir = tempfile::tempdir().unwrap(); + let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap(); + let inner = Arc::new(LanceIndexStore::new( + Arc::new(ObjectStore::local()), + index_dir, + Arc::new(LanceCache::no_cache()), + )) as Arc; + let store = FailNewFileStore { inner }; + + let reprioritized = store.with_io_priority(7); + + let schema = Arc::new(arrow_schema::Schema::new(vec![arrow_schema::Field::new( + "x", + DataType::UInt64, + false, + )])); + let err = reprioritized + .new_index_file("test", schema) + .await + .err() + .expect("new_index_file should fail"); + assert!(format!("{err}").contains("injected FMIndex write failure")); + } + #[tokio::test(flavor = "multi_thread")] async fn test_plugin_train_streams_multiple_partitions() { fn training_batch( From 67c88fba4233d87601c47730b2f6908c3ae1fa56 Mon Sep 17 00:00:00 2001 From: Yang Cen Date: Fri, 26 Jun 2026 22:15:39 +0800 Subject: [PATCH 004/177] feat(bitpacking): add owned bitpacking codecs (#7496) ## What changed This PR narrows OSS-1346 to codec replacement work only. It adds Lance-owned `BitPacker4x` and internal `BitPacker8x` implementations in `lance-bitpacking`, and switches existing FTS posting-list users from the external `bitpacking` runtime dependency to `lance-bitpacking`. The external `bitpacking` crate remains only as a dev-dependency for byte-for-byte compatibility tests against the legacy 4x/8x formats. ## Why FTS currently depends on an external bitpacking crate for 128-value posting blocks. Keeping the implementation in `lance-bitpacking` lets Lance own the codec while preserving existing 128-block wire compatibility. The 8x implementation is included as an internal algorithm building block only; this PR does not expose a new block-size option. ## How it works - Exposes `BitPacker` / `BitPacker4x` from `lance-bitpacking` with the same call shape used by existing FTS code. - Adds owned backends for 128-value 4x blocks: - SSE3 on `x86_64` - NEON on `aarch64` - scalar fallback when SIMD backends are unavailable - Adds an internal 256-value `BitPacker8x` algorithm: - AVX2 on `x86_64` - NEON on little-endian `aarch64` - scalar fallback when AVX2 or NEON is unavailable - not re-exported from `lance-bitpacking` - Does not add or change FTS `block_size` parameters, metadata, Python, Java, or docs. ## Validation - `cargo fmt --all` - `cargo test -p lance-bitpacking -- --nocapture` - `cargo check -p lance-index --tests` - `cargo check -p lance --tests` - `cargo clippy -p lance-bitpacking --tests -- -D warnings` - `cargo clippy -p lance-index --tests -- -D warnings` - `cargo clippy -p lance --tests -- -D warnings` - `cargo clippy --all --tests --benches -- -D warnings` - `cargo metadata --manifest-path python/Cargo.toml --locked --format-version=1 --no-deps` - `CARGO_TARGET_DIR=/tmp/lance-target-oss1346-aarch64 cargo check -p lance-bitpacking --tests --target aarch64-apple-darwin` - `CARGO_TARGET_DIR=/tmp/lance-target-oss1346-x86 cargo check -p lance-bitpacking --tests --target x86_64-apple-darwin` - `git diff --check` --- Cargo.lock | 6 +- LICENSE | 27 + python/Cargo.lock | 14 +- rust/compression/bitpacking/Cargo.toml | 4 + .../src/bitpacker_internal/bitpacker4x.rs | 820 ++++++++++++++++ .../src/bitpacker_internal/bitpacker8x.rs | 873 ++++++++++++++++++ .../src/bitpacker_internal/macros.rs | 669 ++++++++++++++ .../bitpacking/src/bitpacker_internal/mod.rs | 146 +++ rust/compression/bitpacking/src/lib.rs | 103 +++ rust/lance-index/Cargo.toml | 2 +- .../src/scalar/inverted/builder.rs | 2 +- .../src/scalar/inverted/encoding.rs | 2 +- rust/lance/Cargo.toml | 2 +- rust/lance/src/dataset/mem_wal/index/fts.rs | 2 +- 14 files changed, 2654 insertions(+), 18 deletions(-) create mode 100644 rust/compression/bitpacking/src/bitpacker_internal/bitpacker4x.rs create mode 100644 rust/compression/bitpacking/src/bitpacker_internal/bitpacker8x.rs create mode 100644 rust/compression/bitpacking/src/bitpacker_internal/macros.rs create mode 100644 rust/compression/bitpacking/src/bitpacker_internal/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 11d2cbe555e..d58494f3e48 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4402,7 +4402,6 @@ dependencies = [ "aws-credential-types", "aws-sdk-dynamodb", "aws-sdk-s3", - "bitpacking", "byteorder", "bytes", "chrono", @@ -4428,6 +4427,7 @@ dependencies = [ "humantime", "itertools 0.14.0", "lance-arrow", + "lance-bitpacking", "lance-core", "lance-datafusion", "lance-datagen", @@ -4534,6 +4534,8 @@ name = "lance-bitpacking" version = "9.0.0-beta.8" dependencies = [ "arrayref", + "bitpacking", + "crunchy", "paste", "seq-macro", ] @@ -4778,7 +4780,6 @@ dependencies = [ "async-channel", "async-recursion", "async-trait", - "bitpacking", "bitvec", "bytes", "chrono", @@ -4802,6 +4803,7 @@ dependencies = [ "jsonb", "lance-arrow", "lance-arrow-stats", + "lance-bitpacking", "lance-core", "lance-datafusion", "lance-datagen", diff --git a/LICENSE b/LICENSE index 79de57d6670..cd4c38213a9 100644 --- a/LICENSE +++ b/LICENSE @@ -226,3 +226,30 @@ under the MIT license: SOFTWARE. https://github.com/pola-rs/polars/blob/main/LICENSE + +-------------------------------------------------------------------------------- + +This project includes code adapted from the quickwit-oss/bitpacking crate, which +is licensed under the MIT license: + + Copyright (c) 2016 Paul Masurel + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +https://github.com/quickwit-oss/bitpacking/blob/main/LICENSE diff --git a/python/Cargo.lock b/python/Cargo.lock index 88bfda071ba..0b6fa85e2dc 100644 --- a/python/Cargo.lock +++ b/python/Cargo.lock @@ -1048,15 +1048,6 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -[[package]] -name = "bitpacking" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" -dependencies = [ - "crunchy", -] - [[package]] name = "bitvec" version = "1.1.1" @@ -4076,7 +4067,6 @@ dependencies = [ "async_cell", "aws-credential-types", "aws-sdk-dynamodb", - "bitpacking", "byteorder", "bytes", "chrono", @@ -4095,6 +4085,7 @@ dependencies = [ "humantime", "itertools 0.14.0", "lance-arrow", + "lance-bitpacking", "lance-core", "lance-datafusion", "lance-encoding", @@ -4177,6 +4168,7 @@ name = "lance-bitpacking" version = "9.0.0-beta.8" dependencies = [ "arrayref", + "crunchy", "paste", "seq-macro", ] @@ -4370,7 +4362,6 @@ dependencies = [ "async-channel", "async-recursion", "async-trait", - "bitpacking", "bitvec", "bytes", "chrono", @@ -4391,6 +4382,7 @@ dependencies = [ "jsonb", "lance-arrow", "lance-arrow-stats", + "lance-bitpacking", "lance-core", "lance-datafusion", "lance-datagen", diff --git a/rust/compression/bitpacking/Cargo.toml b/rust/compression/bitpacking/Cargo.toml index f086ede8baa..56db321ac6d 100644 --- a/rust/compression/bitpacking/Cargo.toml +++ b/rust/compression/bitpacking/Cargo.toml @@ -13,8 +13,12 @@ rust-version.workspace = true [dependencies] arrayref = "0.3" +crunchy = "0.2" paste = "1" seq-macro = "0.3" +[dev-dependencies] +bitpacking = { workspace = true, features = ["bitpacker4x"] } + [lints] workspace = true diff --git a/rust/compression/bitpacking/src/bitpacker_internal/bitpacker4x.rs b/rust/compression/bitpacking/src/bitpacker_internal/bitpacker4x.rs new file mode 100644 index 00000000000..ca3c68908ef --- /dev/null +++ b/rust/compression/bitpacking/src/bitpacker_internal/bitpacker4x.rs @@ -0,0 +1,820 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +use super::BitPacker; + +use crate::bitpacker_internal::{Available, UnsafeBitPacker}; + +const BLOCK_LEN: usize = 32 * 4; + +#[cfg(target_arch = "x86_64")] +mod sse3 { + + use super::BLOCK_LEN; + use crate::bitpacker_internal::Available; + + use std::arch::x86_64::__m128i as DataType; + use std::arch::x86_64::_mm_and_si128 as op_and; + use std::arch::x86_64::_mm_lddqu_si128 as load_unaligned; + use std::arch::x86_64::_mm_or_si128 as op_or; + use std::arch::x86_64::_mm_set1_epi32 as set1; + use std::arch::x86_64::_mm_slli_epi32 as left_shift_32; + use std::arch::x86_64::_mm_srli_epi32 as right_shift_32; + use std::arch::x86_64::_mm_storeu_si128 as store_unaligned; + use std::arch::x86_64::{ + _mm_add_epi32, _mm_cvtsi128_si32, _mm_shuffle_epi32, _mm_slli_si128, _mm_srli_si128, + _mm_sub_epi32, + }; + + #[allow(non_snake_case)] + #[inline] + unsafe fn or_collapse_to_u32(accumulator: DataType) -> u32 { + let a__b__c__d_ = accumulator; + let ______a__b_ = _mm_srli_si128(a__b__c__d_, 8); + let a__b__ca_db = op_or(a__b__c__d_, ______a__b_); + let ___a__b__ca = _mm_srli_si128(a__b__ca_db, 4); + let _______cadb = op_or(a__b__ca_db, ___a__b__ca); + _mm_cvtsi128_si32(_______cadb) as u32 + } + + #[target_feature(enable = "sse3")] + unsafe fn compute_delta(curr: DataType, prev: DataType) -> DataType { + _mm_sub_epi32( + curr, + op_or(_mm_slli_si128(curr, 4), _mm_srli_si128(prev, 12)), + ) + } + + #[target_feature(enable = "sse3")] + #[allow(non_snake_case)] + #[inline] + unsafe fn integrate_delta(prev: DataType, delta: DataType) -> DataType { + let offset = _mm_shuffle_epi32(prev, 0xff); + let a__b__c__d_ = delta; + let ______a__b_ = _mm_slli_si128(delta, 8); + let a__b__ca_db = _mm_add_epi32(______a__b_, a__b__c__d_); + let ___a__b__ca = _mm_slli_si128(a__b__ca_db, 4); + let a_ab_abc_abcd: DataType = _mm_add_epi32(___a__b__ca, a__b__ca_db); + _mm_add_epi32(offset, a_ab_abc_abcd) + } + + #[target_feature(enable = "sse3")] + #[inline] + unsafe fn add(left: DataType, right: DataType) -> DataType { + _mm_add_epi32(left, right) + } + + unsafe fn sub(left: DataType, right: DataType) -> DataType { + _mm_sub_epi32(left, right) + } + + declare_bitpacker!(target_feature(enable = "sse3")); + + impl Available for UnsafeBitPackerImpl { + fn available() -> bool { + is_x86_feature_detected!("sse3") + } + } +} + +#[cfg(all(target_arch = "aarch64", target_endian = "little"))] +mod neon { + + use super::BLOCK_LEN; + use crate::bitpacker_internal::Available; + use std::arch::aarch64::{ + uint32x4_t, vaddq_u32, vandq_u32, vdupq_n_u32, vextq_u32, vgetq_lane_u32, vld1q_u32, + vorrq_u32, vshlq_n_u32, vshrq_n_u32, vst1q_u32, vsubq_u32, + }; + + pub(crate) type DataType = uint32x4_t; + + #[inline] + /// Creates a vector with all elements set to `el`. + unsafe fn set1(el: i32) -> DataType { + vdupq_n_u32(el as u32) + } + + #[inline] + unsafe fn right_shift_32(el: DataType) -> DataType { + const { + assert!(N >= 0); + assert!(N <= 32); + } + + // We unroll here because vshrq_n_u32 only accepts constants from 1 to 32. + match N { + 0 => el, + 1 => vshrq_n_u32::<1>(el), + 2 => vshrq_n_u32::<2>(el), + 3 => vshrq_n_u32::<3>(el), + 4 => vshrq_n_u32::<4>(el), + 5 => vshrq_n_u32::<5>(el), + 6 => vshrq_n_u32::<6>(el), + 7 => vshrq_n_u32::<7>(el), + 8 => vshrq_n_u32::<8>(el), + 9 => vshrq_n_u32::<9>(el), + 10 => vshrq_n_u32::<10>(el), + 11 => vshrq_n_u32::<11>(el), + 12 => vshrq_n_u32::<12>(el), + 13 => vshrq_n_u32::<13>(el), + 14 => vshrq_n_u32::<14>(el), + 15 => vshrq_n_u32::<15>(el), + 16 => vshrq_n_u32::<16>(el), + 17 => vshrq_n_u32::<17>(el), + 18 => vshrq_n_u32::<18>(el), + 19 => vshrq_n_u32::<19>(el), + 20 => vshrq_n_u32::<20>(el), + 21 => vshrq_n_u32::<21>(el), + 22 => vshrq_n_u32::<22>(el), + 23 => vshrq_n_u32::<23>(el), + 24 => vshrq_n_u32::<24>(el), + 25 => vshrq_n_u32::<25>(el), + 26 => vshrq_n_u32::<26>(el), + 27 => vshrq_n_u32::<27>(el), + 28 => vshrq_n_u32::<28>(el), + 29 => vshrq_n_u32::<29>(el), + 30 => vshrq_n_u32::<30>(el), + 31 => vshrq_n_u32::<31>(el), + 32 => vdupq_n_u32(0), + _ => core::hint::unreachable_unchecked(), + } + } + + #[inline] + unsafe fn left_shift_32(el: DataType) -> DataType { + const { + assert!(N >= 0); + assert!(N <= 32); + } + + // We unroll here because vshlq_n_u32 only accepts constants from 0 to 31. + match N { + 0 => el, + 1 => vshlq_n_u32::<1>(el), + 2 => vshlq_n_u32::<2>(el), + 3 => vshlq_n_u32::<3>(el), + 4 => vshlq_n_u32::<4>(el), + 5 => vshlq_n_u32::<5>(el), + 6 => vshlq_n_u32::<6>(el), + 7 => vshlq_n_u32::<7>(el), + 8 => vshlq_n_u32::<8>(el), + 9 => vshlq_n_u32::<9>(el), + 10 => vshlq_n_u32::<10>(el), + 11 => vshlq_n_u32::<11>(el), + 12 => vshlq_n_u32::<12>(el), + 13 => vshlq_n_u32::<13>(el), + 14 => vshlq_n_u32::<14>(el), + 15 => vshlq_n_u32::<15>(el), + 16 => vshlq_n_u32::<16>(el), + 17 => vshlq_n_u32::<17>(el), + 18 => vshlq_n_u32::<18>(el), + 19 => vshlq_n_u32::<19>(el), + 20 => vshlq_n_u32::<20>(el), + 21 => vshlq_n_u32::<21>(el), + 22 => vshlq_n_u32::<22>(el), + 23 => vshlq_n_u32::<23>(el), + 24 => vshlq_n_u32::<24>(el), + 25 => vshlq_n_u32::<25>(el), + 26 => vshlq_n_u32::<26>(el), + 27 => vshlq_n_u32::<27>(el), + 28 => vshlq_n_u32::<28>(el), + 29 => vshlq_n_u32::<29>(el), + 30 => vshlq_n_u32::<30>(el), + 31 => vshlq_n_u32::<31>(el), + 32 => vdupq_n_u32(0), + _ => core::hint::unreachable_unchecked(), + } + } + + use vorrq_u32 as op_or; + + #[inline] + unsafe fn op_and(left: DataType, right: DataType) -> DataType { + vandq_u32(left, right) + } + + #[inline] + unsafe fn load_unaligned(addr: *const DataType) -> DataType { + vld1q_u32(addr.cast::()) + } + + #[inline] + unsafe fn store_unaligned(addr: *mut DataType, data: DataType) { + vst1q_u32(addr.cast::(), data); + } + + #[inline] + /// Collapses the vector by performing a bitwise OR across all lanes + unsafe fn or_collapse_to_u32(acc: DataType) -> u32 { + vgetq_lane_u32(acc, 0) + | vgetq_lane_u32(acc, 1) + | vgetq_lane_u32(acc, 2) + | vgetq_lane_u32(acc, 3) + } + + unsafe fn compute_delta(curr: DataType, prev: DataType) -> DataType { + // Build a vector with [prev[3], curr[0], curr[1], curr[2]] + let prev_shifted = vextq_u32(prev, curr, 3); + vsubq_u32(curr, prev_shifted) + } + + #[allow(non_snake_case)] + #[inline] + unsafe fn integrate_delta(prev: DataType, delta: DataType) -> DataType { + let base = vdupq_n_u32(vgetq_lane_u32(prev, 3)); + let zero = vdupq_n_u32(0); + let a__b__c__d_ = delta; + let ______a__b_ = vextq_u32(zero, a__b__c__d_, 2); + let a__b__ca_db = vaddq_u32(______a__b_, a__b__c__d_); + let ___a__b__ca = vextq_u32(zero, a__b__ca_db, 3); + let a_ab_abc_abcd = vaddq_u32(___a__b__ca, a__b__ca_db); + vaddq_u32(base, a_ab_abc_abcd) + } + + #[inline] + unsafe fn add(left: DataType, right: DataType) -> DataType { + vaddq_u32(left, right) + } + + #[inline] + unsafe fn sub(left: DataType, right: DataType) -> DataType { + vsubq_u32(left, right) + } + + declare_bitpacker!(target_feature(enable = "neon")); + + impl Available for UnsafeBitPackerImpl { + fn available() -> bool { + std::arch::is_aarch64_feature_detected!("neon") + } + } +} + +mod scalar { + + use super::BLOCK_LEN; + use crate::bitpacker_internal::Available; + use std::ptr; + + pub(crate) type DataType = [u32; 4]; + + pub(crate) fn set1(el: i32) -> DataType { + [el as u32; 4] + } + + pub(crate) fn right_shift_32(el: DataType) -> DataType { + [el[0] >> N, el[1] >> N, el[2] >> N, el[3] >> N] + } + + pub(crate) fn left_shift_32(el: DataType) -> DataType { + [el[0] << N, el[1] << N, el[2] << N, el[3] << N] + } + + pub(crate) fn op_or(left: DataType, right: DataType) -> DataType { + [ + left[0] | right[0], + left[1] | right[1], + left[2] | right[2], + left[3] | right[3], + ] + } + + pub(crate) fn op_and(left: DataType, right: DataType) -> DataType { + [ + left[0] & right[0], + left[1] & right[1], + left[2] & right[2], + left[3] & right[3], + ] + } + + pub(crate) unsafe fn load_unaligned(addr: *const DataType) -> DataType { + ptr::read_unaligned(addr) + } + + pub(crate) unsafe fn store_unaligned(addr: *mut DataType, data: DataType) { + ptr::write_unaligned(addr, data); + } + + pub(crate) fn or_collapse_to_u32(accumulator: DataType) -> u32 { + (accumulator[0] | accumulator[1]) | (accumulator[2] | accumulator[3]) + } + + fn compute_delta(curr: DataType, prev: DataType) -> DataType { + [ + curr[0].wrapping_sub(prev[3]), + curr[1].wrapping_sub(curr[0]), + curr[2].wrapping_sub(curr[1]), + curr[3].wrapping_sub(curr[2]), + ] + } + + fn integrate_delta(offset: DataType, delta: DataType) -> DataType { + let el0 = offset[3].wrapping_add(delta[0]); + let el1 = el0.wrapping_add(delta[1]); + let el2 = el1.wrapping_add(delta[2]); + let el3 = el2.wrapping_add(delta[3]); + [el0, el1, el2, el3] + } + + pub(crate) fn add(left: DataType, right: DataType) -> DataType { + [ + left[0].wrapping_add(right[0]), + left[1].wrapping_add(right[1]), + left[2].wrapping_add(right[2]), + left[3].wrapping_add(right[3]), + ] + } + + pub(crate) fn sub(left: DataType, right: DataType) -> DataType { + [ + left[0].wrapping_sub(right[0]), + left[1].wrapping_sub(right[1]), + left[2].wrapping_sub(right[2]), + left[3].wrapping_sub(right[3]), + ] + } + + // The `allow(unused)` is here to put an attribute that has no effect. + // + // For other bitpacker, we enable specific CPU instruction set, but for the + // scalar bitpacker none is required. + declare_bitpacker!(allow(unused)); + + impl Available for UnsafeBitPackerImpl { + fn available() -> bool { + true + } + } +} + +#[derive(Clone, Copy)] +enum InstructionSet { + #[cfg(target_arch = "x86_64")] + SSE3, + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + NEON, + Scalar, +} + +/// `BitPacker4x` packs integers in groups of 4. This gives an opportunity +/// to leverage `SSE3` instructions to encode and decode the stream. +/// +/// One block must contain `128 integers`. +#[derive(Clone, Copy)] +pub struct BitPacker4x(InstructionSet); + +impl BitPacker4x { + #[cfg(target_arch = "x86_64")] + pub(crate) fn new_sse() -> Option { + sse3::UnsafeBitPackerImpl::available().then_some(BitPacker4x(InstructionSet::SSE3)) + } + + #[cfg(not(target_arch = "x86_64"))] + pub(crate) fn new_sse() -> Option { + None + } + + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + pub(crate) fn new_neon() -> Option { + neon::UnsafeBitPackerImpl::available().then_some(BitPacker4x(InstructionSet::NEON)) + } + + #[cfg(not(all(target_arch = "aarch64", target_endian = "little")))] + pub(crate) fn new_neon() -> Option { + None + } + + pub(crate) fn new_scalar() -> Self { + BitPacker4x(InstructionSet::Scalar) + } +} + +impl BitPacker for BitPacker4x { + const BLOCK_LEN: usize = BLOCK_LEN; + + fn new() -> Self { + Self::new_sse() + .or_else(Self::new_neon) + .unwrap_or_else(Self::new_scalar) + } + + fn compress(&self, decompressed: &[u32], compressed: &mut [u8], num_bits: u8) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => { + sse3::UnsafeBitPackerImpl::compress(decompressed, compressed, num_bits) + } + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => { + neon::UnsafeBitPackerImpl::compress(decompressed, compressed, num_bits) + } + InstructionSet::Scalar => { + scalar::UnsafeBitPackerImpl::compress(decompressed, compressed, num_bits) + } + } + } + } + + fn compress_sorted( + &self, + initial: u32, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => sse3::UnsafeBitPackerImpl::compress_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::compress_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::compress_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + } + } + } + + fn compress_strictly_sorted( + &self, + initial: Option, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => sse3::UnsafeBitPackerImpl::compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + } + } + } + + fn decompress(&self, compressed: &[u8], decompressed: &mut [u32], num_bits: u8) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => { + sse3::UnsafeBitPackerImpl::decompress(compressed, decompressed, num_bits) + } + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => { + neon::UnsafeBitPackerImpl::decompress(compressed, decompressed, num_bits) + } + InstructionSet::Scalar => { + scalar::UnsafeBitPackerImpl::decompress(compressed, decompressed, num_bits) + } + } + } + } + + fn decompress_strictly_sorted( + &self, + initial: Option, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => sse3::UnsafeBitPackerImpl::decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + } + } + } + + fn decompress_sorted( + &self, + initial: u32, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => sse3::UnsafeBitPackerImpl::decompress_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::decompress_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::decompress_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + } + } + } + + fn num_bits(&self, decompressed: &[u32]) -> u8 { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => sse3::UnsafeBitPackerImpl::num_bits(decompressed), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::num_bits(decompressed), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::num_bits(decompressed), + } + } + } + + fn num_bits_sorted(&self, initial: u32, decompressed: &[u32]) -> u8 { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => { + sse3::UnsafeBitPackerImpl::num_bits_sorted(initial, decompressed) + } + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => { + neon::UnsafeBitPackerImpl::num_bits_sorted(initial, decompressed) + } + InstructionSet::Scalar => { + scalar::UnsafeBitPackerImpl::num_bits_sorted(initial, decompressed) + } + } + } + } + + fn num_bits_strictly_sorted(&self, initial: Option, decompressed: &[u32]) -> u8 { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::SSE3 => { + sse3::UnsafeBitPackerImpl::num_bits_strictly_sorted(initial, decompressed) + } + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => { + neon::UnsafeBitPackerImpl::num_bits_strictly_sorted(initial, decompressed) + } + InstructionSet::Scalar => { + scalar::UnsafeBitPackerImpl::num_bits_strictly_sorted(initial, decompressed) + } + } + } + } +} + +#[cfg(any( + target_arch = "x86_64", + all(target_arch = "aarch64", target_endian = "little") +))] +#[cfg(any())] +mod tests { + use super::BLOCK_LEN; + use super::scalar; + use crate::bitpacker_internal::Available; + use crate::tests::test_util_compatible; + use crate::{BitPacker, BitPacker4x}; + + #[cfg(target_arch = "x86_64")] + #[test] + fn test_compatible_sse3() { + use super::sse3; + if sse3::UnsafeBitPackerImpl::available() { + test_util_compatible::( + BLOCK_LEN, + ); + } + } + + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + #[test] + fn test_compatible_neon() { + use super::neon; + if neon::UnsafeBitPackerImpl::available() { + test_util_compatible::( + BLOCK_LEN, + ); + } + } + + #[test] + fn test_delta_bit_width_32() { + let values = vec![i32::max_value() as u32 + 1; BitPacker4x::BLOCK_LEN]; + let bit_packer = BitPacker4x::new(); + let bit_width = bit_packer.num_bits_sorted(0, &values); + assert_eq!(bit_width, 32); + + let mut block = vec![0u8; BitPacker4x::compressed_block_size(bit_width)]; + bit_packer.compress_sorted(0, &values, &mut block, bit_width); + + let mut decoded_values = vec![0x10101010; BitPacker4x::BLOCK_LEN]; + bit_packer.decompress_sorted(0, &block, &mut decoded_values, bit_width); + + assert_eq!(values, decoded_values); + } + + #[test] + fn test_bit_width_32() { + let mut values = vec![i32::max_value() as u32 + 1; BitPacker4x::BLOCK_LEN]; + values[0] = 0; + let bit_packer = BitPacker4x::new(); + let bit_width = bit_packer.num_bits(&values); + assert_eq!(bit_width, 32); + + let mut block = vec![0u8; BitPacker4x::compressed_block_size(bit_width)]; + bit_packer.compress(&values, &mut block, bit_width); + + let mut decoded_values = vec![0x10101010; BitPacker4x::BLOCK_LEN]; + bit_packer.decompress(&block, &mut decoded_values, bit_width); + + assert_eq!(values, decoded_values); + } +} + +#[cfg(test)] +mod tests { + use super::{BLOCK_LEN, BitPacker4x}; + use crate::bitpacker_internal::BitPacker; + use bitpacking::{BitPacker as ExternalBitPacker, BitPacker4x as ExternalBitPacker4x}; + + fn mask_for_width(width: u8) -> u32 { + match width { + 0 => 0, + 32 => u32::MAX, + _ => (1u32 << width) - 1, + } + } + + fn raw_values(width: u8) -> Vec { + let mask = mask_for_width(width); + (0..BLOCK_LEN) + .map(|idx| ((idx * 17 + 3) as u32) & mask) + .collect() + } + + fn sorted_values(width: u8) -> (u32, Vec) { + if width == 0 { + return (11, vec![11; BLOCK_LEN]); + } + if width == 32 { + return (0, vec![u32::MAX; BLOCK_LEN]); + } + + let mask = mask_for_width(width).min(127); + let initial = 11u32; + let mut current = initial; + let values = (0..BLOCK_LEN) + .map(|idx| { + current += (idx as u32 * 7 + 1) & mask; + current + }) + .collect(); + (initial, values) + } + + fn strictly_sorted_values(width: u8) -> (Option, Vec) { + let mask = mask_for_width(width).min(127); + let mut current = 0u32; + let values = (0..BLOCK_LEN) + .map(|idx| { + if idx == 0 { + current = 0; + } else { + current += 1 + ((idx as u32 * 5) & mask); + } + current + }) + .collect(); + (None, values) + } + + #[test] + fn scalar_backend_matches_external_bitpacker4x() { + let scalar = BitPacker4x::new_scalar(); + let external = ExternalBitPacker4x::new(); + + for width in 0..=32 { + let values = raw_values(width); + assert_eq!(scalar.num_bits(&values), external.num_bits(&values)); + + let mut actual = vec![0u8; BitPacker4x::compressed_block_size(width)]; + let actual_len = scalar.compress(&values, &mut actual, width); + + let mut expected = vec![0u8; ExternalBitPacker4x::compressed_block_size(width)]; + let expected_len = external.compress(&values, &mut expected, width); + + assert_eq!(actual_len, expected_len); + assert_eq!(actual, expected, "raw width {width}"); + + let mut decoded = vec![0u32; BLOCK_LEN]; + assert_eq!(scalar.decompress(&actual, &mut decoded, width), actual_len); + assert_eq!(decoded, values); + + let (initial, values) = sorted_values(width); + assert_eq!( + scalar.num_bits_sorted(initial, &values), + external.num_bits_sorted(initial, &values) + ); + + let mut actual = vec![0u8; BitPacker4x::compressed_block_size(width)]; + let actual_len = scalar.compress_sorted(initial, &values, &mut actual, width); + + let mut expected = vec![0u8; ExternalBitPacker4x::compressed_block_size(width)]; + let expected_len = external.compress_sorted(initial, &values, &mut expected, width); + + assert_eq!(actual_len, expected_len); + assert_eq!(actual, expected, "sorted width {width}"); + + let mut decoded = vec![0u32; BLOCK_LEN]; + assert_eq!( + scalar.decompress_sorted(initial, &actual, &mut decoded, width), + actual_len + ); + assert_eq!(decoded, values); + } + } + + #[test] + fn scalar_backend_matches_external_strictly_sorted_bitpacker4x() { + let scalar = BitPacker4x::new_scalar(); + let external = ExternalBitPacker4x::new(); + + for width in 0..=16 { + let (initial, values) = strictly_sorted_values(width); + let num_bits = external.num_bits_strictly_sorted(initial, &values); + assert_eq!(scalar.num_bits_strictly_sorted(initial, &values), num_bits); + + let mut actual = vec![0u8; BitPacker4x::compressed_block_size(num_bits)]; + let actual_len = + scalar.compress_strictly_sorted(initial, &values, &mut actual, num_bits); + + let mut expected = vec![0u8; ExternalBitPacker4x::compressed_block_size(num_bits)]; + let expected_len = + external.compress_strictly_sorted(initial, &values, &mut expected, num_bits); + + assert_eq!(actual_len, expected_len); + assert_eq!(actual, expected, "strict width {width}"); + + let mut decoded = vec![0u32; BLOCK_LEN]; + assert_eq!( + scalar.decompress_strictly_sorted(initial, &actual, &mut decoded, num_bits), + actual_len + ); + assert_eq!(decoded, values); + } + } +} diff --git a/rust/compression/bitpacking/src/bitpacker_internal/bitpacker8x.rs b/rust/compression/bitpacking/src/bitpacker_internal/bitpacker8x.rs new file mode 100644 index 00000000000..188a1f4ce2a --- /dev/null +++ b/rust/compression/bitpacking/src/bitpacker_internal/bitpacker8x.rs @@ -0,0 +1,873 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +use super::BitPacker; + +use crate::bitpacker_internal::{Available, UnsafeBitPacker}; + +const BLOCK_LEN: usize = 32 * 8; + +#[cfg(target_arch = "x86_64")] +mod avx2 { + use super::BLOCK_LEN; + use crate::bitpacker_internal::Available; + + use std::arch::x86_64::__m256i as DataType; + use std::arch::x86_64::_mm256_and_si256 as op_and; + use std::arch::x86_64::_mm256_lddqu_si256 as load_unaligned; + use std::arch::x86_64::_mm256_or_si256 as op_or; + use std::arch::x86_64::_mm256_set1_epi32 as set1; + use std::arch::x86_64::_mm256_slli_epi32 as left_shift_32; + use std::arch::x86_64::_mm256_srli_epi32 as right_shift_32; + use std::arch::x86_64::_mm256_storeu_si256 as store_unaligned; + + use std::arch::x86_64::{ + _mm256_add_epi32, _mm256_extract_epi32, _mm256_permute2f128_si256, _mm256_shuffle_epi32, + _mm256_slli_si256, _mm256_srli_si256, _mm256_sub_epi32, + }; + + #[allow(non_snake_case)] + unsafe fn or_collapse_to_u32(accumulator: DataType) -> u32 { + let a__b__c__d__e__f__g__h_ = accumulator; + let ______a__b________e__f = _mm256_srli_si256(a__b__c__d__e__f__g__h_, 8); + let a__b__ca_db_e__f__ge_hf = op_or(a__b__c__d__e__f__g__h_, ______a__b________e__f); + let ___a__b__ca____e__f__ge = _mm256_srli_si256(a__b__ca_db_e__f__ge_hf, 4); + let _________cadb______gehf = op_or(a__b__ca_db_e__f__ge_hf, ___a__b__ca____e__f__ge); + let cadb = _mm256_extract_epi32(_________cadb______gehf, 0); + let gehf = _mm256_extract_epi32(_________cadb______gehf, 4); + (cadb | gehf) as u32 + } + + unsafe fn compute_delta(curr: DataType, prev: DataType) -> DataType { + let left_shift = _mm256_slli_si256(curr, 4); + let curr_shift = _mm256_srli_si256(curr, 12); + let curr_right_only = _mm256_permute2f128_si256(curr_shift, curr_shift, 8); + let prev_shift = _mm256_srli_si256(prev, 12); + let sub_left = _mm256_permute2f128_si256(prev_shift, prev_shift, 3 | (8 << 4)); + let diff = op_or(left_shift, op_or(curr_right_only, sub_left)); + _mm256_sub_epi32(curr, diff) + } + + #[allow(non_snake_case)] + unsafe fn integrate_delta(prev: DataType, delta: DataType) -> DataType { + let offset_repeat = _mm256_shuffle_epi32(prev, 0xff); + let offset = _mm256_permute2f128_si256(offset_repeat, offset_repeat, 3 | (8 << 4)); + let a__b__c__d__e__f__g__h__ = delta; + let ______a__b________e__f__ = _mm256_slli_si256(delta, 8); + let a__b__ca_db_e__f__ge_fh_ = + _mm256_add_epi32(a__b__c__d__e__f__g__h__, ______a__b________e__f__); + let ___a__b__ca____e__f__ge_ = _mm256_slli_si256(a__b__ca_db_e__f__ge_fh_, 4); + let halved_prefix_sum = + _mm256_add_epi32(___a__b__ca____e__f__ge_, a__b__ca_db_e__f__ge_fh_); + let offsetted_halved_prefix_sum = _mm256_add_epi32(halved_prefix_sum, offset); + let select_last_low = _mm256_shuffle_epi32(offsetted_halved_prefix_sum, 0xff); + let high_offset = _mm256_permute2f128_si256(select_last_low, select_last_low, 8); + _mm256_add_epi32(high_offset, offsetted_halved_prefix_sum) + } + + unsafe fn add(left: DataType, right: DataType) -> DataType { + _mm256_add_epi32(left, right) + } + + unsafe fn sub(left: DataType, right: DataType) -> DataType { + _mm256_sub_epi32(left, right) + } + + declare_bitpacker!(target_feature(enable = "avx2")); + + impl Available for UnsafeBitPackerImpl { + fn available() -> bool { + is_x86_feature_detected!("avx2") + } + } +} + +#[cfg(all(target_arch = "aarch64", target_endian = "little"))] +mod neon { + use super::BLOCK_LEN; + use crate::bitpacker_internal::Available; + use std::arch::aarch64::{ + uint32x4_t, vaddq_u32, vandq_u32, vdupq_n_u32, vextq_u32, vgetq_lane_u32, vld1q_u32, + vorrq_u32, vshlq_n_u32, vshrq_n_u32, vst1q_u32, vsubq_u32, + }; + + pub(crate) type DataType = [uint32x4_t; 2]; + + #[inline] + unsafe fn set1(el: i32) -> DataType { + let lanes = vdupq_n_u32(el as u32); + [lanes, lanes] + } + + #[inline] + unsafe fn right_shift_32(el: DataType) -> DataType { + const { + assert!(N >= 0); + assert!(N <= 32); + } + + match N { + 0 => el, + 1 => [vshrq_n_u32::<1>(el[0]), vshrq_n_u32::<1>(el[1])], + 2 => [vshrq_n_u32::<2>(el[0]), vshrq_n_u32::<2>(el[1])], + 3 => [vshrq_n_u32::<3>(el[0]), vshrq_n_u32::<3>(el[1])], + 4 => [vshrq_n_u32::<4>(el[0]), vshrq_n_u32::<4>(el[1])], + 5 => [vshrq_n_u32::<5>(el[0]), vshrq_n_u32::<5>(el[1])], + 6 => [vshrq_n_u32::<6>(el[0]), vshrq_n_u32::<6>(el[1])], + 7 => [vshrq_n_u32::<7>(el[0]), vshrq_n_u32::<7>(el[1])], + 8 => [vshrq_n_u32::<8>(el[0]), vshrq_n_u32::<8>(el[1])], + 9 => [vshrq_n_u32::<9>(el[0]), vshrq_n_u32::<9>(el[1])], + 10 => [vshrq_n_u32::<10>(el[0]), vshrq_n_u32::<10>(el[1])], + 11 => [vshrq_n_u32::<11>(el[0]), vshrq_n_u32::<11>(el[1])], + 12 => [vshrq_n_u32::<12>(el[0]), vshrq_n_u32::<12>(el[1])], + 13 => [vshrq_n_u32::<13>(el[0]), vshrq_n_u32::<13>(el[1])], + 14 => [vshrq_n_u32::<14>(el[0]), vshrq_n_u32::<14>(el[1])], + 15 => [vshrq_n_u32::<15>(el[0]), vshrq_n_u32::<15>(el[1])], + 16 => [vshrq_n_u32::<16>(el[0]), vshrq_n_u32::<16>(el[1])], + 17 => [vshrq_n_u32::<17>(el[0]), vshrq_n_u32::<17>(el[1])], + 18 => [vshrq_n_u32::<18>(el[0]), vshrq_n_u32::<18>(el[1])], + 19 => [vshrq_n_u32::<19>(el[0]), vshrq_n_u32::<19>(el[1])], + 20 => [vshrq_n_u32::<20>(el[0]), vshrq_n_u32::<20>(el[1])], + 21 => [vshrq_n_u32::<21>(el[0]), vshrq_n_u32::<21>(el[1])], + 22 => [vshrq_n_u32::<22>(el[0]), vshrq_n_u32::<22>(el[1])], + 23 => [vshrq_n_u32::<23>(el[0]), vshrq_n_u32::<23>(el[1])], + 24 => [vshrq_n_u32::<24>(el[0]), vshrq_n_u32::<24>(el[1])], + 25 => [vshrq_n_u32::<25>(el[0]), vshrq_n_u32::<25>(el[1])], + 26 => [vshrq_n_u32::<26>(el[0]), vshrq_n_u32::<26>(el[1])], + 27 => [vshrq_n_u32::<27>(el[0]), vshrq_n_u32::<27>(el[1])], + 28 => [vshrq_n_u32::<28>(el[0]), vshrq_n_u32::<28>(el[1])], + 29 => [vshrq_n_u32::<29>(el[0]), vshrq_n_u32::<29>(el[1])], + 30 => [vshrq_n_u32::<30>(el[0]), vshrq_n_u32::<30>(el[1])], + 31 => [vshrq_n_u32::<31>(el[0]), vshrq_n_u32::<31>(el[1])], + 32 => set1(0), + _ => core::hint::unreachable_unchecked(), + } + } + + #[inline] + unsafe fn left_shift_32(el: DataType) -> DataType { + const { + assert!(N >= 0); + assert!(N <= 32); + } + + match N { + 0 => el, + 1 => [vshlq_n_u32::<1>(el[0]), vshlq_n_u32::<1>(el[1])], + 2 => [vshlq_n_u32::<2>(el[0]), vshlq_n_u32::<2>(el[1])], + 3 => [vshlq_n_u32::<3>(el[0]), vshlq_n_u32::<3>(el[1])], + 4 => [vshlq_n_u32::<4>(el[0]), vshlq_n_u32::<4>(el[1])], + 5 => [vshlq_n_u32::<5>(el[0]), vshlq_n_u32::<5>(el[1])], + 6 => [vshlq_n_u32::<6>(el[0]), vshlq_n_u32::<6>(el[1])], + 7 => [vshlq_n_u32::<7>(el[0]), vshlq_n_u32::<7>(el[1])], + 8 => [vshlq_n_u32::<8>(el[0]), vshlq_n_u32::<8>(el[1])], + 9 => [vshlq_n_u32::<9>(el[0]), vshlq_n_u32::<9>(el[1])], + 10 => [vshlq_n_u32::<10>(el[0]), vshlq_n_u32::<10>(el[1])], + 11 => [vshlq_n_u32::<11>(el[0]), vshlq_n_u32::<11>(el[1])], + 12 => [vshlq_n_u32::<12>(el[0]), vshlq_n_u32::<12>(el[1])], + 13 => [vshlq_n_u32::<13>(el[0]), vshlq_n_u32::<13>(el[1])], + 14 => [vshlq_n_u32::<14>(el[0]), vshlq_n_u32::<14>(el[1])], + 15 => [vshlq_n_u32::<15>(el[0]), vshlq_n_u32::<15>(el[1])], + 16 => [vshlq_n_u32::<16>(el[0]), vshlq_n_u32::<16>(el[1])], + 17 => [vshlq_n_u32::<17>(el[0]), vshlq_n_u32::<17>(el[1])], + 18 => [vshlq_n_u32::<18>(el[0]), vshlq_n_u32::<18>(el[1])], + 19 => [vshlq_n_u32::<19>(el[0]), vshlq_n_u32::<19>(el[1])], + 20 => [vshlq_n_u32::<20>(el[0]), vshlq_n_u32::<20>(el[1])], + 21 => [vshlq_n_u32::<21>(el[0]), vshlq_n_u32::<21>(el[1])], + 22 => [vshlq_n_u32::<22>(el[0]), vshlq_n_u32::<22>(el[1])], + 23 => [vshlq_n_u32::<23>(el[0]), vshlq_n_u32::<23>(el[1])], + 24 => [vshlq_n_u32::<24>(el[0]), vshlq_n_u32::<24>(el[1])], + 25 => [vshlq_n_u32::<25>(el[0]), vshlq_n_u32::<25>(el[1])], + 26 => [vshlq_n_u32::<26>(el[0]), vshlq_n_u32::<26>(el[1])], + 27 => [vshlq_n_u32::<27>(el[0]), vshlq_n_u32::<27>(el[1])], + 28 => [vshlq_n_u32::<28>(el[0]), vshlq_n_u32::<28>(el[1])], + 29 => [vshlq_n_u32::<29>(el[0]), vshlq_n_u32::<29>(el[1])], + 30 => [vshlq_n_u32::<30>(el[0]), vshlq_n_u32::<30>(el[1])], + 31 => [vshlq_n_u32::<31>(el[0]), vshlq_n_u32::<31>(el[1])], + 32 => set1(0), + _ => core::hint::unreachable_unchecked(), + } + } + + #[inline] + unsafe fn op_or(left: DataType, right: DataType) -> DataType { + [vorrq_u32(left[0], right[0]), vorrq_u32(left[1], right[1])] + } + + #[inline] + unsafe fn op_and(left: DataType, right: DataType) -> DataType { + [vandq_u32(left[0], right[0]), vandq_u32(left[1], right[1])] + } + + #[inline] + unsafe fn load_unaligned(addr: *const DataType) -> DataType { + let ptr = addr.cast::(); + [vld1q_u32(ptr), vld1q_u32(ptr.add(4))] + } + + #[inline] + unsafe fn store_unaligned(addr: *mut DataType, data: DataType) { + let ptr = addr.cast::(); + vst1q_u32(ptr, data[0]); + vst1q_u32(ptr.add(4), data[1]); + } + + #[inline] + unsafe fn or_collapse_to_u32(accumulator: DataType) -> u32 { + vgetq_lane_u32(accumulator[0], 0) + | vgetq_lane_u32(accumulator[0], 1) + | vgetq_lane_u32(accumulator[0], 2) + | vgetq_lane_u32(accumulator[0], 3) + | vgetq_lane_u32(accumulator[1], 0) + | vgetq_lane_u32(accumulator[1], 1) + | vgetq_lane_u32(accumulator[1], 2) + | vgetq_lane_u32(accumulator[1], 3) + } + + #[inline] + unsafe fn compute_delta(curr: DataType, prev: DataType) -> DataType { + [ + vsubq_u32(curr[0], vextq_u32(prev[1], curr[0], 3)), + vsubq_u32(curr[1], vextq_u32(curr[0], curr[1], 3)), + ] + } + + #[allow(non_snake_case)] + #[inline] + unsafe fn integrate_half(base: u32, delta: uint32x4_t) -> uint32x4_t { + let base = vdupq_n_u32(base); + let zero = vdupq_n_u32(0); + let a__b__c__d_ = delta; + let ______a__b_ = vextq_u32(zero, a__b__c__d_, 2); + let a__b__ca_db = vaddq_u32(______a__b_, a__b__c__d_); + let ___a__b__ca = vextq_u32(zero, a__b__ca_db, 3); + let a_ab_abc_abcd = vaddq_u32(___a__b__ca, a__b__ca_db); + vaddq_u32(base, a_ab_abc_abcd) + } + + #[inline] + unsafe fn integrate_delta(prev: DataType, delta: DataType) -> DataType { + let low = integrate_half(vgetq_lane_u32(prev[1], 3), delta[0]); + let high = integrate_half(vgetq_lane_u32(low, 3), delta[1]); + [low, high] + } + + #[inline] + unsafe fn add(left: DataType, right: DataType) -> DataType { + [vaddq_u32(left[0], right[0]), vaddq_u32(left[1], right[1])] + } + + #[inline] + unsafe fn sub(left: DataType, right: DataType) -> DataType { + [vsubq_u32(left[0], right[0]), vsubq_u32(left[1], right[1])] + } + + declare_bitpacker!(target_feature(enable = "neon")); + + impl Available for UnsafeBitPackerImpl { + fn available() -> bool { + std::arch::is_aarch64_feature_detected!("neon") + } + } +} + +mod scalar { + use super::BLOCK_LEN; + use crate::bitpacker_internal::Available; + use std::ptr; + + pub(crate) type DataType = [u32; 8]; + + pub(crate) fn set1(el: i32) -> DataType { + [el as u32; 8] + } + + pub(crate) fn right_shift_32(el: DataType) -> DataType { + [ + el[0] >> N, + el[1] >> N, + el[2] >> N, + el[3] >> N, + el[4] >> N, + el[5] >> N, + el[6] >> N, + el[7] >> N, + ] + } + + pub(crate) fn left_shift_32(el: DataType) -> DataType { + [ + el[0] << N, + el[1] << N, + el[2] << N, + el[3] << N, + el[4] << N, + el[5] << N, + el[6] << N, + el[7] << N, + ] + } + + pub(crate) fn op_or(left: DataType, right: DataType) -> DataType { + [ + left[0] | right[0], + left[1] | right[1], + left[2] | right[2], + left[3] | right[3], + left[4] | right[4], + left[5] | right[5], + left[6] | right[6], + left[7] | right[7], + ] + } + + pub(crate) fn op_and(left: DataType, right: DataType) -> DataType { + [ + left[0] & right[0], + left[1] & right[1], + left[2] & right[2], + left[3] & right[3], + left[4] & right[4], + left[5] & right[5], + left[6] & right[6], + left[7] & right[7], + ] + } + + pub(crate) unsafe fn load_unaligned(addr: *const DataType) -> DataType { + ptr::read_unaligned(addr) + } + + pub(crate) unsafe fn store_unaligned(addr: *mut DataType, data: DataType) { + ptr::write_unaligned(addr, data); + } + + pub(crate) fn or_collapse_to_u32(accumulator: DataType) -> u32 { + ((accumulator[0] | accumulator[1]) | (accumulator[2] | accumulator[3])) + | ((accumulator[4] | accumulator[5]) | (accumulator[6] | accumulator[7])) + } + + fn compute_delta(curr: DataType, prev: DataType) -> DataType { + [ + curr[0].wrapping_sub(prev[7]), + curr[1].wrapping_sub(curr[0]), + curr[2].wrapping_sub(curr[1]), + curr[3].wrapping_sub(curr[2]), + curr[4].wrapping_sub(curr[3]), + curr[5].wrapping_sub(curr[4]), + curr[6].wrapping_sub(curr[5]), + curr[7].wrapping_sub(curr[6]), + ] + } + + fn integrate_delta(offset: DataType, delta: DataType) -> DataType { + let el0 = offset[7].wrapping_add(delta[0]); + let el1 = el0.wrapping_add(delta[1]); + let el2 = el1.wrapping_add(delta[2]); + let el3 = el2.wrapping_add(delta[3]); + let el4 = el3.wrapping_add(delta[4]); + let el5 = el4.wrapping_add(delta[5]); + let el6 = el5.wrapping_add(delta[6]); + let el7 = el6.wrapping_add(delta[7]); + [el0, el1, el2, el3, el4, el5, el6, el7] + } + + pub(crate) fn add(left: DataType, right: DataType) -> DataType { + [ + left[0].wrapping_add(right[0]), + left[1].wrapping_add(right[1]), + left[2].wrapping_add(right[2]), + left[3].wrapping_add(right[3]), + left[4].wrapping_add(right[4]), + left[5].wrapping_add(right[5]), + left[6].wrapping_add(right[6]), + left[7].wrapping_add(right[7]), + ] + } + + pub(crate) fn sub(left: DataType, right: DataType) -> DataType { + [ + left[0].wrapping_sub(right[0]), + left[1].wrapping_sub(right[1]), + left[2].wrapping_sub(right[2]), + left[3].wrapping_sub(right[3]), + left[4].wrapping_sub(right[4]), + left[5].wrapping_sub(right[5]), + left[6].wrapping_sub(right[6]), + left[7].wrapping_sub(right[7]), + ] + } + + // The `allow(unused)` is here to put an attribute that has no effect. + // + // For other bitpackers, we enable a specific CPU instruction set, but for + // the scalar bitpacker none is required. + declare_bitpacker!(allow(unused)); + + impl Available for UnsafeBitPackerImpl { + fn available() -> bool { + true + } + } +} + +#[derive(Clone, Copy)] +enum InstructionSet { + #[cfg(target_arch = "x86_64")] + AVX2, + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + NEON, + Scalar, +} + +/// Internal 8-wide bitpacker implementation. +/// +/// One block contains 256 integers. This stays private to avoid exposing a new +/// block-size choice through the public Lance bitpacking API. +#[derive(Clone, Copy)] +pub(crate) struct BitPacker8x(InstructionSet); + +impl BitPacker8x { + #[cfg(target_arch = "x86_64")] + pub(crate) fn new_avx2() -> Option { + avx2::UnsafeBitPackerImpl::available().then_some(BitPacker8x(InstructionSet::AVX2)) + } + + #[cfg(not(target_arch = "x86_64"))] + pub(crate) fn new_avx2() -> Option { + None + } + + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + pub(crate) fn new_neon() -> Option { + neon::UnsafeBitPackerImpl::available().then_some(BitPacker8x(InstructionSet::NEON)) + } + + #[cfg(not(all(target_arch = "aarch64", target_endian = "little")))] + pub(crate) fn new_neon() -> Option { + None + } + + pub(crate) fn new_scalar() -> Self { + BitPacker8x(InstructionSet::Scalar) + } +} + +impl BitPacker for BitPacker8x { + const BLOCK_LEN: usize = BLOCK_LEN; + + fn new() -> Self { + Self::new_avx2() + .or_else(Self::new_neon) + .unwrap_or_else(Self::new_scalar) + } + + fn compress(&self, decompressed: &[u32], compressed: &mut [u8], num_bits: u8) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => { + avx2::UnsafeBitPackerImpl::compress(decompressed, compressed, num_bits) + } + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => { + neon::UnsafeBitPackerImpl::compress(decompressed, compressed, num_bits) + } + InstructionSet::Scalar => { + scalar::UnsafeBitPackerImpl::compress(decompressed, compressed, num_bits) + } + } + } + } + + fn compress_sorted( + &self, + initial: u32, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => avx2::UnsafeBitPackerImpl::compress_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::compress_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::compress_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + } + } + } + + fn compress_strictly_sorted( + &self, + initial: Option, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => avx2::UnsafeBitPackerImpl::compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::compress_strictly_sorted( + initial, + decompressed, + compressed, + num_bits, + ), + } + } + } + + fn decompress(&self, compressed: &[u8], decompressed: &mut [u32], num_bits: u8) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => { + avx2::UnsafeBitPackerImpl::decompress(compressed, decompressed, num_bits) + } + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => { + neon::UnsafeBitPackerImpl::decompress(compressed, decompressed, num_bits) + } + InstructionSet::Scalar => { + scalar::UnsafeBitPackerImpl::decompress(compressed, decompressed, num_bits) + } + } + } + } + + fn decompress_sorted( + &self, + initial: u32, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => avx2::UnsafeBitPackerImpl::decompress_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::decompress_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::decompress_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + } + } + } + + fn decompress_strictly_sorted( + &self, + initial: Option, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => avx2::UnsafeBitPackerImpl::decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::decompress_strictly_sorted( + initial, + compressed, + decompressed, + num_bits, + ), + } + } + } + + fn num_bits(&self, decompressed: &[u32]) -> u8 { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => avx2::UnsafeBitPackerImpl::num_bits(decompressed), + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => neon::UnsafeBitPackerImpl::num_bits(decompressed), + InstructionSet::Scalar => scalar::UnsafeBitPackerImpl::num_bits(decompressed), + } + } + } + + fn num_bits_sorted(&self, initial: u32, decompressed: &[u32]) -> u8 { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => { + avx2::UnsafeBitPackerImpl::num_bits_sorted(initial, decompressed) + } + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => { + neon::UnsafeBitPackerImpl::num_bits_sorted(initial, decompressed) + } + InstructionSet::Scalar => { + scalar::UnsafeBitPackerImpl::num_bits_sorted(initial, decompressed) + } + } + } + } + + fn num_bits_strictly_sorted(&self, initial: Option, decompressed: &[u32]) -> u8 { + unsafe { + match self.0 { + #[cfg(target_arch = "x86_64")] + InstructionSet::AVX2 => { + avx2::UnsafeBitPackerImpl::num_bits_strictly_sorted(initial, decompressed) + } + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + InstructionSet::NEON => { + neon::UnsafeBitPackerImpl::num_bits_strictly_sorted(initial, decompressed) + } + InstructionSet::Scalar => { + scalar::UnsafeBitPackerImpl::num_bits_strictly_sorted(initial, decompressed) + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::BitPacker8x; + use crate::bitpacker_internal::BitPacker; + use bitpacking::{BitPacker as ExternalBitPacker, BitPacker8x as ExternalBitPacker8x}; + + fn mask_for_width(width: u8) -> u32 { + match width { + 0 => 0, + 32 => u32::MAX, + _ => (1u32 << width) - 1, + } + } + + fn raw_values(width: u8, seed: u64) -> Vec { + let mask = mask_for_width(width); + let mut state = seed; + (0..BitPacker8x::BLOCK_LEN) + .map(|idx| { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + match seed % 4 { + 0 => 0, + 1 => mask, + 2 => idx as u32 & mask, + _ => state as u32 & mask, + } + }) + .collect() + } + + fn sorted_values(width: u8, seed: u64) -> (u32, Vec) { + if width == 0 { + return (17, vec![17; BitPacker8x::BLOCK_LEN]); + } + if width == 32 { + return (0, vec![u32::MAX; BitPacker8x::BLOCK_LEN]); + } + + let mask = mask_for_width(width).min(127); + let mut state = seed; + let mut current = 17u32; + let values = (0..BitPacker8x::BLOCK_LEN) + .map(|_| { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + current += state as u32 & mask; + current + }) + .collect(); + (17, values) + } + + fn strictly_sorted_values(width: u8, seed: u64) -> (Option, Vec) { + let mask = mask_for_width(width).min(127); + let mut state = seed; + let mut current = 0u32; + let values = (0..BitPacker8x::BLOCK_LEN) + .map(|idx| { + if idx == 0 { + current = 0; + } else { + state ^= state << 13; + state ^= state >> 7; + state ^= state << 17; + current += 1 + (state as u32 & mask); + } + current + }) + .collect(); + (None, values) + } + + fn assert_raw_compatible(ours: BitPacker8x, external: ExternalBitPacker8x) { + for width in 0..=32 { + for seed in [0, 1, 2, 123456789] { + let values = raw_values(width, seed); + assert_eq!(ours.num_bits(&values), external.num_bits(&values)); + + let mut actual = vec![0u8; BitPacker8x::compressed_block_size(width)]; + let actual_len = ours.compress(&values, &mut actual, width); + + let mut expected = vec![0u8; ExternalBitPacker8x::compressed_block_size(width)]; + let expected_len = external.compress(&values, &mut expected, width); + + assert_eq!(actual_len, expected_len); + assert_eq!(actual, expected, "raw width {width} seed {seed}"); + + let mut decoded = vec![0u32; BitPacker8x::BLOCK_LEN]; + assert_eq!(ours.decompress(&actual, &mut decoded, width), actual_len); + assert_eq!(decoded, values); + } + } + } + + fn assert_sorted_compatible(ours: BitPacker8x, external: ExternalBitPacker8x) { + for width in 0..=32 { + for seed in [0, 1, 2, 123456789] { + let (initial, values) = sorted_values(width, seed); + assert_eq!( + ours.num_bits_sorted(initial, &values), + external.num_bits_sorted(initial, &values) + ); + + let mut actual = vec![0u8; BitPacker8x::compressed_block_size(width)]; + let actual_len = ours.compress_sorted(initial, &values, &mut actual, width); + + let mut expected = vec![0u8; ExternalBitPacker8x::compressed_block_size(width)]; + let expected_len = external.compress_sorted(initial, &values, &mut expected, width); + + assert_eq!(actual_len, expected_len); + assert_eq!(actual, expected, "sorted width {width} seed {seed}"); + + let mut decoded = vec![0u32; BitPacker8x::BLOCK_LEN]; + assert_eq!( + ours.decompress_sorted(initial, &actual, &mut decoded, width), + actual_len + ); + assert_eq!(decoded, values); + } + } + } + + fn assert_strictly_sorted_compatible(ours: BitPacker8x, external: ExternalBitPacker8x) { + for width in 0..=16 { + for seed in [0, 1, 2, 123456789] { + let (initial, values) = strictly_sorted_values(width, seed); + let num_bits = external.num_bits_strictly_sorted(initial, &values); + assert_eq!(ours.num_bits_strictly_sorted(initial, &values), num_bits); + + let mut actual = vec![0u8; BitPacker8x::compressed_block_size(num_bits)]; + let actual_len = + ours.compress_strictly_sorted(initial, &values, &mut actual, num_bits); + + let mut expected = vec![0u8; ExternalBitPacker8x::compressed_block_size(num_bits)]; + let expected_len = + external.compress_strictly_sorted(initial, &values, &mut expected, num_bits); + + assert_eq!(actual_len, expected_len); + assert_eq!(actual, expected, "strict width {width} seed {seed}"); + + let mut decoded = vec![0u32; BitPacker8x::BLOCK_LEN]; + assert_eq!( + ours.decompress_strictly_sorted(initial, &actual, &mut decoded, num_bits), + actual_len + ); + assert_eq!(decoded, values); + } + } + } + + #[test] + fn bitpacker8x_raw_compatible_with_external_bitpacking() { + assert_raw_compatible(BitPacker8x::new(), ExternalBitPacker8x::new()); + } + + #[test] + fn bitpacker8x_sorted_compatible_with_external_bitpacking() { + assert_sorted_compatible(BitPacker8x::new(), ExternalBitPacker8x::new()); + } + + #[test] + fn scalar_backend_matches_external_bitpacker8x() { + let scalar = BitPacker8x::new_scalar(); + let external = ExternalBitPacker8x::new(); + + assert_raw_compatible(scalar, external); + assert_sorted_compatible(scalar, external); + } + + #[test] + fn scalar_backend_matches_external_strictly_sorted_bitpacker8x() { + let scalar = BitPacker8x::new_scalar(); + let external = ExternalBitPacker8x::new(); + + assert_strictly_sorted_compatible(scalar, external); + } + + #[cfg(all(target_arch = "aarch64", target_endian = "little"))] + #[test] + fn neon_backend_matches_external_bitpacker8x() { + if let Some(neon) = BitPacker8x::new_neon() { + let external = ExternalBitPacker8x::new(); + + assert_raw_compatible(neon, external); + assert_sorted_compatible(neon, external); + assert_strictly_sorted_compatible(neon, external); + } + } +} diff --git a/rust/compression/bitpacking/src/bitpacker_internal/macros.rs b/rust/compression/bitpacking/src/bitpacker_internal/macros.rs new file mode 100644 index 00000000000..e79686fe9af --- /dev/null +++ b/rust/compression/bitpacking/src/bitpacker_internal/macros.rs @@ -0,0 +1,669 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +macro_rules! pack_unpack_with_bits { + + ($name:ident, $n:expr, $cpufeature:meta) => { + + + mod $name { + + use crunchy::unroll; + use super::BLOCK_LEN; + use super::{Sink, Transformer}; + use super::{DataType, + set1, + right_shift_32, + left_shift_32, + op_or, + op_and, + load_unaligned, + store_unaligned}; + + const NUM_BITS: usize = $n; + const NUM_BYTES_PER_BLOCK: usize = NUM_BITS * BLOCK_LEN / 8; + + #[$cpufeature] + pub(crate) unsafe fn pack(input_arr: &[u32], output_arr: &mut [u8], mut delta_computer: TDeltaComputer) -> usize { + assert_eq!(input_arr.len(), BLOCK_LEN, "Input block too small {}, (expected {})", input_arr.len(), BLOCK_LEN); + assert!(output_arr.len() >= NUM_BYTES_PER_BLOCK, "Output array too small (numbits {}). {} <= {}", NUM_BITS, output_arr.len(), NUM_BYTES_PER_BLOCK); + + let input_ptr = input_arr.as_ptr().cast::(); + let mut output_ptr = output_arr.as_mut_ptr().cast::(); + let mut out_register: DataType = delta_computer.transform(load_unaligned(input_ptr)); + + unroll! { + for iter in 0..30 { + const i: usize = 1 + iter; + + const bits_filled: usize = i * NUM_BITS; + const inner_cursor: usize = bits_filled % 32; + const remaining: usize = 32 - inner_cursor; + + let offset_ptr = input_ptr.add(i); + let in_register: DataType = delta_computer.transform(load_unaligned(offset_ptr)); + + out_register = + if inner_cursor > 0 { + op_or(out_register, left_shift_32::<{inner_cursor as i32}>(in_register)) + } else { + in_register + }; + + if remaining <= NUM_BITS { + store_unaligned(output_ptr, out_register); + output_ptr = output_ptr.add(1); + if 0 < remaining && remaining < NUM_BITS { + out_register = right_shift_32::<{remaining as i32}>(in_register); + } + } + } + } + let in_register: DataType = delta_computer.transform(load_unaligned(input_ptr.add(31))); + out_register = if 32 - NUM_BITS > 0 { + op_or(out_register, left_shift_32::<{32 - NUM_BITS as i32}>(in_register)) + } else { + op_or(out_register, in_register) + }; + store_unaligned(output_ptr, out_register); + + NUM_BYTES_PER_BLOCK + } + + #[$cpufeature] + pub(crate) unsafe fn unpack(compressed: &[u8], mut output: Output) -> usize { + + assert!(compressed.len() >= NUM_BYTES_PER_BLOCK, "Compressed array seems too small. ({} < {}) ", compressed.len(), NUM_BYTES_PER_BLOCK); + + let mut input_ptr = compressed.as_ptr().cast::(); + + let mask_scalar: u32 = ((1u64 << NUM_BITS) - 1u64) as u32; + let mask = set1(mask_scalar as i32); + + let mut in_register: DataType = load_unaligned(input_ptr); + + let out_register = op_and(in_register, mask); + output.process(out_register); + + unroll! { + for iter in 0..31 { + const i: usize = iter + 1; + + const inner_cursor: usize = (i * NUM_BITS) % 32; + const inner_capacity: usize = 32 - inner_cursor; + + let shifted_in_register = if inner_cursor != 0 { + right_shift_32::<{inner_cursor as i32}>(in_register) + } else { + in_register + }; + let mut out_register: DataType = op_and(shifted_in_register, mask); + + // We consumed our current quadruplets entirely. + // We therefore read another one. + if inner_capacity <= NUM_BITS && i != 31 { + input_ptr = input_ptr.add(1); + in_register = load_unaligned(input_ptr); + + // This quadruplets is actually cutting one of + // our `DataType`. We need to read the next one. + if inner_capacity < NUM_BITS { + let shifted = if inner_capacity != 0 { + left_shift_32::<{inner_capacity as i32}>(in_register) + } else { + in_register + }; + let masked = op_and(shifted, mask); + out_register = op_or(out_register, masked); + } + } + + output.process(out_register); + } + } + + + NUM_BYTES_PER_BLOCK + } + } + } +} + +macro_rules! pack_unpack_with_bits_32 { + ($cpufeature:meta) => { + mod pack_unpack_with_bits_32 { + use super::BLOCK_LEN; + use super::{DataType, load_unaligned, store_unaligned}; + use super::{Sink, Transformer}; + use crunchy::unroll; + + const NUM_BITS: usize = 32; + const NUM_BYTES_PER_BLOCK: usize = NUM_BITS * BLOCK_LEN / 8; + + #[$cpufeature] + pub(crate) unsafe fn pack( + input_arr: &[u32], + output_arr: &mut [u8], + mut delta_computer: TDeltaComputer, + ) -> usize { + assert_eq!( + input_arr.len(), + BLOCK_LEN, + "Input block too small {}, (expected {})", + input_arr.len(), + BLOCK_LEN + ); + assert!( + output_arr.len() >= NUM_BYTES_PER_BLOCK, + "Output array too small (numbits {}). {} <= {}", + NUM_BITS, + output_arr.len(), + NUM_BYTES_PER_BLOCK + ); + + let input_ptr: *const DataType = input_arr.as_ptr().cast::(); + let output_ptr = output_arr.as_mut_ptr().cast::(); + unroll! { + for i in 0..32 { + let input_offset_ptr = input_ptr.add(i); + let output_offset_ptr = output_ptr.add(i); + let input_register = load_unaligned(input_offset_ptr); + let output_register = delta_computer.transform(input_register); + store_unaligned(output_offset_ptr, output_register); + } + } + NUM_BYTES_PER_BLOCK + } + + #[$cpufeature] + pub(crate) unsafe fn unpack( + compressed: &[u8], + mut output: Output, + ) -> usize { + assert!( + compressed.len() >= NUM_BYTES_PER_BLOCK, + "Compressed array seems too small. ({} < {}) ", + compressed.len(), + NUM_BYTES_PER_BLOCK + ); + let input_ptr = compressed.as_ptr().cast::(); + for i in 0..32 { + let input_offset_ptr = input_ptr.add(i); + let in_register: DataType = load_unaligned(input_offset_ptr); + output.process(in_register); + } + NUM_BYTES_PER_BLOCK + } + } + }; +} + +macro_rules! declare_bitpacker { + ($cpufeature:meta) => { + use super::super::UnsafeBitPacker; + use super::super::most_significant_bit; + use crunchy::unroll; + + pack_unpack_with_bits!(pack_unpack_with_bits_1, 1, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_2, 2, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_3, 3, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_4, 4, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_5, 5, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_6, 6, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_7, 7, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_8, 8, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_9, 9, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_10, 10, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_11, 11, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_12, 12, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_13, 13, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_14, 14, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_15, 15, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_16, 16, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_17, 17, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_18, 18, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_19, 19, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_20, 20, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_21, 21, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_22, 22, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_23, 23, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_24, 24, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_25, 25, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_26, 26, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_27, 27, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_28, 28, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_29, 29, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_30, 30, $cpufeature); + pack_unpack_with_bits!(pack_unpack_with_bits_31, 31, $cpufeature); + pack_unpack_with_bits_32!($cpufeature); + + unsafe fn compress_generic( + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + delta_computer: DeltaComputer, + ) -> usize { + match num_bits { + 0 => 0, + 1 => pack_unpack_with_bits_1::pack(decompressed, compressed, delta_computer), + 2 => pack_unpack_with_bits_2::pack(decompressed, compressed, delta_computer), + 3 => pack_unpack_with_bits_3::pack(decompressed, compressed, delta_computer), + 4 => pack_unpack_with_bits_4::pack(decompressed, compressed, delta_computer), + 5 => pack_unpack_with_bits_5::pack(decompressed, compressed, delta_computer), + 6 => pack_unpack_with_bits_6::pack(decompressed, compressed, delta_computer), + 7 => pack_unpack_with_bits_7::pack(decompressed, compressed, delta_computer), + 8 => pack_unpack_with_bits_8::pack(decompressed, compressed, delta_computer), + 9 => pack_unpack_with_bits_9::pack(decompressed, compressed, delta_computer), + 10 => pack_unpack_with_bits_10::pack(decompressed, compressed, delta_computer), + 11 => pack_unpack_with_bits_11::pack(decompressed, compressed, delta_computer), + 12 => pack_unpack_with_bits_12::pack(decompressed, compressed, delta_computer), + 13 => pack_unpack_with_bits_13::pack(decompressed, compressed, delta_computer), + 14 => pack_unpack_with_bits_14::pack(decompressed, compressed, delta_computer), + 15 => pack_unpack_with_bits_15::pack(decompressed, compressed, delta_computer), + 16 => pack_unpack_with_bits_16::pack(decompressed, compressed, delta_computer), + 17 => pack_unpack_with_bits_17::pack(decompressed, compressed, delta_computer), + 18 => pack_unpack_with_bits_18::pack(decompressed, compressed, delta_computer), + 19 => pack_unpack_with_bits_19::pack(decompressed, compressed, delta_computer), + 20 => pack_unpack_with_bits_20::pack(decompressed, compressed, delta_computer), + 21 => pack_unpack_with_bits_21::pack(decompressed, compressed, delta_computer), + 22 => pack_unpack_with_bits_22::pack(decompressed, compressed, delta_computer), + 23 => pack_unpack_with_bits_23::pack(decompressed, compressed, delta_computer), + 24 => pack_unpack_with_bits_24::pack(decompressed, compressed, delta_computer), + 25 => pack_unpack_with_bits_25::pack(decompressed, compressed, delta_computer), + 26 => pack_unpack_with_bits_26::pack(decompressed, compressed, delta_computer), + 27 => pack_unpack_with_bits_27::pack(decompressed, compressed, delta_computer), + 28 => pack_unpack_with_bits_28::pack(decompressed, compressed, delta_computer), + 29 => pack_unpack_with_bits_29::pack(decompressed, compressed, delta_computer), + 30 => pack_unpack_with_bits_30::pack(decompressed, compressed, delta_computer), + 31 => pack_unpack_with_bits_31::pack(decompressed, compressed, delta_computer), + 32 => pack_unpack_with_bits_32::pack(decompressed, compressed, delta_computer), + _ => { + panic!("Num bits must be <= 32. Was {}.", num_bits); + } + } + } + + pub trait Transformer { + unsafe fn transform(&mut self, data: DataType) -> DataType; + } + + struct NoDelta; + + impl Transformer for NoDelta { + #[inline] + unsafe fn transform(&mut self, current: DataType) -> DataType { + current + } + } + + struct DeltaComputer { + pub previous: DataType, + } + + impl Transformer for DeltaComputer { + #[inline] + unsafe fn transform(&mut self, current: DataType) -> DataType { + let result = compute_delta(current, self.previous); + self.previous = current; + result + } + } + + struct StrictDeltaComputer { + pub previous: DataType, + } + + impl Transformer for StrictDeltaComputer { + #[inline] + unsafe fn transform(&mut self, current: DataType) -> DataType { + let result = compute_delta(current, self.previous); + self.previous = current; + sub(result, set1(1)) + } + } + + pub trait Sink { + unsafe fn process(&mut self, data_type: DataType); + } + + struct Store { + output_ptr: *mut DataType, + } + + impl Store { + fn new(output_ptr: *mut DataType) -> Store { + Store { output_ptr } + } + } + + struct DeltaIntegrate { + current: DataType, + output_ptr: *mut DataType, + } + + impl DeltaIntegrate { + unsafe fn new(initial: u32, output_ptr: *mut DataType) -> DeltaIntegrate { + DeltaIntegrate { + current: set1(initial as i32), + output_ptr, + } + } + } + + impl Sink for DeltaIntegrate { + #[inline] + unsafe fn process(&mut self, delta: DataType) { + self.current = integrate_delta(self.current, delta); + store_unaligned(self.output_ptr, self.current); + self.output_ptr = self.output_ptr.add(1); + } + } + + struct StrictDeltaIntegrate { + current: DataType, + output_ptr: *mut DataType, + } + + impl StrictDeltaIntegrate { + unsafe fn new(initial: u32, output_ptr: *mut DataType) -> StrictDeltaIntegrate { + StrictDeltaIntegrate { + current: set1(initial as i32), + output_ptr, + } + } + } + + impl Sink for StrictDeltaIntegrate { + #[inline] + unsafe fn process(&mut self, delta: DataType) { + self.current = integrate_delta(self.current, add(delta, set1(1))); + store_unaligned(self.output_ptr, self.current); + self.output_ptr = self.output_ptr.add(1); + } + } + + impl Sink for Store { + #[inline] + unsafe fn process(&mut self, out_register: DataType) { + store_unaligned(self.output_ptr, out_register); + self.output_ptr = self.output_ptr.add(1); + } + } + + #[inline] + unsafe fn decompress_to( + compressed: &[u8], + mut sink: Output, + num_bits: u8, + ) -> usize { + match num_bits { + 0 => { + let zero = set1(0i32); + for _ in 0..32 { + sink.process(zero); + } + 0 + } + 1 => pack_unpack_with_bits_1::unpack(compressed, sink), + 2 => pack_unpack_with_bits_2::unpack(compressed, sink), + 3 => pack_unpack_with_bits_3::unpack(compressed, sink), + 4 => pack_unpack_with_bits_4::unpack(compressed, sink), + 5 => pack_unpack_with_bits_5::unpack(compressed, sink), + 6 => pack_unpack_with_bits_6::unpack(compressed, sink), + 7 => pack_unpack_with_bits_7::unpack(compressed, sink), + 8 => pack_unpack_with_bits_8::unpack(compressed, sink), + 9 => pack_unpack_with_bits_9::unpack(compressed, sink), + 10 => pack_unpack_with_bits_10::unpack(compressed, sink), + 11 => pack_unpack_with_bits_11::unpack(compressed, sink), + 12 => pack_unpack_with_bits_12::unpack(compressed, sink), + 13 => pack_unpack_with_bits_13::unpack(compressed, sink), + 14 => pack_unpack_with_bits_14::unpack(compressed, sink), + 15 => pack_unpack_with_bits_15::unpack(compressed, sink), + 16 => pack_unpack_with_bits_16::unpack(compressed, sink), + 17 => pack_unpack_with_bits_17::unpack(compressed, sink), + 18 => pack_unpack_with_bits_18::unpack(compressed, sink), + 19 => pack_unpack_with_bits_19::unpack(compressed, sink), + 20 => pack_unpack_with_bits_20::unpack(compressed, sink), + 21 => pack_unpack_with_bits_21::unpack(compressed, sink), + 22 => pack_unpack_with_bits_22::unpack(compressed, sink), + 23 => pack_unpack_with_bits_23::unpack(compressed, sink), + 24 => pack_unpack_with_bits_24::unpack(compressed, sink), + 25 => pack_unpack_with_bits_25::unpack(compressed, sink), + 26 => pack_unpack_with_bits_26::unpack(compressed, sink), + 27 => pack_unpack_with_bits_27::unpack(compressed, sink), + 28 => pack_unpack_with_bits_28::unpack(compressed, sink), + 29 => pack_unpack_with_bits_29::unpack(compressed, sink), + 30 => pack_unpack_with_bits_30::unpack(compressed, sink), + 31 => pack_unpack_with_bits_31::unpack(compressed, sink), + 32 => pack_unpack_with_bits_32::unpack(compressed, sink), + _ => { + panic!("Num bits must be <= 32. Was {}.", num_bits); + } + } + } + + pub struct UnsafeBitPackerImpl; + + impl UnsafeBitPacker for UnsafeBitPackerImpl { + const BLOCK_LEN: usize = BLOCK_LEN; + + #[$cpufeature] + unsafe fn compress(decompressed: &[u32], compressed: &mut [u8], num_bits: u8) -> usize { + compress_generic(decompressed, compressed, num_bits, NoDelta) + } + + #[$cpufeature] + unsafe fn compress_sorted( + initial: u32, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize { + let delta_computer = DeltaComputer { + previous: set1(initial as i32), + }; + compress_generic(decompressed, compressed, num_bits, delta_computer) + } + + #[$cpufeature] + unsafe fn compress_strictly_sorted( + initial: Option, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize { + // to allow encoding [0, 1, 2, ..], we need to permit an initial value "lower" than + // zero. To get a clean api, that value is None, but in practice, as we work on + // wrapping integers, u32::MAX/-1 does the job just fine. + let initial = initial.unwrap_or(u32::MAX); + let delta_computer = StrictDeltaComputer { + previous: set1(initial as i32), + }; + compress_generic(decompressed, compressed, num_bits, delta_computer) + } + + #[$cpufeature] + unsafe fn decompress( + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + assert!( + decompressed.len() >= BLOCK_LEN, + "The output array is not large enough : ({} >= {})", + decompressed.len(), + BLOCK_LEN + ); + let output_ptr = decompressed.as_mut_ptr().cast::(); + let output = Store::new(output_ptr); + decompress_to(compressed, output, num_bits) + } + + #[$cpufeature] + unsafe fn decompress_sorted( + initial: u32, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + assert!( + decompressed.len() >= BLOCK_LEN, + "The output array is not large enough : ({} >= {})", + decompressed.len(), + BLOCK_LEN + ); + let output_ptr = decompressed.as_mut_ptr().cast::(); + let output = DeltaIntegrate::new(initial, output_ptr); + decompress_to(compressed, output, num_bits) + } + + #[$cpufeature] + unsafe fn decompress_strictly_sorted( + initial: Option, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize { + assert!( + decompressed.len() >= BLOCK_LEN, + "The output array is not large enough : ({} >= {})", + decompressed.len(), + BLOCK_LEN + ); + let initial = initial.unwrap_or(u32::MAX); + let output_ptr = decompressed.as_mut_ptr().cast::(); + let output = StrictDeltaIntegrate::new(initial, output_ptr); + decompress_to(compressed, output, num_bits) + } + + #[$cpufeature] + unsafe fn num_bits(decompressed: &[u32]) -> u8 { + assert_eq!( + decompressed.len(), + BLOCK_LEN, + "`decompressed`'s len is not `BLOCK_LEN={}`", + BLOCK_LEN + ); + let data: *const DataType = decompressed.as_ptr().cast::(); + let mut accumulator = load_unaligned(data); + unroll! { + for iter in 0..31 { + let i = iter + 1; + let newvec = load_unaligned(data.add(i)); + accumulator = op_or(accumulator, newvec); + } + } + most_significant_bit(or_collapse_to_u32(accumulator)) + } + + #[$cpufeature] + unsafe fn num_bits_sorted(initial: u32, decompressed: &[u32]) -> u8 { + assert_eq!( + decompressed.len(), + BLOCK_LEN, + "`decompressed`'s len is not `BLOCK_LEN={}`", + BLOCK_LEN + ); + let initial_vec = set1(initial as i32); + let data: *const DataType = decompressed.as_ptr().cast::(); + + let first = load_unaligned(data); + let mut accumulator = compute_delta(load_unaligned(data), initial_vec); + let mut previous = first; + + unroll! { + for iter in 0..30 { + let i = iter + 1; + let current = load_unaligned(data.add(i)); + let delta = compute_delta(current, previous); + accumulator = op_or(accumulator, delta); + previous = current; + } + } + let current = load_unaligned(data.add(31)); + let delta = compute_delta(current, previous); + accumulator = op_or(accumulator, delta); + most_significant_bit(or_collapse_to_u32(accumulator)) + } + + #[$cpufeature] + unsafe fn num_bits_strictly_sorted(initial: Option, decompressed: &[u32]) -> u8 { + assert_eq!( + decompressed.len(), + BLOCK_LEN, + "`decompressed`'s len is not `BLOCK_LEN={}`", + BLOCK_LEN + ); + let initial = initial.unwrap_or(u32::MAX); + let initial_vec = set1(initial as i32); + let one = set1(1); + let data: *const DataType = decompressed.as_ptr().cast::(); + + let first = load_unaligned(data); + let mut accumulator = sub(compute_delta(load_unaligned(data), initial_vec), one); + let mut previous = first; + + unroll! { + for iter in 0..30 { + let i = iter + 1; + let current = load_unaligned(data.add(i)); + let delta = sub(compute_delta(current, previous), one); + accumulator = op_or(accumulator, delta); + previous = current; + } + } + let current = load_unaligned(data.add(31)); + let delta = sub(compute_delta(current, previous), one); + accumulator = op_or(accumulator, delta); + most_significant_bit(or_collapse_to_u32(accumulator)) + } + } + + #[cfg(any())] + mod tests { + use super::super::UnsafeBitPacker; + use super::UnsafeBitPackerImpl; + use crate::Available; + use crate::tests::{DeltaKind, test_suite_compress_decompress}; + + #[test] + fn test_num_bits() { + if UnsafeBitPackerImpl::available() { + for num_bits in 0..32 { + for pos in 0..32 { + let mut vals = [0u32; UnsafeBitPackerImpl::BLOCK_LEN]; + if num_bits > 0 { + vals[pos] = 1 << (num_bits - 1); + } + assert_eq!( + unsafe { UnsafeBitPackerImpl::num_bits(&vals[..]) }, + num_bits + ); + } + } + } + } + + #[test] + fn test_bitpacker() { + if UnsafeBitPackerImpl::available() { + test_suite_compress_decompress::(DeltaKind::NoDelta); + } + } + + #[test] + fn test_bitpacker_delta() { + if UnsafeBitPackerImpl::available() { + test_suite_compress_decompress::(DeltaKind::Delta); + } + } + + #[test] + fn test_bitpacker_strict_delta() { + if UnsafeBitPackerImpl::available() { + test_suite_compress_decompress::(DeltaKind::StrictDelta); + } + } + } + }; +} diff --git a/rust/compression/bitpacking/src/bitpacker_internal/mod.rs b/rust/compression/bitpacking/src/bitpacker_internal/mod.rs new file mode 100644 index 00000000000..c287a29da0b --- /dev/null +++ b/rust/compression/bitpacking/src/bitpacker_internal/mod.rs @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +// Lance-owned u32 SIMD bitpacking kernels. +// +// This is adapted from the MIT-licensed `bitpacking` crate so Lance can keep the +// hot FTS posting-list bitpacking implementation inside lance-bitpacking while +// preserving byte compatibility with the existing 4x format. + +#![allow(dead_code)] +#![allow(unsafe_op_in_unsafe_fn)] +#![allow(clippy::redundant_pub_crate)] +#![allow(clippy::upper_case_acronyms)] +#![allow(clippy::use_self)] + +#[macro_use] +mod macros; + +mod bitpacker4x; +mod bitpacker8x; + +pub use bitpacker4x::BitPacker4x; + +pub(crate) trait Available { + fn available() -> bool; +} + +pub(crate) trait UnsafeBitPacker { + const BLOCK_LEN: usize; + + unsafe fn compress(decompressed: &[u32], compressed: &mut [u8], num_bits: u8) -> usize; + + unsafe fn compress_sorted( + initial: u32, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize; + + unsafe fn compress_strictly_sorted( + initial: Option, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize; + + unsafe fn decompress(compressed: &[u8], decompressed: &mut [u32], num_bits: u8) -> usize; + + unsafe fn decompress_sorted( + initial: u32, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize; + + unsafe fn decompress_strictly_sorted( + initial: Option, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize; + + unsafe fn num_bits(decompressed: &[u32]) -> u8; + + unsafe fn num_bits_sorted(initial: u32, decompressed: &[u32]) -> u8; + + unsafe fn num_bits_strictly_sorted(initial: Option, decompressed: &[u32]) -> u8; +} + +/// Block bitpacker for fixed-size `u32` blocks. +/// +/// Implementations own runtime SIMD dispatch and use caller-provided buffers. +/// Packed bytes are stable for a given implementation and bit width. +pub trait BitPacker: Sized + Clone + Copy { + /// Number of `u32` values in one physical block. + const BLOCK_LEN: usize; + + /// Select the best supported implementation for the current CPU. + /// + /// Lance uses SIMD backends when available and falls back to a scalar + /// backend otherwise, matching the existing allocation-free call shape used + /// by the upstream `bitpacking` crate. + fn new() -> Self; + + /// Compress one full block of raw values into `compressed`. + fn compress(&self, decompressed: &[u32], compressed: &mut [u8], num_bits: u8) -> usize; + + /// Delta-compress one full non-decreasing block into `compressed`. + fn compress_sorted( + &self, + initial: u32, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize; + + /// Delta-compress one full strictly increasing block into `compressed`. + fn compress_strictly_sorted( + &self, + initial: Option, + decompressed: &[u32], + compressed: &mut [u8], + num_bits: u8, + ) -> usize; + + /// Decompress one raw block into `decompressed`. + fn decompress(&self, compressed: &[u8], decompressed: &mut [u32], num_bits: u8) -> usize; + + /// Decompress one delta-compressed non-decreasing block. + fn decompress_sorted( + &self, + initial: u32, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize; + + /// Decompress one delta-compressed strictly increasing block. + fn decompress_strictly_sorted( + &self, + initial: Option, + compressed: &[u8], + decompressed: &mut [u32], + num_bits: u8, + ) -> usize; + + /// Return the minimum bit width needed to represent a full raw block. + fn num_bits(&self, decompressed: &[u32]) -> u8; + + /// Return the minimum bit width needed to represent deltas in a full block. + fn num_bits_sorted(&self, initial: u32, decompressed: &[u32]) -> u8; + + /// Return the minimum bit width needed to represent strict deltas in a full block. + fn num_bits_strictly_sorted(&self, initial: Option, decompressed: &[u32]) -> u8; + + /// Return the byte size of one compressed block at `num_bits`. + #[must_use] + fn compressed_block_size(num_bits: u8) -> usize { + Self::BLOCK_LEN * num_bits as usize / 8 + } +} + +#[inline] +fn most_significant_bit(value: u32) -> u8 { + (u32::BITS - value.leading_zeros()) as u8 +} diff --git a/rust/compression/bitpacking/src/lib.rs b/rust/compression/bitpacking/src/lib.rs index 0101c4a1df0..df2313bed5d 100644 --- a/rust/compression/bitpacking/src/lib.rs +++ b/rust/compression/bitpacking/src/lib.rs @@ -16,6 +16,10 @@ use arrayref::{array_mut_ref, array_ref}; use core::mem::size_of; +mod bitpacker_internal; + +pub use bitpacker_internal::{BitPacker, BitPacker4x}; + pub const FL_ORDER: [usize; 8] = [0, 4, 2, 6, 1, 5, 3, 7]; pub trait FastLanes: Sized + Copy { @@ -1709,6 +1713,7 @@ pack_64!(pack_64_64, 64); #[cfg(test)] mod test { use super::*; + use bitpacking::{BitPacker as ExternalBitPacker, BitPacker4x as ExternalBitPacker4x}; use core::array; // a fast random number generator pub struct XorShift { @@ -1730,6 +1735,104 @@ mod test { } } + fn mask_for_width(width: u8) -> u32 { + match width { + 0 => 0, + 32 => u32::MAX, + _ => (1u32 << width) - 1, + } + } + + fn raw_bitpacker4x_case(width: u8, seed: u64) -> Vec { + let mask = mask_for_width(width); + let mut rng = XorShift::new(seed); + (0..BitPacker4x::BLOCK_LEN) + .map(|idx| match seed % 4 { + 0 => 0, + 1 => mask, + 2 => idx as u32 & mask, + _ => (rng.next() as u32) & mask, + }) + .collect() + } + + fn sorted_bitpacker4x_case(width: u8, seed: u64) -> (u32, Vec) { + if width == 0 { + return (17, vec![17; BitPacker4x::BLOCK_LEN]); + } + if width == 32 { + return (0, vec![u32::MAX; BitPacker4x::BLOCK_LEN]); + } + + let mask = mask_for_width(width).min(127); + let mut rng = XorShift::new(seed); + let mut current = 17u32; + let values = (0..BitPacker4x::BLOCK_LEN) + .map(|_| { + current += (rng.next() as u32) & mask; + current + }) + .collect(); + (17, values) + } + + #[test] + fn test_bitpacker4x_raw_compatible_with_external_bitpacking() { + let ours = BitPacker4x::new(); + let external = ExternalBitPacker4x::new(); + + for width in 0..=32 { + for seed in [0, 1, 2, 123456789] { + let values = raw_bitpacker4x_case(width, seed); + assert_eq!(ours.num_bits(&values), external.num_bits(&values)); + + let mut actual = vec![0u8; BitPacker4x::compressed_block_size(width)]; + let actual_len = ours.compress(&values, &mut actual, width); + + let mut expected = vec![0u8; ExternalBitPacker4x::compressed_block_size(width)]; + let expected_len = external.compress(&values, &mut expected, width); + + assert_eq!(actual_len, expected_len); + assert_eq!(actual, expected, "width {width} seed {seed}"); + + let mut decoded = vec![0u32; BitPacker4x::BLOCK_LEN]; + let consumed = ours.decompress(&actual, &mut decoded, width); + assert_eq!(consumed, actual_len); + assert_eq!(decoded, values); + } + } + } + + #[test] + fn test_bitpacker4x_sorted_compatible_with_external_bitpacking() { + let ours = BitPacker4x::new(); + let external = ExternalBitPacker4x::new(); + + for width in 0..=32 { + for seed in [0, 1, 2, 123456789] { + let (initial, values) = sorted_bitpacker4x_case(width, seed); + assert_eq!( + ours.num_bits_sorted(initial, &values), + external.num_bits_sorted(initial, &values) + ); + + let mut actual = vec![0u8; BitPacker4x::compressed_block_size(width)]; + let actual_len = ours.compress_sorted(initial, &values, &mut actual, width); + + let mut expected = vec![0u8; ExternalBitPacker4x::compressed_block_size(width)]; + let expected_len = external.compress_sorted(initial, &values, &mut expected, width); + + assert_eq!(actual_len, expected_len); + assert_eq!(actual, expected, "width {width} seed {seed}"); + + let mut decoded = vec![0u32; BitPacker4x::BLOCK_LEN]; + let consumed = ours.decompress_sorted(initial, &actual, &mut decoded, width); + assert_eq!(consumed, actual_len); + assert_eq!(decoded, values); + } + } + } + // a macro version of this function generalize u8, u16, u32, u64 takes very long time for a test build, so I // write it for each type separately fn pack_unpack_u8(bit_width: usize) { diff --git a/rust/lance-index/Cargo.toml b/rust/lance-index/Cargo.toml index 85de43c0f9b..b9d1a5fde29 100644 --- a/rust/lance-index/Cargo.toml +++ b/rust/lance-index/Cargo.toml @@ -44,6 +44,7 @@ lance-encoding.workspace = true lance-file.workspace = true lance-geo = { workspace = true, optional = true } lance-io.workspace = true +lance-bitpacking.workspace = true libsais-rs = "0.2" lance-linalg.workspace = true lance-select.workspace = true @@ -70,7 +71,6 @@ bytes.workspace = true chrono.workspace = true uuid.workspace = true async-channel = "2.3.1" -bitpacking = { version = "0.9.2", features = ["bitpacker4x"] } rand_distr.workspace = true lance-datagen.workspace = true rangemap.workspace = true diff --git a/rust/lance-index/src/scalar/inverted/builder.rs b/rust/lance-index/src/scalar/inverted/builder.rs index a53b6ddd7dc..fe72540b422 100644 --- a/rust/lance-index/src/scalar/inverted/builder.rs +++ b/rust/lance-index/src/scalar/inverted/builder.rs @@ -15,13 +15,13 @@ use arrow::array::AsArray; use arrow::datatypes; use arrow_array::{Array, BinaryArray, RecordBatch, UInt64Array}; use arrow_schema::{DataType, Field, Schema, SchemaRef}; -use bitpacking::{BitPacker, BitPacker4x}; use bytes::Bytes; use datafusion::execution::{RecordBatchStream, SendableRecordBatchStream}; use fst::Streamer; use futures::{Stream, StreamExt, TryStreamExt}; use lance_arrow::json::JSON_EXT_NAME; use lance_arrow::{ARROW_EXT_NAME_KEY, iter_str_array}; +use lance_bitpacking::{BitPacker, BitPacker4x}; use lance_core::cache::LanceCache; use lance_core::deepsize::DeepSizeOf; use lance_core::error::LanceOptionExt; diff --git a/rust/lance-index/src/scalar/inverted/encoding.rs b/rust/lance-index/src/scalar/inverted/encoding.rs index fabc4ac9261..d75fa742ee7 100644 --- a/rust/lance-index/src/scalar/inverted/encoding.rs +++ b/rust/lance-index/src/scalar/inverted/encoding.rs @@ -6,7 +6,7 @@ use std::io::Write; use super::builder::BLOCK_SIZE; use super::index::{PositionStreamCodec, PostingTailCodec}; use arrow::array::LargeBinaryBuilder; -use bitpacking::{BitPacker, BitPacker4x}; +use lance_bitpacking::{BitPacker, BitPacker4x}; use lance_core::{Error, Result}; // we compress the posting list to multiple blocks of fixed number of elements (BLOCK_SIZE), diff --git a/rust/lance/Cargo.toml b/rust/lance/Cargo.toml index 100aa42ea20..e5fc4fab63f 100644 --- a/rust/lance/Cargo.toml +++ b/rust/lance/Cargo.toml @@ -32,6 +32,7 @@ lance-namespace = { workspace = true } lance-select = { workspace = true } lance-tokenizer = { workspace = true } lance-table = { workspace = true } +lance-bitpacking = { workspace = true } arrow-arith = { workspace = true } arrow-array = { workspace = true } arrow-buffer = { workspace = true } @@ -43,7 +44,6 @@ arrow-schema = { workspace = true } arrow-select = { workspace = true } async-recursion.workspace = true async-trait.workspace = true -bitpacking = { workspace = true } byteorder.workspace = true bytes.workspace = true chrono.workspace = true diff --git a/rust/lance/src/dataset/mem_wal/index/fts.rs b/rust/lance/src/dataset/mem_wal/index/fts.rs index 8c6c9d85de7..522c4c98231 100644 --- a/rust/lance/src/dataset/mem_wal/index/fts.rs +++ b/rust/lance/src/dataset/mem_wal/index/fts.rs @@ -54,9 +54,9 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use arc_swap::ArcSwap; use arrow_array::{Array, LargeStringArray, RecordBatch, StringArray, StringViewArray}; use arrow_schema::DataType; -use bitpacking::{BitPacker, BitPacker4x}; use crossbeam_skiplist::SkipMap; use fst::{Map, Streamer}; +use lance_bitpacking::{BitPacker, BitPacker4x}; use lance_core::{Error, Result}; use lance_index::scalar::InvertedIndexParams; use lance_index::scalar::inverted::query::Operator; From a1cae2d14a57674f61c776e50244ad8289ee7053 Mon Sep 17 00:00:00 2001 From: Dan Rammer Date: Fri, 26 Jun 2026 10:06:00 -0500 Subject: [PATCH 005/177] feat(mem_wal): non-blocking ShardWriter::delete_no_wait (#7483) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Adds `ShardWriter::delete_no_wait`, the delete analog of `put_no_wait`: it inserts the tombstone into the in-memory tier (visible to reads on this writer the instant it returns) and hands back the durability watcher *without* awaiting it. `delete` becomes a thin wrapper that awaits the watcher. ## Why Lets a caller hold an external lock across only the in-memory tombstone insert and await durability after releasing it — matching how `put_no_wait` is already used so flushes still coalesce. A downstream WAL serializes deletes against partial-update merges under a per-bucket lock; with the blocking `delete` it would hold that lock across durability, and `delete_no_wait` lets it release first. ## Tests - `test_shard_writer_delete_no_wait_visible_before_durability` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- rust/lance/src/dataset/mem_wal/write.rs | 148 +++++++++++++++++++++++- 1 file changed, 146 insertions(+), 2 deletions(-) diff --git a/rust/lance/src/dataset/mem_wal/write.rs b/rust/lance/src/dataset/mem_wal/write.rs index af96e2bfa98..6afcff0cc97 100644 --- a/rust/lance/src/dataset/mem_wal/write.rs +++ b/rust/lance/src/dataset/mem_wal/write.rs @@ -1661,6 +1661,26 @@ impl ShardWriter { /// ``` #[instrument(name = "sw_delete", level = "info", skip_all, fields(batch_count = keys.len(), shard_id = %self.config.shard_id))] pub async fn delete(&self, keys: Vec) -> Result { + let (result, watcher) = self.delete_no_wait(keys).await?; + // Wait for durability if configured (mirrors `put` → `put_memtable`). + if let Some(mut watcher) = watcher { + watcher.wait().await?; + } + Ok(result) + } + + /// Like [`Self::delete`], but returns the durability watcher *without* + /// awaiting it — the tombstone lands in the in-memory tier the instant this + /// returns, but the index-driven LSM read only folds it once the watcher + /// resolves the flush (which advances the visibility watermark and updates + /// the PK index). The delete analog of [`Self::put_no_wait`], so a caller + /// can hold an external lock across only the in-memory insert and await + /// durability after releasing it. MemTable mode only. + #[instrument(name = "sw_delete_no_wait", level = "info", skip_all, fields(batch_count = keys.len(), shard_id = %self.config.shard_id))] + pub async fn delete_no_wait( + &self, + keys: Vec, + ) -> Result<(WriteResult, Option)> { if keys.is_empty() { return Err(Error::invalid_input("Cannot delete with empty key list")); } @@ -1687,7 +1707,7 @@ impl ShardWriter { build_tombstone_batch(&k, &writer_state.schema, &writer_state.pk_columns) }) .collect::>>()?; - self.put_memtable(tombstones, state, writer_state, backpressure) + self.put_memtable_no_wait(tombstones, state, writer_state, backpressure) .await } WriterMode::WalOnly { .. } => Err(Error::invalid_input( @@ -1697,7 +1717,9 @@ impl ShardWriter { } /// Like [`Self::put`], but returns the durability watcher *without* awaiting - /// it. The row is visible to reads on this writer the instant this returns; + /// it. The row lands in the in-memory tier the instant this returns, but the + /// index-driven LSM read only surfaces it once the watcher resolves the + /// flush (which advances the visibility watermark and updates the indexes); /// the caller awaits durability via the watcher (`None` when `durable_write` /// is off). /// @@ -3160,6 +3182,128 @@ mod tests { writer.close().await.unwrap(); } + /// `delete_no_wait` lands the tombstone in the in-memory tier (visible at + /// the batch-store level the instant it returns) and hands back the + /// durability watcher *without* awaiting it. Index-driven LSM read + /// visibility — folding the tombstone out — follows once the watcher + /// resolves the flush that advances the visibility watermark and updates + /// the PK index. The delete analog of + /// `test_put_no_wait_durable_visible_then_durable`. + #[tokio::test] + async fn test_shard_writer_delete_no_wait_durable_visible_after_watcher() { + use crate::dataset::mem_wal::scanner::LsmScanner; + use futures::TryStreamExt; + + let (store, base_path, base_uri, _temp) = create_local_store().await; + let schema = create_pk_test_schema(); + let config = ShardWriterConfig { + shard_id: Uuid::new_v4(), + durable_write: true, + ..Default::default() + }; + let shard_id = config.shard_id; + let writer = ShardWriter::open( + store, + base_path, + base_uri.clone(), + config, + schema.clone(), + vec![], + ) + .await + .unwrap(); + + writer + .put(vec![create_test_batch(&schema, 0, 5)]) + .await + .unwrap(); + + // Tombstone id=2 without blocking on durability. + let (_result, watcher) = writer + .delete_no_wait(vec![id_only_keys(&[2])]) + .await + .unwrap(); + + // The tombstone is in the in-memory tier immediately (5 rows + 1 + // tombstone), even though the index-driven read can't fold it until the + // flush behind the watcher lands. `durable_write` is on, so a watcher is + // returned to await. + assert_eq!(writer.memtable_stats().await.unwrap().row_count, 6); + let mut watcher = watcher.expect("durable_write returns a watcher"); + + // Awaiting the watcher waits for the flush, which advances the + // visibility watermark and updates the PK index — only then does the LSM + // read fold the delete. + watcher.wait().await.unwrap(); + + let refs = writer.in_memory_memtable_refs().await.unwrap(); + let scanner = LsmScanner::without_base_table( + schema.clone(), + base_uri, + vec![], + vec!["id".to_string()], + ) + .with_in_memory_memtables(shard_id, refs); + let batches: Vec = scanner + .try_into_stream() + .await + .unwrap() + .try_collect() + .await + .unwrap(); + let mut ids: Vec = Vec::new(); + for b in &batches { + let arr = b + .column_by_name("id") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + ids.extend((0..arr.len()).map(|i| arr.value(i))); + } + ids.sort_unstable(); + assert_eq!( + ids, + vec![0, 1, 3, 4], + "delete folded by the LSM read once the watcher resolves" + ); + + writer.close().await.unwrap(); + } + + /// With `durable_write` off, `delete_no_wait` returns no watcher (nothing to + /// await), but the tombstone still lands in the in-memory tier. The delete + /// analog of `test_put_no_wait_non_durable_returns_no_watcher`. + #[tokio::test] + async fn test_shard_writer_delete_no_wait_non_durable_returns_no_watcher() { + let (store, base_path, base_uri, _temp) = create_local_store().await; + let schema = create_pk_test_schema(); + let config = ShardWriterConfig { + shard_id: Uuid::new_v4(), + durable_write: false, + ..Default::default() + }; + let writer = ShardWriter::open(store, base_path, base_uri, config, schema.clone(), vec![]) + .await + .unwrap(); + + writer + .put(vec![create_test_batch(&schema, 0, 5)]) + .await + .unwrap(); + + let (_result, watcher) = writer + .delete_no_wait(vec![id_only_keys(&[2])]) + .await + .unwrap(); + assert!(watcher.is_none(), "non-durable delete has nothing to await"); + + // Tombstone landed in the in-memory tier (5 rows + 1 tombstone). + assert_eq!(writer.memtable_stats().await.unwrap().row_count, 6); + + writer.close().await.unwrap(); + } + /// Read every surviving `id` through the full LSM read path (which folds /// `NOT _tombstone` per source) over the writer's *flushed* generations, /// with an optional filter. Mirrors how a query reads a WAL table after a From 59cf5c74e6664b2b6d2cbab325789f7b0b0af7b7 Mon Sep 17 00:00:00 2001 From: Wyatt Alt Date: Fri, 26 Jun 2026 10:20:07 -0700 Subject: [PATCH 006/177] fix: invalidate indices for fields rewritten by Merge (#7491) The Merge arm only dropped indices for fields removed from the schema, so a field rewritten in place (new backing data file, same field id) kept its index covering the fragment with stale entries. Index-served queries then silently returned wrong results -- a filter on the rewritten column matches the old values rather than the current data. Prune the rewritten fragment from any index over a field whose backing data file changed, reusing the same primitive the DataReplacement arm already uses. --- .../src/dataset/tests/dataset_merge_update.rs | 180 +++++++++++++++++- rust/lance/src/dataset/transaction.rs | 70 ++++++- 2 files changed, 245 insertions(+), 5 deletions(-) diff --git a/rust/lance/src/dataset/tests/dataset_merge_update.rs b/rust/lance/src/dataset/tests/dataset_merge_update.rs index 73804d7b4c0..bd45397eabc 100644 --- a/rust/lance/src/dataset/tests/dataset_merge_update.rs +++ b/rust/lance/src/dataset/tests/dataset_merge_update.rs @@ -36,7 +36,7 @@ use lance_datagen::{BatchCount, RowCount, array, gen_batch}; use lance_file::version::LanceFileVersion; use lance_file::writer::FileWriter; use lance_io::utils::CachedFileSize; -use lance_table::format::DataFile; +use lance_table::format::{DataFile, Fragment}; use crate::dataset::write::merge_insert::{WhenMatched, WhenNotMatched}; use futures::TryStreamExt; @@ -2240,6 +2240,184 @@ async fn test_data_replacement_invalidates_index_bitmap() { ); } +/// Run a predicate over `col` twice -- once index-served, once via a forced flat scan +/// (`use_scalar_index(false)`) -- assert the two agree, and return the matching `col` +/// values sorted. Equality is the index-consistency invariant: a divergence means the +/// index served rows that disagree with the underlying data. +async fn index_consistent_values(dataset: &Dataset, col: &str, predicate: &str) -> Vec { + let sorted = |batch: &RecordBatch| -> Vec { + let mut v: Vec = batch + .column_by_name(col) + .unwrap() + .as_primitive::() + .iter() + .flatten() + .collect(); + v.sort(); + v + }; + + let indexed = dataset + .scan() + .filter(predicate) + .unwrap() + .try_into_batch() + .await + .unwrap(); + let flat = dataset + .scan() + .use_scalar_index(false) + .filter(predicate) + .unwrap() + .try_into_batch() + .await + .unwrap(); + + let indexed_vals = sorted(&indexed); + let flat_vals = sorted(&flat); + assert_eq!( + indexed_vals, flat_vals, + "index-served `{predicate}` disagrees with a flat scan" + ); + indexed_vals +} + +/// Build a Merge overlay fragment that rewrites a single `field_id` in place: tombstone +/// (-2) the field in `prev`'s existing data files and back it with `new_file` (a new +/// single-column file) instead. A file left with no live field is dropped. This is the +/// manifest shape an in-place column rewrite produces when it falls back from a +/// DataReplacement to a Merge. +fn build_overlay_frag(prev: &Fragment, field_id: i32, new_file: &str) -> Fragment { + let mut overlay = prev.clone(); + overlay.files = prev + .files + .iter() + .filter_map(|df| { + let masked: Vec = df + .fields + .iter() + .map(|&f| if f == field_id { -2 } else { f }) + .collect(); + if masked.iter().all(|&f| f == -2) { + return None; // file holds only the tombstoned field + } + let mut m = df.clone(); + m.fields = masked.into(); + Some(m) + }) + .collect(); + overlay.add_file( + new_file, + vec![field_id], + vec![0], + &LanceFileVersion::default(), + None, + ); + overlay +} + +/// A `Merge` that rewrites an indexed column's data in place must keep that column's +/// index consistent: a query the index serves must return the same rows as a flat scan +/// of the rewritten data. The overlay fragment tombstones (-2) the column's field id in +/// the existing data file and appends a new file for it, so the field stays in the +/// schema and its index is retained -- the rewritten fragment must be pruned from that +/// index. This is the shape produced when an in-place column rewrite cannot be expressed +/// as a DataReplacement (e.g. an `update` has merged the fragment's column files) and +/// falls back to a Merge overlay. +#[tokio::test] +async fn test_merge_rewriting_indexed_column_keeps_index_consistent() { + let schema = Arc::new(ArrowSchema::new(vec![ + ArrowField::new("a", DataType::Int32, true), + ArrowField::new("b", DataType::Int32, true), + ])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(Int32Array::from(vec![1, 2, 3, 4])), + Arc::new(Int32Array::from(vec![10, 20, 30, 40])), + ], + ) + .unwrap(); + let reader = RecordBatchIterator::new(vec![Ok(batch)], schema.clone()); + let mut dataset = Dataset::write(reader, "memory://merge_index_rewrite", None) + .await + .unwrap(); + + dataset + .create_index( + &["a"], + IndexType::BTree, + None, + &ScalarIndexParams::default(), + false, + ) + .await + .unwrap(); + + // Baseline: the index serves correct results before the rewrite. + assert_eq!( + index_consistent_values(&dataset, "a", "a >= 3").await, + vec![3, 4] + ); + + let a_field_id = 0i32; + + // Write a new single-column file holding `a`'s replacement values. + let a_only = Arc::new(ArrowSchema::new(vec![ArrowField::new( + "a", + DataType::Int32, + true, + )])); + let new_a = RecordBatch::try_new( + a_only.clone(), + vec![Arc::new(Int32Array::from(vec![91, 92, 93, 94]))], + ) + .unwrap(); + let new_a_path = dataset.data_dir().join("merge_new_a.lance"); + let object_writer = dataset.object_store.create(&new_a_path).await.unwrap(); + let mut writer = FileWriter::try_new( + object_writer, + a_only.as_ref().try_into().unwrap(), + Default::default(), + ) + .unwrap(); + writer.write_batch(&new_a).await.unwrap(); + writer.finish().await.unwrap(); + + // Overlay that file onto fragment 0, rewriting `a` in place. + let prev = dataset.get_fragment(0).unwrap().metadata().clone(); + let overlay = build_overlay_frag(&prev, a_field_id, "merge_new_a.lance"); + + let read_version = dataset.version().version; + let dataset = Dataset::commit( + WriteDestination::Dataset(Arc::new(dataset)), + Operation::Merge { + fragments: vec![overlay], + schema: schema.as_ref().try_into().unwrap(), + }, + Some(read_version), + None, + None, + Arc::new(Default::default()), + false, + ) + .await + .unwrap(); + + // `a` now holds [91, 92, 93, 94]; an index-served query must reflect that. + assert_eq!( + index_consistent_values(&dataset, "a", "a >= 90").await, + vec![91, 92, 93, 94], + "index-served `a >= 90` must return the rewritten values" + ); + assert!( + index_consistent_values(&dataset, "a", "a < 90") + .await + .is_empty(), + "no row satisfies `a < 90` after the rewrite" + ); +} + /// DataReplacement on an indexed column should remove the fragment from /// fragment_bitmap AND add it to invalidated_fragment_bitmap so that /// stale index entries are blocked at query time. diff --git a/rust/lance/src/dataset/transaction.rs b/rust/lance/src/dataset/transaction.rs index 4555cd7ee6c..3c5438f26a5 100644 --- a/rust/lance/src/dataset/transaction.rs +++ b/rust/lance/src/dataset/transaction.rs @@ -2111,13 +2111,12 @@ impl Transaction { final_fragments.extend(maybe_existing_fragments?.clone()); } Operation::Merge { fragments, .. } => { + let existing_fragments = maybe_existing_fragments?; let mut merged_fragments = fragments.clone(); if next_row_id.is_some() { let new_version = current_manifest.map(|m| m.version + 1).unwrap_or(1); - let prev_by_id: HashMap = maybe_existing_fragments? - .iter() - .map(|f| (f.id, f)) - .collect(); + let prev_by_id: HashMap = + existing_fragments.iter().map(|f| (f.id, f)).collect(); for fragment in merged_fragments.iter_mut() { match prev_by_id.get(&fragment.id) { Some(prev) => { @@ -2144,6 +2143,15 @@ impl Transaction { } final_fragments.extend(merged_fragments); + // A Merge can rewrite a column's data file in place; the field stays + // in the schema, so the index is retained -- prune its now-stale + // entries for the rewritten fragments. + Self::prune_merge_rewritten_fields_from_indices( + &mut final_indices, + existing_fragments, + fragments, + ); + // Some fields that have indices may have been removed, so we should // remove those indices as well. Self::retain_relevant_indices(&mut final_indices, &schema, &final_fragments) @@ -2623,6 +2631,60 @@ impl Transaction { } } + /// Map each (non-tombstoned) field id in a fragment to the path of the data + /// file that backs it. + fn fragment_field_paths(frag: &Fragment) -> HashMap { + let mut map = HashMap::new(); + for file in &frag.files { + for &field_id in file.fields.iter() { + if field_id >= 0 { + map.insert(field_id, file.path.as_str()); + } + } + } + map + } + + /// A `Merge` can rewrite a column's data *in place* -- the field stays in the + /// schema but its backing data file changes (the overlay fragment carries a new + /// file for the field and tombstones its old field id). `retain_relevant_indices` + /// only drops indices for *removed* fields, so without this the index keeps + /// covering the rewritten fragments with stale entries. Remove each such fragment + /// from any index covering a field whose backing data file changed. + fn prune_merge_rewritten_fields_from_indices( + indices: &mut [IndexMetadata], + prev_fragments: &[Fragment], + new_fragments: &[Fragment], + ) { + let prev_by_id: HashMap = + prev_fragments.iter().map(|f| (f.id, f)).collect(); + for new_frag in new_fragments { + let Some(prev) = prev_by_id.get(&new_frag.id) else { + continue; // brand-new fragment: nothing stale to prune + }; + let prev_paths = Self::fragment_field_paths(prev); + let new_paths = Self::fragment_field_paths(new_frag); + // Fields still present whose backing file path changed == rewritten data. + let changed: Vec = prev_paths + .iter() + .filter(|(field_id, prev_path)| { + new_paths + .get(*field_id) + .is_some_and(|new_path| new_path != *prev_path) + }) + .map(|(field_id, _)| *field_id as u32) + .collect(); + if changed.is_empty() { + continue; + } + Self::prune_updated_fields_from_indices( + indices, + std::slice::from_ref(new_frag), + &changed, + ); + } + } + fn is_vector_index(index: &IndexMetadata) -> bool { if let Some(details) = &index.index_details { details.type_url.ends_with("VectorIndexDetails") From 898ad320435bfffbb547663ed45d4be4c722d690 Mon Sep 17 00:00:00 2001 From: Yuang Gao <91290127+YuangGao@users.noreply.github.com> Date: Mon, 29 Jun 2026 00:31:45 -0700 Subject: [PATCH 007/177] ci(python): typecheck arrow.py and test_arrow.py (#7265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Add `lance/arrow.py` and `tests/test_arrow.py` to the pyright include list and fix the resulting type errors. Closes #3292. - **arrow.py**: narrow union types via typed locals / `typing.cast`, annotate the internal decoder return types, switch the fallback to `pyarrow.compute as pc`, and add a single `# pyright: ignore[reportOptionalCall]` for `tf.stack` (typed as `None` by tensorflow's incomplete stubs). - **lance/lance/__init__.pyi**: fix the `bfloat16_array` stub signature from `List[str | None]` to `Sequence[float | None]` — the Rust function takes `Vec>`, and `Sequence` (covariant) lets `list[float]` callers type-check. - **test_arrow.py**: add `import lance.arrow` so the `lance.arrow.*` attribute accesses resolve. ## Test plan `tensorflow` is linux-only in `pyproject.toml`, so it was installed locally to mirror the CI (Linux) lint environment before checking. - `pyright` → **0 errors** (15 warnings, all pre-existing missing-stub warnings for pyarrow/pandas/etc.) - `ruff format --check` + `ruff check` → pass - `pytest python/tests/test_arrow.py` → **20 passed** --- python/pyproject.toml | 2 + python/python/lance/arrow.py | 56 +++++++++++++++++--------- python/python/lance/lance/__init__.pyi | 2 +- python/python/tests/test_arrow.py | 3 +- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/python/pyproject.toml b/python/pyproject.toml index d863fe38517..dd11344324c 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -114,6 +114,8 @@ include = [ "python/lance/schema.py", "python/lance/file.py", "python/lance/util.py", + "python/lance/arrow.py", + "python/tests/test_arrow.py", ] # Dependencies like pyarrow make this difficult to enforce strictly. reportMissingTypeStubs = "warning" diff --git a/python/python/lance/arrow.py b/python/python/lance/arrow.py index 54da15705f8..da022cdc8f6 100644 --- a/python/python/lance/arrow.py +++ b/python/python/lance/arrow.py @@ -4,10 +4,12 @@ """Extensions to PyArrows.""" import json +import typing from pathlib import Path from typing import Callable, Iterable, Optional, Union import pyarrow as pa +import pyarrow.compute as pc from ._arrow.bf16 import ( # noqa: F401 BFloat16, @@ -19,8 +21,10 @@ from .lance import bfloat16_array __all__ = [ + "BFloat16", "BFloat16Array", "BFloat16Type", + "PandasBFloat16Array", "bfloat16_array", "cast", "EncodedImageType", @@ -220,14 +224,15 @@ def from_uris( ['file::///tmp/1.png'] """ + storage: pa.Array if isinstance(uris, (pa.StringArray, pa.LargeStringArray)): - pass + storage = uris elif isinstance(uris, Iterable): - uris = pa.array((str(uri) for uri in uris), type=pa.string()) + storage = pa.array((str(uri) for uri in uris), type=pa.string()) else: raise TypeError("Cannot build a ImageURIArray from {}".format(type(uris))) - return cls.from_storage(ImageURIType(uris.type), uris) + return cls.from_storage(ImageURIType(storage.type), storage) def read_uris(self, storage_type=pa.binary()) -> "EncodedImageArray": """ @@ -268,7 +273,8 @@ def download(url): print("Failed to reach the server: ", e.reason) elif hasattr(e, "code"): print( - "The server could not fulfill the request. Error code: ", e.code + "The server could not fulfill the request. Error code: ", + getattr(e, "code"), ) images = [] @@ -277,7 +283,9 @@ def download(url): if parsed_uri.scheme in ("http", "https"): images.append(download(uri)) else: - filesystem, path = fs.FileSystem.from_uri(uri.as_py()) + filesystem, path = fs.FileSystem.from_uri( # pyright: ignore[reportPrivateImportUsage] + uri.as_py() + ) with filesystem.open_input_stream(path) as f: images.append(f.read()) @@ -297,7 +305,7 @@ def __repr__(self): def pillow_metadata_decoder(images): import io - from PIL import Image + from PIL import Image # pyright: ignore[reportMissingImports] img = Image.open(io.BytesIO(images[0].as_py())) return img @@ -365,22 +373,27 @@ def to_tensor( if not decoder: - def pillow_decoder(images): + def pillow_decoder(images) -> "np.ndarray": import io - from PIL import Image + from PIL import Image # pyright: ignore[reportMissingImports] return np.stack( - [Image.open(io.BytesIO(img)) for img in images.to_pylist()] + [ + np.asarray(Image.open(io.BytesIO(img))) + for img in images.to_pylist() + ] ) - def tensorflow_decoder(images): + def tensorflow_decoder(images) -> "np.ndarray": import tensorflow as tf decoded_to_tensor = tuple( tf.io.decode_image(img) for img in images.to_pylist() ) - return tf.stack(decoded_to_tensor, axis=0).numpy() + return tf.stack( # pyright: ignore[reportOptionalCall] + decoded_to_tensor, axis=0 + ).numpy() decoders = [ ("tensorflow", tensorflow_decoder), @@ -401,9 +414,10 @@ def tensorflow_decoder(images): image_array = decoder(self.storage) if isinstance(image_array, pa.FixedShapeTensorType): - shape = image_array.shape - arrow_type = image_array.storage_type - tensor_array = image_array + tensor = typing.cast("pa.Array", image_array) + shape = tensor.shape + arrow_type = tensor.storage_type + tensor_array = tensor else: shape = image_array.shape[1:] arrow_type = pa.from_numpy_dtype(image_array.dtype) @@ -476,7 +490,7 @@ def to_encoded(self, encoder=None, storage_type=pa.binary()) -> "EncodedImageArr def pillow_encoder(x): import io - from PIL import Image + from PIL import Image # pyright: ignore[reportMissingImports] encoded_images = [] for y in x: @@ -571,7 +585,8 @@ def cast( + f"got: {target_type}" ) np_arr = arr.to_numpy() - float_arr = np_arr.astype(target_type.to_pandas_dtype()) + float_type = typing.cast("pa.DataType", target_type) + float_arr = np_arr.astype(float_type.to_pandas_dtype()) return pa.array(float_arr) elif isinstance(target_type, BFloat16Type) or target_type in ["bfloat16", "bf16"]: if not pa.types.is_floating(arr.type): @@ -586,15 +601,16 @@ def cast( target_type ): # Casting fixed size list to fixed size list - if arr.type.list_size != target_type.list_size: + list_type = typing.cast("pa.DataType", target_type) + if arr.type.list_size != list_type.list_size: raise ValueError( "Only support casting fixed size list to fixed size list " f"with the same size, got: {arr.type} to {target_type}" ) - values = cast(arr.values, target_type.value_type) + values = cast(arr.values, list_type.value_type) return pa.FixedSizeListArray.from_arrays( - values=values, list_size=target_type.list_size + values=values, list_size=list_type.list_size ) # Fallback to normal cast. - return pa.compute.cast(arr, target_type, *args, **kwargs) + return pc.cast(arr, target_type, *args, **kwargs) diff --git a/python/python/lance/lance/__init__.pyi b/python/python/lance/lance/__init__.pyi index 33b375ce1cd..7934817d042 100644 --- a/python/python/lance/lance/__init__.pyi +++ b/python/python/lance/lance/__init__.pyi @@ -757,7 +757,7 @@ class BFloat16: def __gt__(self, other: BFloat16) -> bool: ... def __ge__(self, other: BFloat16) -> bool: ... -def bfloat16_array(values: List[str | None]) -> BFloat16Array: ... +def bfloat16_array(values: Sequence[float | None]) -> BFloat16Array: ... class PyFullTextQuery: @staticmethod diff --git a/python/python/tests/test_arrow.py b/python/python/tests/test_arrow.py index b6e6024e0e7..92ab52021ff 100644 --- a/python/python/tests/test_arrow.py +++ b/python/python/tests/test_arrow.py @@ -7,10 +7,11 @@ from pathlib import Path import lance +import lance.arrow import numpy as np import pandas as pd import pyarrow as pa -import pytest +import pytest # pyright: ignore[reportMissingImports] from lance.arrow import ( BFloat16, BFloat16Array, From c9b53251c111581a601d6e27b29ebf788a3ebfda Mon Sep 17 00:00:00 2001 From: Xuanwo Date: Mon, 29 Jun 2026 17:25:03 +0800 Subject: [PATCH 008/177] fix: account for SQ offset in dot distance (#7481) This fixes low recall for IVF_SQ and IVF_HNSW_SQ indexes using Dot distance when the scalar quantization bounds include negative values. The previous SQ Dot path computed dot distance directly in quantized u8 code space. That drops the affine offset from scalar quantization, so zero-centered embeddings can be ranked incorrectly. The updated path computes Dot distance against the dequantized SQ value model while keeping the stored-code HNSW path allocation-free and able to use the u8 dot kernel. The change also adds regression coverage for a crafted negative-bound SQ Dot case and end-to-end IVF_SQ / IVF_HNSW_SQ Dot recall on negative-valued vectors. --- rust/lance-index/src/vector/sq/storage.rs | 307 ++++++++++++++++++++-- rust/lance/src/index/vector/ivf/v2.rs | 27 ++ 2 files changed, 313 insertions(+), 21 deletions(-) diff --git a/rust/lance-index/src/vector/sq/storage.rs b/rust/lance-index/src/vector/sq/storage.rs index 1e5eebda0d9..785aadc289d 100644 --- a/rust/lance-index/src/vector/sq/storage.rs +++ b/rust/lance-index/src/vector/sq/storage.rs @@ -12,12 +12,14 @@ use arrow_array::{ }; use arrow_schema::{DataType, SchemaRef}; use async_trait::async_trait; +use lance_arrow::ArrowFloatType; use lance_core::deepsize::DeepSizeOf; use lance_core::{Error, ROW_ID, Result}; use lance_file::previous::reader::FileReader as PreviousFileReader; use lance_io::object_store::ObjectStore; -use lance_linalg::distance::{DistanceType, dot_distance, l2_u8::l2_u8}; +use lance_linalg::distance::{DistanceType, dot_u8::dot_u8, l2_u8::l2_u8}; use lance_table::format::SelfDescribingFileReader; +use num_traits::AsPrimitive; use object_store::path::Path; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -29,7 +31,7 @@ use crate::{ vector::{ SQ_CODE_COLUMN, quantizer::{QuantizerMetadata, QuantizerStorage}, - storage::{DistCalculator, VectorStore}, + storage::{DistCalculator, DistanceCalculatorOptions, QueryResidual, VectorStore}, transform::Transformer, }, }; @@ -370,30 +372,118 @@ impl VectorStore for ScalarQuantizationStorage { SQDistCalculator::new(query, self, self.quantizer.bounds()) } + fn dist_calculator_with_scratch<'a>( + &'a self, + query: ArrayRef, + _dist_q_c: f32, + _residual: Option>, + f32_scratch: &'a mut Vec, + _options: DistanceCalculatorOptions, + ) -> Self::DistanceCalculator<'a> { + SQDistCalculator::new_with_scratch(query, self, self.quantizer.bounds(), f32_scratch) + } + fn dist_calculator_from_id(&self, id: u32) -> Self::DistanceCalculator<'_> { let (offset, chunk) = self.chunk(id); let query_sq_code = chunk.sq_code_slice(id - offset); let bounds = self.quantizer.bounds(); + let lower_bound = bounds.start as f32; + let value_scale = sq_value_scale(&bounds); + let query_dot = match self.distance_type { + DistanceType::Dot => Some(SQDotQuery::from_sq_code(query_sq_code)), + _ => None, + }; SQDistCalculator { query_sq_code: SQQueryCode::Borrowed(query_sq_code), + query_dot, scale: sq_distance_scale(&bounds), + lower_bound, + value_scale, storage: self, } } } +#[inline] +fn sq_value_scale(bounds: &Range) -> f32 { + (bounds.end - bounds.start) as f32 / 255.0_f32 +} + #[inline] fn sq_distance_scale(bounds: &Range) -> f32 { - let range = (bounds.end - bounds.start) as f32; - (range * range) / (255.0_f32 * 255.0_f32) + let scale = sq_value_scale(bounds); + scale * scale } pub struct SQDistCalculator<'a> { query_sq_code: SQQueryCode<'a>, + query_dot: Option>, scale: f32, + lower_bound: f32, + value_scale: f32, storage: &'a ScalarQuantizationStorage, } +enum SQDotQuery<'a> { + Values { values: SQFloatQuery<'a>, sum: f32 }, + SqCode { code: &'a [u8], sum: f32 }, +} + +enum SQFloatQuery<'a> { + Borrowed(&'a [f32]), + Owned(Vec), +} + +impl SQFloatQuery<'_> { + fn as_slice(&self) -> &[f32] { + match self { + Self::Borrowed(values) => values, + Self::Owned(values) => values, + } + } +} + +impl<'a> SQDotQuery<'a> { + fn from_values(values: &[T::Native]) -> Self + where + T::Native: AsPrimitive, + { + let values: Vec<_> = values.iter().map(|v| v.as_()).collect(); + let sum = values.iter().sum(); + Self::Values { + values: SQFloatQuery::Owned(values), + sum, + } + } + + fn from_values_with_scratch( + values: &[T::Native], + scratch: &'a mut Vec, + ) -> Self + where + T::Native: AsPrimitive, + { + scratch.clear(); + scratch.extend(values.iter().map(|v| v.as_())); + let sum = scratch.iter().sum(); + Self::Values { + values: SQFloatQuery::Borrowed(scratch.as_slice()), + sum, + } + } + + fn from_sq_code(sq_code: &'a [u8]) -> Self { + Self::SqCode { + code: sq_code, + sum: sq_code_sum(sq_code), + } + } +} + +fn sq_code_sum(sq_code: &[u8]) -> f32 { + sq_code.iter().map(|code| *code as u32).sum::() as f32 +} + enum SQQueryCode<'a> { Borrowed(&'a [u8]), Owned(Vec), @@ -415,26 +505,127 @@ impl<'a> SQDistCalculator<'a> { // since we search 10s-100s of partitions, we can afford the overhead // this could be annoying at indexing time for HNSW, which requires constructing the // dist calculator frequently. However, HNSW isn't first-class citizen in Lance yet. so be it. - let query_sq_code = match query.data_type() { - DataType::Float16 => { - scale_to_u8::(query.as_primitive::().values(), &bounds) - } - DataType::Float32 => { - scale_to_u8::(query.as_primitive::().values(), &bounds) + let (query_sq_code, query_dot) = match storage.distance_type { + DistanceType::Dot => { + let query_dot = match query.data_type() { + DataType::Float16 => SQDotQuery::from_values::( + query.as_primitive::().values(), + ), + DataType::Float32 => SQDotQuery::from_values::( + query.as_primitive::().values(), + ), + DataType::Float64 => SQDotQuery::from_values::( + query.as_primitive::().values(), + ), + _ => { + panic!("Unsupported data type for ScalarQuantizationStorage"); + } + }; + (SQQueryCode::Owned(Vec::new()), Some(query_dot)) } - DataType::Float64 => { - scale_to_u8::(query.as_primitive::().values(), &bounds) + DistanceType::L2 | DistanceType::Cosine => { + let query_sq_code = match query.data_type() { + DataType::Float16 => scale_to_u8::( + query.as_primitive::().values(), + &bounds, + ), + DataType::Float32 => scale_to_u8::( + query.as_primitive::().values(), + &bounds, + ), + DataType::Float64 => scale_to_u8::( + query.as_primitive::().values(), + &bounds, + ), + _ => { + panic!("Unsupported data type for ScalarQuantizationStorage"); + } + }; + (SQQueryCode::Owned(query_sq_code), None) } + _ => panic!("We should not reach here: sq distance can only be L2 or Dot"), + }; + let lower_bound = bounds.start as f32; + let value_scale = sq_value_scale(&bounds); + Self { + query_sq_code, + query_dot, + scale: sq_distance_scale(&bounds), + lower_bound, + value_scale, + storage, + } + } + + fn new_with_scratch( + query: ArrayRef, + storage: &'a ScalarQuantizationStorage, + bounds: Range, + f32_scratch: &'a mut Vec, + ) -> Self { + if storage.distance_type != DistanceType::Dot { + return Self::new(query, storage, bounds); + } + + let query_dot = match query.data_type() { + DataType::Float16 => SQDotQuery::from_values_with_scratch::( + query.as_primitive::().values(), + f32_scratch, + ), + DataType::Float32 => SQDotQuery::from_values_with_scratch::( + query.as_primitive::().values(), + f32_scratch, + ), + DataType::Float64 => SQDotQuery::from_values_with_scratch::( + query.as_primitive::().values(), + f32_scratch, + ), _ => { panic!("Unsupported data type for ScalarQuantizationStorage"); } }; + let lower_bound = bounds.start as f32; + let value_scale = sq_value_scale(&bounds); Self { - query_sq_code: SQQueryCode::Owned(query_sq_code), + query_sq_code: SQQueryCode::Owned(Vec::new()), + query_dot: Some(query_dot), scale: sq_distance_scale(&bounds), + lower_bound, + value_scale, storage, } } + + fn dot_distance(&self, sq_code: &[u8]) -> f32 { + let query = self + .query_dot + .as_ref() + .expect("SQ dot distance requires a dot query"); + let dot = match query { + SQDotQuery::Values { values, sum } => { + let values = values.as_slice(); + self.lower_bound * *sum + + self.value_scale + * sq_code + .iter() + .zip(values.iter()) + .map(|(code, query_value)| *code as f32 * *query_value) + .sum::() + } + SQDotQuery::SqCode { + code: query_sq_code, + sum: query_code_sum, + } => { + let dim = sq_code.len() as f32; + let code_dot = dot_u8(sq_code, query_sq_code) as f32; + let code_sum = sq_code_sum(sq_code); + dim * self.lower_bound * self.lower_bound + + self.lower_bound * self.value_scale * (code_sum + *query_code_sum) + + self.scale * code_dot + } + }; + 1.0 - dot + } } impl DistCalculator for SQDistCalculator<'_> { @@ -442,12 +633,13 @@ impl DistCalculator for SQDistCalculator<'_> { let (offset, chunk) = self.storage.chunk(id); let sq_code = chunk.sq_code_slice(id - offset); let query_sq_code = self.query_sq_code.as_slice(); - let dist = match self.storage.distance_type { - DistanceType::L2 | DistanceType::Cosine => l2_u8(sq_code, query_sq_code) as f32, - DistanceType::Dot => dot_distance(sq_code, query_sq_code), + match self.storage.distance_type { + DistanceType::L2 | DistanceType::Cosine => { + l2_u8(sq_code, query_sq_code) as f32 * self.scale + } + DistanceType::Dot => self.dot_distance(sq_code), _ => panic!("We should not reach here: sq distance can only be L2 or Dot"), - }; - dist * self.scale + } } fn distance_all(&self, _k_hint: usize) -> Vec { @@ -473,9 +665,8 @@ impl DistCalculator for SQDistCalculator<'_> { c.sq_codes .values() .chunks_exact(c.dim()) - .map(|sq_codes| dot_distance(sq_codes, query_sq_code)) + .map(|sq_codes| self.dot_distance(sq_codes)) }) - .map(|dist| dist * self.scale) .collect(), _ => panic!("We should not reach here: sq distance can only be L2 or Dot"), } @@ -511,7 +702,7 @@ mod tests { use std::iter::repeat_with; use std::sync::Arc; - use arrow_array::FixedSizeListArray; + use arrow_array::{FixedSizeListArray, Float32Array}; use arrow_schema::{DataType, Field, Schema}; use lance_arrow::FixedSizeListArrayExt; use lance_testing::datagen::generate_random_array; @@ -541,6 +732,31 @@ mod tests { RecordBatch::try_new(schema, vec![Arc::new(row_ids), Arc::new(code_arr)]).unwrap() } + fn create_record_batch_with_sq_codes( + row_ids: Vec, + sq_codes: Vec, + dim: usize, + ) -> RecordBatch { + assert_eq!(sq_codes.len(), row_ids.len() * dim); + + let row_ids = UInt64Array::from_iter_values(row_ids); + let sq_code = UInt8Array::from_iter_values(sq_codes); + let code_arr = FixedSizeListArray::try_new_from_values(sq_code, dim as i32).unwrap(); + + let schema = Arc::new(Schema::new(vec![ + Field::new(ROW_ID, DataType::UInt64, false), + Field::new( + SQ_CODE_COLUMN, + DataType::FixedSizeList( + Arc::new(Field::new("item", DataType::UInt8, true)), + dim as i32, + ), + false, + ), + ])); + RecordBatch::try_new(schema, vec![Arc::new(row_ids), Arc::new(code_arr)]).unwrap() + } + #[test] fn test_get_chunks() { const DIM: usize = 64; @@ -592,4 +808,53 @@ mod tests { assert_eq!(offset, 400); assert_eq!(chunk.row_id(5), 105); } + + #[test] + fn test_dot_distance_accounts_for_sq_offset() { + const DIM: usize = 2; + + let storage = ScalarQuantizationStorage::try_new( + 8, + DistanceType::Dot, + -1.0..1.0, + [create_record_batch_with_sq_codes( + vec![0, 1], + vec![ + 255, 255, // [1.0, 1.0] + 0, 191, // [-1.0, 0.498] + ], + DIM, + )], + None, + ) + .unwrap(); + + let query = Arc::new(Float32Array::from(vec![-1.0, 1.0])) as ArrayRef; + let calculator = storage.dist_calculator(query.clone(), 0.0); + let distances = calculator.distance_all(2); + + assert!( + distances[1] < distances[0], + "expected [-1.0, 0.498] to rank before [1.0, 1.0], got {distances:?}" + ); + assert!((calculator.distance(0) - 1.0).abs() < 1e-6); + assert!((calculator.distance(1) - -0.49803925).abs() < 1e-6); + + let mut scratch = Vec::new(); + let scratch_distances = { + let scratch_calculator = storage.dist_calculator_with_scratch( + query, + 0.0, + None, + &mut scratch, + DistanceCalculatorOptions::default(), + ); + scratch_calculator.distance_all(2) + }; + assert_eq!(scratch_distances, distances); + assert_eq!(scratch.len(), DIM); + + let stored_query_calculator = storage.dist_calculator_from_id(0); + assert!((stored_query_calculator.distance(1) - 1.5019608).abs() < 1e-6); + } } diff --git a/rust/lance/src/index/vector/ivf/v2.rs b/rust/lance/src/index/vector/ivf/v2.rs index 40227d2d020..1b7b2141fd0 100644 --- a/rust/lance/src/index/vector/ivf/v2.rs +++ b/rust/lance/src/index/vector/ivf/v2.rs @@ -4399,6 +4399,17 @@ mod tests { test_remap(params, nlist, recall_requirement).await; } + #[tokio::test] + async fn test_build_ivf_sq_dot_with_negative_values() { + let nlist = 4; + let ivf_params = IvfBuildParams::new(nlist); + let sq_params = SQBuildParams::default(); + let params = + VectorIndexParams::with_ivf_sq_params(DistanceType::Dot, ivf_params, sq_params); + + test_index_impl::(params, nlist, 0.75, -1.0..1.0, None).await; + } + // RQ doesn't perform well for random data // need to verify recall with real-world dataset (e.g. sift1m) #[rstest] @@ -4571,6 +4582,22 @@ mod tests { test_remap(params, nlist, recall_requirement).await; } + #[tokio::test] + async fn test_create_ivf_hnsw_sq_dot_with_negative_values() { + let nlist = 4; + let ivf_params = IvfBuildParams::new(nlist); + let sq_params = SQBuildParams::default(); + let hnsw_params = HnswBuildParams::default(); + let params = VectorIndexParams::with_ivf_hnsw_sq_params( + DistanceType::Dot, + ivf_params, + hnsw_params, + sq_params, + ); + + test_index_impl::(params, nlist, 0.75, -1.0..1.0, None).await; + } + #[rstest] #[case(4, DistanceType::L2, 0.9)] #[case(4, DistanceType::Cosine, 0.9)] From 6a2a7d473f698018dec04817f2c5afabeebc2f24 Mon Sep 17 00:00:00 2001 From: Dan Rammer Date: Mon, 29 Jun 2026 10:52:51 -0500 Subject: [PATCH 009/177] fix(merge-insert): apply Delete/Fail on the indexed-scan path (#7484) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem An indexed merge-insert delete by a composite primary key whose columns are **all** indexed silently removes nothing. `merge_insert(when_matched = Delete, use_index = true)` reports `deleted = 0`, the matched rows stay live in the table, and resurface in later reads. ## Root cause `WhenMatched::Delete` is only implemented in the v2 plan (`DeleteOnlyMergeInsertExec`), which never uses a scalar index. When every join column is indexed, `can_use_create_plan` routes the merge to the legacy `Merger` instead (the only engine with the indexed-scan probe). That engine's matched-row handler only ever distinguished `DoNothing` from "update" — it folded **both** `Delete` and `Fail` into the update path: - a fully-indexed `Delete` rewrote the matched rows in place → 0 deletes; - a fully-indexed `Fail` silently updated instead of erroring. A single indexed column hits the same path, so this was never composite-specific — composite keys just make it the common case (partial-column updates require an index on every PK column). ## Fix Make the legacy `Merger` dispatch every `WhenMatched` variant explicitly, mirroring the v2 classifier (`merge_insert_action`): - **`Delete`** collects matched row ids as deletions and emits no replacement batch. - **`Fail`** aborts on any match, with the same message as the v2 path. - A **delete-only commit branch** drains the merger and applies the deletions (resolving row ids → addresses via the row-id index for stable row ids) without writing fragments — this keeps the O(keys) indexed delete instead of falling back to a full table scan. - The partial-schema (column-rewrite) commit branch physically cannot express row deletions, so combining `Delete` with inserts from a partial-schema source now returns a descriptive error rather than silently dropping the deletes. ## Tests `cargo test -p lance --lib dataset::write::merge_insert` (152 passing), covering: - composite-key indexed delete (index on first / second / both / neither — the both-indexed case is the original bug); - single-column indexed delete; - multi-fragment + stable-row-id variants, including an appended fragment neither index covers (unindexed-remainder union); - `Delete` combined with `InsertAll` (not delete-only); - `Fail` on an indexed key (match aborts, no-match inserts cleanly). `cargo fmt` and `cargo clippy -p lance --lib --tests -- -D warnings` are clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- rust/lance/src/dataset/write/merge_insert.rs | 872 ++++++++++++++++-- .../dataset/write/merge_insert/exec/delete.rs | 39 +- .../dataset/write/merge_insert/exec/write.rs | 23 + 3 files changed, 861 insertions(+), 73 deletions(-) diff --git a/rust/lance/src/dataset/write/merge_insert.rs b/rust/lance/src/dataset/write/merge_insert.rs index 3889bdc66d5..c43d66fbd1a 100644 --- a/rust/lance/src/dataset/write/merge_insert.rs +++ b/rust/lance/src/dataset/write/merge_insert.rs @@ -1691,7 +1691,17 @@ impl MergeInsertJob { } } + // A partial-schema source that both deletes matched rows and inserts + // unmatched rows cannot be expressed by the indexed-scan delete path + // (the delete cannot be folded into a partial write). Keep it off the + // scalar-index route so it falls through to the v2 plan, which handles + // delete + insert directly. + let is_partial_delete_with_insert = is_subset_schema + && self.params.insert_not_matched + && matches!(self.params.when_matched, WhenMatched::Delete); + let would_use_scalar_index = if self.params.use_index + && !is_partial_delete_with_insert && matches!( self.params.delete_not_matched_by_source, WhenNotMatchedBySource::Keep @@ -1785,7 +1795,66 @@ impl MergeInsertJob { .try_flatten(); let stream = RecordBatchStreamAdapter::new(merger_schema, stream); - let (operation, affected_rows) = if !is_full_schema { + // A partial-schema source can patch columns or delete matched rows, but + // not both in one commit: the writer rejects subschema rows up front and + // the delete cannot be folded into the write. Reject that combination. + if !is_full_schema + && matches!(self.params.when_matched, WhenMatched::Delete) + && self.params.insert_not_matched + { + return Err(Error::not_supported_source("Combining when_matched(Delete) with inserts from a partial-schema source is not supported; provide the full target schema in the source".into())); + } + + // The commit strategy follows what the merge does to matched rows. A + // pure delete (no inserts) writes nothing: the merger emits no batches + // and only records the matched row ids. This holds for any source schema + // width, so it is keyed on the operation rather than `is_full_schema`. + let is_delete_only = matches!(self.params.when_matched, WhenMatched::Delete) + && !self.params.insert_not_matched; + + let (operation, affected_rows) = if is_delete_only { + // Consume the stream so the merger records the matched row ids in + // `deleted_rows`; it produces no batches. + let drained: Vec = Box::pin(stream).try_collect().await?; + debug_assert!(drained.is_empty(), "delete-only merge must not emit rows"); + + let removed_row_ids = Arc::into_inner(deleted_rows).unwrap().into_inner().unwrap(); + let removed_row_addr_vec = + if let Some(row_id_index) = get_row_id_index(&self.dataset).await? { + removed_row_ids + .iter() + .filter_map(|id| row_id_index.get(*id).map(|address| address.into())) + .collect::>() + } else { + removed_row_ids + }; + let removed_row_addrs = RoaringTreemap::from_iter(removed_row_addr_vec.into_iter()); + + let (updated_fragments, removed_fragment_ids) = + Self::apply_deletions(&self.dataset, &removed_row_addrs).await?; + + let operation = Operation::Update { + removed_fragment_ids, + updated_fragments, + new_fragments: vec![], + fields_modified: vec![], + merged_generations: self.params.merged_generations.clone(), + fields_for_preserving_frag_bitmap: full_schema + .fields + .iter() + .map(|f| f.id as u32) + .collect(), + update_mode: Some(RewriteRows), + inserted_rows_filter: None, // not implemented for v1 + updated_fragment_offsets: None, + }; + + let affected_rows = Some(RowAddrTreeMap::from(removed_row_addrs)); + (operation, affected_rows) + } else if !is_full_schema { + // Non-delete partial-schema merge: patch the provided columns into + // existing fragments in place. (Delete is handled above; a wider + // full source takes the row-rewrite branch below.) if !matches!( self.params.delete_not_matched_by_source, WhenNotMatchedBySource::Keep @@ -2341,40 +2410,27 @@ impl Merger { // borrow checker (the stream needs to be `sync` since it crosses an await point) let mut deleted_row_ids = self.deleted_rows.lock().unwrap(); - if self.params.when_matched != WhenMatched::DoNothing { - let mut matched = arrow::compute::filter_record_batch(&batch, &in_both)?; - - if let Some(match_filter) = self.match_filter_expr { - let unzipped = unzip_batch(&matched, &self.schema); - let filtered = match_filter.evaluate(&unzipped)?; - match filtered { - ColumnarValue::Array(mask) => { - // Some rows matched, filter down and replace those rows - matched = arrow::compute::filter_record_batch(&matched, mask.as_boolean())?; - } - ColumnarValue::Scalar(scalar) => { - if let ScalarValue::Boolean(Some(true)) = scalar { - // All rows matched, go ahead and replace the whole batch - } else { - // Nothing matched, replace nothing - matched = RecordBatch::new_empty(matched.schema()); - } - } - } - } - - merge_statistics.num_updated_rows += matched.num_rows() as u64; - - // If the filter eliminated all rows then its important we don't try and write - // the batch at all. Writing an empty batch currently panics - if matched.num_rows() > 0 { + // Each `WhenMatched` variant handles the matched rows (`in_both`) + // differently. + let match_filter_expr = self.match_filter_expr; + match &self.params.when_matched { + WhenMatched::DoNothing => {} + WhenMatched::Delete => { + // Matched rows are removed, not rewritten: record their row ids + // for the commit to delete and emit no replacement batch. A + // source with duplicate keys matches the same target row more + // than once; apply the same `source_dedupe_behavior` policy as + // updates so a duplicate either aborts (`Fail`) or is skipped + // and counted once (`FirstSeen`) — the commit deletes the row a + // single time regardless. + let matched = arrow::compute::filter_record_batch(&batch, &in_both)?; let row_ids = matched.column(row_id_col).as_primitive::(); let mut processed_row_ids = self.processed_row_ids.lock().unwrap(); - let mut keep_indices: Vec = Vec::with_capacity(matched.num_rows()); for (row_idx, &row_id) in row_ids.values().iter().enumerate() { if processed_row_ids.insert(row_id) { - keep_indices.push(row_idx as u32); + merge_statistics.num_deleted_rows += 1; + deleted_row_ids.push(row_id); } else { match self.params.source_dedupe_behavior { SourceDedupeBehavior::Fail => { @@ -2385,54 +2441,115 @@ impl Merger { )); } SourceDedupeBehavior::FirstSeen => { - // Skip this duplicate row (don't add to keep_indices) + merge_statistics.num_skipped_duplicates += 1; } } } } - drop(processed_row_ids); - - // Filter out duplicate rows if any were skipped - let num_skipped = matched.num_rows() - keep_indices.len(); - if num_skipped > 0 { - merge_statistics.num_skipped_duplicates += num_skipped as u64; - merge_statistics.num_updated_rows -= num_skipped as u64; - - let indices = UInt32Array::from(keep_indices); - matched = take_record_batch(&matched, &indices)?; + } + WhenMatched::Fail => { + // Any matched row aborts the whole operation. + if let Some(row_idx) = (0..in_both.len()).find(|&i| in_both.value(i)) { + return Err(DataFusionError::Execution(format!( + "Merge insert failed: found matching row with key values: {}", + format_key_values_on_columns(&batch, row_idx, &self.params.on) + ))); + } + } + WhenMatched::UpdateAll | WhenMatched::UpdateIf(_) | WhenMatched::UpdateIfExpr(_) => { + let mut matched = arrow::compute::filter_record_batch(&batch, &in_both)?; + + if let Some(match_filter) = match_filter_expr { + let unzipped = unzip_batch(&matched, &self.schema); + let filtered = match_filter.evaluate(&unzipped)?; + match filtered { + ColumnarValue::Array(mask) => { + // Some rows matched, filter down and replace those rows + matched = + arrow::compute::filter_record_batch(&matched, mask.as_boolean())?; + } + ColumnarValue::Scalar(scalar) => { + if let ScalarValue::Boolean(Some(true)) = scalar { + // All rows matched, go ahead and replace the whole batch + } else { + // Nothing matched, replace nothing + matched = RecordBatch::new_empty(matched.schema()); + } + } + } } - // Only process and write if there are remaining rows after filtering duplicates + merge_statistics.num_updated_rows += matched.num_rows() as u64; + + // If the filter eliminated all rows then its important we don't try and write + // the batch at all. Writing an empty batch currently panics if matched.num_rows() > 0 { - // Get row_ids again after filtering (if any duplicates were removed) let row_ids = matched.column(row_id_col).as_primitive::(); - deleted_row_ids.extend(row_ids.values()); - if self.enable_stable_row_ids { - self.updating_row_ids - .lock() - .unwrap() - .capture(row_ids.values())?; + + let mut processed_row_ids = self.processed_row_ids.lock().unwrap(); + let mut keep_indices: Vec = Vec::with_capacity(matched.num_rows()); + for (row_idx, &row_id) in row_ids.values().iter().enumerate() { + if processed_row_ids.insert(row_id) { + keep_indices.push(row_idx as u32); + } else { + match self.params.source_dedupe_behavior { + SourceDedupeBehavior::Fail => { + return Err(create_duplicate_row_error( + &matched, + row_idx, + &self.params.on, + )); + } + SourceDedupeBehavior::FirstSeen => { + // Skip this duplicate row (don't add to keep_indices) + } + } + } } + drop(processed_row_ids); - let projection = if let Some(row_addr_col) = row_addr_col { - let mut cols = Vec::from_iter(left_cols.iter().cloned()); - cols.push(row_addr_col); - cols - } else { - #[allow(clippy::redundant_clone)] - left_cols.clone() - }; - let matched = matched.project(&projection)?; - // The payload columns of an outer join are always nullable. We need to restore - // non-nullable to columns that were originally non-nullable. This should be safe - // since the not_matched rows should all be valid on the right_cols - // - // Sadly we can't use with_schema because it doesn't let you toggle nullability - let matched = RecordBatch::try_new( - self.output_schema.clone(), - Vec::from_iter(matched.columns().iter().cloned()), - )?; - batches.push(Ok(matched)); + // Filter out duplicate rows if any were skipped + let num_skipped = matched.num_rows() - keep_indices.len(); + if num_skipped > 0 { + merge_statistics.num_skipped_duplicates += num_skipped as u64; + merge_statistics.num_updated_rows -= num_skipped as u64; + + let indices = UInt32Array::from(keep_indices); + matched = take_record_batch(&matched, &indices)?; + } + + // Only process and write if there are remaining rows after filtering duplicates + if matched.num_rows() > 0 { + // Get row_ids again after filtering (if any duplicates were removed) + let row_ids = matched.column(row_id_col).as_primitive::(); + deleted_row_ids.extend(row_ids.values()); + if self.enable_stable_row_ids { + self.updating_row_ids + .lock() + .unwrap() + .capture(row_ids.values())?; + } + + let projection = if let Some(row_addr_col) = row_addr_col { + let mut cols = Vec::from_iter(left_cols.iter().cloned()); + cols.push(row_addr_col); + cols + } else { + #[allow(clippy::redundant_clone)] + left_cols.clone() + }; + let matched = matched.project(&projection)?; + // The payload columns of an outer join are always nullable. We need to restore + // non-nullable to columns that were originally non-nullable. This should be safe + // since the not_matched rows should all be valid on the right_cols + // + // Sadly we can't use with_schema because it doesn't let you toggle nullability + let matched = RecordBatch::try_new( + self.output_schema.clone(), + Vec::from_iter(matched.columns().iter().cloned()), + )?; + batches.push(Ok(matched)); + } } } } @@ -4067,6 +4184,625 @@ mod tests { assert_eq!(updated_ds.count_rows(None).await.unwrap(), 3); } + /// Composite-key delete-only merge_insert (`when_matched(Delete)`, + /// `when_not_matched_by_source(Keep)`) removes the matched rows for every + /// combination of which join columns carry a scalar index, including when + /// every column is indexed. + #[rstest::rstest] + #[case::index_on_both(true, true)] + #[case::index_on_first(true, false)] + #[case::index_on_second(false, true)] + #[case::no_index(false, false)] + #[tokio::test] + async fn test_indexed_merge_insert_composite_key_delete( + #[case] index_on_a: bool, + #[case] index_on_b: bool, + ) { + let initial = record_batch!( + ("a", Int32, [1, 1, 2, 2]), + ("b", Int32, [10, 20, 10, 20]), + ("value", Int32, [100, 200, 300, 400]) + ) + .unwrap(); + let schema = initial.schema(); + + let mut ds = Dataset::write( + RecordBatchIterator::new(vec![Ok(initial.clone())], schema.clone()), + "memory://", + None, + ) + .await + .unwrap(); + + let params = ScalarIndexParams::default(); + if index_on_a { + ds.create_index(&["a"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + } + if index_on_b { + ds.create_index(&["b"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + } + + // Delete (1, 10) by composite key. Only key columns in the source. + let source = record_batch!(("a", Int32, [1]), ("b", Int32, [10])).unwrap(); + + let (updated_ds, stats) = + MergeInsertBuilder::try_new(Arc::new(ds), vec!["a".to_string(), "b".to_string()]) + .unwrap() + .when_matched(WhenMatched::Delete) + .when_not_matched(WhenNotMatched::DoNothing) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(source.clone())], + source.schema(), + ))) + .await + .unwrap(); + + assert_eq!(stats.num_deleted_rows, 1, "matched row must be deleted"); + assert_eq!(updated_ds.count_rows(None).await.unwrap(), 3); + assert_eq!( + updated_ds + .count_rows(Some("a = 1 AND b = 10".to_string())) + .await + .unwrap(), + 0, + "(1, 10) must be gone after the delete" + ); + // The sibling key (1, 20) must remain untouched. + assert_eq!( + updated_ds + .count_rows(Some("a = 1 AND b = 20 AND value = 200".to_string())) + .await + .unwrap(), + 1, + ); + } + + /// Delete-only merge_insert on a single indexed key removes the matched + /// rows (the indexed path is not exclusive to composite keys). + #[tokio::test] + async fn test_indexed_merge_insert_single_key_delete() { + let initial = record_batch!( + ("id", Int32, [1, 2, 3, 4]), + ("value", Int32, [10, 20, 30, 40]) + ) + .unwrap(); + let schema = initial.schema(); + + let mut ds = Dataset::write( + RecordBatchIterator::new(vec![Ok(initial.clone())], schema.clone()), + "memory://", + None, + ) + .await + .unwrap(); + + ds.create_index( + &["id"], + IndexType::Scalar, + None, + &ScalarIndexParams::default(), + false, + ) + .await + .unwrap(); + + let source = record_batch!(("id", Int32, [2, 4])).unwrap(); + + let (updated_ds, stats) = MergeInsertBuilder::try_new(Arc::new(ds), vec!["id".to_string()]) + .unwrap() + .when_matched(WhenMatched::Delete) + .when_not_matched(WhenNotMatched::DoNothing) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(source.clone())], + source.schema(), + ))) + .await + .unwrap(); + + assert_eq!(stats.num_deleted_rows, 2); + assert_eq!(updated_ds.count_rows(None).await.unwrap(), 2); + assert_eq!( + updated_ds + .count_rows(Some("id = 2 OR id = 4".to_string())) + .await + .unwrap(), + 0, + ); + } + + /// `when_matched(Fail)` on an indexed key aborts the operation when a + /// source row matches an existing key, and inserts cleanly when none do. + #[tokio::test] + async fn test_indexed_merge_insert_when_matched_fail() { + let initial = + record_batch!(("id", Int32, [1, 2, 3]), ("value", Int32, [10, 20, 30])).unwrap(); + let schema = initial.schema(); + + let mut ds = Dataset::write( + RecordBatchIterator::new(vec![Ok(initial.clone())], schema.clone()), + "memory://", + None, + ) + .await + .unwrap(); + + ds.create_index( + &["id"], + IndexType::Scalar, + None, + &ScalarIndexParams::default(), + false, + ) + .await + .unwrap(); + let ds = Arc::new(ds); + + // A source row matching an existing key must fail the operation. + let matching = record_batch!(("id", Int32, [2]), ("value", Int32, [999])).unwrap(); + let err = MergeInsertBuilder::try_new(ds.clone(), vec!["id".to_string()]) + .unwrap() + .when_matched(WhenMatched::Fail) + .when_not_matched(WhenNotMatched::InsertAll) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(matching.clone())], + matching.schema(), + ))) + .await + .unwrap_err(); + let msg = err.to_string(); + assert!(msg.contains("Merge insert failed"), "got: {msg}"); + assert!(msg.contains("found matching row"), "got: {msg}"); + + // A source with no matching key inserts without failing. + let new_rows = record_batch!(("id", Int32, [4]), ("value", Int32, [40])).unwrap(); + let (updated_ds, stats) = MergeInsertBuilder::try_new(ds.clone(), vec!["id".to_string()]) + .unwrap() + .when_matched(WhenMatched::Fail) + .when_not_matched(WhenNotMatched::InsertAll) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(new_rows.clone())], + new_rows.schema(), + ))) + .await + .unwrap(); + assert_eq!(stats.num_inserted_rows, 1); + assert_eq!(stats.num_updated_rows, 0); + assert_eq!(updated_ds.count_rows(None).await.unwrap(), 4); + } + + /// Fully-indexed composite-key `when_matched(Delete)` combined with + /// `when_not_matched(InsertAll)` must both delete matched rows and write + /// the inserted rows. + #[tokio::test] + async fn test_indexed_merge_insert_composite_key_delete_with_insert() { + let initial = record_batch!( + ("a", Int32, [1, 1, 2, 2]), + ("b", Int32, [10, 20, 10, 20]), + ("value", Int32, [100, 200, 300, 400]) + ) + .unwrap(); + let schema = initial.schema(); + + let mut ds = Dataset::write( + RecordBatchIterator::new(vec![Ok(initial.clone())], schema.clone()), + "memory://", + None, + ) + .await + .unwrap(); + + let params = ScalarIndexParams::default(); + ds.create_index(&["a"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + ds.create_index(&["b"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + + // Source matches (1, 10) -> delete, and (3, 30) is new -> insert. + let source = record_batch!( + ("a", Int32, [1, 3]), + ("b", Int32, [10, 30]), + ("value", Int32, [999, 333]) + ) + .unwrap(); + + let (updated_ds, stats) = + MergeInsertBuilder::try_new(Arc::new(ds), vec!["a".to_string(), "b".to_string()]) + .unwrap() + .when_matched(WhenMatched::Delete) + .when_not_matched(WhenNotMatched::InsertAll) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(source.clone())], + source.schema(), + ))) + .await + .unwrap(); + + assert_eq!(stats.num_deleted_rows, 1); + assert_eq!(stats.num_inserted_rows, 1); + // 4 - 1 deleted + 1 inserted = 4. + assert_eq!(updated_ds.count_rows(None).await.unwrap(), 4); + assert_eq!( + updated_ds + .count_rows(Some("a = 1 AND b = 10".to_string())) + .await + .unwrap(), + 0, + "matched row must be deleted, not updated" + ); + assert_eq!( + updated_ds + .count_rows(Some("a = 3 AND b = 30 AND value = 333".to_string())) + .await + .unwrap(), + 1, + "unmatched source row must be inserted" + ); + } + + /// A delete whose source contains duplicate keys matching the same target + /// row applies `source_dedupe_behavior` on the indexed-scan path, exactly + /// like an update: the default `Fail` aborts (naming the ambiguous key), + /// while `FirstSeen` removes and counts the row once and reports the extra + /// match as a skipped duplicate. + #[rstest::rstest] + #[case::fail(SourceDedupeBehavior::Fail)] + #[case::first_seen(SourceDedupeBehavior::FirstSeen)] + #[tokio::test] + async fn test_indexed_merge_insert_delete_source_duplicates( + #[case] behavior: SourceDedupeBehavior, + ) { + let initial = record_batch!( + ("a", Int32, [1, 1, 2, 2]), + ("b", Int32, [10, 20, 10, 20]), + ("value", Int32, [100, 200, 300, 400]) + ) + .unwrap(); + let schema = initial.schema(); + + let mut ds = Dataset::write( + RecordBatchIterator::new(vec![Ok(initial.clone())], schema.clone()), + "memory://", + None, + ) + .await + .unwrap(); + + // Index every join column so the merge takes the indexed-scan delete path. + let params = ScalarIndexParams::default(); + ds.create_index(&["a"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + ds.create_index(&["b"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + + // Two source rows collide on the same target key (1, 10). + let source = record_batch!(("a", Int32, [1, 1]), ("b", Int32, [10, 10])).unwrap(); + + let result = + MergeInsertBuilder::try_new(Arc::new(ds), vec!["a".to_string(), "b".to_string()]) + .unwrap() + .when_matched(WhenMatched::Delete) + .when_not_matched(WhenNotMatched::DoNothing) + .source_dedupe_behavior(behavior) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(source.clone())], + source.schema(), + ))) + .await; + + if behavior == SourceDedupeBehavior::Fail { + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Ambiguous merge inserts") && err.contains("a = 1"), + "Fail must abort naming the ambiguous key, got: {err}" + ); + return; + } + + let (updated_ds, stats) = result.unwrap(); + assert_eq!(stats.num_deleted_rows, 1); + assert_eq!(stats.num_skipped_duplicates, 1); + assert_eq!(updated_ds.count_rows(None).await.unwrap(), 3); + assert_eq!( + updated_ds + .count_rows(Some("a = 1 AND b = 10".to_string())) + .await + .unwrap(), + 0, + "the matched row must be removed exactly once" + ); + } + + /// The v2 plans apply the same `source_dedupe_behavior` to deletes when the + /// source has duplicate keys matching one target row — covering both + /// `FullSchemaMergeInsertExec` (`Delete + InsertAll`) and + /// `DeleteOnlyMergeInsertExec` (pure delete). No scalar index, so routing + /// stays on the v2 path. + #[rstest::rstest] + #[case::full_schema_fail(true, SourceDedupeBehavior::Fail)] + #[case::full_schema_first_seen(true, SourceDedupeBehavior::FirstSeen)] + #[case::delete_only_fail(false, SourceDedupeBehavior::Fail)] + #[case::delete_only_first_seen(false, SourceDedupeBehavior::FirstSeen)] + #[tokio::test] + async fn test_v2_merge_insert_delete_source_duplicates( + #[case] with_insert: bool, + #[case] behavior: SourceDedupeBehavior, + ) { + let initial = + record_batch!(("a", Int32, [1, 2, 3]), ("value", Int32, [10, 20, 30])).unwrap(); + let schema = initial.schema(); + + let ds = Dataset::write( + RecordBatchIterator::new(vec![Ok(initial.clone())], schema.clone()), + "memory://", + None, + ) + .await + .unwrap(); + + // Two source rows collide on target key a=1. With insert, a=4 is new. + let (source, when_not_matched, expected_inserted, expected_total) = if with_insert { + ( + record_batch!(("a", Int32, [1, 1, 4]), ("value", Int32, [99, 99, 40])).unwrap(), + WhenNotMatched::InsertAll, + 1, + 3, // 3 - 1 deleted + 1 inserted + ) + } else { + ( + record_batch!(("a", Int32, [1, 1])).unwrap(), + WhenNotMatched::DoNothing, + 0, + 2, // 3 - 1 deleted + ) + }; + + let result = MergeInsertBuilder::try_new(Arc::new(ds), vec!["a".to_string()]) + .unwrap() + .when_matched(WhenMatched::Delete) + .when_not_matched(when_not_matched) + .source_dedupe_behavior(behavior) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(source.clone())], + source.schema(), + ))) + .await; + + if behavior == SourceDedupeBehavior::Fail { + let err = result.unwrap_err().to_string(); + assert!( + err.contains("Ambiguous merge inserts") && err.contains("a = 1"), + "Fail must abort naming the ambiguous key, got: {err}" + ); + return; + } + + let (updated_ds, stats) = result.unwrap(); + assert_eq!(stats.num_deleted_rows, 1, "the matched row is removed once"); + assert_eq!(stats.num_skipped_duplicates, 1); + assert_eq!(stats.num_inserted_rows, expected_inserted); + assert_eq!(updated_ds.count_rows(None).await.unwrap(), expected_total); + assert_eq!( + updated_ds + .count_rows(Some("a = 1".to_string())) + .await + .unwrap(), + 0, + "the matched row must be removed exactly once" + ); + } + + /// A partial-schema source that combines `when_matched(Delete)` with + /// `when_not_matched(InsertAll)` must succeed even when every join key is + /// indexed. The indexed-scan delete path cannot fold a delete into a + /// partial write, so this case routes to the v2 plan (which fills omitted + /// nullable target columns) instead of being rejected. + #[tokio::test] + async fn test_indexed_merge_insert_partial_schema_delete_with_insert() { + // Target carries two nullable non-key columns; the source omits `note`. + let full_schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + Field::new("value", DataType::Int32, true), + Field::new("note", DataType::Utf8, true), + ])); + let full_batch = RecordBatch::try_new( + full_schema.clone(), + vec![ + Arc::new(Int32Array::from(vec![1, 1, 2, 2])), + Arc::new(Int32Array::from(vec![10, 20, 10, 20])), + Arc::new(Int32Array::from(vec![100, 200, 300, 400])), + Arc::new(StringArray::from(vec!["w", "x", "y", "z"])), + ], + ) + .unwrap(); + + let mut ds = Dataset::write( + RecordBatchIterator::new(vec![Ok(full_batch)], full_schema.clone()), + "memory://", + None, + ) + .await + .unwrap(); + + let params = ScalarIndexParams::default(); + ds.create_index(&["a"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + ds.create_index(&["b"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + + // Source deletes matched (1, 10) and inserts new (3, 30), omitting `note`. + let partial_schema = Arc::new(Schema::new(vec![ + Field::new("a", DataType::Int32, false), + Field::new("b", DataType::Int32, false), + Field::new("value", DataType::Int32, true), + ])); + let source = RecordBatch::try_new( + partial_schema.clone(), + vec![ + Arc::new(Int32Array::from(vec![1, 3])), + Arc::new(Int32Array::from(vec![10, 30])), + Arc::new(Int32Array::from(vec![999, 333])), + ], + ) + .unwrap(); + + let (updated_ds, stats) = + MergeInsertBuilder::try_new(Arc::new(ds), vec!["a".to_string(), "b".to_string()]) + .unwrap() + .when_matched(WhenMatched::Delete) + .when_not_matched(WhenNotMatched::InsertAll) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(source.clone())], + source.schema(), + ))) + .await + .unwrap(); + + assert_eq!(stats.num_deleted_rows, 1); + assert_eq!(stats.num_inserted_rows, 1); + // 4 - 1 deleted + 1 inserted = 4. + assert_eq!(updated_ds.count_rows(None).await.unwrap(), 4); + assert_eq!( + updated_ds + .count_rows(Some("a = 1 AND b = 10".to_string())) + .await + .unwrap(), + 0, + "matched row must be deleted, not updated" + ); + // Inserted row carries the omitted `note` column as NULL. + assert_eq!( + updated_ds + .count_rows(Some( + "a = 3 AND b = 30 AND value = 333 AND note IS NULL".to_string() + )) + .await + .unwrap(), + 1, + "unmatched source row must be inserted with omitted column NULL-filled" + ); + } + + /// Fully-indexed composite-key delete across multiple fragments, with + /// stable row ids on/off. Exercises the indexed-scan delete commit + /// path: matched row ids are resolved to addresses (via the row-id + /// index when stable) and removed without rewriting any fragments. + /// Also covers an appended fragment that neither index covers, so the + /// delete must reach rows via the unindexed-remainder union too. + #[rstest::rstest] + #[case(true)] + #[case(false)] + #[tokio::test] + async fn test_indexed_merge_insert_composite_key_delete_multi_fragment( + #[case] enable_stable_row_ids: bool, + ) { + let initial = record_batch!( + ("a", Int32, [1, 1, 2, 2]), + ("b", Int32, [10, 20, 10, 20]), + ("value", Int32, [100, 200, 300, 400]) + ) + .unwrap(); + let schema = initial.schema(); + + // One row per fragment so the delete spans multiple fragments. + let mut ds = Dataset::write( + RecordBatchIterator::new(vec![Ok(initial.clone())], schema.clone()), + "memory://", + Some(WriteParams { + max_rows_per_file: 1, + enable_stable_row_ids, + ..Default::default() + }), + ) + .await + .unwrap(); + + let params = ScalarIndexParams::default(); + ds.create_index(&["a"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + ds.create_index(&["b"], IndexType::Scalar, None, ¶ms, false) + .await + .unwrap(); + + // Append a row AFTER the indices are built so it lives in a fragment + // neither index covers. The delete must still reach it. + let appended = record_batch!( + ("a", Int32, [3]), + ("b", Int32, [30]), + ("value", Int32, [500]) + ) + .unwrap(); + ds.append( + RecordBatchIterator::new(vec![Ok(appended.clone())], appended.schema()), + None, + ) + .await + .unwrap(); + + // Delete an indexed row (1, 10) and the unindexed appended row (3, 30). + let source = record_batch!(("a", Int32, [1, 3]), ("b", Int32, [10, 30])).unwrap(); + + let (updated_ds, stats) = + MergeInsertBuilder::try_new(Arc::new(ds), vec!["a".to_string(), "b".to_string()]) + .unwrap() + .when_matched(WhenMatched::Delete) + .when_not_matched(WhenNotMatched::DoNothing) + .try_build() + .unwrap() + .execute_reader(Box::new(RecordBatchIterator::new( + vec![Ok(source.clone())], + source.schema(), + ))) + .await + .unwrap(); + + assert_eq!(stats.num_deleted_rows, 2); + assert_eq!(updated_ds.count_rows(None).await.unwrap(), 3); + assert_eq!( + updated_ds + .count_rows(Some("(a = 1 AND b = 10) OR (a = 3 AND b = 30)".to_string())) + .await + .unwrap(), + 0, + "both matched rows must be gone" + ); + // Untouched rows survive with their original values. + assert_eq!( + updated_ds + .count_rows(Some("a = 2 AND b = 20 AND value = 400".to_string())) + .await + .unwrap(), + 1, + ); + } + /// Composite-key `MapIndexExec` formats its Display so plans expose /// every probed column, and `with_new_children` round-trips the full /// lookup list rather than collapsing back to a single-column form. diff --git a/rust/lance/src/dataset/write/merge_insert/exec/delete.rs b/rust/lance/src/dataset/write/merge_insert/exec/delete.rs index be7897e2441..0c63b99fe69 100644 --- a/rust/lance/src/dataset/write/merge_insert/exec/delete.rs +++ b/rust/lance/src/dataset/write/merge_insert/exec/delete.rs @@ -22,7 +22,10 @@ use roaring::RoaringTreemap; use crate::Dataset; use crate::dataset::transaction::{Operation, Transaction}; use crate::dataset::write::merge_insert::assign_action::Action; -use crate::dataset::write::merge_insert::{MERGE_ACTION_COLUMN, MergeInsertParams, MergeStats}; +use crate::dataset::write::merge_insert::{ + MERGE_ACTION_COLUMN, MergeInsertParams, MergeStats, SourceDedupeBehavior, + create_duplicate_row_error, +}; use super::{MergeInsertMetrics, apply_deletions}; @@ -101,6 +104,8 @@ impl DeleteOnlyMergeInsertExec { async fn collect_deletions( mut input_stream: SendableRecordBatchStream, metrics: MergeInsertMetrics, + source_dedupe_behavior: SourceDedupeBehavior, + on_columns: &[String], ) -> DFResult { let schema = input_stream.schema(); @@ -156,8 +161,25 @@ impl DeleteOnlyMergeInsertExec { if action == Action::Delete && !row_addr_array.is_null(row_idx) { let row_addr = row_addr_array.value(row_idx); - delete_row_addrs.insert(row_addr); - metrics.num_deleted_rows.add(1); + // The treemap dedupes addresses, so a repeat insert signals + // a duplicate source row matching the same target; apply the + // same dedupe policy as updates. (Delete-only never carries + // `delete_not_matched_by_source`, so every delete here is a + // source match.) + if delete_row_addrs.insert(row_addr) { + metrics.num_deleted_rows.add(1); + } else { + match source_dedupe_behavior { + SourceDedupeBehavior::Fail => { + return Err(create_duplicate_row_error( + &batch, row_idx, on_columns, + )); + } + SourceDedupeBehavior::FirstSeen => { + metrics.num_skipped_duplicates.add(1); + } + } + } } } } @@ -261,9 +283,16 @@ impl ExecutionPlan for DeleteOnlyMergeInsertExec { let transaction_holder = self.transaction.clone(); let affected_rows_holder = self.affected_rows.clone(); let merged_generations = self.params.merged_generations.clone(); + let source_dedupe_behavior = self.params.source_dedupe_behavior; + let on_columns = self.params.on.clone(); let result_stream = futures::stream::once(async move { - let delete_row_addrs = Self::collect_deletions(input_stream, metrics).await?; + // `metrics` is moved into `collect_deletions`; keep a handle on the + // skipped-duplicate counter so it can be folded into the stats below. + let skipped_duplicates = metrics.num_skipped_duplicates.clone(); + let delete_row_addrs = + Self::collect_deletions(input_stream, metrics, source_dedupe_behavior, &on_columns) + .await?; let (updated_fragments, removed_fragment_ids) = apply_deletions(&dataset, &delete_row_addrs) @@ -297,7 +326,7 @@ impl ExecutionPlan for DeleteOnlyMergeInsertExec { bytes_written: 0, num_files_written: 0, num_attempts: 1, - num_skipped_duplicates: 0, + num_skipped_duplicates: skipped_duplicates.value() as u64, }; if let Ok(mut transaction_guard) = transaction_holder.lock() { diff --git a/rust/lance/src/dataset/write/merge_insert/exec/write.rs b/rust/lance/src/dataset/write/merge_insert/exec/write.rs index d80c3b084f6..c3c3c71b6a3 100644 --- a/rust/lance/src/dataset/write/merge_insert/exec/write.rs +++ b/rust/lance/src/dataset/write/merge_insert/exec/write.rs @@ -103,6 +103,29 @@ impl MergeState { // Delete action - only delete, don't write back if !row_addr_array.is_null(row_idx) { let row_addr = row_addr_array.value(row_idx); + let row_id = row_id_array.value(row_idx); + + // A source with duplicate keys matches the same target row + // more than once; apply the same dedupe policy as updates. + // (Target-only deletes from `delete_not_matched_by_source` + // also reach here but never duplicate, so they never trip + // `Fail`.) + if !self.processed_row_ids.insert(row_id) { + match self.source_dedupe_behavior { + SourceDedupeBehavior::Fail => { + return Err(create_duplicate_row_error( + batch, + row_idx, + &self.on_columns, + )); + } + SourceDedupeBehavior::FirstSeen => { + self.metrics.num_skipped_duplicates.add(1); + return Ok(None); // Skip this duplicate row + } + } + } + self.delete_row_addrs.insert(row_addr); self.metrics.num_deleted_rows.add(1); } From f59b48469c12e3ae508e969259ef04a8d41d7613 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Mon, 29 Jun 2026 09:56:10 -0700 Subject: [PATCH 010/177] feat(index): support distributed LabelList scalar index builds (#7223) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `LabelListIndexPlugin::train_index` unconditionally rejected fragment-scoped training (`fragment_ids.is_some()`), so a LabelList index could not be built through the distributed / segmented path (per-fragment `execute_uncommitted` + `merge_existing_index_segments`) the way BTree, Inverted, Bitmap, and FM already are. This makes LabelList a first-class distributed scalar index. ## Changes - `lance-index/src/scalar/label_list.rs`: `train_index` ignores `fragment_ids` and builds over the already fragment-scoped stream (mirrors `FMIndexPlugin`); a partial index over a fragment subset is correct since it covers exactly those rows. Add `merge_label_list_indices`, which unions the per-segment bitmap states and the `list_nulls` row sets. LabelList wraps a `BitmapIndex` plus a null-row set and distributed segments cover disjoint rows, so this is a cheap union (the same operation `LabelListIndex::update` performs) — no source re-scan. Mirrors `merge_bitmap_indices` but also carries `list_nulls`. - `lance/src/index/scalar/label_list.rs` (new): `merge_segments` opens each source segment as a `LabelListIndex` and calls `merge_label_list_indices` (mirrors `scalar/bitmap.rs`). - `lance/src/index.rs`: `merge_existing_index_segments` routes `all_label_list` segments (details `LabelListIndexDetails`) to the new merge; add `segment_has_label_list_details`. - `lance/src/index/scalar.rs`: declare the `label_list` module. Covered by `test_label_list_merge_existing_index_segments`: one LabelList segment per fragment, merged via `merge_existing_index_segments`, answers `array_has_any` across both fragments with the same row count as a pre-index full scan. --- rust/lance-index/src/scalar/label_list.rs | 98 ++++++- rust/lance/src/index.rs | 18 +- rust/lance/src/index/create.rs | 311 +++++++++++++++++++++- rust/lance/src/index/scalar.rs | 1 + rust/lance/src/index/scalar/label_list.rs | 148 ++++++++++ 5 files changed, 565 insertions(+), 11 deletions(-) create mode 100644 rust/lance/src/index/scalar/label_list.rs diff --git a/rust/lance-index/src/scalar/label_list.rs b/rust/lance-index/src/scalar/label_list.rs index 77232952419..1b4d36faebf 100644 --- a/rust/lance-index/src/scalar/label_list.rs +++ b/rust/lance-index/src/scalar/label_list.rs @@ -28,7 +28,10 @@ use lance_select::{NullableRowAddrSet, RowAddrTreeMap, RowSetOps}; use roaring::RoaringBitmap; use tracing::instrument; -use super::{AnyQuery, IndexFile, IndexStore, LabelListQuery, ScalarIndex, bitmap::BitmapIndex}; +use super::{ + AnyQuery, IndexFile, IndexStore, LabelListQuery, OldIndexDataFilter, ScalarIndex, + bitmap::BitmapIndex, +}; use super::{BuiltinIndexType, SargableQuery, ScalarIndexParams}; use super::{MetricsCollector, SearchResult}; use crate::frag_reuse::FragReuseIndex; @@ -486,6 +489,87 @@ async fn write_label_list_bitmap_index( .await } +/// Merge multiple LabelList index segments into a single index. +/// +/// A [`LabelListIndex`] is a [`BitmapIndex`] over the unnested list values plus a +/// separate `list_nulls` row set. Because distributed segments cover disjoint rows +/// (distinct fragments), merging is a cheap union of the underlying bitmap states +/// and of the `list_nulls` sets — no re-scan of source data is required. This +/// mirrors [`crate::scalar::bitmap::merge_bitmap_indices`] but also carries the +/// per-segment `list_nulls`. When `old_data_filter` is provided, rows from +/// retired fragments are removed from both the value bitmaps and `list_nulls`. +pub async fn merge_label_list_indices( + source_indices: &[Arc], + dest_store: &dyn IndexStore, + old_data_filter: Option, + progress: Arc, +) -> Result { + if source_indices.is_empty() { + return Err(Error::invalid_input( + "LabelList segment merge requires at least one source segment".to_string(), + )); + } + + let value_type = source_indices[0].values_index.value_type().clone(); + let mut merged_state = HashMap::::new(); + let mut merged_nulls = RowAddrTreeMap::new(); + + progress + .stage_start( + "merge_label_list_segments", + Some(source_indices.len() as u64), + "segments", + ) + .await?; + for (idx, source_index) in source_indices.iter().enumerate() { + if source_index.values_index.value_type() != &value_type { + return Err(Error::invalid_input(format!( + "LabelList segment has value type {:?}, expected {:?}", + source_index.values_index.value_type(), + value_type + ))); + } + + let state = source_index.values_index.load_bitmap_index_state().await?; + for (key, mut bitmap) in state { + if let Some(filter) = old_data_filter.as_ref() { + filter.retain_old_rows(&mut bitmap); + } + if bitmap.is_empty() { + continue; + } + merged_state + .entry(key) + .and_modify(|existing| *existing |= &bitmap) + .or_insert(bitmap); + } + let mut list_nulls = source_index.list_nulls.as_ref().clone(); + if let Some(filter) = old_data_filter.as_ref() { + filter.retain_old_rows(&mut list_nulls); + } + merged_nulls |= &list_nulls; + progress + .stage_progress("merge_label_list_segments", (idx + 1) as u64) + .await?; + } + progress.stage_complete("merge_label_list_segments").await?; + + progress + .stage_start("write_label_list_index", Some(1), "files") + .await?; + let file = + write_label_list_bitmap_index(merged_state, dest_store, &value_type, &merged_nulls).await?; + progress.stage_progress("write_label_list_index", 1).await?; + progress.stage_complete("write_label_list_index").await?; + + Ok(CreatedIndex { + index_details: prost_types::Any::from_msg(&pbold::LabelListIndexDetails::default()) + .unwrap(), + index_version: LABEL_LIST_INDEX_VERSION, + files: vec![file], + }) +} + /// The serializable state of a [`LabelListIndex`]. /// /// `LabelListIndex` is a thin wrapper around a [`BitmapIndex`] plus a separate @@ -634,15 +718,13 @@ impl ScalarIndexPlugin for LabelListIndexPlugin { data: SendableRecordBatchStream, index_store: &dyn IndexStore, _request: Box, - fragment_ids: Option>, + // Training over a fragment subset is supported for distributed builds: the + // provided `data` stream is already scoped to those fragments, so a partial + // index covering exactly those rows is produced. Segments are recombined by + // `merge_label_list_indices`. + _fragment_ids: Option>, _progress: Arc, ) -> Result { - if fragment_ids.is_some() { - return Err(Error::invalid_input_source( - "LabelList index does not support fragment training".into(), - )); - } - let schema = data.schema(); let field = schema .column_with_name(VALUE_COLUMN_NAME) diff --git a/rust/lance/src/index.rs b/rust/lance/src/index.rs index d98ab8701c1..7649def53f0 100644 --- a/rust/lance/src/index.rs +++ b/rust/lance/src/index.rs @@ -292,6 +292,13 @@ fn segment_has_fmindex_details(segment: &IndexMetadata) -> bool { .is_some_and(|details| details.type_url.ends_with("FMIndexDetails")) } +fn segment_has_label_list_details(segment: &IndexMetadata) -> bool { + segment + .index_details + .as_ref() + .is_some_and(|details| details.type_url.ends_with("LabelListIndexDetails")) +} + // Cache keys for different index types #[derive(Debug, Clone)] pub(crate) struct LegacyVectorIndexCacheKey<'a> { @@ -1149,7 +1156,14 @@ impl DatasetIndexExt for Dataset { let all_btree = source_segments.iter().all(segment_has_btree_details); let all_fmindex = source_segments.iter().all(segment_has_fmindex_details); let all_zonemap = source_segments.iter().all(segment_has_zonemap_details); - if !all_vector && !all_inverted && !all_bitmap && !all_btree && !all_fmindex && !all_zonemap + let all_label_list = source_segments.iter().all(segment_has_label_list_details); + if !all_vector + && !all_inverted + && !all_bitmap + && !all_btree + && !all_fmindex + && !all_zonemap + && !all_label_list { return Err(Error::invalid_input( "merge_existing_index_segments requires all segments to have the same supported index type" @@ -1170,6 +1184,8 @@ impl DatasetIndexExt for Dataset { crate::index::scalar::fmindex::merge_segments(self, source_segments).await? } else if all_bitmap { crate::index::scalar::bitmap::merge_segments(self, source_segments).await? + } else if all_label_list { + crate::index::scalar::label_list::merge_segments(self, source_segments).await? } else if all_zonemap { crate::index::scalar::zonemap::merge_segments(self, source_segments).await? } else { diff --git a/rust/lance/src/index/create.rs b/rust/lance/src/index/create.rs index 19f79f8dd66..702a8c4e49f 100644 --- a/rust/lance/src/index/create.rs +++ b/rust/lance/src/index/create.rs @@ -847,9 +847,9 @@ mod tests { use crate::dataset::{WriteMode, WriteParams}; use crate::index::{DatasetIndexExt, IndexSegment}; use crate::utils::test::{DatagenExt, FragmentCount, FragmentRowCount}; - use arrow::datatypes::{Float32Type, Int32Type}; + use arrow::datatypes::{Float32Type, Int32Type, Int64Type}; use arrow_array::cast::AsArray; - use arrow_array::{FixedSizeListArray, RecordBatchIterator}; + use arrow_array::{Array, FixedSizeListArray, ListArray, RecordBatchIterator}; use arrow_array::{Int32Array, RecordBatch, StringArray}; use arrow_schema::{DataType, Field as ArrowField, Schema as ArrowSchema}; use datafusion::common::ScalarValue; @@ -2271,6 +2271,313 @@ mod tests { ); } + // Distributed LabelList build: one segment per fragment via + // `execute_uncommitted`, then `merge_existing_index_segments` consolidates them + // into a single canonical segment that answers `array_has_any` across all rows. + #[tokio::test] + async fn test_label_list_merge_existing_index_segments() { + use lance_index::scalar::{LabelListQuery, SearchResult}; + + // Open `segment` and count rows whose `labels` list contains `label`. + async fn count_has_any(dataset: &Dataset, segment: &IndexMetadata, label: i64) -> usize { + let field_path = dataset.schema().field_path(segment.fields[0]).unwrap(); + let index = crate::index::scalar::open_scalar_index( + dataset, + &field_path, + segment, + &NoOpMetricsCollector, + ) + .await + .unwrap(); + let query = LabelListQuery::HasAnyLabel(vec![ScalarValue::Int64(Some(label))]); + match index.search(&query, &NoOpMetricsCollector).await.unwrap() { + SearchResult::Exact(row_addrs) => { + row_addrs.true_rows().row_addrs().unwrap().count() + } + other => panic!("expected exact result, got {other:?}"), + } + } + + let tmpdir = TempStrDir::default(); + let dataset_uri = format!("file://{}", tmpdir.as_str()); + + // 4000 rows across two 2000-row fragments; each `labels` list cycles over 1..=5. + let mut dataset = gen_batch() + .col( + "labels", + lance_datagen::array::rand_list_any( + lance_datagen::array::cycle::(vec![1, 2, 3, 4, 5]), + false, + ), + ) + .into_dataset( + &dataset_uri, + FragmentCount::from(2), + FragmentRowCount::from(2000), + ) + .await + .unwrap(); + + // Ground truth via a full scan before any index exists. + let expected = dataset + .scan() + .project(&["labels"]) + .unwrap() + .filter("array_has_any(labels, [3])") + .unwrap() + .try_into_batch() + .await + .unwrap() + .num_rows(); + assert!( + expected > 0, + "test dataset must contain at least one row whose labels include 3" + ); + + // One LabelList segment per fragment, committed as a multi-segment index. + let params = + ScalarIndexParams::for_builtin(lance_index::scalar::BuiltinIndexType::LabelList); + let mut staged = Vec::new(); + for fragment in dataset.get_fragments() { + staged.push( + CreateIndexBuilder::new(&mut dataset, &["labels"], IndexType::LabelList, ¶ms) + .name("labels_idx".to_string()) + .fragments(vec![fragment.id() as u32]) + .execute_uncommitted() + .await + .unwrap(), + ); + } + dataset + .commit_existing_index_segments("labels_idx", "labels", staged) + .await + .unwrap(); + + // Merge the two per-fragment segments into a single segment covering both. + let merged = dataset + .merge_existing_index_segments( + dataset.load_indices_by_name("labels_idx").await.unwrap(), + ) + .await + .unwrap(); + assert_eq!( + merged.fragment_bitmap.as_ref().unwrap(), + &roaring::RoaringBitmap::from_iter([0u32, 1]) + ); + assert!( + merged + .index_details + .as_ref() + .unwrap() + .type_url + .ends_with("LabelListIndexDetails") + ); + // The merged segment returns every row whose labels include 3 across both + // fragments — i.e. the per-fragment bitmaps and null sets were unioned. + assert_eq!(count_has_any(&dataset, &merged, 3).await, expected); + } + + fn label_list_batch(labels: Vec>>>) -> (Arc, RecordBatch) { + let labels = ListArray::from_iter_primitive::(labels); + let row_count = labels.len(); + let schema = Arc::new(ArrowSchema::new(vec![ + ArrowField::new("id", DataType::Int32, false), + ArrowField::new("labels", labels.data_type().clone(), true), + ])); + let ids = Int32Array::from_iter_values((0..row_count).map(|id| id as i32)); + let batch = + RecordBatch::try_new(schema.clone(), vec![Arc::new(ids), Arc::new(labels)]).unwrap(); + (schema, batch) + } + + #[tokio::test] + async fn test_label_list_merge_existing_index_segments_drops_retired_fragments() { + use lance_index::scalar::{LabelListQuery, SearchResult}; + + async fn search_counts( + dataset: &Dataset, + segment: &IndexMetadata, + label: i64, + ) -> (usize, usize) { + let field_path = dataset.schema().field_path(segment.fields[0]).unwrap(); + let index = crate::index::scalar::open_scalar_index( + dataset, + &field_path, + segment, + &NoOpMetricsCollector, + ) + .await + .unwrap(); + let query = LabelListQuery::HasAnyLabel(vec![ScalarValue::Int64(Some(label))]); + match index.search(&query, &NoOpMetricsCollector).await.unwrap() { + SearchResult::Exact(row_addrs) => ( + row_addrs.true_rows().row_addrs().unwrap().count(), + row_addrs.null_rows().row_addrs().unwrap().count(), + ), + other => panic!("expected exact result, got {other:?}"), + } + } + + let tmpdir = TempStrDir::default(); + let dataset_uri = format!("file://{}", tmpdir.as_str()); + let (schema, batch) = label_list_batch(vec![ + Some(vec![Some(1)]), + None, + Some(vec![Some(1)]), + Some(vec![Some(1)]), + Some(vec![Some(2)]), + Some(vec![Some(2)]), + Some(vec![Some(2)]), + Some(vec![Some(2)]), + ]); + let reader = RecordBatchIterator::new(vec![Ok(batch)], schema); + let mut dataset = Dataset::write( + reader, + &dataset_uri, + Some(WriteParams { + max_rows_per_file: 4, + mode: WriteMode::Overwrite, + enable_stable_row_ids: true, + ..Default::default() + }), + ) + .await + .unwrap(); + + let params = + ScalarIndexParams::for_builtin(lance_index::scalar::BuiltinIndexType::LabelList); + let mut staged = Vec::new(); + for fragment in dataset.get_fragments() { + staged.push( + CreateIndexBuilder::new(&mut dataset, &["labels"], IndexType::LabelList, ¶ms) + .name("labels_retired_idx".to_string()) + .fragments(vec![fragment.id() as u32]) + .execute_uncommitted() + .await + .unwrap(), + ); + } + dataset + .commit_existing_index_segments("labels_retired_idx", "labels", staged) + .await + .unwrap(); + + dataset.delete("id < 4").await.unwrap(); + crate::dataset::optimize::compact_files( + &mut dataset, + crate::dataset::optimize::CompactionOptions { + target_rows_per_fragment: 4, + ..Default::default() + }, + None, + ) + .await + .unwrap(); + assert!( + !dataset.fragment_bitmap.contains(0), + "compaction should retire fragment 0" + ); + + let merged = dataset + .merge_existing_index_segments( + dataset + .load_indices_by_name("labels_retired_idx") + .await + .unwrap(), + ) + .await + .unwrap(); + let coverage = merged.fragment_bitmap.as_ref().unwrap(); + assert!(!coverage.contains(0), "must drop retired frag 0"); + assert!(coverage.contains(1), "must keep live indexed frag 1"); + + let (retired_true, _) = search_counts(&dataset, &merged, 1).await; + assert_eq!( + retired_true, 0, + "must filter value bitmaps from retired fragments" + ); + + let (live_true, null_rows) = search_counts(&dataset, &merged, 2).await; + assert_eq!(live_true, 4, "must keep live fragment rows"); + assert_eq!( + null_rows, 0, + "must filter list_nulls from retired fragments" + ); + } + + #[tokio::test] + async fn test_label_list_merge_rejects_nullable_segment_missing_null_metadata() { + use crate::dataset::index::LanceIndexStoreExt; + use lance_index::scalar::IndexStore; + use lance_index::scalar::label_list::{ + BITMAP_LOOKUP_NAME, LABEL_LIST_NULLS_METADATA_KEY, LABEL_LIST_NULLS_MIN_VERSION, + }; + use lance_index::scalar::lance_format::LanceIndexStore; + + let tmpdir = TempStrDir::default(); + let dataset_uri = format!("file://{}", tmpdir.as_str()); + let (schema, batch) = label_list_batch(vec![ + Some(vec![Some(1)]), + None, + Some(vec![Some(2)]), + Some(vec![Some(3)]), + ]); + let reader = RecordBatchIterator::new(vec![Ok(batch)], schema); + let mut dataset = Dataset::write( + reader, + &dataset_uri, + Some(WriteParams { + mode: WriteMode::Overwrite, + ..Default::default() + }), + ) + .await + .unwrap(); + + let params = + ScalarIndexParams::for_builtin(lance_index::scalar::BuiltinIndexType::LabelList); + let segment = + CreateIndexBuilder::new(&mut dataset, &["labels"], IndexType::LabelList, ¶ms) + .name("labels_legacy_idx".to_string()) + .execute_uncommitted() + .await + .unwrap(); + + let source_store = LanceIndexStore::from_dataset_for_existing(&dataset, &segment) + .await + .unwrap(); + let reader = source_store + .open_index_file(BITMAP_LOOKUP_NAME) + .await + .unwrap(); + let batch = reader.read_range(0..reader.num_rows(), None).await.unwrap(); + let source_schema: ArrowSchema = reader.schema().into(); + let schema_without_metadata = Arc::new(ArrowSchema::new(source_schema.fields().clone())); + + let legacy_uuid = Uuid::new_v4(); + let legacy_store = LanceIndexStore::from_dataset_for_new(&dataset, &legacy_uuid).unwrap(); + let mut writer = legacy_store + .new_index_file(BITMAP_LOOKUP_NAME, schema_without_metadata) + .await + .unwrap(); + writer.write_record_batch(batch).await.unwrap(); + let legacy_file = writer.finish().await.unwrap(); + + let mut legacy_segment = segment.clone(); + legacy_segment.uuid = legacy_uuid; + legacy_segment.index_version = LABEL_LIST_NULLS_MIN_VERSION; + legacy_segment.files = Some(vec![legacy_file]); + + let err = dataset + .merge_existing_index_segments(vec![legacy_segment]) + .await + .unwrap_err(); + assert!( + err.to_string().contains(LABEL_LIST_NULLS_METADATA_KEY), + "unexpected error: {err}" + ); + } + #[tokio::test] async fn test_commit_existing_index_supports_local_hnsw_segments() { let tmpdir = TempStrDir::default(); diff --git a/rust/lance/src/index/scalar.rs b/rust/lance/src/index/scalar.rs index ae2478589fb..baa2056d939 100644 --- a/rust/lance/src/index/scalar.rs +++ b/rust/lance/src/index/scalar.rs @@ -8,6 +8,7 @@ pub(crate) mod bitmap; pub(crate) mod btree; pub(crate) mod fmindex; pub(crate) mod inverted; +pub(crate) mod label_list; pub(crate) mod zonemap; pub use inverted::{load_segment_details, load_segments}; diff --git a/rust/lance/src/index/scalar/label_list.rs b/rust/lance/src/index/scalar/label_list.rs new file mode 100644 index 00000000000..27bc49643bb --- /dev/null +++ b/rust/lance/src/index/scalar/label_list.rs @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +use lance_index::metrics::NoOpMetricsCollector; +use lance_index::scalar::IndexStore; +use lance_index::scalar::label_list::{ + BITMAP_LOOKUP_NAME, LABEL_LIST_NULLS_METADATA_KEY, LABEL_LIST_NULLS_MIN_VERSION, LabelListIndex, +}; +use lance_index::scalar::lance_format::LanceIndexStore; +use lance_table::format::IndexMetadata; +use roaring::RoaringBitmap; +use std::sync::Arc; +use uuid::Uuid; + +use crate::{Dataset, Error, Result, dataset::index::LanceIndexStoreExt}; + +async fn validate_nullable_segment_for_merge( + dataset: &Dataset, + field_id: i32, + segment: &IndexMetadata, +) -> Result<()> { + let field = dataset.schema().field_by_id(field_id).ok_or_else(|| { + Error::invalid_input(format!( + "merge_existing_index_segments: field id {} does not exist", + field_id + )) + })?; + + if !field.nullable { + return Ok(()); + } + + if segment.index_version < LABEL_LIST_NULLS_MIN_VERSION { + return Err(Error::invalid_input(format!( + "Cannot merge nullable LabelList segment {} because it was created before list-null metadata was required. Rebuild the segment instead.", + segment.uuid + ))); + } + + let index_store = LanceIndexStore::from_dataset_for_existing(dataset, segment).await?; + let reader = index_store.open_index_file(BITMAP_LOOKUP_NAME).await?; + if !reader + .schema() + .metadata + .contains_key(LABEL_LIST_NULLS_METADATA_KEY) + { + return Err(Error::invalid_input(format!( + "Cannot merge nullable LabelList segment {} because it is missing required metadata key {}. Rebuild the segment instead.", + segment.uuid, LABEL_LIST_NULLS_METADATA_KEY + ))); + } + + Ok(()) +} + +/// Merge one caller-defined group of source LabelList segments into a single segment. +/// +/// A LabelList index is a bitmap over the unnested list values plus a `list_nulls` +/// row set, so segments are recombined by unioning the underlying bitmap states and +/// null sets (see [`lance_index::scalar::label_list::merge_label_list_indices`]) +/// rather than rebuilding from source text. +pub(in crate::index) async fn merge_segments( + dataset: &Dataset, + segments: Vec, +) -> Result { + if segments.is_empty() { + return Err(Error::index("No segment metadata was provided".to_string())); + } + + let field_id = *segments[0].fields.first().ok_or_else(|| { + Error::invalid_input(format!( + "CreateIndex: segment {} is missing field ids", + segments[0].uuid + )) + })?; + let field_path = dataset.schema().field_path(field_id)?; + + let dataset_fragments = dataset.fragment_bitmap.as_ref(); + let mut effective_old_frags = RoaringBitmap::new(); + let mut deleted_old_frags = RoaringBitmap::new(); + for segment in &segments { + if segment.fragment_bitmap.is_none() { + return Err(Error::invalid_input(format!( + "CreateIndex: segment {} is missing fragment coverage", + segment.uuid + ))); + } + if let Some(effective) = segment.effective_fragment_bitmap(dataset_fragments) { + effective_old_frags |= effective; + } + if let Some(deleted) = segment.deleted_fragment_bitmap(dataset_fragments) { + deleted_old_frags |= deleted; + } + } + + let fragment_bitmap = effective_old_frags.clone(); + let old_data_filter = if deleted_old_frags.is_empty() { + None + } else { + crate::index::append::build_old_data_filter( + dataset, + &effective_old_frags, + &deleted_old_frags, + ) + .await? + }; + + let mut source_indices = Vec::with_capacity(segments.len()); + for segment in &segments { + validate_nullable_segment_for_merge(dataset, field_id, segment).await?; + let scalar_index = + super::open_scalar_index(dataset, &field_path, segment, &NoOpMetricsCollector).await?; + let label_list_index = scalar_index + .as_any() + .downcast_ref::() + .ok_or_else(|| { + Error::index(format!( + "merge_existing_index_segments: expected label list segment {}, got {:?}", + segment.uuid, + scalar_index.index_type() + )) + })?; + source_indices.push(Arc::new(label_list_index.clone())); + } + + let new_uuid = Uuid::new_v4(); + let new_store = LanceIndexStore::from_dataset_for_new(dataset, &new_uuid)?; + let created_index = lance_index::scalar::label_list::merge_label_list_indices( + &source_indices, + &new_store, + old_data_filter, + lance_index::progress::noop_progress(), + ) + .await?; + + Ok(IndexMetadata { + uuid: new_uuid, + fields: vec![field_id], + dataset_version: dataset.manifest.version, + fragment_bitmap: Some(fragment_bitmap), + index_details: Some(Arc::new(created_index.index_details)), + index_version: created_index.index_version as i32, + created_at: Some(chrono::Utc::now()), + base_id: None, + files: Some(created_index.files), + ..segments[0].clone() + }) +} From 6d02a5780e147150f3528a63ab76c6162da864a0 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Mon, 29 Jun 2026 09:57:37 -0700 Subject: [PATCH 011/177] fix: improve FM index query performance (#7507) Summary: - Improve FM `contains` queries so normal reads demand-load needed wavelet blocks and page nearby blocks instead of prewarming whole partitions. - Add chunked explicit FM prewarm, parallel partition loading/search, and resumable partition builds. - Add FM `contains` benchmark and FM index management tooling. Optimizations applied: - Parallel FM search across partitions/segments. - Removed query-time full-partition prewarm; explicit prewarm still fully warms the index. - Chunked contiguous wavelet-row prewarm with `LANCE_FMINDEX_PREWARM_CHUNK_BYTES` and `LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY`. - Cold-read demand paging with `LANCE_FMINDEX_DEMAND_PAGE_BYTES` to reduce object-store RPS. - Parallelized FM metadata/partition loading. - Added resumable FM partition creation and explicit `--index-uuid` recovery support. - Final benchmark layout: 1 logical FM segment, `LANCE_FMINDEX_PARTITION_ROWS=100000`, large partition-byte cap, 1,000 partitions. Performance: Dataset: 100M-row `az://datasets/mmlb/mmlb_100m_fts_en_fm_20260626.lance`. Query workload: 4 sampled 5-term patterns from `summary_in_image`, `contains(full_content, pattern)`, `k=100`, `_rowid` only (`projection=[]`, `row_id=true`). | Run | Index layout | Prewarm | Query result | | --- | --- | --- | --- | | Baseline | 6 segments / 10k partitions | 6,525s / 108.8m | 1t: 1.58 qps, mean 633ms, p95 768ms; 8t: 12.48 qps, mean 320ms, p95 320ms | | Demand-load + chunked prewarm | 6 segments / 10k partitions | 2,182s / 36.4m, 3.0x faster | 1t: 5.38 qps, mean 185ms, p95 307ms; 8t: 12.12 qps, mean 326ms, p95 330ms | | Single segment | 1 segment / 100k-row partitions | 356s / 5.94m, 18.3x faster than baseline | 1t: 22.38 qps, mean 44.6ms, p95 54.4ms; 8t: 84.40 qps, mean 42.8ms, p95 47.3ms | Index size: - Final FM index UUID `78600545-625f-40e2-8790-204f35097ec0`: 1,000 partition files, 920,064,767,792 bytes / 856.9 GiB / 0.837 TiB. - Previous 6-segment FM layout was 920,202,650,467 bytes / 857.0 GiB, so the final relayout is effectively size-neutral. --- rust/lance-index/src/scalar/fmindex.rs | 1019 +++++++++++++++++++++-- rust/lance/Cargo.toml | 8 + rust/lance/src/bin/fm_contains_bench.rs | 610 ++++++++++++++ rust/lance/src/bin/fm_index_tool.rs | 161 ++++ 4 files changed, 1744 insertions(+), 54 deletions(-) create mode 100644 rust/lance/src/bin/fm_contains_bench.rs create mode 100644 rust/lance/src/bin/fm_index_tool.rs diff --git a/rust/lance-index/src/scalar/fmindex.rs b/rust/lance-index/src/scalar/fmindex.rs index dde9bcff2a9..82266036f94 100644 --- a/rust/lance-index/src/scalar/fmindex.rs +++ b/rust/lance-index/src/scalar/fmindex.rs @@ -22,13 +22,14 @@ use std::cmp::Reverse; use std::collections::{BinaryHeap, HashMap}; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, OnceLock}; use arrow_array::RecordBatch; use arrow_schema::{DataType, Field}; use async_trait::async_trait; use datafusion::execution::SendableRecordBatchStream; -use futures::StreamExt; +use futures::{StreamExt, TryStreamExt}; use lance_core::cache::LanceCache; use lance_core::deepsize::DeepSizeOf; use lance_core::utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu}; @@ -53,6 +54,9 @@ const FMINDEX_INDEX_VERSION: u32 = 10; const BLOCK_WORDS: usize = 4096; const PARTITION_SIZE: usize = 10_000; const DEFAULT_PARTITION_SIZE_BYTES: usize = 16 * 1024 * 1024; +const DEFAULT_DEMAND_PAGE_TARGET_BYTES: usize = 512 * 1024; +const DEFAULT_PREWARM_CHUNK_TARGET_BYTES: usize = 8 * 1024 * 1024; +const FMINDEX_PARTITION_FINGERPRINT_KEY: &str = "partition_fingerprint"; const SENTINEL_BYTE: u8 = 0xFF; /// SA sampling rate. Store every D-th SA entry. Locate walks at most D LF steps. @@ -83,11 +87,49 @@ static LANCE_FMINDEX_WRITE_QUEUE_SIZE: std::sync::LazyLock = .parse() .expect("failed to parse LANCE_FMINDEX_WRITE_QUEUE_SIZE") }); +static LANCE_FMINDEX_RESUME_EXISTING_PARTITIONS: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + std::env::var("LANCE_FMINDEX_RESUME_EXISTING_PARTITIONS") + .map(|value| { + matches!( + value.as_str(), + "1" | "true" | "TRUE" | "True" | "yes" | "YES" + ) + }) + .unwrap_or(false) + }); +static LANCE_FMINDEX_PREWARM_CHUNK_BYTES: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + std::env::var("LANCE_FMINDEX_PREWARM_CHUNK_BYTES") + .unwrap_or_else(|_| DEFAULT_PREWARM_CHUNK_TARGET_BYTES.to_string()) + .parse() + .expect("failed to parse LANCE_FMINDEX_PREWARM_CHUNK_BYTES") + }); +static LANCE_FMINDEX_DEMAND_PAGE_BYTES: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + std::env::var("LANCE_FMINDEX_DEMAND_PAGE_BYTES") + .unwrap_or_else(|_| DEFAULT_DEMAND_PAGE_TARGET_BYTES.to_string()) + .parse() + .expect("failed to parse LANCE_FMINDEX_DEMAND_PAGE_BYTES") + }); +static LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY: std::sync::LazyLock = + std::sync::LazyLock::new(|| { + std::env::var("LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY") + .unwrap_or_else(|_| "1".to_string()) + .parse() + .expect("failed to parse LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY") + }); fn fmindex_partition_path(partition_id: u64) -> String { format!("part_{partition_id}_fm.lance") } +fn fmindex_partition_id_from_path(path: &str) -> Option { + path.strip_prefix("part_") + .and_then(|r| r.strip_suffix("_fm.lance")) + .and_then(|s| s.parse::().ok()) +} + fn fmindex_num_workers() -> usize { (*LANCE_FMINDEX_NUM_WORKERS).max(1) } @@ -104,6 +146,26 @@ fn fmindex_write_queue_size() -> usize { (*LANCE_FMINDEX_WRITE_QUEUE_SIZE).max(1) } +fn fmindex_resume_existing_partitions() -> bool { + *LANCE_FMINDEX_RESUME_EXISTING_PARTITIONS +} + +fn fmindex_prewarm_chunk_bytes() -> usize { + (*LANCE_FMINDEX_PREWARM_CHUNK_BYTES).max(BLOCK_WORDS * 8) +} + +fn fmindex_demand_page_bytes() -> usize { + (*LANCE_FMINDEX_DEMAND_PAGE_BYTES).max(BLOCK_WORDS * 8) +} + +fn fmindex_demand_page_rows() -> usize { + (fmindex_demand_page_bytes() / (BLOCK_WORDS * 8)).max(1) +} + +fn fmindex_prewarm_chunk_concurrency() -> usize { + (*LANCE_FMINDEX_PREWARM_CHUNK_CONCURRENCY).max(1) +} + // ── Bitvector with O(1) rank ───────────────────────────────────────────────── const SUPERBLOCK_BITS: usize = 512; @@ -474,18 +536,82 @@ impl LazyRankBitVec { } } - /// Pre-load all blocks into memory. Call this before sync rank/access operations - /// to avoid the need for `block_in_place` during queries. - async fn load_all_blocks(&self) -> Result<()> { - for (idx, lock) in self.blocks.iter().enumerate() { - if lock.get().is_none() { - let words = self.load_block(idx).await?; - let _ = lock.set(words); + async fn load_block_if_needed(&self, idx: usize) -> Result<()> { + if idx >= self.blocks.len() { + return Err(Error::index(format!( + "FM-Index block {idx} is out of range for {} blocks", + self.blocks.len() + ))); + } + if self.blocks[idx].get().is_none() { + self.load_page_containing(idx).await?; + } + Ok(()) + } + + async fn load_page_containing(&self, idx: usize) -> Result<()> { + let page_rows = fmindex_demand_page_rows(); + let start = (idx / page_rows) * page_rows; + let end = (start + page_rows).min(self.blocks.len()); + if self.blocks[start..end] + .iter() + .all(|block| block.get().is_some()) + { + return Ok(()); + } + + let batch = self + .reader + .read_range( + self.block_row_offset + start..self.block_row_offset + end, + Some(&["words"]), + ) + .await?; + if batch.num_rows() != end - start { + return Err(Error::index(format!( + "expected {} FM-Index block rows, got {}", + end - start, + batch.num_rows() + ))); + } + + let col = Self::words_column(&batch)?; + for row in 0..batch.num_rows() { + let block_idx = start + row; + if self.blocks[block_idx].get().is_none() { + let words = Self::decode_words(col.value(row)); + let _ = self.blocks[block_idx].set(words); } } Ok(()) } + async fn load_block_for_rank(&self, pos: usize) -> Result<()> { + if pos > self.len { + return Err(Error::invalid_input(format!( + "FM-Index rank position {pos} exceeds bitvector length {}", + self.len + ))); + } + if pos == 0 { + return Ok(()); + } + if pos == self.len && pos.is_multiple_of(BLOCK_BITS) { + if let Some(last_idx) = self.blocks.len().checked_sub(1) { + return self.load_block_if_needed(last_idx).await; + } + return Ok(()); + } + if pos.is_multiple_of(BLOCK_BITS) { + return Ok(()); + } + self.load_block_if_needed(pos / BLOCK_BITS).await + } + + async fn load_block_for_access(&self, pos: usize) -> Result<()> { + self.load_block_if_needed(pos / BLOCK_BITS).await + } + #[inline] fn ensure_block(&self, idx: usize) -> &[u64] { self.blocks[idx].get_or_init(|| { @@ -502,27 +628,60 @@ impl LazyRankBitVec { .reader .read_range(row..row + 1, Some(&["words"])) .await?; - let col = batch + let col = Self::words_column(&batch)?; + Ok(Self::decode_words(col.value(0))) + } + + fn words_column(batch: &RecordBatch) -> Result<&arrow_array::LargeBinaryArray> { + batch .column(0) .as_any() .downcast_ref::() - .ok_or_else(|| Error::invalid_input("expected LargeBinary words column"))?; - Ok(col - .value(0) - .chunks_exact(8) + .ok_or_else(|| Error::invalid_input("expected LargeBinary words column")) + } + + fn decode_words(raw: &[u8]) -> Vec { + raw.chunks_exact(8) .map(|c| u64::from_le_bytes(c.try_into().unwrap())) - .collect()) + .collect() + } + + fn terminal_rank1(&self) -> usize { + if self.len == 0 { + return 0; + } + let Some(last_idx) = self.blocks.len().checked_sub(1) else { + return 0; + }; + let mut count = self.prefix_ranks.get(last_idx).copied().unwrap_or(0) as usize; + let block = self.ensure_block(last_idx); + let local = self.len - last_idx * BLOCK_BITS; + let full_words = local / 64; + let trailing_bits = local % 64; + for w in &block[..full_words] { + count += w.count_ones() as usize; + } + if trailing_bits > 0 { + count += (block[full_words] & ((1u64 << trailing_bits) - 1)).count_ones() as usize; + } + count } #[inline] fn rank1(&self, pos: usize) -> usize { + debug_assert!(pos <= self.len); if pos == 0 { return 0; } let bi = pos / BLOCK_BITS; let local = pos % BLOCK_BITS; if local == 0 { - return self.prefix_ranks[bi] as usize; + if let Some(prefix_rank) = self.prefix_ranks.get(bi) { + return *prefix_rank as usize; + } + if pos == self.len { + return self.terminal_rank1(); + } } let mut count = self.prefix_ranks[bi] as usize; let block = self.ensure_block(bi); @@ -544,6 +703,7 @@ impl LazyRankBitVec { #[inline] fn get(&self, pos: usize) -> bool { + debug_assert!(pos < self.len); let bi = pos / BLOCK_BITS; let local = pos % BLOCK_BITS; let block = self.ensure_block(bi); @@ -579,9 +739,105 @@ impl std::fmt::Debug for LazyHuffmanWaveletTree { impl LazyHuffmanWaveletTree { /// Pre-load all wavelet tree blocks into memory. async fn load_all(&self) -> Result<()> { - for node in &self.nodes { - node.load_all_blocks().await?; + if self.nodes.is_empty() { + return Ok(()); + } + + #[derive(Clone, Copy)] + struct RowRange { + start: usize, + end: usize, + node_idx: usize, } + + let mut ranges = self + .nodes + .iter() + .enumerate() + .filter(|(_, node)| !node.blocks.is_empty()) + .map(|(node_idx, node)| RowRange { + start: node.block_row_offset, + end: node.block_row_offset + node.blocks.len(), + node_idx, + }) + .collect::>(); + ranges.sort_by_key(|range| range.start); + let Some(total_rows) = ranges.iter().map(|range| range.end).max() else { + return Ok(()); + }; + let ranges = Arc::new(ranges); + + let chunk_loaded = |start: usize, end: usize| { + ranges.iter().all(|range| { + let overlap_start = start.max(range.start); + let overlap_end = end.min(range.end); + if overlap_start >= overlap_end { + return true; + } + let node = &self.nodes[range.node_idx]; + node.blocks[overlap_start - range.start..overlap_end - range.start] + .iter() + .all(|block| block.get().is_some()) + }) + }; + + let chunk_rows = (fmindex_prewarm_chunk_bytes() / (BLOCK_WORDS * 8)).max(1); + let reader = Arc::clone(&self.nodes[0].reader); + let chunks = (0..total_rows) + .step_by(chunk_rows) + .map(|start| { + let end = (start + chunk_rows).min(total_rows); + (start, end) + }) + .filter(|(start, end)| !chunk_loaded(*start, *end)) + .collect::>(); + + futures::stream::iter(chunks) + .map(|(start, end)| { + let reader = Arc::clone(&reader); + async move { + let batch = reader.read_range(start..end, Some(&["words"])).await?; + Result::<_>::Ok((start, end, batch)) + } + }) + .buffer_unordered(fmindex_prewarm_chunk_concurrency()) + .try_for_each(|(start, end, batch)| { + let ranges = Arc::clone(&ranges); + let nodes = &self.nodes; + async move { + if batch.num_rows() != end - start { + return Err(Error::index(format!( + "expected {} FM-Index block rows, got {}", + end - start, + batch.num_rows() + ))); + } + + let col = LazyRankBitVec::words_column(&batch)?; + let mut range_idx = ranges.partition_point(|range| range.end <= start); + for row in 0..batch.num_rows() { + let absolute_row = start + row; + while range_idx < ranges.len() && absolute_row >= ranges[range_idx].end { + range_idx += 1; + } + if range_idx == ranges.len() || absolute_row < ranges[range_idx].start { + continue; + } + + let range = ranges[range_idx]; + let block_idx = absolute_row - range.start; + let node = &nodes[range.node_idx]; + if node.blocks[block_idx].get().is_none() { + let words = LazyRankBitVec::decode_words(col.value(row)); + let _ = node.blocks[block_idx].set(words); + } + } + + Ok(()) + } + }) + .await?; + Ok(()) } @@ -610,6 +866,31 @@ impl LazyHuffmanWaveletTree { } } + async fn access_async(&self, mut pos: usize) -> Result { + if self.nodes.is_empty() { + return Ok(0); + } + let mut node_idx = 0; + loop { + self.nodes[node_idx].load_block_for_access(pos).await?; + let bit = self.nodes[node_idx].get(pos); + let (ref left, ref right) = self.children[node_idx]; + if bit { + pos = self.nodes[node_idx].rank1(pos); + match right { + WaveletChild::Leaf(b) => return Ok(*b), + WaveletChild::Node(next) => node_idx = *next, + } + } else { + pos = self.nodes[node_idx].rank0(pos); + match left { + WaveletChild::Leaf(b) => return Ok(*b), + WaveletChild::Node(next) => node_idx = *next, + } + } + } + } + #[inline] fn rank(&self, c: u8, pos: usize) -> usize { let code = &self.codes[c as usize]; @@ -629,6 +910,26 @@ impl LazyHuffmanWaveletTree { hi - lo } + async fn rank_async(&self, c: u8, pos: usize) -> Result { + let code = &self.codes[c as usize]; + if code.length == 0 { + return Ok(0); + } + let (mut lo, mut hi) = (0, pos); + for (level, &nid) in code.node_path.iter().enumerate() { + self.nodes[nid].load_block_for_rank(lo).await?; + self.nodes[nid].load_block_for_rank(hi).await?; + if (code.bits >> (code.length - 1 - level as u8)) & 1 == 0 { + lo = self.nodes[nid].rank0(lo); + hi = self.nodes[nid].rank0(hi); + } else { + lo = self.nodes[nid].rank1(lo); + hi = self.nodes[nid].rank1(hi); + } + } + Ok(hi - lo) + } + #[inline] fn rank_pair(&self, c: u8, lo: usize, hi: usize) -> (usize, usize) { let code = &self.codes[c as usize]; @@ -650,6 +951,29 @@ impl LazyHuffmanWaveletTree { (l - s, h - s) } + async fn rank_pair_async(&self, c: u8, lo: usize, hi: usize) -> Result<(usize, usize)> { + let code = &self.codes[c as usize]; + if code.length == 0 { + return Ok((0, 0)); + } + let (mut s, mut l, mut h) = (0, lo, hi); + for (level, &nid) in code.node_path.iter().enumerate() { + self.nodes[nid].load_block_for_rank(s).await?; + self.nodes[nid].load_block_for_rank(l).await?; + self.nodes[nid].load_block_for_rank(h).await?; + if (code.bits >> (code.length - 1 - level as u8)) & 1 == 0 { + s = self.nodes[nid].rank0(s); + l = self.nodes[nid].rank0(l); + h = self.nodes[nid].rank0(h); + } else { + s = self.nodes[nid].rank1(s); + l = self.nodes[nid].rank1(l); + h = self.nodes[nid].rank1(h); + } + } + Ok((l - s, h - s)) + } + fn deep_size(&self) -> usize { self.nodes.iter().map(|n| n.deep_size()).sum::() + self @@ -998,12 +1322,18 @@ struct LazyFMIndex { sa_samples: Vec, doc_start_positions: Vec, c_table: Vec, + fully_prewarmed: AtomicBool, } impl LazyFMIndex { /// Pre-load all wavelet tree blocks before sync search operations. async fn prewarm(&self) -> Result<()> { - self.wavelet.load_all().await + if self.fully_prewarmed.load(Ordering::Acquire) { + return Ok(()); + } + self.wavelet.load_all().await.inspect(|_| { + self.fully_prewarmed.store(true, Ordering::Release); + }) } fn backward_search(&self, pattern: &[u8]) -> (usize, usize) { @@ -1023,6 +1353,23 @@ impl LazyFMIndex { (lo, hi) } + async fn backward_search_async(&self, pattern: &[u8]) -> Result<(usize, usize)> { + if pattern.is_empty() || self.wavelet.len == 0 { + return Ok((0, 0)); + } + let (mut lo, mut hi) = (0, self.wavelet.len); + for &b in pattern.iter().rev() { + let c = self.c_table[b as usize]; + let (occ_lo, occ_hi) = self.wavelet.rank_pair_async(b, lo, hi).await?; + lo = c + occ_lo; + hi = c + occ_hi; + if lo >= hi { + return Ok((0, 0)); + } + } + Ok((lo, hi)) + } + #[inline] fn locate(&self, mut pos: usize) -> usize { let mut steps = 0; @@ -1042,6 +1389,24 @@ impl LazyFMIndex { } } + async fn locate_async(&self, mut pos: usize) -> Result { + let mut steps = 0; + let n = self.wavelet.len; + loop { + if pos.is_multiple_of(SA_SAMPLE_RATE) && (pos / SA_SAMPLE_RATE) < self.sa_samples.len() + { + return Ok((self.sa_samples[pos / SA_SAMPLE_RATE] as usize + steps) % n); + } + let c = self.wavelet.access_async(pos).await?; + pos = self.c_table[c as usize] + self.wavelet.rank_async(c, pos).await?; + steps += 1; + if steps >= n { + log::warn!("FM-Index SA locate exceeded {n} steps, possible index corruption"); + return Ok(0); + } + } + } + #[inline] fn doc_for_position(&self, text_pos: usize) -> usize { let tp = text_pos as u64; @@ -1085,6 +1450,24 @@ impl LazyFMIndex { result } + async fn search_row_addrs_async(&self, pattern: &[u8]) -> Result> { + let (lo, hi) = self.backward_search_async(pattern).await?; + if lo >= hi { + return Ok(Vec::new()); + } + let mut seen = std::collections::HashSet::new(); + let mut result = Vec::new(); + for i in lo..hi { + let text_pos = self.locate_async(i).await?; + let doc_idx = self.doc_for_position(text_pos); + let row_addr = self.row_ids[doc_idx]; + if seen.insert(row_addr) { + result.push(row_addr); + } + } + Ok(result) + } + #[allow(clippy::too_many_arguments)] async fn from_reader( reader: Arc, @@ -1196,6 +1579,7 @@ impl LazyFMIndex { sa_samples, doc_start_positions, c_table, + fully_prewarmed: AtomicBool::new(false), }) } @@ -1220,6 +1604,7 @@ struct FMIndexPartition { #[derive(Debug)] pub struct FMIndexScalarIndex { partitions: Vec>, + io_parallelism: usize, } impl DeepSizeOf for FMIndexScalarIndex { @@ -1307,12 +1692,7 @@ impl FMIndexScalarIndex { let files = store.list_files_with_sizes().await?; let mut pfiles: Vec<(u64, String)> = Vec::new(); for f in &files { - if let Some(id) = f - .path - .strip_prefix("part_") - .and_then(|r| r.strip_suffix("_fm.lance")) - .and_then(|s| s.parse::().ok()) - { + if let Some(id) = fmindex_partition_id_from_path(&f.path) { pfiles.push((id, f.path.clone())); } } @@ -1320,13 +1700,73 @@ impl FMIndexScalarIndex { return Err(Error::invalid_input("no FM-Index partition files found")); } pfiles.sort_by_key(|(id, _)| *id); - let mut parts = Vec::with_capacity(pfiles.len()); - for (id, name) in &pfiles { - parts.push(Arc::new( - Self::load_partition(store.as_ref(), name, *id).await?, - )); - } - Ok(Arc::new(Self { partitions: parts })) + let io_parallelism = store.io_parallelism().max(1); + let mut parts = futures::stream::iter(pfiles.into_iter()) + .map(|(id, name)| { + let store = Arc::clone(&store); + async move { + let partition = Self::load_partition(store.as_ref(), &name, id).await?; + Result::<_>::Ok((id, Arc::new(partition))) + } + }) + .buffer_unordered(io_parallelism) + .try_collect::>() + .await?; + parts.sort_by_key(|(id, _)| *id); + let parts = parts + .into_iter() + .map(|(_, partition)| partition) + .collect::>(); + Ok(Arc::new(Self { + partitions: parts, + io_parallelism, + })) + } + + fn partition_parallelism(&self) -> usize { + self.io_parallelism.max(1).min(self.partitions.len().max(1)) + } + + async fn prewarm_partitions(&self) -> Result<()> { + futures::stream::iter(self.partitions.iter().cloned()) + .map(|partition| async move { partition.fm.prewarm().await }) + .buffer_unordered(self.partition_parallelism()) + .try_collect::>() + .await?; + Ok(()) + } + + async fn search_string_contains(&self, pattern: &[u8]) -> Result { + use lance_select::RowAddrTreeMap; + + let pattern: Arc<[u8]> = Arc::from(pattern); + let tree = futures::stream::iter(self.partitions.iter().cloned()) + .map(|partition| { + let pattern = Arc::clone(&pattern); + async move { + if partition.fm.fully_prewarmed.load(Ordering::Acquire) { + spawn_cpu(move || { + Result::>::Ok(partition.fm.search_row_addrs(pattern.as_ref())) + }) + .await + } else { + partition.fm.search_row_addrs_async(pattern.as_ref()).await + } + } + }) + .buffer_unordered(self.partition_parallelism()) + .try_fold(RowAddrTreeMap::new(), |mut tree, row_addrs| async move { + for row_addr in row_addrs { + tree.insert(row_addr); + } + Result::Ok(tree) + }) + .await?; + + Ok(SearchResult::Exact(lance_select::NullableRowAddrSet::new( + tree, + Default::default(), + ))) } } @@ -1339,7 +1779,7 @@ impl Index for FMIndexScalarIndex { self } async fn prewarm(&self) -> Result<()> { - Ok(()) + self.prewarm_partitions().await } fn statistics(&self) -> Result { Ok(serde_json::json!({ @@ -1376,19 +1816,7 @@ impl ScalarIndex for FMIndexScalarIndex { .ok_or_else(|| Error::invalid_input("Fm only supports TextQuery"))?; match tq { TextQuery::StringContains(pattern) => { - let pb = pattern.as_bytes(); - use lance_select::RowAddrTreeMap; - let mut tree = RowAddrTreeMap::new(); - for p in &self.partitions { - p.fm.prewarm().await?; - for row_addr in p.fm.search_row_addrs(pb) { - tree.insert(row_addr); - } - } - Ok(SearchResult::Exact(lance_select::NullableRowAddrSet::new( - tree, - Default::default(), - ))) + self.search_string_contains(pattern.as_bytes()).await } // Regex queries are routed only to the ngram index (the FM-index's // query parser advertises `supports_regex = false`), so this is @@ -1445,6 +1873,7 @@ struct FMIndexPartitionConfig { max_rows: usize, max_bytes: usize, queue_size: usize, + resume_existing: bool, } impl FMIndexPartitionConfig { @@ -1454,6 +1883,7 @@ impl FMIndexPartitionConfig { max_rows: fmindex_partition_rows(), max_bytes: fmindex_partition_bytes(), queue_size: fmindex_write_queue_size(), + resume_existing: fmindex_resume_existing_partitions(), } } @@ -1463,6 +1893,7 @@ impl FMIndexPartitionConfig { max_rows: self.max_rows.max(1), max_bytes: self.max_bytes.max(1), queue_size: self.queue_size.max(1), + resume_existing: self.resume_existing, } } } @@ -1493,6 +1924,23 @@ async fn write_partitioned_fmindex_stream_with_config( async_channel::Receiver, ) = async_channel::bounded(config.queue_size); let store = store.clone_arc(); + let mut completed_files = if config.resume_existing { + store + .list_files_with_sizes() + .await? + .into_iter() + .filter_map(|file| fmindex_partition_id_from_path(&file.path).map(|id| (id, file))) + .collect::>() + } else { + HashMap::new() + }; + if !completed_files.is_empty() { + log::info!( + "resuming FMIndex build with {} existing partition files", + completed_files.len() + ); + } + let mut files = Vec::new(); let mut worker_tasks = Vec::with_capacity(config.num_workers); for _ in 0..config.num_workers { let receiver = receiver.clone(); @@ -1541,12 +1989,17 @@ async fn write_partitioned_fmindex_stream_with_config( config.max_rows, config.max_bytes, ) { - send_fmindex_partition( + finish_fmindex_partition( &sender, + store.as_ref(), &mut partition, &mut partition_bytes, partition_id, config.max_rows, + &mut FMIndexPartitionResumeState { + completed_files: &mut completed_files, + output_files: &mut files, + }, ) .await?; partition_id += 1; @@ -1556,12 +2009,17 @@ async fn write_partitioned_fmindex_stream_with_config( } if !partition.is_empty() { - send_fmindex_partition( + finish_fmindex_partition( &sender, + store.as_ref(), &mut partition, &mut partition_bytes, partition_id, config.max_rows, + &mut FMIndexPartitionResumeState { + completed_files: &mut completed_files, + output_files: &mut files, + }, ) .await?; } @@ -1571,7 +2029,6 @@ async fn write_partitioned_fmindex_stream_with_config( .await; drop(sender); - let mut files = Vec::new(); let mut worker_error = None; for worker_task in worker_tasks { match worker_task.await { @@ -1594,6 +2051,14 @@ async fn write_partitioned_fmindex_stream_with_config( return Err(err); } producer_result?; + + for (partition_id, file) in completed_files.drain() { + log::info!( + "deleting stale FMIndex partition {partition_id} from previous build: {}", + file.path + ); + store.delete_index_file(&file.path).await?; + } if files.is_empty() { return Ok(vec![write_empty_fmindex_partition(store.as_ref()).await?]); } @@ -1615,15 +2080,34 @@ fn fmindex_partition_limit_reached( rows >= max_rows || bytes >= max_bytes } -async fn send_fmindex_partition( +struct FMIndexPartitionResumeState<'a> { + completed_files: &'a mut HashMap, + output_files: &'a mut Vec<(u64, IndexFile)>, +} + +async fn finish_fmindex_partition( sender: &async_channel::Sender, + store: &dyn IndexStore, partition: &mut Vec<(u64, Vec)>, partition_bytes: &mut usize, partition_id: u64, max_rows: usize, + resume_state: &mut FMIndexPartitionResumeState<'_>, ) -> Result<()> { let texts = std::mem::replace(partition, Vec::with_capacity(max_rows.min(PARTITION_SIZE))); *partition_bytes = 0; + if let Some(file) = resume_state.completed_files.remove(&partition_id) { + let fingerprint = fmindex_partition_fingerprint(&texts); + if fmindex_partition_matches(store, &file, &fingerprint).await? { + resume_state.output_files.push((partition_id, file)); + return Ok(()); + } + log::info!( + "rebuilding stale FMIndex partition {partition_id}: {}", + file.path + ); + store.delete_index_file(&file.path).await?; + } sender .send(FMIndexPartitionJob { partition_id, @@ -1637,6 +2121,41 @@ async fn send_fmindex_partition( }) } +fn fmindex_partition_fingerprint(texts: &[(u64, Vec)]) -> String { + const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325; + const FNV_PRIME: u64 = 0x0000_0100_0000_01b3; + + fn update(hash: &mut u64, bytes: &[u8]) { + for byte in bytes { + *hash ^= *byte as u64; + *hash = hash.wrapping_mul(FNV_PRIME); + } + } + + let mut hash = FNV_OFFSET; + update(&mut hash, b"lance-fmindex-partition-v1"); + update(&mut hash, &(texts.len() as u64).to_le_bytes()); + for (row_id, bytes) in texts { + update(&mut hash, &row_id.to_le_bytes()); + update(&mut hash, &(bytes.len() as u64).to_le_bytes()); + update(&mut hash, bytes); + } + format!("{hash:016x}") +} + +async fn fmindex_partition_matches( + store: &dyn IndexStore, + file: &IndexFile, + expected_fingerprint: &str, +) -> Result { + let reader = store.open_index_file(&file.path).await?; + Ok(reader + .schema() + .metadata + .get(FMINDEX_PARTITION_FINGERPRINT_KEY) + .is_some_and(|fingerprint| fingerprint == expected_fingerprint)) +} + fn sanitize_text_bytes(bytes: &[u8]) -> Vec { bytes .iter() @@ -1764,7 +2283,12 @@ fn hex_decode(s: &str) -> Result> { /// - Wavelet block rows (BWT nodes) /// - SA sample blocks (packed u64 in LargeBinary) /// - Metadata: c_table, huffman_codes, tree_topology, row_ids, doc_start_positions -async fn write_fmindex(fm: &FMIndex, store: &dyn IndexStore, filename: &str) -> Result { +async fn write_fmindex( + fm: &FMIndex, + store: &dyn IndexStore, + filename: &str, + partition_fingerprint: Option<&str>, +) -> Result { let schema = Arc::new(FMIndex::block_schema()); let mut writer = store.new_index_file(filename, schema.clone()).await?; @@ -1814,6 +2338,12 @@ async fn write_fmindex(fm: &FMIndex, store: &dyn IndexStore, filename: &str) -> metadata.insert("total_wavelet_rows".into(), nw.to_string()); metadata.insert("sa_sample_rate".into(), SA_SAMPLE_RATE.to_string()); metadata.insert("alphabet_size".into(), fm.alphabet_size.to_string()); + if let Some(fingerprint) = partition_fingerprint { + metadata.insert( + FMINDEX_PARTITION_FINGERPRINT_KEY.into(), + fingerprint.to_string(), + ); + } metadata.insert("c_table".into(), hex_encode(&fm.serialize_c_table())); metadata.insert( "huffman_codes".into(), @@ -1858,9 +2388,16 @@ async fn write_fmindex_partition( store: &dyn IndexStore, partition_id: u64, ) -> Result { + let fingerprint = fmindex_partition_fingerprint(texts); let refs: Vec<(u64, &[u8])> = texts.iter().map(|(id, t)| (*id, t.as_slice())).collect(); let fm = FMIndex::build(&refs)?; - write_fmindex(&fm, store, &fmindex_partition_path(partition_id)).await + write_fmindex( + &fm, + store, + &fmindex_partition_path(partition_id), + Some(&fingerprint), + ) + .await } async fn write_fmindex_partition_owned( @@ -1868,18 +2405,26 @@ async fn write_fmindex_partition_owned( store: Arc, partition_id: u64, ) -> Result<(u64, IndexFile)> { + let fingerprint = fmindex_partition_fingerprint(&texts); let fm = spawn_cpu(move || { let refs: Vec<(u64, &[u8])> = texts.iter().map(|(id, t)| (*id, t.as_slice())).collect(); FMIndex::build(&refs) }) .await?; - let file = write_fmindex(&fm, store.as_ref(), &fmindex_partition_path(partition_id)).await?; + let file = write_fmindex( + &fm, + store.as_ref(), + &fmindex_partition_path(partition_id), + Some(&fingerprint), + ) + .await?; Ok((partition_id, file)) } async fn write_empty_fmindex_partition(store: &dyn IndexStore) -> Result { let fm = FMIndex::build(&[])?; - write_fmindex(&fm, store, &fmindex_partition_path(0)).await + let fingerprint = fmindex_partition_fingerprint(&[]); + write_fmindex(&fm, store, &fmindex_partition_path(0), Some(&fingerprint)).await } // ── Plugin ─────────────────────────────────────────────────────────────────── @@ -2043,6 +2588,29 @@ mod tests { } } + fn loaded_wavelet_blocks(index: &FMIndexScalarIndex) -> usize { + index + .partitions + .iter() + .flat_map(|partition| partition.fm.wavelet.nodes.iter()) + .map(|node| { + node.blocks + .iter() + .filter(|block| block.get().is_some()) + .count() + }) + .sum() + } + + fn total_wavelet_blocks(index: &FMIndexScalarIndex) -> usize { + index + .partitions + .iter() + .flat_map(|partition| partition.fm.wavelet.nodes.iter()) + .map(|node| node.blocks.len()) + .sum() + } + #[test] fn test_fmindex_build_and_search() { let texts: Vec<(u64, &[u8])> = vec![ @@ -2334,7 +2902,7 @@ mod tests { )); // Write - write_fmindex(&fm, store.as_ref(), &fmindex_partition_path(0)) + write_fmindex(&fm, store.as_ref(), &fmindex_partition_path(0), None) .await .unwrap(); @@ -2429,6 +2997,144 @@ mod tests { } } + #[tokio::test(flavor = "multi_thread")] + async fn test_public_prewarm_loads_lazy_blocks() { + let docs: Vec> = (0..30) + .map(|i| format!("document {i} hello world test data").into_bytes()) + .collect(); + let texts: Vec<(u64, Vec)> = docs + .into_iter() + .enumerate() + .map(|(i, d)| (i as u64, d)) + .collect(); + + let tempdir = tempfile::tempdir().unwrap(); + let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap(); + let store = Arc::new(LanceIndexStore::new( + Arc::new(ObjectStore::local()), + index_dir, + Arc::new(LanceCache::no_cache()), + )); + + write_partitioned_fmindex(&texts, store.as_ref()) + .await + .unwrap(); + + let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache()) + .await + .unwrap(); + let total_blocks = total_wavelet_blocks(index.as_ref()); + assert!(total_blocks > 0); + assert_eq!(loaded_wavelet_blocks(index.as_ref()), 0); + + index.prewarm().await.unwrap(); + assert_eq!(loaded_wavelet_blocks(index.as_ref()), total_blocks); + + let r = index + .search( + &TextQuery::StringContains("hello world".to_string()), + &crate::metrics::NoOpMetricsCollector, + ) + .await + .unwrap(); + match r { + SearchResult::Exact(set) => { + assert_eq!(set.len(), Some(30)); + } + _ => panic!("expected exact result"), + } + assert_eq!(loaded_wavelet_blocks(index.as_ref()), total_blocks); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_lazy_rank_terminal_block_boundary() { + let tempdir = tempfile::tempdir().unwrap(); + let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap(); + let store = Arc::new(LanceIndexStore::new( + Arc::new(ObjectStore::local()), + index_dir, + Arc::new(LanceCache::no_cache()), + )); + let schema = Arc::new(arrow_schema::Schema::new(vec![Field::new( + "words", + DataType::LargeBinary, + false, + )])); + let words = vec![u64::MAX; BLOCK_WORDS]; + let bytes = FMIndex::u64_to_bytes(&words); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(LargeBinaryArray::from(vec![bytes.as_slice()]))], + ) + .unwrap(); + let mut writer = store.new_index_file("rank.lance", schema).await.unwrap(); + writer.write_record_batch(batch).await.unwrap(); + writer.finish().await.unwrap(); + + let reader = store.open_index_file("rank.lance").await.unwrap(); + let bitvec = LazyRankBitVec::new(vec![0], 1, reader, 0, BLOCK_BITS); + bitvec.load_block_for_rank(BLOCK_BITS).await.unwrap(); + + assert_eq!(bitvec.rank1(BLOCK_BITS), BLOCK_BITS); + assert_eq!(bitvec.rank0(BLOCK_BITS), 0); + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_contains_search_does_not_full_prewarm_partitions() { + let docs: Vec> = (0..30) + .map(|i| format!("document {i} hello world test data").into_bytes()) + .collect(); + let texts: Vec<(u64, Vec)> = docs + .into_iter() + .enumerate() + .map(|(i, d)| (i as u64, d)) + .collect(); + + let tempdir = tempfile::tempdir().unwrap(); + let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap(); + let store = Arc::new(LanceIndexStore::new( + Arc::new(ObjectStore::local()), + index_dir, + Arc::new(LanceCache::no_cache()), + )); + + write_partitioned_fmindex(&texts, store.as_ref()) + .await + .unwrap(); + + let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache()) + .await + .unwrap(); + assert_eq!(loaded_wavelet_blocks(index.as_ref()), 0); + assert!( + index + .partitions + .iter() + .all(|partition| !partition.fm.fully_prewarmed.load(Ordering::Acquire)) + ); + + let r = index + .search( + &TextQuery::StringContains("document 15".to_string()), + &crate::metrics::NoOpMetricsCollector, + ) + .await + .unwrap(); + match r { + SearchResult::Exact(set) => { + assert_eq!(set.len(), Some(1)); + } + _ => panic!("expected exact result"), + } + + assert!( + index + .partitions + .iter() + .all(|partition| !partition.fm.fully_prewarmed.load(Ordering::Acquire)) + ); + } + #[tokio::test(flavor = "multi_thread")] async fn test_plugin_train_and_load() { let docs = vec!["hello world", "hello rust", "goodbye world"]; @@ -2531,6 +3237,7 @@ mod tests { max_rows: 100, max_bytes: 5, queue_size: 1, + resume_existing: false, }, ) .await @@ -2559,6 +3266,209 @@ mod tests { } } + #[tokio::test(flavor = "multi_thread")] + async fn test_stream_train_resumes_existing_partitions() { + let schema = Arc::new(arrow_schema::Schema::new(vec![ + arrow_schema::Field::new( + crate::scalar::registry::VALUE_COLUMN_NAME, + DataType::Utf8, + false, + ), + arrow_schema::Field::new(ROW_ADDR, DataType::UInt64, false), + ])); + let tempdir = tempfile::tempdir().unwrap(); + let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap(); + let store = Arc::new(LanceIndexStore::new( + Arc::new(ObjectStore::local()), + index_dir, + Arc::new(LanceCache::no_cache()), + )); + + let first_batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(StringArray::from(vec!["first partition"])), + Arc::new(UInt64Array::from(vec![0])), + ], + ) + .unwrap(); + let first_stream = + RecordBatchStreamAdapter::new(schema.clone(), stream::iter(vec![Ok(first_batch)])); + let first_files = write_partitioned_fmindex_stream_with_config( + Box::pin(first_stream), + store.as_ref(), + FMIndexPartitionConfig { + num_workers: 1, + max_rows: 1, + max_bytes: 1024, + queue_size: 1, + resume_existing: false, + }, + ) + .await + .unwrap(); + assert_eq!(first_files.len(), 1); + assert_eq!(first_files[0].path, fmindex_partition_path(0)); + + let resumed_batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(StringArray::from(vec![ + "first partition", + "second partition", + ])), + Arc::new(UInt64Array::from(vec![0, 1])), + ], + ) + .unwrap(); + let resumed_stream = + RecordBatchStreamAdapter::new(schema.clone(), stream::iter(vec![Ok(resumed_batch)])); + let resumed_files = write_partitioned_fmindex_stream_with_config( + Box::pin(resumed_stream), + store.as_ref(), + FMIndexPartitionConfig { + num_workers: 1, + max_rows: 1, + max_bytes: 1024, + queue_size: 1, + resume_existing: true, + }, + ) + .await + .unwrap(); + + let resumed_paths = resumed_files + .iter() + .map(|file| file.path.clone()) + .collect::>(); + assert_eq!( + resumed_paths, + vec![fmindex_partition_path(0), fmindex_partition_path(1)] + ); + + let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache()) + .await + .unwrap(); + let r = index + .search( + &TextQuery::StringContains("partition".to_string()), + &crate::metrics::NoOpMetricsCollector, + ) + .await + .unwrap(); + match r { + SearchResult::Exact(set) => { + assert_eq!(set.len(), Some(2)); + } + _ => panic!("expected exact result"), + } + } + + #[tokio::test(flavor = "multi_thread")] + async fn test_stream_train_rebuilds_stale_resume_partitions() { + let schema = Arc::new(arrow_schema::Schema::new(vec![ + arrow_schema::Field::new( + crate::scalar::registry::VALUE_COLUMN_NAME, + DataType::Utf8, + false, + ), + arrow_schema::Field::new(ROW_ADDR, DataType::UInt64, false), + ])); + let tempdir = tempfile::tempdir().unwrap(); + let index_dir = Path::from_filesystem_path(tempdir.path()).unwrap(); + let store = Arc::new(LanceIndexStore::new( + Arc::new(ObjectStore::local()), + index_dir, + Arc::new(LanceCache::no_cache()), + )); + + let stale_batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(StringArray::from(vec!["stale zero", "stale one"])), + Arc::new(UInt64Array::from(vec![0, 1])), + ], + ) + .unwrap(); + let stale_stream = + RecordBatchStreamAdapter::new(schema.clone(), stream::iter(vec![Ok(stale_batch)])); + write_partitioned_fmindex_stream_with_config( + Box::pin(stale_stream), + store.as_ref(), + FMIndexPartitionConfig { + num_workers: 1, + max_rows: 1, + max_bytes: 1024, + queue_size: 1, + resume_existing: false, + }, + ) + .await + .unwrap(); + + let fresh_batch = RecordBatch::try_new( + schema.clone(), + vec![ + Arc::new(StringArray::from(vec!["fresh zero"])), + Arc::new(UInt64Array::from(vec![0])), + ], + ) + .unwrap(); + let fresh_stream = + RecordBatchStreamAdapter::new(schema.clone(), stream::iter(vec![Ok(fresh_batch)])); + let fresh_files = write_partitioned_fmindex_stream_with_config( + Box::pin(fresh_stream), + store.as_ref(), + FMIndexPartitionConfig { + num_workers: 1, + max_rows: 10, + max_bytes: 1024, + queue_size: 1, + resume_existing: true, + }, + ) + .await + .unwrap(); + + assert_eq!(fresh_files.len(), 1); + assert_eq!(fresh_files[0].path, fmindex_partition_path(0)); + let remaining_paths = store + .list_files_with_sizes() + .await + .unwrap() + .iter() + .map(|file| file.path.clone()) + .collect::>(); + assert_eq!(remaining_paths, vec![fmindex_partition_path(0)]); + + let index = FMIndexScalarIndex::load(store, None, &LanceCache::no_cache()) + .await + .unwrap(); + let r = index + .search( + &TextQuery::StringContains("fresh zero".to_string()), + &crate::metrics::NoOpMetricsCollector, + ) + .await + .unwrap(); + match r { + SearchResult::Exact(set) => assert_eq!(set.len(), Some(1)), + _ => panic!("expected exact result"), + } + + let r = index + .search( + &TextQuery::StringContains("stale one".to_string()), + &crate::metrics::NoOpMetricsCollector, + ) + .await + .unwrap(); + match r { + SearchResult::Exact(set) => assert_eq!(set.len(), Some(0)), + _ => panic!("expected exact result"), + } + } + #[tokio::test(flavor = "multi_thread")] async fn test_stream_train_propagates_worker_write_error() { let schema = Arc::new(arrow_schema::Schema::new(vec![ @@ -2596,6 +3506,7 @@ mod tests { max_rows: 1, max_bytes: 1024, queue_size: 1, + resume_existing: false, }, ) .await diff --git a/rust/lance/Cargo.toml b/rust/lance/Cargo.toml index e5fc4fab63f..36ea5facc29 100644 --- a/rust/lance/Cargo.toml +++ b/rust/lance/Cargo.toml @@ -171,6 +171,14 @@ slow_tests = [] name = "lq" required-features = ["cli"] +[[bin]] +name = "fm_contains_bench" +required-features = ["cli"] + +[[bin]] +name = "fm_index_tool" +required-features = ["cli"] + [[bench]] name = "scalar_index" harness = false diff --git a/rust/lance/src/bin/fm_contains_bench.rs b/rust/lance/src/bin/fm_contains_bench.rs new file mode 100644 index 00000000000..b62ad8699f2 --- /dev/null +++ b/rust/lance/src/bin/fm_contains_bench.rs @@ -0,0 +1,610 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +#![allow(clippy::print_stdout)] + +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use arrow_array::{Array, LargeStringArray, RecordBatch, StringArray, UInt64Array}; +use clap::Parser; +use futures::future::join_all; +use lance::dataset::builder::DatasetBuilder; +use lance::dataset::{Dataset, ReadParams}; +use lance::index::DatasetIndexExt; +use lance::{Error, Result}; +use lance_io::object_store::{ObjectStoreParams, StorageOptionsAccessor}; +use serde::Serialize; +use tokio::sync::Semaphore; +use tokio::time::timeout; + +const ROW_ID_COLUMN: &str = "_rowid"; + +#[derive(Parser, Debug, Clone)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg( + long, + default_value = "az://datasets/mmlb/mmlb_100m_fts_en_fm_20260626.lance" + )] + uri: String, + + #[arg(long, default_value = "full_content_idx")] + index_name: String, + + #[arg(long, default_value = "full_content")] + text_column: String, + + #[arg(long, default_value = "summary_in_image")] + pattern_source_column: String, + + #[arg(long, default_value = "oailancepub")] + storage_account: String, + + #[arg(long, value_parser = parse_storage_option)] + storage_option: Vec<(String, String)>, + + #[arg(long, default_value_t = 1_099_511_627_776)] + index_cache_bytes: usize, + + #[arg(long, default_value_t = 8_589_934_592)] + metadata_cache_bytes: usize, + + #[arg(long, default_value_t = 5_000)] + sample_rows: usize, + + #[arg(long, default_value_t = 5)] + query_term_count: usize, + + #[arg(long, default_value_t = 100)] + k: i64, + + #[arg(long, default_value_t = 0)] + warmup: usize, + + #[arg(long, default_value_t = 4)] + queries: usize, + + #[arg(long, default_value = "1 2 4 8")] + threads: String, + + #[arg(long, default_value_t = 60.0)] + query_timeout_secs: f64, + + #[arg(long, default_value_t = false)] + skip_prewarm: bool, + + #[arg(long, default_value_t = true)] + explain: bool, + + #[arg(long)] + output_json: String, + + #[arg(long)] + output_csv: String, +} + +#[derive(Debug, Clone, Serialize)] +struct QueryPattern { + query_id: String, + pattern: String, + filter_sql: String, +} + +#[derive(Debug, Serialize)] +struct QueryResult { + query_id: String, + pattern: String, + filter_sql: String, + success: bool, + error: Option, + elapsed_ms: f64, + result_count: usize, + checksum: u64, +} + +#[derive(Debug, Serialize)] +struct BatchSummary { + round_type: String, + threads: usize, + queries: usize, + successes: usize, + success_rate: f64, + wall_s: f64, + qps: f64, + mean_ms: f64, + p50_ms: f64, + p95_ms: f64, + p99_ms: f64, + avg_result_count: f64, + max_result_count: usize, + checksum: u64, + results: Vec, +} + +#[derive(Debug, Serialize)] +struct BenchmarkOutput { + uri: String, + index_name: String, + text_column: String, + pattern_source_column: String, + sample_rows: usize, + query_term_count: usize, + k: i64, + warmup: usize, + queries: usize, + threads: Vec, + query_timeout_secs: f64, + index_cache_bytes: usize, + metadata_cache_bytes: usize, + prewarm_ms: Option, + explain_plan: Option, + summaries: Vec, +} + +fn parse_threads(value: &str) -> std::result::Result, String> { + let mut threads = Vec::new(); + for part in value.replace(',', " ").split_whitespace() { + let thread_count = part + .parse::() + .map_err(|err| format!("invalid thread count {part:?}: {err}"))?; + if thread_count == 0 { + return Err("thread counts must be greater than zero".to_string()); + } + threads.push(thread_count); + } + if threads.is_empty() { + return Err("at least one thread count is required".to_string()); + } + Ok(threads) +} + +fn parse_storage_option(value: &str) -> std::result::Result<(String, String), String> { + let Some((key, val)) = value.split_once('=') else { + return Err("storage options must be key=value".to_string()); + }; + if key.is_empty() { + return Err("storage option key cannot be empty".to_string()); + } + Ok((key.to_string(), val.to_string())) +} + +fn sql_quote(value: &str) -> String { + format!("'{}'", value.replace('\'', "''")) +} + +fn query_terms_from_text(text: &str, term_count: usize) -> Result { + let terms = text.split_whitespace().take(term_count).collect::>(); + if terms.len() < term_count { + return Err(Error::invalid_input(format!( + "sampled query text has {} terms, but requested {}", + terms.len(), + term_count + ))); + } + Ok(terms.join(" ")) +} + +fn string_value(batch: &RecordBatch, column: &str, row: usize) -> Result> { + let array = batch + .column_by_name(column) + .ok_or_else(|| Error::invalid_input(format!("column {column:?} not found")))?; + if array.is_null(row) { + return Ok(None); + } + if let Some(strings) = array.as_any().downcast_ref::() { + return Ok(Some(strings.value(row).to_string())); + } + if let Some(strings) = array.as_any().downcast_ref::() { + return Ok(Some(strings.value(row).to_string())); + } + Err(Error::invalid_input(format!( + "column {column:?} must be Utf8 or LargeUtf8, got {:?}", + array.data_type() + ))) +} + +async fn open_dataset(args: &Args) -> Result { + let mut options = HashMap::from([("account_name".to_string(), args.storage_account.clone())]); + for (key, val) in &args.storage_option { + options.insert(key.clone(), val.clone()); + } + let read_params = ReadParams { + index_cache_size_bytes: args.index_cache_bytes, + metadata_cache_size_bytes: args.metadata_cache_bytes, + store_options: Some(ObjectStoreParams { + storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options( + options, + ))), + ..Default::default() + }), + ..Default::default() + }; + + DatasetBuilder::from_uri(&args.uri) + .with_read_params(read_params) + .load() + .await +} + +async fn sample_patterns(dataset: &Dataset, args: &Args) -> Result> { + let total = args.warmup + args.queries; + let mut scanner = dataset.scan(); + scanner + .project(&[args.pattern_source_column.as_str()])? + .limit(Some(args.sample_rows as i64), None)?; + let batch = scanner.try_into_batch().await?; + let mut patterns = Vec::with_capacity(total); + let mut seen = std::collections::HashSet::new(); + + for row in 0..batch.num_rows() { + let Some(text) = string_value(&batch, &args.pattern_source_column, row)? else { + continue; + }; + let pattern = query_terms_from_text(&text, args.query_term_count)?; + if seen.insert(pattern.clone()) { + let filter_sql = format!( + "contains({}, {})", + args.text_column, + sql_quote(pattern.as_str()) + ); + patterns.push(QueryPattern { + query_id: format!("Q{:06}", patterns.len() + 1), + pattern, + filter_sql, + }); + if patterns.len() == total { + return Ok(patterns); + } + } + } + + Err(Error::invalid_input(format!( + "sampled only {} unique patterns, need {}; increase --sample-rows", + patterns.len(), + total + ))) +} + +async fn explain_first_query( + dataset: &Dataset, + pattern: &QueryPattern, + args: &Args, +) -> Result { + let mut scanner = dataset.scan(); + scanner + .with_row_id() + .project::<&str>(&[])? + .filter(&pattern.filter_sql)? + .limit(Some(args.k), None)?; + scanner.explain_plan(false).await +} + +async fn execute_query( + dataset: Arc, + pattern: &QueryPattern, + args: &Args, +) -> Result { + let start = Instant::now(); + let mut scanner = dataset.scan(); + scanner + .with_row_id() + .project::<&str>(&[])? + .filter(&pattern.filter_sql)? + .limit(Some(args.k), None)?; + let batch = scanner.try_into_batch().await?; + let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0; + let row_ids = batch + .column_by_name(ROW_ID_COLUMN) + .ok_or_else(|| Error::invalid_input(format!("{ROW_ID_COLUMN} column missing")))?; + let row_ids = row_ids + .as_any() + .downcast_ref::() + .ok_or_else(|| Error::invalid_input(format!("{ROW_ID_COLUMN} must be UInt64")))?; + let mut checksum = 0_u64; + for row in 0..row_ids.len() { + if !row_ids.is_null(row) { + checksum ^= row_ids.value(row); + } + } + Ok(QueryResult { + query_id: pattern.query_id.clone(), + pattern: pattern.pattern.clone(), + filter_sql: pattern.filter_sql.clone(), + success: true, + error: None, + elapsed_ms, + result_count: batch.num_rows(), + checksum, + }) +} + +async fn run_one(dataset: Arc, pattern: QueryPattern, args: Arc) -> QueryResult { + let start = Instant::now(); + let result = if args.query_timeout_secs > 0.0 { + timeout( + Duration::from_secs_f64(args.query_timeout_secs), + execute_query(dataset, &pattern, &args), + ) + .await + .map_err(|_| { + Error::io(format!( + "query timed out after {}s", + args.query_timeout_secs + )) + }) + .and_then(|inner| inner) + } else { + execute_query(dataset, &pattern, &args).await + }; + + match result { + Ok(result) => result, + Err(err) => QueryResult { + query_id: pattern.query_id, + pattern: pattern.pattern, + filter_sql: pattern.filter_sql, + success: false, + error: Some(err.to_string()), + elapsed_ms: start.elapsed().as_secs_f64() * 1000.0, + result_count: 0, + checksum: 0, + }, + } +} + +async fn run_batch( + dataset: Arc, + args: Arc, + patterns: &[QueryPattern], + thread_count: usize, + round_type: &str, +) -> BatchSummary { + let wall_start = Instant::now(); + let semaphore = Arc::new(Semaphore::new(thread_count)); + let tasks = patterns + .iter() + .map(|pattern| { + let pattern = pattern.clone(); + let dataset = Arc::clone(&dataset); + let args = Arc::clone(&args); + let semaphore = Arc::clone(&semaphore); + tokio::spawn(async move { + let _permit = semaphore.acquire_owned().await.expect("semaphore closed"); + run_one(dataset, pattern, args).await + }) + }) + .collect::>(); + + let results = join_all(tasks) + .await + .into_iter() + .map(|result| match result { + Ok(query_result) => query_result, + Err(err) => QueryResult { + query_id: "join_error".to_string(), + pattern: String::new(), + filter_sql: String::new(), + success: false, + error: Some(err.to_string()), + elapsed_ms: 0.0, + result_count: 0, + checksum: 0, + }, + }) + .collect::>(); + summarize(round_type, thread_count, results, wall_start.elapsed()) +} + +fn percentile(values: &[f64], pct: usize) -> f64 { + if values.is_empty() { + return 0.0; + } + let mut ordered = values.to_vec(); + ordered.sort_by(|left, right| left.total_cmp(right)); + let idx = ((ordered.len() * pct).div_ceil(100)).saturating_sub(1); + ordered[idx.min(ordered.len() - 1)] +} + +fn summarize( + round_type: &str, + threads: usize, + results: Vec, + wall: Duration, +) -> BatchSummary { + let successes = results.iter().filter(|result| result.success).count(); + let latencies = results + .iter() + .filter(|result| result.success) + .map(|result| result.elapsed_ms) + .collect::>(); + let result_counts = results + .iter() + .filter(|result| result.success) + .map(|result| result.result_count) + .collect::>(); + let wall_s = wall.as_secs_f64(); + BatchSummary { + round_type: round_type.to_string(), + threads, + queries: results.len(), + successes, + success_rate: if results.is_empty() { + 0.0 + } else { + successes as f64 / results.len() as f64 + }, + wall_s, + qps: if wall_s == 0.0 { + 0.0 + } else { + successes as f64 / wall_s + }, + mean_ms: if latencies.is_empty() { + 0.0 + } else { + latencies.iter().sum::() / latencies.len() as f64 + }, + p50_ms: percentile(&latencies, 50), + p95_ms: percentile(&latencies, 95), + p99_ms: percentile(&latencies, 99), + avg_result_count: if result_counts.is_empty() { + 0.0 + } else { + result_counts.iter().sum::() as f64 / result_counts.len() as f64 + }, + max_result_count: result_counts.into_iter().max().unwrap_or(0), + checksum: results + .iter() + .filter(|result| result.success) + .map(|result| result.checksum) + .fold(0, |acc, checksum| acc ^ checksum), + results, + } +} + +fn write_csv(path: &str, summaries: &[BatchSummary]) -> Result<()> { + let file = File::create(path)?; + let mut writer = BufWriter::new(file); + writeln!( + writer, + "round_type,threads,queries,successes,success_rate,wall_s,qps,mean_ms,p50_ms,p95_ms,p99_ms,avg_result_count,max_result_count,checksum" + )?; + for summary in summaries { + writeln!( + writer, + "{},{},{},{},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{},{}", + csv_escape(&summary.round_type), + summary.threads, + summary.queries, + summary.successes, + summary.success_rate, + summary.wall_s, + summary.qps, + summary.mean_ms, + summary.p50_ms, + summary.p95_ms, + summary.p99_ms, + summary.avg_result_count, + summary.max_result_count, + summary.checksum + )?; + } + Ok(()) +} + +fn csv_escape(value: &str) -> String { + if value.contains([',', '"', '\n']) { + format!("\"{}\"", value.replace('"', "\"\"")) + } else { + value.to_string() + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Arc::new(Args::parse()); + let thread_counts = parse_threads(&args.threads).map_err(Error::invalid_input)?; + if args.k <= 0 { + return Err(Error::invalid_input("--k must be greater than zero")); + } + if args.query_term_count == 0 { + return Err(Error::invalid_input( + "--query-term-count must be greater than zero", + )); + } + if args.query_timeout_secs < 0.0 { + return Err(Error::invalid_input( + "--query-timeout-secs must be non-negative", + )); + } + + println!("opening dataset {}", args.uri); + let dataset = Arc::new(open_dataset(&args).await?); + println!("dataset version {}", dataset.version().version); + + let patterns = sample_patterns(&dataset, &args).await?; + println!("sampled {} patterns", patterns.len()); + + let explain_plan = if args.explain { + Some(explain_first_query(&dataset, &patterns[0], &args).await?) + } else { + None + }; + + let prewarm_ms = if args.skip_prewarm { + None + } else { + println!("prewarming index {}", args.index_name); + let start = Instant::now(); + dataset.prewarm_index(&args.index_name).await?; + let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0; + println!("prewarm finished in {:.3} ms", elapsed_ms); + Some(elapsed_ms) + }; + + let warmup_patterns = &patterns[..args.warmup]; + let query_patterns = &patterns[args.warmup..]; + let mut summaries = Vec::new(); + for thread_count in thread_counts.iter().copied() { + if !warmup_patterns.is_empty() { + summaries.push( + run_batch( + Arc::clone(&dataset), + Arc::clone(&args), + warmup_patterns, + thread_count, + "warmup", + ) + .await, + ); + } + let summary = run_batch( + Arc::clone(&dataset), + Arc::clone(&args), + query_patterns, + thread_count, + "test", + ) + .await; + println!( + "test threads={} queries={} successes={} wall_s={:.3} qps={:.3} mean_ms={:.3} p95_ms={:.3}", + summary.threads, + summary.queries, + summary.successes, + summary.wall_s, + summary.qps, + summary.mean_ms, + summary.p95_ms + ); + summaries.push(summary); + } + + let output = BenchmarkOutput { + uri: args.uri.clone(), + index_name: args.index_name.clone(), + text_column: args.text_column.clone(), + pattern_source_column: args.pattern_source_column.clone(), + sample_rows: args.sample_rows, + query_term_count: args.query_term_count, + k: args.k, + warmup: args.warmup, + queries: args.queries, + threads: thread_counts, + query_timeout_secs: args.query_timeout_secs, + index_cache_bytes: args.index_cache_bytes, + metadata_cache_bytes: args.metadata_cache_bytes, + prewarm_ms, + explain_plan, + summaries, + }; + + let mut json = serde_json::to_string_pretty(&output)?; + json.push('\n'); + std::fs::write(&args.output_json, json)?; + write_csv(&args.output_csv, &output.summaries)?; + Ok(()) +} diff --git a/rust/lance/src/bin/fm_index_tool.rs b/rust/lance/src/bin/fm_index_tool.rs new file mode 100644 index 00000000000..1eb03b78b26 --- /dev/null +++ b/rust/lance/src/bin/fm_index_tool.rs @@ -0,0 +1,161 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: Copyright The Lance Authors + +#![allow(clippy::print_stdout)] + +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Instant; + +use clap::{Parser, ValueEnum}; +use lance::dataset::ReadParams; +use lance::dataset::builder::DatasetBuilder; +use lance::index::{CreateIndexBuilder, DatasetIndexExt}; +use lance::{Error, Result}; +use lance_index::scalar::{BuiltinIndexType, ScalarIndexParams}; +use lance_io::object_store::{ObjectStoreParams, StorageOptionsAccessor}; +use uuid::Uuid; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[arg(value_enum)] + action: Action, + + #[arg(long)] + uri: String, + + #[arg(long, default_value = "full_content_idx")] + index_name: String, + + #[arg(long, default_value = "full_content")] + column: String, + + #[arg(long, default_value = "oailancepub")] + storage_account: String, + + #[arg(long, value_parser = parse_storage_option)] + storage_option: Vec<(String, String)>, + + #[arg(long, default_value_t = 1)] + num_segments: u32, + + #[arg(long)] + index_uuid: Option, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, ValueEnum)] +enum Action { + List, + Drop, + Create, +} + +fn parse_storage_option(value: &str) -> std::result::Result<(String, String), String> { + let Some((key, val)) = value.split_once('=') else { + return Err("storage options must be key=value".to_string()); + }; + if key.is_empty() { + return Err("storage option key cannot be empty".to_string()); + } + Ok((key.to_string(), val.to_string())) +} + +async fn open_dataset(args: &Args) -> Result { + let mut options = HashMap::from([("account_name".to_string(), args.storage_account.clone())]); + for (key, val) in &args.storage_option { + options.insert(key.clone(), val.clone()); + } + let read_params = ReadParams { + store_options: Some(ObjectStoreParams { + storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options( + options, + ))), + ..Default::default() + }), + ..Default::default() + }; + + DatasetBuilder::from_uri(&args.uri) + .with_read_params(read_params) + .load() + .await +} + +fn fm_params(num_segments: u32) -> ScalarIndexParams { + let params = ScalarIndexParams::for_builtin(BuiltinIndexType::Fm); + if num_segments == 1 { + params + } else { + params.with_params(&serde_json::json!({ "num_segments": num_segments })) + } +} + +#[tokio::main] +async fn main() -> Result<()> { + let args = Args::parse(); + if args.num_segments == 0 { + return Err(Error::invalid_input( + "--num-segments must be greater than 0", + )); + } + + println!("opening dataset {}", args.uri); + let mut dataset = open_dataset(&args).await?; + println!("dataset version {}", dataset.version().version); + + match args.action { + Action::List => { + let indices = dataset.load_indices().await?; + println!("indices={}", indices.len()); + for index in indices.iter() { + println!( + "name={} uuid={} version={} fields={:?} fragments={}", + index.name, + index.uuid, + index.index_version, + index.fields, + index + .fragment_bitmap + .as_ref() + .map(|bitmap| bitmap.len()) + .unwrap_or(0) + ); + } + } + Action::Drop => { + let start = Instant::now(); + dataset.drop_index(&args.index_name).await?; + println!( + "dropped index {} in {:.3}s; new version {}", + args.index_name, + start.elapsed().as_secs_f64(), + dataset.version().version + ); + } + Action::Create => { + let params = fm_params(args.num_segments); + let start = Instant::now(); + let mut builder = CreateIndexBuilder::new( + &mut dataset, + &[args.column.as_str()], + lance_index::IndexType::Fm, + ¶ms, + ) + .name(args.index_name.clone()) + .replace(true); + if let Some(index_uuid) = args.index_uuid { + builder = builder.index_uuid(index_uuid); + } + let metadata = builder.await?; + println!( + "created index {} uuid={} in {:.3}s; new version {}", + metadata.name, + metadata.uuid, + start.elapsed().as_secs_f64(), + dataset.version().version + ); + } + } + Ok(()) +} From 268d3d79dd2ec841dc03cd431c5d5e8cd56f2def Mon Sep 17 00:00:00 2001 From: Lance Release Bot Date: Mon, 29 Jun 2026 17:24:25 +0000 Subject: [PATCH 012/177] chore: release beta version 9.0.0-beta.9 --- .bumpversion.toml | 2 +- Cargo.lock | 48 +++++++++++++++++----------------- Cargo.toml | 44 +++++++++++++++---------------- java/lance-jni/Cargo.lock | 54 +++++++++++++++++---------------------- java/lance-jni/Cargo.toml | 2 +- java/pom.xml | 2 +- python/Cargo.lock | 40 ++++++++++++++--------------- python/Cargo.toml | 2 +- 8 files changed, 93 insertions(+), 101 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index a8e8d03e983..4633f741828 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "9.0.0-beta.8" +current_version = "9.0.0-beta.9" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(-(?P(beta|rc))\\.(?P\\d+))?" serialize = [ "{major}.{minor}.{patch}-{prerelease}.{prerelease_num}", diff --git a/Cargo.lock b/Cargo.lock index d58494f3e48..92976691d0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3076,7 +3076,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "rand 0.9.4", @@ -4380,7 +4380,7 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "all_asserts", "approx", @@ -4483,7 +4483,7 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -4531,7 +4531,7 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrayref", "bitpacking", @@ -4542,7 +4542,7 @@ dependencies = [ [[package]] name = "lance-core" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -4582,7 +4582,7 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -4615,7 +4615,7 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -4634,7 +4634,7 @@ dependencies = [ [[package]] name = "lance-derive" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "proc-macro2", "quote", @@ -4643,7 +4643,7 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-arith", "arrow-array", @@ -4688,7 +4688,7 @@ dependencies = [ [[package]] name = "lance-examples" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "all_asserts", "arrow", @@ -4714,7 +4714,7 @@ dependencies = [ [[package]] name = "lance-file" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-arith", "arrow-array", @@ -4753,7 +4753,7 @@ dependencies = [ [[package]] name = "lance-geo" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "datafusion", "geo-traits", @@ -4767,7 +4767,7 @@ dependencies = [ [[package]] name = "lance-index" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "approx", "arc-swap", @@ -4844,7 +4844,7 @@ dependencies = [ [[package]] name = "lance-io" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-arith", @@ -4892,7 +4892,7 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "approx", "arrow-array", @@ -4912,7 +4912,7 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "async-trait", @@ -4924,7 +4924,7 @@ dependencies = [ [[package]] name = "lance-namespace-datafusion" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-schema", @@ -4940,7 +4940,7 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -5004,7 +5004,7 @@ dependencies = [ [[package]] name = "lance-select" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -5022,7 +5022,7 @@ dependencies = [ [[package]] name = "lance-table" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -5068,7 +5068,7 @@ dependencies = [ [[package]] name = "lance-test-macros" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "proc-macro2", "quote", @@ -5077,7 +5077,7 @@ dependencies = [ [[package]] name = "lance-testing" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-schema", @@ -5090,7 +5090,7 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "icu_segmenter", "jieba-rs", @@ -5103,7 +5103,7 @@ dependencies = [ [[package]] name = "lance-tools" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "clap", "lance-core", diff --git a/Cargo.toml b/Cargo.toml index 5eeccfa7806..46e418badf5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ resolver = "3" [workspace.package] -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" edition = "2024" authors = ["Lance Devs "] license = "Apache-2.0" @@ -57,27 +57,27 @@ rust-version = "1.91.0" [workspace.dependencies] arc-swap = "1.7" libc = "0.2.176" -lance = { version = "=9.0.0-beta.8", path = "./rust/lance", default-features = false } -lance-arrow = { version = "=9.0.0-beta.8", path = "./rust/lance-arrow" } -lance-core = { version = "=9.0.0-beta.8", path = "./rust/lance-core" } -lance-datafusion = { version = "=9.0.0-beta.8", path = "./rust/lance-datafusion" } -lance-datagen = { version = "=9.0.0-beta.8", path = "./rust/lance-datagen" } -lance-derive = { version = "=9.0.0-beta.8", path = "./rust/lance-derive" } -lance-encoding = { version = "=9.0.0-beta.8", path = "./rust/lance-encoding" } -lance-file = { version = "=9.0.0-beta.8", path = "./rust/lance-file" } -lance-geo = { version = "=9.0.0-beta.8", path = "./rust/lance-geo" } -lance-index = { version = "=9.0.0-beta.8", path = "./rust/lance-index" } -lance-io = { version = "=9.0.0-beta.8", path = "./rust/lance-io", default-features = false } -lance-linalg = { version = "=9.0.0-beta.8", path = "./rust/lance-linalg" } -lance-namespace = { version = "=9.0.0-beta.8", path = "./rust/lance-namespace" } -lance-namespace-impls = { version = "=9.0.0-beta.8", path = "./rust/lance-namespace-impls" } +lance = { version = "=9.0.0-beta.9", path = "./rust/lance", default-features = false } +lance-arrow = { version = "=9.0.0-beta.9", path = "./rust/lance-arrow" } +lance-core = { version = "=9.0.0-beta.9", path = "./rust/lance-core" } +lance-datafusion = { version = "=9.0.0-beta.9", path = "./rust/lance-datafusion" } +lance-datagen = { version = "=9.0.0-beta.9", path = "./rust/lance-datagen" } +lance-derive = { version = "=9.0.0-beta.9", path = "./rust/lance-derive" } +lance-encoding = { version = "=9.0.0-beta.9", path = "./rust/lance-encoding" } +lance-file = { version = "=9.0.0-beta.9", path = "./rust/lance-file" } +lance-geo = { version = "=9.0.0-beta.9", path = "./rust/lance-geo" } +lance-index = { version = "=9.0.0-beta.9", path = "./rust/lance-index" } +lance-io = { version = "=9.0.0-beta.9", path = "./rust/lance-io", default-features = false } +lance-linalg = { version = "=9.0.0-beta.9", path = "./rust/lance-linalg" } +lance-namespace = { version = "=9.0.0-beta.9", path = "./rust/lance-namespace" } +lance-namespace-impls = { version = "=9.0.0-beta.9", path = "./rust/lance-namespace-impls" } lance-namespace-datafusion = { version = "=7.0.0-beta.9", path = "./rust/lance-namespace-datafusion" } lance-namespace-reqwest-client = "0.8.6" -lance-select = { version = "=9.0.0-beta.8", path = "./rust/lance-select" } -lance-tokenizer = { version = "=9.0.0-beta.8", path = "./rust/lance-tokenizer" } -lance-table = { version = "=9.0.0-beta.8", path = "./rust/lance-table" } -lance-test-macros = { version = "=9.0.0-beta.8", path = "./rust/lance-test-macros" } -lance-testing = { version = "=9.0.0-beta.8", path = "./rust/lance-testing" } +lance-select = { version = "=9.0.0-beta.9", path = "./rust/lance-select" } +lance-tokenizer = { version = "=9.0.0-beta.9", path = "./rust/lance-tokenizer" } +lance-table = { version = "=9.0.0-beta.9", path = "./rust/lance-table" } +lance-test-macros = { version = "=9.0.0-beta.9", path = "./rust/lance-test-macros" } +lance-testing = { version = "=9.0.0-beta.9", path = "./rust/lance-testing" } approx = "0.5.1" # Note that this one does not include pyarrow arrow = { version = "58.0.0", optional = false, features = ["prettyprint"] } @@ -104,7 +104,7 @@ half = { "version" = "2.1", default-features = false, features = [ "num-traits", "std", ] } -lance-bitpacking = { version = "=9.0.0-beta.8", path = "./rust/compression/bitpacking" } +lance-bitpacking = { version = "=9.0.0-beta.9", path = "./rust/compression/bitpacking" } bitpacking = "0.9" bitvec = "1" bytes = "1.11.1" @@ -143,7 +143,7 @@ datafusion-substrait = { version = "53.0.0", default-features = false } dirs = "6.0.0" either = "1.0" fst = { version = "0.4.7", features = ["levenshtein"] } -fsst = { version = "=9.0.0-beta.8", path = "./rust/compression/fsst" } +fsst = { version = "=9.0.0-beta.9", path = "./rust/compression/fsst" } futures = "0.3" geoarrow-array = "0.8" geoarrow-schema = "0.8" diff --git a/java/lance-jni/Cargo.lock b/java/lance-jni/Cargo.lock index 1801b6cfc35..c29e4bbb118 100644 --- a/java/lance-jni/Cargo.lock +++ b/java/lance-jni/Cargo.lock @@ -918,15 +918,6 @@ version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" -[[package]] -name = "bitpacking" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019" -dependencies = [ - "crunchy", -] - [[package]] name = "bitvec" version = "1.1.1" @@ -2479,7 +2470,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "rand 0.9.4", @@ -3656,7 +3647,7 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arc-swap", "arrow", @@ -3673,7 +3664,6 @@ dependencies = [ "async-trait", "async_cell", "aws-credential-types", - "bitpacking", "byteorder", "bytes", "chrono", @@ -3692,6 +3682,7 @@ dependencies = [ "humantime", "itertools 0.14.0", "lance-arrow", + "lance-bitpacking", "lance-core", "lance-datafusion", "lance-encoding", @@ -3729,7 +3720,7 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -3771,16 +3762,17 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrayref", + "crunchy", "paste", "seq-macro", ] [[package]] name = "lance-core" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -3818,7 +3810,7 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -3850,7 +3842,7 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -3867,7 +3859,7 @@ dependencies = [ [[package]] name = "lance-derive" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "proc-macro2", "quote", @@ -3876,7 +3868,7 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-arith", "arrow-array", @@ -3911,7 +3903,7 @@ dependencies = [ [[package]] name = "lance-file" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-arith", "arrow-array", @@ -3941,7 +3933,7 @@ dependencies = [ [[package]] name = "lance-geo" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "datafusion", "geo-traits", @@ -3955,7 +3947,7 @@ dependencies = [ [[package]] name = "lance-index" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arc-swap", "arrow", @@ -3967,7 +3959,6 @@ dependencies = [ "async-channel", "async-recursion", "async-trait", - "bitpacking", "bitvec", "bytes", "chrono", @@ -3987,6 +3978,7 @@ dependencies = [ "jsonb", "lance-arrow", "lance-arrow-stats", + "lance-bitpacking", "lance-core", "lance-datafusion", "lance-datagen", @@ -4023,7 +4015,7 @@ dependencies = [ [[package]] name = "lance-io" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-arith", @@ -4064,7 +4056,7 @@ dependencies = [ [[package]] name = "lance-jni" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -4100,7 +4092,7 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -4116,7 +4108,7 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "async-trait", @@ -4128,7 +4120,7 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-ipc", @@ -4177,7 +4169,7 @@ dependencies = [ [[package]] name = "lance-select" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -4192,7 +4184,7 @@ dependencies = [ [[package]] name = "lance-table" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -4229,7 +4221,7 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "icu_segmenter", "rust-stemmers", diff --git a/java/lance-jni/Cargo.toml b/java/lance-jni/Cargo.toml index 103800e5cc5..947b8f51a47 100644 --- a/java/lance-jni/Cargo.toml +++ b/java/lance-jni/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lance-jni" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" edition = "2024" authors = ["Lance Devs "] rust-version = "1.91" diff --git a/java/pom.xml b/java/pom.xml index bac93e26278..2a368055d1e 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ org.lance lance-core Lance Core - 9.0.0-beta.8 + 9.0.0-beta.9 jar Lance Format Java API diff --git a/python/Cargo.lock b/python/Cargo.lock index 0b6fa85e2dc..339138db7b5 100644 --- a/python/Cargo.lock +++ b/python/Cargo.lock @@ -2850,7 +2850,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "rand 0.9.4", @@ -4049,7 +4049,7 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arc-swap", "arrow", @@ -4123,7 +4123,7 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -4165,7 +4165,7 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrayref", "crunchy", @@ -4175,7 +4175,7 @@ dependencies = [ [[package]] name = "lance-core" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -4245,7 +4245,7 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -4262,7 +4262,7 @@ dependencies = [ [[package]] name = "lance-derive" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "proc-macro2", "quote", @@ -4271,7 +4271,7 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-arith", "arrow-array", @@ -4306,7 +4306,7 @@ dependencies = [ [[package]] name = "lance-file" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-arith", "arrow-array", @@ -4336,7 +4336,7 @@ dependencies = [ [[package]] name = "lance-geo" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "datafusion", "geo-traits", @@ -4350,7 +4350,7 @@ dependencies = [ [[package]] name = "lance-index" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arc-swap", "arrow", @@ -4419,7 +4419,7 @@ dependencies = [ [[package]] name = "lance-io" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-arith", @@ -4460,7 +4460,7 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -4476,7 +4476,7 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "async-trait", @@ -4488,7 +4488,7 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-ipc", @@ -4537,7 +4537,7 @@ dependencies = [ [[package]] name = "lance-select" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow-array", "arrow-buffer", @@ -4552,7 +4552,7 @@ dependencies = [ [[package]] name = "lance-table" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "arrow", "arrow-array", @@ -4591,7 +4591,7 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "icu_segmenter", "jieba-rs", @@ -6029,7 +6029,7 @@ dependencies = [ [[package]] name = "pylance" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" dependencies = [ "alloc-stdlib", "arrow", diff --git a/python/Cargo.toml b/python/Cargo.toml index f4164000fc5..4557b0f262b 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pylance" -version = "9.0.0-beta.8" +version = "9.0.0-beta.9" edition = "2024" authors = ["Lance Devs "] license = "Apache-2.0" From 2d89b3c4330541315a4e12eebe62635bb9992eb0 Mon Sep 17 00:00:00 2001 From: Dan Rammer Date: Mon, 29 Jun 2026 12:37:14 -0500 Subject: [PATCH 013/177] fix(mem-wal): apply cross-generation block-list to in-memory scan arms (#7489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What The WAL fresh-tier vector and FTS search arms apply a per-generation recency filter (`NewestPkFilterExec`) to every in-memory memtable — but they only apply the cross-generation block-list (`PkBlockFilterExec`) to flushed/base sources, never to in-memory ones. This PR applies the block-list to in-memory arms too. ## Why `LsmDataSourceCollector::in_memory_sources` emits **both** the live active memtable **and** every frozen-awaiting-flush memtable as `ActiveMemTable` sources, each with its own `index_store`. The recency filter queries only *that generation's* PK index, so it is purely within-generation. When a PK is inserted in one generation and later deleted or updated in a **newer** in-memory generation, the older (frozen) generation's arm keeps the stale matching row: the newer generation's tombstone/update lives in a different `index_store` the frozen arm never consults. This happens whenever an insert and its later delete/update straddle a freeze boundary — common under a short flush interval, which freezes memtables continuously. The cross-generation block-list (`NEWER(G)`) is the only mechanism that resolves this, and `compute_source_block_lists` already computes it for in-memory sources (snapshot-bounded by each generation's `max_visible_row`, so a not-yet-visible write can't over-block). The arms just discarded it. ## Change - `vector_search.rs`: after `NewestPkFilterExec`, also wrap in `PkBlockFilterExec` when the source has a non-empty block set. - `fts_search.rs`: unify the dedup loop so every source applies the block-list when blocked; the within-generation recency filter is already applied inside `build_source_plan` for in-memory sources. The newest memtable per shard has no newer generation, so its block set is empty (`None`) and it is unaffected — only older frozen generations are now filtered. Each in-memory generation gets both within-generation recency and cross-generation supersession. ## Tests - `cross_gen_stale_version_blocked_by_newer_memtable` (vector): a PK at a near vector in a frozen generation and at a far vector in the active generation. Before the fix the stale near copy leaks alongside the newest far version; after, only the newest survives. - `cross_gen_stale_update_blocked_by_newer_memtable` (FTS): a PK matching "alpha" in a frozen generation, updated to "beta" in the active generation; an "alpha" search must not return the stale frozen row. Both fail before this change and pass after. The existing single-generation recency tests (`keeps_only_the_newest_visible_position_per_pk`, `active_stale_update_predicate_crossing_leaks`) still pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../src/dataset/mem_wal/scanner/fts_search.rs | 131 ++++++++++++--- .../dataset/mem_wal/scanner/vector_search.rs | 158 +++++++++++++++++- 2 files changed, 268 insertions(+), 21 deletions(-) diff --git a/rust/lance/src/dataset/mem_wal/scanner/fts_search.rs b/rust/lance/src/dataset/mem_wal/scanner/fts_search.rs index e7c8d205d5d..d4c056ae644 100644 --- a/rust/lance/src/dataset/mem_wal/scanner/fts_search.rs +++ b/rust/lance/src/dataset/mem_wal/scanner/fts_search.rs @@ -181,7 +181,6 @@ impl LsmFtsSearchPlanner { let arm_inputs: Vec<_> = sources .iter() .map(|source| { - let is_active = matches!(source, LsmDataSource::ActiveMemTable { .. }); let blocked = block_lists.get(&(source.shard_id(), source.generation())); // Over-fetch a blocked source so the post-filter still yields k live // rows. The active arm returns all matches (no builder limit), so its @@ -191,36 +190,35 @@ impl LsmFtsSearchPlanner { } else { k }; - (source, is_active, blocked, fetch_k) + (source, blocked, fetch_k) }) .collect(); - let built = - futures::future::try_join_all(arm_inputs.iter().map(|(source, _, _, fetch_k)| { - Box::pin(self.build_source_plan(source, column, &query, *fetch_k, projection)) - })) - .await?; + let built = futures::future::try_join_all(arm_inputs.iter().map(|(source, _, fetch_k)| { + Box::pin(self.build_source_plan(source, column, &query, *fetch_k, projection)) + })) + .await?; let mut per_source_plans: Vec> = Vec::with_capacity(sources.len()); - for ((_, is_active, blocked, _), plan) in arm_inputs.iter().zip(built) { - let is_active = *is_active; + for ((_source, blocked, _), plan) in arm_inputs.iter().zip(built) { let blocked = *blocked; - // Dedup, mirroring LsmVectorSearchPlanner: - // * active: already wrapped in `NewestPkFilterExec` inside + // Within-generation dedup is already applied per source: + // * active/frozen in-memory: `NewestPkFilterExec` inside // `build_source_plan` (drops predicate-crossing stale hits, which a // result-set dedup can't catch). - // * flushed/base: drop rows superseded by a newer generation via the - // block-list (within-gen is handled by the flushed deletion vector). - let deduped = if is_active { - plan - } else if let Some(set) = blocked { - Arc::new(PkBlockFilterExec::new( + // * flushed: the on-disk deletion vector. + // Cross-generation supersession (a newer gen's write or tombstone) is + // applied uniformly here via the block-list — including frozen + // in-memory generations, whose per-gen recency filter can't see a + // newer gen. The newest generation of each shard has no newer gen + // (`blocked` is None) and is unaffected. + let deduped = match blocked { + Some(set) => Arc::new(PkBlockFilterExec::new( plan, self.pk_columns.clone(), set.clone(), k, - )) as Arc - } else { - plan + )) as Arc, + None => plan, }; // Normalize to canonical. This also drops the active arm's _rowid, @@ -845,4 +843,97 @@ mod tests { "live pk=2 ('alpha foo') must still match 'alpha'; got ids={ids:?}" ); } + + #[tokio::test] + async fn cross_gen_stale_update_blocked_by_newer_memtable() { + // The cross-generation analog of `active_stale_update_predicate_crossing_leaks`: + // pk=1's stale "alpha" version lives in a FROZEN memtable and its fresh + // "beta" version in the ACTIVE one. The frozen arm's recency filter is + // per-generation and can't see the newer gen, so only the cross-gen + // block-list can drop the stale "alpha" hit. The cluster constantly + // freezes memtables, so an insert and its later update/delete split + // across in-memory generations — this is the residual fuzz phantom. + let schema = fts_schema(); + + // Frozen gen=1: pk=1 "alpha lance" (matches), pk=2 "alpha foo" (live). + let frozen_store = Arc::new(BatchStore::with_capacity(16)); + let mut frozen_idx = IndexStore::new(); + frozen_idx.enable_pk_index(&[("id".to_string(), 0)]); + frozen_idx.add_fts("text_fts".to_string(), 1, "text".to_string()); + let fb = make_batch(&schema, &[1, 2], &["alpha lance", "alpha foo"]); + let (bp, off, _) = frozen_store.append(fb.clone()).unwrap(); + frozen_idx + .insert_with_batch_position(&fb, off, Some(bp)) + .unwrap(); + + // Active gen=2: pk=1 updated to "beta lance" (no longer matches "alpha"). + let active_store = Arc::new(BatchStore::with_capacity(16)); + let mut active_idx = IndexStore::new(); + active_idx.enable_pk_index(&[("id".to_string(), 0)]); + active_idx.add_fts("text_fts".to_string(), 1, "text".to_string()); + let ab = make_batch(&schema, &[1], &["beta lance"]); + let (bp, off, _) = active_store.append(ab.clone()).unwrap(); + active_idx + .insert_with_batch_position(&ab, off, Some(bp)) + .unwrap(); + + let tmp = tempfile::tempdir().unwrap(); + let base_uri = format!("{}/base", tmp.path().to_str().unwrap()); + let collector = LsmDataSourceCollector::without_base_table(base_uri, vec![]) + .with_in_memory_memtables( + uuid::Uuid::new_v4(), + InMemoryMemTables { + active: InMemoryMemTableRef { + batch_store: active_store, + index_store: Arc::new(active_idx), + schema: schema.clone(), + generation: 2, + }, + frozen: vec![InMemoryMemTableRef { + batch_store: frozen_store, + index_store: Arc::new(frozen_idx), + schema: schema.clone(), + generation: 1, + }], + }, + ); + + let planner = LsmFtsSearchPlanner::new(collector, vec!["id".to_string()], schema); + let plan = planner + .plan_search( + "text", + FullTextSearchQuery::new("alpha".to_string()), + 10, + None, + ) + .await + .expect("planner should produce a plan"); + + let ctx = datafusion::prelude::SessionContext::new(); + let stream = plan.execute(0, ctx.task_ctx()).unwrap(); + let batches: Vec = stream.try_collect().await.unwrap(); + + let mut ids: Vec = Vec::new(); + for b in &batches { + let col = b + .column_by_name("id") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + for i in 0..b.num_rows() { + ids.push(col.value(i)); + } + } + + assert!( + !ids.contains(&1), + "stale frozen pk=1 ('alpha lance', now 'beta lance' in the active gen) \ + leaked on an 'alpha' search; got ids={ids:?}" + ); + assert!( + ids.contains(&2), + "live pk=2 ('alpha foo', only in the frozen gen) must still match; got ids={ids:?}" + ); + } } diff --git a/rust/lance/src/dataset/mem_wal/scanner/vector_search.rs b/rust/lance/src/dataset/mem_wal/scanner/vector_search.rs index 7f849f3d8bf..1c00d4ec15c 100644 --- a/rust/lance/src/dataset/mem_wal/scanner/vector_search.rs +++ b/rust/lance/src/dataset/mem_wal/scanner/vector_search.rs @@ -315,7 +315,22 @@ impl LsmVectorSearchPlanner { batch_store, active_max_visible.expect("active arm returns its max_visible snapshot"), )); - sort_by_distance(filtered, k)? + // Cross-generation supersession: a frozen (older) in-memory + // generation can be superseded by a newer generation's write or + // tombstone. The recency filter above is within-generation only; + // the block-list is the sole cross-gen mechanism. The newest + // active memtable has no newer generation (`blocked` is None), so + // this is a no-op there — only older frozen gens are filtered. + let blocked_filtered: Arc = match blocked { + Some(set) => Arc::new(super::exec::PkBlockFilterExec::new( + filtered, + self.pk_columns.clone(), + set.clone(), + k, + )), + None => filtered, + }; + sort_by_distance(blocked_filtered, k)? } else { match blocked { Some(set) => Arc::new(super::exec::PkBlockFilterExec::new( @@ -622,6 +637,26 @@ mod tests { .unwrap() } + /// One row with an explicit `(id, vector)` — unlike [`create_test_batch`], + /// the vector is not derived from `id`, so the same PK can carry different + /// vectors across generations (an update / move-out-of-neighborhood). + fn create_id_vector_batch(schema: &ArrowSchema, id: i32, vector: [f32; 4]) -> RecordBatch { + use arrow_array::builder::Float32Builder; + let mut builder = FixedSizeListBuilder::new(Float32Builder::new(), 4); + for v in vector { + builder.values().append_value(v); + } + builder.append(true); + RecordBatch::try_new( + Arc::new(schema.clone()), + vec![ + Arc::new(Int32Array::from(vec![id])), + Arc::new(builder.finish()), + ], + ) + .unwrap() + } + async fn create_dataset(uri: &str, batches: Vec) -> Dataset { let schema = batches[0].schema(); let has_id = schema.column_with_name("id").is_some(); @@ -808,6 +843,127 @@ mod tests { ); } + #[tokio::test] + async fn cross_gen_stale_version_blocked_by_newer_memtable() { + // A PK updated ACROSS a freeze boundary: its old (near) version lives in + // a frozen memtable, its new (far) version in the active one. Both are + // in-memory `ActiveMemTable` sources, so each gets only a per-generation + // recency filter (`NewestPkFilterExec`) — which can't see the newer gen. + // The cross-generation block-list is the only thing that can drop the + // stale near copy; if the active arm discards it, the frozen near version + // leaks as a phantom near-neighbor. This is the residual wallop fuzz + // delete/update phantom: the cluster constantly freezes memtables + // (flush_interval ~250ms), so an insert and its later delete/update split + // across in-memory generations. + use crate::dataset::mem_wal::scanner::collector::{InMemoryMemTableRef, InMemoryMemTables}; + use crate::dataset::mem_wal::write::{BatchStore, IndexStore}; + use datafusion::prelude::SessionContext; + use futures::TryStreamExt; + + let schema = create_vector_schema(); + let temp_dir = tempfile::tempdir().unwrap(); + let base_uri = format!("{}/base", temp_dir.path().to_str().unwrap()); + // Base seed far from the query; id=1 is not in the base. + let base_dataset = + Arc::new(create_dataset(&base_uri, vec![create_test_batch(&schema, &[99])]).await); + + // One in-memory memtable holding `id` at `vector`, with a PK + HNSW index. + let make_gen = |id: i32, vector: [f32; 4], generation: u64| { + let batch_store = Arc::new(BatchStore::with_capacity(16)); + let mut index_store = IndexStore::new(); + index_store.enable_pk_index(&[("id".to_string(), 0)]); + index_store.add_hnsw( + "vector_hnsw".to_string(), + 1, + "vector".to_string(), + lance_linalg::distance::DistanceType::L2, + 64, + 8, + ); + let batch = create_id_vector_batch(&schema, id, vector); + batch_store.append(batch.clone()).unwrap(); + index_store + .insert_with_batch_position(&batch, 0, Some(0)) + .unwrap(); + InMemoryMemTableRef { + batch_store, + index_store: Arc::new(index_store), + schema: schema.clone(), + generation, + } + }; + + // Frozen gen=1: id=1 @ the query vector (distance ~0). + let frozen = make_gen(1, [0.1, 0.2, 0.3, 0.4], 1); + // Active gen=2: id=1 moved FAR — the newest version of the PK. + let active = make_gen(1, [9.0, 9.0, 9.0, 9.0], 2); + + let shard_id = uuid::Uuid::new_v4(); + let collector = LsmDataSourceCollector::new(base_dataset, vec![]).with_in_memory_memtables( + shard_id, + InMemoryMemTables { + active, + frozen: vec![frozen], + }, + ); + + let planner = LsmVectorSearchPlanner::new( + collector, + vec!["id".to_string()], + schema, + "vector".to_string(), + lance_linalg::distance::DistanceType::L2, + ); + + let query = create_query_vector(); + let plan = planner + .plan_search(&query, 3, 1, None, false, 1.0) + .await + .expect("planner should produce a plan"); + let ctx = SessionContext::new(); + let batches: Vec = plan + .execute(0, ctx.task_ctx()) + .unwrap() + .try_collect() + .await + .unwrap(); + + let mut id1_distances = Vec::new(); + for b in &batches { + let ids = b + .column_by_name("id") + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + let dist = b + .column_by_name(DISTANCE_COLUMN) + .unwrap() + .as_any() + .downcast_ref::() + .unwrap(); + for i in 0..b.num_rows() { + if ids.value(i) == 1 { + id1_distances.push(dist.value(i)); + } + } + } + + // id=1 must surface at most once, and only at its NEWEST (far) distance — + // never the stale near ~0 copy the frozen generation still indexes. + assert!( + id1_distances.len() <= 1, + "id=1 leaked its stale frozen copy (appears {} times): {id1_distances:?}", + id1_distances.len() + ); + if let Some(d) = id1_distances.first() { + assert!( + *d > 1.0, + "id=1 surfaced at the stale near distance {d}; its newest version is far" + ); + } + } + #[tokio::test] async fn test_vector_search_with_projection_returns_distance_and_pk() { // Regression for: active arm previously did NOT call `build_projection_for_knn`, From e25b71e74b89d10c57b412d111bde087117383f3 Mon Sep 17 00:00:00 2001 From: Lance Release Bot Date: Mon, 29 Jun 2026 17:54:41 +0000 Subject: [PATCH 014/177] chore: release beta version 9.0.0-beta.10 --- .bumpversion.toml | 2 +- Cargo.lock | 48 +++++++++++++++++++-------------------- Cargo.toml | 44 +++++++++++++++++------------------ java/lance-jni/Cargo.lock | 40 ++++++++++++++++---------------- java/lance-jni/Cargo.toml | 2 +- java/pom.xml | 2 +- python/Cargo.lock | 40 ++++++++++++++++---------------- python/Cargo.toml | 2 +- 8 files changed, 90 insertions(+), 90 deletions(-) diff --git a/.bumpversion.toml b/.bumpversion.toml index 4633f741828..f0c7104df34 100644 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [tool.bumpversion] -current_version = "9.0.0-beta.9" +current_version = "9.0.0-beta.10" parse = "(?P\\d+)\\.(?P\\d+)\\.(?P\\d+)(-(?P(beta|rc))\\.(?P\\d+))?" serialize = [ "{major}.{minor}.{patch}-{prerelease}.{prerelease_num}", diff --git a/Cargo.lock b/Cargo.lock index 92976691d0b..dbd012d8517 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3076,7 +3076,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "rand 0.9.4", @@ -4380,7 +4380,7 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "all_asserts", "approx", @@ -4483,7 +4483,7 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -4531,7 +4531,7 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrayref", "bitpacking", @@ -4542,7 +4542,7 @@ dependencies = [ [[package]] name = "lance-core" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -4582,7 +4582,7 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -4615,7 +4615,7 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -4634,7 +4634,7 @@ dependencies = [ [[package]] name = "lance-derive" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "proc-macro2", "quote", @@ -4643,7 +4643,7 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-arith", "arrow-array", @@ -4688,7 +4688,7 @@ dependencies = [ [[package]] name = "lance-examples" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "all_asserts", "arrow", @@ -4714,7 +4714,7 @@ dependencies = [ [[package]] name = "lance-file" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-arith", "arrow-array", @@ -4753,7 +4753,7 @@ dependencies = [ [[package]] name = "lance-geo" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "datafusion", "geo-traits", @@ -4767,7 +4767,7 @@ dependencies = [ [[package]] name = "lance-index" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "approx", "arc-swap", @@ -4844,7 +4844,7 @@ dependencies = [ [[package]] name = "lance-io" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-arith", @@ -4892,7 +4892,7 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "approx", "arrow-array", @@ -4912,7 +4912,7 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "async-trait", @@ -4924,7 +4924,7 @@ dependencies = [ [[package]] name = "lance-namespace-datafusion" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-schema", @@ -4940,7 +4940,7 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -5004,7 +5004,7 @@ dependencies = [ [[package]] name = "lance-select" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -5022,7 +5022,7 @@ dependencies = [ [[package]] name = "lance-table" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -5068,7 +5068,7 @@ dependencies = [ [[package]] name = "lance-test-macros" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "proc-macro2", "quote", @@ -5077,7 +5077,7 @@ dependencies = [ [[package]] name = "lance-testing" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-schema", @@ -5090,7 +5090,7 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "icu_segmenter", "jieba-rs", @@ -5103,7 +5103,7 @@ dependencies = [ [[package]] name = "lance-tools" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "clap", "lance-core", diff --git a/Cargo.toml b/Cargo.toml index 46e418badf5..28decda5801 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ resolver = "3" [workspace.package] -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" edition = "2024" authors = ["Lance Devs "] license = "Apache-2.0" @@ -57,27 +57,27 @@ rust-version = "1.91.0" [workspace.dependencies] arc-swap = "1.7" libc = "0.2.176" -lance = { version = "=9.0.0-beta.9", path = "./rust/lance", default-features = false } -lance-arrow = { version = "=9.0.0-beta.9", path = "./rust/lance-arrow" } -lance-core = { version = "=9.0.0-beta.9", path = "./rust/lance-core" } -lance-datafusion = { version = "=9.0.0-beta.9", path = "./rust/lance-datafusion" } -lance-datagen = { version = "=9.0.0-beta.9", path = "./rust/lance-datagen" } -lance-derive = { version = "=9.0.0-beta.9", path = "./rust/lance-derive" } -lance-encoding = { version = "=9.0.0-beta.9", path = "./rust/lance-encoding" } -lance-file = { version = "=9.0.0-beta.9", path = "./rust/lance-file" } -lance-geo = { version = "=9.0.0-beta.9", path = "./rust/lance-geo" } -lance-index = { version = "=9.0.0-beta.9", path = "./rust/lance-index" } -lance-io = { version = "=9.0.0-beta.9", path = "./rust/lance-io", default-features = false } -lance-linalg = { version = "=9.0.0-beta.9", path = "./rust/lance-linalg" } -lance-namespace = { version = "=9.0.0-beta.9", path = "./rust/lance-namespace" } -lance-namespace-impls = { version = "=9.0.0-beta.9", path = "./rust/lance-namespace-impls" } +lance = { version = "=9.0.0-beta.10", path = "./rust/lance", default-features = false } +lance-arrow = { version = "=9.0.0-beta.10", path = "./rust/lance-arrow" } +lance-core = { version = "=9.0.0-beta.10", path = "./rust/lance-core" } +lance-datafusion = { version = "=9.0.0-beta.10", path = "./rust/lance-datafusion" } +lance-datagen = { version = "=9.0.0-beta.10", path = "./rust/lance-datagen" } +lance-derive = { version = "=9.0.0-beta.10", path = "./rust/lance-derive" } +lance-encoding = { version = "=9.0.0-beta.10", path = "./rust/lance-encoding" } +lance-file = { version = "=9.0.0-beta.10", path = "./rust/lance-file" } +lance-geo = { version = "=9.0.0-beta.10", path = "./rust/lance-geo" } +lance-index = { version = "=9.0.0-beta.10", path = "./rust/lance-index" } +lance-io = { version = "=9.0.0-beta.10", path = "./rust/lance-io", default-features = false } +lance-linalg = { version = "=9.0.0-beta.10", path = "./rust/lance-linalg" } +lance-namespace = { version = "=9.0.0-beta.10", path = "./rust/lance-namespace" } +lance-namespace-impls = { version = "=9.0.0-beta.10", path = "./rust/lance-namespace-impls" } lance-namespace-datafusion = { version = "=7.0.0-beta.9", path = "./rust/lance-namespace-datafusion" } lance-namespace-reqwest-client = "0.8.6" -lance-select = { version = "=9.0.0-beta.9", path = "./rust/lance-select" } -lance-tokenizer = { version = "=9.0.0-beta.9", path = "./rust/lance-tokenizer" } -lance-table = { version = "=9.0.0-beta.9", path = "./rust/lance-table" } -lance-test-macros = { version = "=9.0.0-beta.9", path = "./rust/lance-test-macros" } -lance-testing = { version = "=9.0.0-beta.9", path = "./rust/lance-testing" } +lance-select = { version = "=9.0.0-beta.10", path = "./rust/lance-select" } +lance-tokenizer = { version = "=9.0.0-beta.10", path = "./rust/lance-tokenizer" } +lance-table = { version = "=9.0.0-beta.10", path = "./rust/lance-table" } +lance-test-macros = { version = "=9.0.0-beta.10", path = "./rust/lance-test-macros" } +lance-testing = { version = "=9.0.0-beta.10", path = "./rust/lance-testing" } approx = "0.5.1" # Note that this one does not include pyarrow arrow = { version = "58.0.0", optional = false, features = ["prettyprint"] } @@ -104,7 +104,7 @@ half = { "version" = "2.1", default-features = false, features = [ "num-traits", "std", ] } -lance-bitpacking = { version = "=9.0.0-beta.9", path = "./rust/compression/bitpacking" } +lance-bitpacking = { version = "=9.0.0-beta.10", path = "./rust/compression/bitpacking" } bitpacking = "0.9" bitvec = "1" bytes = "1.11.1" @@ -143,7 +143,7 @@ datafusion-substrait = { version = "53.0.0", default-features = false } dirs = "6.0.0" either = "1.0" fst = { version = "0.4.7", features = ["levenshtein"] } -fsst = { version = "=9.0.0-beta.9", path = "./rust/compression/fsst" } +fsst = { version = "=9.0.0-beta.10", path = "./rust/compression/fsst" } futures = "0.3" geoarrow-array = "0.8" geoarrow-schema = "0.8" diff --git a/java/lance-jni/Cargo.lock b/java/lance-jni/Cargo.lock index c29e4bbb118..385dc42570d 100644 --- a/java/lance-jni/Cargo.lock +++ b/java/lance-jni/Cargo.lock @@ -2470,7 +2470,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "rand 0.9.4", @@ -3647,7 +3647,7 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arc-swap", "arrow", @@ -3720,7 +3720,7 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -3762,7 +3762,7 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrayref", "crunchy", @@ -3772,7 +3772,7 @@ dependencies = [ [[package]] name = "lance-core" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -3810,7 +3810,7 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -3842,7 +3842,7 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -3859,7 +3859,7 @@ dependencies = [ [[package]] name = "lance-derive" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "proc-macro2", "quote", @@ -3868,7 +3868,7 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-arith", "arrow-array", @@ -3903,7 +3903,7 @@ dependencies = [ [[package]] name = "lance-file" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-arith", "arrow-array", @@ -3933,7 +3933,7 @@ dependencies = [ [[package]] name = "lance-geo" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "datafusion", "geo-traits", @@ -3947,7 +3947,7 @@ dependencies = [ [[package]] name = "lance-index" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arc-swap", "arrow", @@ -4015,7 +4015,7 @@ dependencies = [ [[package]] name = "lance-io" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-arith", @@ -4056,7 +4056,7 @@ dependencies = [ [[package]] name = "lance-jni" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -4092,7 +4092,7 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -4108,7 +4108,7 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "async-trait", @@ -4120,7 +4120,7 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-ipc", @@ -4169,7 +4169,7 @@ dependencies = [ [[package]] name = "lance-select" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -4184,7 +4184,7 @@ dependencies = [ [[package]] name = "lance-table" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -4221,7 +4221,7 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "icu_segmenter", "rust-stemmers", diff --git a/java/lance-jni/Cargo.toml b/java/lance-jni/Cargo.toml index 947b8f51a47..9d25f734e7a 100644 --- a/java/lance-jni/Cargo.toml +++ b/java/lance-jni/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lance-jni" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" edition = "2024" authors = ["Lance Devs "] rust-version = "1.91" diff --git a/java/pom.xml b/java/pom.xml index 2a368055d1e..0e685723e24 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -7,7 +7,7 @@ org.lance lance-core Lance Core - 9.0.0-beta.9 + 9.0.0-beta.10 jar Lance Format Java API diff --git a/python/Cargo.lock b/python/Cargo.lock index 339138db7b5..14c269bdd47 100644 --- a/python/Cargo.lock +++ b/python/Cargo.lock @@ -2850,7 +2850,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsst" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "rand 0.9.4", @@ -4049,7 +4049,7 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a" [[package]] name = "lance" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arc-swap", "arrow", @@ -4123,7 +4123,7 @@ dependencies = [ [[package]] name = "lance-arrow" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -4165,7 +4165,7 @@ dependencies = [ [[package]] name = "lance-bitpacking" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrayref", "crunchy", @@ -4175,7 +4175,7 @@ dependencies = [ [[package]] name = "lance-core" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -4213,7 +4213,7 @@ dependencies = [ [[package]] name = "lance-datafusion" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -4245,7 +4245,7 @@ dependencies = [ [[package]] name = "lance-datagen" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -4262,7 +4262,7 @@ dependencies = [ [[package]] name = "lance-derive" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "proc-macro2", "quote", @@ -4271,7 +4271,7 @@ dependencies = [ [[package]] name = "lance-encoding" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-arith", "arrow-array", @@ -4306,7 +4306,7 @@ dependencies = [ [[package]] name = "lance-file" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-arith", "arrow-array", @@ -4336,7 +4336,7 @@ dependencies = [ [[package]] name = "lance-geo" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "datafusion", "geo-traits", @@ -4350,7 +4350,7 @@ dependencies = [ [[package]] name = "lance-index" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arc-swap", "arrow", @@ -4419,7 +4419,7 @@ dependencies = [ [[package]] name = "lance-io" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-arith", @@ -4460,7 +4460,7 @@ dependencies = [ [[package]] name = "lance-linalg" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -4476,7 +4476,7 @@ dependencies = [ [[package]] name = "lance-namespace" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "async-trait", @@ -4488,7 +4488,7 @@ dependencies = [ [[package]] name = "lance-namespace-impls" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-ipc", @@ -4537,7 +4537,7 @@ dependencies = [ [[package]] name = "lance-select" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow-array", "arrow-buffer", @@ -4552,7 +4552,7 @@ dependencies = [ [[package]] name = "lance-table" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "arrow", "arrow-array", @@ -4591,7 +4591,7 @@ dependencies = [ [[package]] name = "lance-tokenizer" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "icu_segmenter", "jieba-rs", @@ -6029,7 +6029,7 @@ dependencies = [ [[package]] name = "pylance" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" dependencies = [ "alloc-stdlib", "arrow", diff --git a/python/Cargo.toml b/python/Cargo.toml index 4557b0f262b..d4e4113bdc5 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pylance" -version = "9.0.0-beta.9" +version = "9.0.0-beta.10" edition = "2024" authors = ["Lance Devs "] license = "Apache-2.0" From 3e54532659f5bcf5851e36fd3aa95c475f96b9e4 Mon Sep 17 00:00:00 2001 From: Prashanth Rao <35005448+prrao87@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:57:17 -0400 Subject: [PATCH 015/177] docs: improve phrasing and update format spec image (#7505) Briefly improves the format spec intro wording and updates the lakehouse stack image. --- docs/src/format/index.md | 22 +++++++++++----------- docs/src/images/lakehouse_stack.png | Bin 365967 -> 382727 bytes 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/format/index.md b/docs/src/format/index.md index c6956453b05..055c9129c5f 100644 --- a/docs/src/format/index.md +++ b/docs/src/format/index.md @@ -1,44 +1,44 @@ -# Lance Lakehouse Format Specification +# Lance Lakehouse Format Specifications -Lance is a lakehouse format designed as a stack of interoperating specifications instead of a single file or metadata layout. The storage-facing layers are the file format, table format, index formats, and catalog specifications, with a unified namespace interface sitting above them. +Lance is a lakehouse format defined as a stack of interoperating specifications, rather than as a single file format or metadata layout. The storage-facing layers cover files, tables, indices, and catalogs. A unified namespace interface sits above those layers and gives engines a consistent way to work with Lance tables across catalog implementations. ## Architecture Overview -Modern lakehouses are built from cooperating layers. Lance keeps those layers intentionally decoupled so that the file format, table metadata, indices, and catalogs can evolve independently without forcing lock-in across the stack. +Modern lakehouses are built from complementary layers. Lance keeps those layers intentionally decoupled so that the file format, table metadata, indices, and catalogs can evolve independently without forcing lock-in across the stack. ![Lakehouse Stack](../images/lakehouse_stack.png) At a high level: -- The **file format** stores column data in large random-access-friendly pages and avoids row groups. +- The **file format** stores column data in large pages optimized for random access and avoids row groups. - The **table format** manages fragments, manifests, deletions, schema evolution, and ACID commits. - The **index formats** define redundant search structures such as scalar, vector, full-text, and system indices. - The **catalog specs** define how tables are discovered, registered, and coordinated across engines and services. -- The **namespace client spec** provides a unified interface for engines to interact with any catalog implementation. +- The **namespace client spec** provides a unified interface for engines to interact with any catalog implementations. -The layers are designed so that only table readers, table writers, and index readers or writers need to know the on-disk Lance file layout. +The layers are designed so that only table readers, table writers, and index readers or writers need to understand the on-disk Lance file layout. ## Design Themes ### File Format -The Lance file format is optimized for cloud object storage and highly selective reads. It avoids Parquet-style row groups, uses structural encodings that support efficient random access, and keeps statistics and search structures out of the file format so those concerns can evolve as independent indices. +The Lance file format is optimized for cloud object storage and highly selective reads. It avoids Parquet-style row groups, uses structural encodings for efficient random access, and keeps statistics and search structures out of the file format so those concerns can evolve independently as indices. ### Table Format -The Lance table format stores data in two dimensions: rows are grouped into fragments, and each fragment can contain multiple data files that each contribute a subset of columns. This makes column additions and backfills metadata-heavy instead of rewrite-heavy, which is especially useful for feature engineering and embedding workflows. +The Lance table format organizes data in two dimensions: rows are grouped into fragments, and each fragment can contain multiple data files, each contributing a subset of columns. This makes column additions and backfills primarily metadata operations instead of data rewrites, which is especially useful for feature engineering and embedding workflows. ### Index Formats -Indices are first-class table objects. Lance tables define how indices are discovered, versioned, and coordinated transactionally, while the index formats themselves remain decoupled from both the file encoding and the table manifest structure. +Indices are first-class table objects. Lance tables define how indices are discovered, versioned, and coordinated transactionally. The index formats themselves remain decoupled from both the file encoding and the table manifest structure. ### Catalog Specs -Lance provides storage-native and service-oriented catalog options. The [Directory Catalog](catalog/dir/index.md) supports zero-infrastructure deployments directly on object stores, while the [REST Catalog](catalog/rest/index.md) standardizes enterprise-facing APIs and can act as an external manifest store. +Lance provides both storage-native and service-oriented catalog options. The [Directory Catalog](catalog/dir/index.md) supports zero-infrastructure deployments directly on object stores, while the [REST Catalog](catalog/rest/index.md) standardizes enterprise-facing APIs and can act as an external manifest store. ### Namespace Client Spec -The [Namespace Client Spec](namespace/index.md) provides a unified interface for engines to interact with any catalog implementation, across both Lance native catalog specs and third-party catalog systems, in any programming language. This abstraction allows applications to switch between directory-based, REST-based, or third-party catalogs without changing their code. +The [Namespace Client Spec](namespace/index.md) provides a language-agnostic interface for engines to interact with any catalog implementation, including Lance-native catalogs and third-party catalog systems. This abstraction allows applications to switch between directory-based, REST-based, and third-party catalogs without changing their code. ## Specifications diff --git a/docs/src/images/lakehouse_stack.png b/docs/src/images/lakehouse_stack.png index e34c999efd82666e390dffcb09de1a76572aa416..b3e4db709d6d6f223b92c53c09df835faac36d05 100644 GIT binary patch literal 382727 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 z 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

Uf%pryH;>?O-Z;;h2OhRV%h)K;oIt>oB4eE7xV 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ΝcoHTa8f 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

S}U-uH907lQ%dCVYFgTB zQvaY}%g+@SGg-(PB_yMc?6NOBx>W@Zrd&^*qQ{)gDUG^63v{$u!MaU$g@zs2icdMt zq}=5x3ccquy0m^Dnu44!A2C?~4LEN}rGMO)jq<@PmLa@OM@Xlj`r7cqb`{LNooE|J zk8<_E4_afJbSU@8G%zzEq#}i}rbErmp6UbR7qSO){{95PIoiW&60LZ;3w{36pAQpa z?{BF&i8jxRWd$ca+wa73#8)y5)et1*Gw zPSPCEy=}V+*@85tKG84cgJKxOp2wyykC75-MOh0MLM*`U6HLnv;J`&=sC&|^uxe1o z`RceVjJu@73+Le7V8hL)F0Fx9ALS%2ujLp!vC`^ICL5=cO)CMxs;!W$g2z`gW0gzq z?E$cxyDoi8`{Lsx@KWsA6~z&S?QP|C)p>d>#9y9}8^T4CZ(eVt++-!bBq%v{Kvz9X z+Dl!?X}j{!rhO@5`KOb zbL{GU$%n9*H;#R?>-X?H$H)#epM%^wuw7e3Y~LiZAU1CAQ&8@vBWrYMhGD~rysr6I z<>B_mr?JYbr28m0{PRy?)9Q6H$oQ;f%<6C9>}Q_4&z>!u-SWGT8>&8|4vS0Lxg25|5;K?mLjp(bC0%GXpeTRYbBJ!qe;3QZaqs1#j1n# z3Z@e&a^QYiEL3JSU|PvVcR#3HHUBt+s5Z1t9}>xd0)>b!tDKI=e-C2>Q_ht!8aSi` znF{=pdrhAgMg&?t6&d0+YeVS%}4@V)!YTX2%WD+=))lQUo_;_t+Q3;&13 zQi1d6(PZIo@hjvpb%#9_I1wNQ{YB_ve15h99nIiv-%;o1pKz+r!|}JxP#c^0aT26Dd@A3E^K*}C&6>O=RA+*RmIvq=8n<&QL!=iVf8P+3?`+#K6Y8hSw-#TsBJ<_5XZXsth+UtHkxT|3A9Yu&zZu$ zlVb^z5h*7ee}~X?)t@CRsPL0H<|E^EmEipGxb*arKodIiG~_bA#I0nw^u-AI0dJ$6 zzFdnl#m?2&CNW<=pD5>x!f)g*(#_eCVm=q17?zLNLVTr2r|a*BARPEJ!$}>7{q9P~ zF?b~TB_i7=OLAxuWvC#8Z#%XSRGwe)FD$?>n8}sWeDYf_u`)+&C5b!OP$-vP; z$8=Y{y497Pq&X&JP};AZV7R8RDW&t!F2sz9PQ+y2VE==d@cXOCRkhGRYKRfWkF{q| z#Y4eDys&#D1csP^IfIO>6jDMYi@M0ET_pjbgLF~3EE3_eDlHS8OHn9Qpz|TxVJCq8sY;+!NnpffE56LE7n6B72Z)|tHPlHP8p55N@ zC>oqe*Pp?KK~d&9@wxRoC)FpEr{Fn4?;mFkjyhOwY3Ye=Ia{?ODKe??IdY81-PQL0}TddR2GKQ$&h2`{g8Dz867sQNW>xwVubZR5{f zs5!;uU%&paSHleiA1N;Zr_Q#*w(II#BdQyQ#iWwelOt!Nf%p=mu#RuB1REiTf|ON> zw+aFhi)&S!=)Hz_`nomEBH!<)8CbvIqWj*VXSnS}+-j$=hX^wzF*q}KbGadwb zUyx8ctd|bx; z;ky@3Dcr?EVx-xr5)>D-!K7Ut>9RZAeGJ*IsCho1O9>`}#*?!8+Q=@qk~@`7Twn@J z_+#Hyn1uHyHHiO)odzi-U6+~I@GfjgS}<-VaQZI-B$!5|Cj!?*WSlKj5y9?uoLVWd zR=xR6e1tpe&cBeSgI(D%HyO-UY95Rx`}(d{##D2`78oG{100@4TqNRmz_Z%#Hj|E! z!4lV5yDp)RY^%xpvlrc!T$AuB+hC5(uq?uEFX>IyFVoN;mgpnAB2%YqRgFvVJU_!0ISgEdgdX!1;khO$ zi0C)nIN#VWB4?ajJclpY`T{A{_?8H0z?cXQNz>_*i&=S3-B$ua>s!M?A@NZu2=ag( zwAqh;nrjw0V=3%>7=KA@8g$%UG##F0hmiJkepyb){PQfvBd!0WjqwiEUjS-KnUo5# zcI*BH4@rv*Bs`N2bKOnuRZ7U0&MvrOXj}h9qT2IFo+4$wnZHY!J8wqcfJ$wWZA*b$ zZJ+MAiUEX^hw#ay9o;7y_Y&pCseD7~Y)r8b(Zyc-rr%Q7dUNR>17@ zbiyBS+C5So9flXfS{ zcVA6hOLpY%uzik`P26Y|TZ$jq;?#$8-VfiM`%TbZfUx3cgrAG6R{#&4U+uRZUVIQe zDmuUN?NHojkB!0=gAfVD9==3S4J^QF@&ceTqhtN3X5*497EE;+94(?Pn_mm-Tua4U zSZ1gQw1iMc3iaj%3*`fzBIN7uKx}(CpK0`(GMqeT5St$h@@Uo}8e(U+O9Rw?jXAR> zrh5_7RlVlmbZmW_x<${)&^%6&G`yRvN~pl=&Lcg2;-(wo@R%`y*&@%bZE2Onxs;n)2cFYM^v2s15~lYKZf4Yhv$qv1=AJiqs2#*;?FD zxpIT51&_G#0*iy{5TT=UbWmidP()nYPm$O07i5-={ZAR58gL3I85z!23iLFuKPr_pHTV1UUbGB z0nchj9kY*Vpo)+4+OqsyVpx`^-K;voLx0=MTyNOBww%rO!spSnjHtbL-mhcFi0!0C6bbyN&Jqi->Jw0sc!Bg(*j?7@35!R-hDwVE;Udqpr z4(4d~2)QrryjhuY?7|dWds_^xqZcXrw8M0~XIQ+Y_FD25Vfe}C{B?sAhAi7&T82pf zaprF=>K{;(6Ot(884igL3G4m+#YyNmMiah$QR)kJd*`=z5v=1v*h%!ju4+pGDE@)D z2$?;*i+~K$mPL!cgc~}a4p}fn*PO0&WF3wG+P+YZNT({C$YXzdQYOq?zBAQ7%uTme ztL#WhzqcVxtLfX%2e%wcl(+CeUYvinkAT>hDmrgmGe5h#9U&;BJrFJ}%|vhVQZ?Bh zjpW4!PDCEZrKaz3ggMP{3=v(&8S8DDQQDpqcRG1bnzt>d7{?8^PXQlTnaUp4#9#mY zOS-~c9gQUG7BPEtZbo3a`@zB{`PYJl^lE>VpM~QAH{Wo1kDV0Srbu?e={$^BUkqJ< zt|jL}NJo<57gua%8}*IG7lb74kC-bQhN|mSn)S=xawhdCPRl7eEBH;GkZ>Hp0{0g; z_xzhtuNxD%G+kD-Jx4KJ3j_O~URKh**DR1eLq*m(Koa4Q#zofA{)1 z_{(Ir&z!u;WV0&c8Oa`>-?=*8Vk0zSi4dk%TwA`IfHd-TdwnRv&fQD1T+UyES>!$s!cXt=o};ElfVf#{-PWAKTF@G5%?OKzSZ_&d+@vulBKuuJgblDUtL{ z<->yW@q>9&a7L3|+9?IcNIB-cyYL+Nn ztN!`>5YD~nIf;9MVJ>K)LUr}ygyCa-`wKgx@w=7dK6`J961J#oLphU4L_@L204-P) zjguW*ARXbLd&bs6iN9gt^5B5DyJ_ z3&_sS?m3C5h_$4#UaheUB3(?O7Std7#QRRB>8dHGYz>ynKI*(leNYV=qgignR(bAw z`YMa!eJR6i`E#gF%03O0ZWfeC&K-^{?DivdPw!$0Tzc|TGF-8wlW}^J>MiXo=)Paf zt){X@?Bc!LgH0r>ogIxG^Z=P4BG6_HIfTb6ekw#nTE&6mgQa%SSF1Q6PA|B2AM;Vp z{)SrJetno6n`yExlRT0Kl>zdBd`Tvoy&L;1S4yj(kzDqfOY^*MHOS~~wC+~-_)^S{nifR8iajzUznu7c@#iIPHK%8PlzC!HGZXZ9^me(AT zvFS@RH-7eY_1kFvqN+36vJ!>~J4uFYJTanL4X=AlRa_8+#!XRe?T;9)55msR z+gS=}_|3B|<6moQbX(3Mz*P3;y1q}Hlv2(=w`M`6a~SEE)z`9Y$du!^hT?D7vjUz< zC4OlvBYXXB+?lecv0!Lg-6(xYN*G9rwY2839CaDr(u>*y1M4 zT-q!q5m2WKFfV=wpV|+JpCR)O#kRNT_UBygogQE*OJ0#GAGJYrw2E?$P`A#4q;Jg% z#EPvvI6LlD?*hov?o7;NsCH5V$8WF`nw6wBYTe%I_3Ed_5JL8*6a9iQUFX&K-Q=3x z?wbZ*Fr_LZY*oCgiM@uk7oCP~(xcOSLhv0f9<;6MKoARpy6&y}3OvR8cS*2@+pW&c zD$T}{v)iTz+qikJ^PYsN#`g=Os|sE=_K@qP!@PxH2G-os&0vNhiHM&H`%FY@E(;IR zWd2|z!-%4~J2p}`zPja|_X9@I!sj%8LI`>3wehdCjFRkMZ%YgBXLVpjP|-k=Kp{*{IW+0NkL{=D^bT~2KoCgbwg zy!3{}-GwWge+>d^VJtI`q(80SQrBuZKQ+bD>ItpkvVoZmV?$cj?O?|DaCQ4F$OnS- zhqSH%Ji9%PZ^=i5J-d4~p;XQ@M%L^%?wK3=`?qI9TH4}SnCFz8oId$6M+Z}!{b0uL z5wFiD%9>Q|G&GHfep`h<`scEF=3diaQ=G3o6^uZ(Io@xh4K46lII>K zQ;bOKuxk25%o$3Gl}@3B+z(lNDw#cCq-}^d^yHY_qEBHPcVDUMdYba%Av1k_&oCgC+I20{7MJv zk?Z^JthsX6UcMs@E|!^JQIh1JSIg+%u@vkXR_pSIlQHZfAads%AM%YkjB6Du8s&IO zdvzf}e&e0XoQb7%?pIAfbh5V?>mDh+gm=)YDPeEIpC zgTo}pdvd#ZC|=(NcTvl3BzH=WGH27L>wSz0uNVT$1HZta6^bUrkS)2<6~d?IINVQ_ zKPT*w76)*GW^?)l2k>;VF&Z)Uj68~w%BW6YhEe;+mG^qZ69j4QVw<-nO^HC&Kft>% zO=&^ay69h6fc2*3$S}qYB6}UqH!jg}?AApFTlB6%(JuViwmUUT@o?zuL0ywv10bUx z;>Q^^*@tYp@IVi{S{5IJLT<~ZAT)W}rNXkHhx1h8qLJV?zf>c}e*KW$+`dU|ILLH$ zLW0-Xi%ll+Y0I0?&H>t>Qn^`}W*}-yr!)78)(ypQ!f@z0c@sdYAVFgL=ySN8I#Ci& zUu3cRt=7#L!=5=%1kITlUVCNV1yPoxR%B|Iy^BMq!>ExmcN99r1dKi$84xG_G~G+;!Kfzwwj8? z6!2sJyUE)(#mDWB47?T1GzGPM-Cp3}xEU(3yyc5CuL_7R#@$sh0Qu`7URX0G|9 zv8)%YxfFbG$0l?DWb+8`jF!BccG#J`=tM2v4Os46Nz8d5K!Yc3TL7aF>YN?!i4S$J z^2q_0-wnK&j84&c6E$hP#a6aZ*TSuHJ{^A|nY?p289PgW-{*nO$x(ZNF`Cu`MH(dE zooi}z?J&!J6Z@P!$zB&cnW&uE*}Vg+>isc`;IXZqoPYS1Abd_)Ozh3`c>@0|{_n}B zb-sOed!@)T$&?s&d*$eXqDGF-==1dYj18Z^CW5xNHEJdG#}XAlbw2lhHQlB~&T)7T zc4^32rl8%r+oh#?r@siZ)C$`2!DFT|*=or%zX?HJ8=PEYPlMP2MY>w=)b*g(vkyqtE~P5N zd}jL}zGkPAGDYkyR;#-`P{Y#46u1JwcaoA}w<1I%HXg@<(`QWwlE0!8+&`3bnRZYK(O4Y?!Wd zsnOD4>eEBN8b6wm^16Xo$>TL#Ja0j8jwHl?t#Gfm5OZ6(Fl9DT?5)_Xf^_Ot!RqRH z77*Cr7Tj=Qx>ehk4euQ^??Mg-O}R#Xb&b)RaBDm)931L2MyWwYhE8UoJ%DIVzVWuR z3Q3x!uZ^(_A$o(dIJoa~;pL~|#n+Po20bzX#(y(Tmd@Fs3&rs^9V2v2#gF%LiYT-B z1c6$WRo0;gU2k%BzGdS&;5FTI)P!g!@KFPh!%noHg!av6W5SCZ;8o=mbyWkq-h3gN5G=!+u=s*mH`XHYsh*s=k@Iw0oiF!B)!Kqo z=kR2ejvV@3NL88fpx)YB&MLe)VI9${m3TNCpGaY?$>y7Na=T3mChlEKLeHNrx7>dz z?|!Ba`J~r|vTP^VFkLPe2;LcmdAP#oKzm^DGZ(Jkkd6RQJFVJg)#$@rxq9MbsxzIA|*{$me3*-P{Q^%_d{-4pFaim+3<&y(civ7h6P zd#%+@10<_Ex9)`BDjQoM%h!U>g8Z;;5;{8l_-v}=V@v9KNiC$Kifx%@&$dA7kC^0o z@KK!+dd?U6T`dh2AUtc8K8O#BF>N>-0SGPe(7rfmdiwo1p9)^R((lw43W;6pL)U^M)#)A++ogmBIS==w^OLG+ zR^RU7LfoSsmbWo9TAC6*GeT+fsu;h*=#0T=0Wtet4Zq?m-6hmat;xxI znXSnMPs~wYd^hc-;xPIVmy#mjT4+IW#Q_^kuab?LG+Z3wJ!!OGXJN&p4E#j%I!oC7 z3vW9VujknO(Ve-ut^hgIEbF16&l?1ziSAEJz2D zM5X5}58}m?G(%_6dCfNkoKb}_muI+4jPvyf-)8b#GlU4Q&~e@f*u}^_GKflTf83Fy z@J8|Nr@ZJ_1{KkWQnyyzgi9ub<@0DD;(k*0FBef9G7F?dAY$8+<8rMXcI}AY4=K_? z_{~cU9w?+28PJEw5P4VDb*dl%ccRhMniY2JM837BdObM?>W4`!IQ}=q;0?BxL9w4* z^{@4_>p5>GXK>h4Mq1DI2T2Bd?9uEUb_v}Ro(y#oXsKfo_u_kRNJz`9QdygxeVtwC zIhsHsVfS1Vl6SH2KaZy@ijH-W6g#Ur>}tAs4@Z4dAG>%GCqE%*1P8!Nq58UuWLiZ- zeWM>#EA}M41sAH{RX^UXemm%D31o*mvGEuQn`D@HsTP%&bn`w!qIDg&pa4}_mCWARqZ{PxB(q%K5um!Q2D2Wgw>3qE3PJ`21Iq&1U_Pgd^ z%IT79i2b=&AD}=(*m;>`7Nn?Jobf1(a9TgJN81_0>iJWrD%Jwye$q=HDU4-axtc(q z_ckTDYVmtM8&4@_PnSZVO zDC_*%ROTPummWV43T4Gy*nW9mj5*aTpSz2w` z>}4-%OeM5OzcXDoX@&pW}~a6?fQV3dFind}9vM_SSB$od)HkM(u?|kgSl* zk0Y_&s8P2d$)5EVMloW;k@Xd4idV`Q3(&i{x6}`VsuLE)?>Db3sjR7FSFOfI6J|L~ zhisd2q4&MJw^88D-uB${zEYM!08{|OKIv1@bB4c|&ymg{9^o?(}|w?I<|$RHF2t8tL5KA zCH|&U)bGL6G@p?HlJ#hVw|?lmjHv zsZ^ad&*wYh-6X9I*i(ylTi7-BTU{@2NWNwIw9vLc7%E$SoJXY0S& zAtB;pK;^wcC;ke1vqnxhO3>v>bTdH>8uQizG#c9}y8+UPVy;UXOi+pcq0%GjU02gmwbLNRDI@I&sZrAo2nb@<55`1g{z zDGW4=yvuHAw0e5|I!!n|fP9Sz$~ePA-j_!iYF`qIwxkp@JBWIzwxs$_9?90&qH%E6%?g&-*AaW!C{G(cbf#!5D7V=Jh86 zht0e@n7!^n_D%NgEZj?7n}5vM+ldQsqSO<@?gDyO`DM<)J?E@$`KT_G>CpNb(N_WzUkr0hUQ3xD)@zU%U zlC#FUKsqSpOm&hEVAw?>>7)LJvNzty@RoZ0#j0PAd9Yv3sd21;Y?kBsyOzzh6Mz!j z)`H|{YFEFZDY#qi>F|}$B>X9}kUW{xoCTW{oh`QZ>36hw7dVXR`X`!PW`(*8wjEbO z%Tk8zMG21=!mLZMN2EGqAo%ze9Isc;mtFlYEFfOI3vrxMgb|a&HU#Wz1y3#WW{G8l zNx+a7$-0uo-32OM>jMwW+1cSYKXgi?w02Yuq$Ad?LJY3b{ z`k2e&I`XZew;VL0<^6ci5Fv7sSPqze2pJB7cAqhS9*4%srU-g0JD@lm&@`^TW5s;^ zafIYi{5|{{yZ!)b?Q+lin=Myv;O7JZ)015A%reGOxRzm*YRA`O?G9O=DLhxYj#OEY%(g$))QC_R%4c^K zh7Gg%dWH1rwsl=}+G!3*9k0ZhQo7eHD_g^Q90b?>ETw&VNHNi?E(C=Q;!d>4h+Z3c zLJMOp>m$SN!`BG{k)huQXZl(=;Xmm;avA4xs}=;5h0b7YNkLMxGshv4`}wfJVJ}X% ze#u9qQ&u$G5o9FZUO1j$FD|@acyo3dL*X3Vi$n35x}cDfJz=#i>Dk5$l}|@8CWR=@ znPrW70an~U4oAKpgw|M>1yy@##6$-3yB zq?1vxBM3ndQvv9spSj`waac6eu)JG!oj{VVoI3abgcQYe&wpojc zkBc@@?U;NdI`NDAj`Ekt-yBrD+>A}}7&rgQ^U@9`klJ4giU^RmgKoxH^=SjV`I`E( zBkj`&v4<|FIx2U(x09!>t^6c~TuUux#PES9tJ!u1PJRdDX0-N{A0GBO1dVFlZEUDA z2}6C+NxDngo(P#uX|4E`*>*^}2;~!&Qn}+;y{zrzsOQ#0!7=mheS+0?=szNCvn_*; z2IGk5|#z|l>jZ56Tbpv;XGko57XM%%M737i| z`}*}$uId((wGqpYA70ED3Mbig$d666@fxn~x?+RorE1+T=cW@Z3yW2JMHbv1_;%ld zgg-*dhI_@_e4E!&)(QNP>s)iUBDLzNe!*RZi!6{KHCuCN$KNXV*>4XkF<)yPhCy!w z4P|}NUo+z~Mkmq2d9Ixc>a^aX`G1#nCmxsY#T(*tt83vc+z})GD)0MQ=#6%e`g9=X zv@qLBfYPM%(>)Rsw{HX&eoM^VT19KQ8^d}qMwQ>2NmB*YQs_5>({2!_ z#;86fULoiFWIJb{ua#q-`e$R#*tARNYff5aFMpLElJmD9pQCq8>x(&do~;!z|0rb9 zMx{fgQ>d$O2$6CPlW1z}6Ca^VJl-jzqEdLP7S|T=^cu^9u}Zl+sFye>6cb+G3u)fOuo;l0dBLgXSVYC!T&$v79WO+U*-XtOVvJT(SobS-Y#??(JTEqpO-{<#z|# zBg^!eAcV9u$o@P8U>2!(Mx`I3cWLv+@8)R9+>=w&|6ajj4tm{ioL`Mq_!qRN(sP9c zaah*Q?#5(@{S~gQudx`}t<4?YJnGef9ZL$Q9skS`xt@oAti6_CN$s6jmv*P0OL!h- zN!_S9{LsaXy+d|-P;BLYP6OjYBscLG*m`BGf10R{1FK_#gKvVMW*kOosYl(sse*1h zko8<#KTAvqR>;x&!RF2gm#KxJ3Ga15=W`_Im!x;fVNaKfA}Sn`qN2(o5MA{@>fn;1 zU$14F8YgvC!xy6j$dd8q4xK4t1YgL!RaK=~MH|<%L$>o^#yg6YX!1TCqoPd3&@{c8 z=P;UE2u87xfD(kaCjNOVMgc0Py5Ee%rb3-do(F|E~TBud_^ zqC3+8w@U-G4ZiW7NY;9_0{F{SBfItNy;B#<*~pY-5g8$v24NO0(knTI`u^DIu`phbdoEkVE4`P=*vmu2`S_dFTJjpM`os@xn_Y=rI} zBciFDy9kZEv;M`#FSRCm_U<|k5cy2K5s=du)tt51*<5@eWI~dF5McI1TF2s!w)QPu zPocu3F)dqp!o@gkJ;^{9i*Ai3DIE@gcl#zF=|(wkICt5ef3sPJjCd9#Uv4Y$f^US- zVdBL|YP=Ws7+~dSAlOnU7`XG7d(MJE+tt>4lqJ(FP||ulmwh?Lz5NW`BH=We!LU&f zFTMV`Ry|8BYHCbx;=5-wVC|OxBS<_d=a3*s5xA>9|9sVPi7d&qYOY;CH)Yy+SbCqX z^jvvifH3UV{DsAr*5@(C%@d~=*W3q&dM!}iBE#J1Z_As+Z;Ec43F}Cu>&1EvGd*$L zc0|09_M0pBces8v*j~$OldUy}(yS47hHN=q##-u>MjO>90FrZ-YMsEaO_VF=E)0mx zoQ??VL<7J1tLWDE-Q^m5Y44_2J`O6xFxl{#4=A~dfT|39exJ$1qh&s%k2Z^2UjGzD z39lE%Js!^PvgWba`>T=pf!w_~Hyz%2MF93)n9rf-sl z)arosM!T7%hw%N#bc zLQ(b^0oS0Msljz}c3S-r7s< zkD9l>Ag<+NC^*#~eJ-l8ps{7oFd-EY<&jP#$B zb%_IfQQpQ3+JwsP60N{#3vjW*#(^g)L@LC0~ zZNJSome>U4sm{MIT$P?kdr}`#*4waY8o7;>Xh(OH&QdA8x;yS*_Y2f2s$WG}dl-bV zn_pjkMor!GGae2X>zS^%o-ghYHO!M8{Dw~&qNSkC{ZuROD>uia#-v59^m#x&$sUk@ z<$p9^k)SF23&Ek>{DXYzmb&k3!P)JJ9*|5-~k&K&hDbEtA4EnE{+~;25v$JCTzCwluZ42f!X4n!)F9=Ar8&4oj9qA|w<|^9c8F5NCx?2qU zX%2Wx6da)xuJg|qTD>wRUj>Kx&YND{O{oeZV}W)kPVu@#7v+Iij3F#E&!->!&=xBO9rc^iM)ms6_wgfvdElns0$# zJH=v^9RI=sT;YfyOUe_ega`2tp1SiN*8Ar})Gd<|8=Rla?6ny{>684AbVN=*jf^{1 z!>UV+O39@aXx4+Cf=;Qpb$A{<4X)}Yy#`(BfUPOJ3TUq636(8`_I-GmC(=<#WZ2D* z?xe!SyJL@QH72YX>3ZyM^YQgNGVLuMICaTBpUZs`E&z=)T@U#h>w+bfYwiuI_T5Y^ z#g$ZDsL=WVX%W%*#UItQuD&5V!x7{;^$m~S_jMojT12%rNtlz;lW-}(cTtkFee<1^ z`I8mRH^5c}h4Y<|O263KjU1Na7K+L4j!q8(r^tQV4+%8lRPbw>GeMERx+Het?2Vy&$xb(@^ft9nv++s;&Mlbp*4=GwBU>&N$+ za?WK`SBD`L1A=5C4|WWU?Pqni5;TfHs1uOcl`aBZepJ|EPpPAoV^*m%1I z?A`C`wB2g*dcJWOQ-o{5~uQCvCg;2Z;-@htC3jfiEc z6Oa%s2*xiimNM06)7-}J1s5acJuKNZ)FhosxQiAKG*quw+>pG`0$5EW7vL@Iz4U=W ze|q!RRCx>KR<7_Jx2FP!yhTTU3pN^ZHb2NYrurzf{n*%)RvQF(Ped^k-i%G7%Eiz8 z1Ctjm@%TG6RUK()O_jy`v`kf)~UZWxk52juhR5}^C#xLJYEgz&m(u*;QuE-UB z8U}9uvTx3v8zh9`1h+&T!ahcQ$m0O%aRJ6RB?+gee7?~%DgM?n$z**sMvmMszsOT) z38;Ez~VF)H3)j41ngqPT_#<6wW=criqOxkBz1wA2`0^eI5t0y;xVHH+gS5K4t|P!TnO!DEaL5I`?uw zC6Z~RUb$D34US@UhsbyZ zd!iT&Yja-TtCZd4YiE_y$6BSJ{|XgJqi!k7WNpgebWz@X-_MVKM|B9ZpZoi<3FAt( z3CDhUxyHrdZ8Q4*2P(!KS#{XySu3&DU;3r-qf){)={Ie|oYGLg`zS90Y@YCYj1=Ui zYo9GJF3qTRBnN^WsIL9$T54 zy1=*Jq&GC52Tur>^cL3I0U=(~NMq`}|HXBPN8^#mbvjw1mGB?)C;Q63YSZi^#*{kZ zjl)V_kucR`exq_PsY6{!vz3ARyJ2_fK6?iZ@Wi{fq93XB%=&Is0}PAW4b-e_-%{m| z(m%qHxgbsR^G%*5#87PU=8E0&NwgKzpO2OXZxNC%$oQpXG^S*Jpt198URAo- zf=h)ObgbaKfO-Td!mKMzt5dfwGC_gdPp%_^;qzFv`AcK$WcKXI?(rVv0n1{dKvvJM zk#?hE*uLCOn+Og__lcNZ-U@wA%Yi++&P-KqR5@%VkM=mLf%_fJ*nk_=Xn5(B1gc?d zK)j!DT<-Mj`AEbJ1>A!;)q+HC94NMjuTgyWc5M9CI*~ZmRANOxP@GWZ(72EU_dj~# zQh%mVh(%pp0XTr*!Ot1@?}whx=!V&kz!w1{4UE54-(Fcpa&kpn=mljM@?BtDwa@Vu zI?pgyXdNM0fU|kR=($q^sRW-hQzDqJx#V^Oz<&ajFu~Az+CYy40r5JF>C5etpQ!bc z&|Z0lZ>}Wu-rpc({a5Mx9W_>#>EFUVw#w2|I~5 zY;R~56>oF^amh{|CuQd61TXk-JqCBzw=;s{#N7hNw;eINsJk9yq2d!o2Mhg4b$&^Z z0`z6-mpJ{Yxiy02sge6v&gd8$VEitmG{HkkQx)vVHja%Ck?ZZcXm_Niic#nyoC#iC zSneKQJ1L;LVUT=612|xUTc~)*h2r~)^*ErKr9qxn9Oc}8jw!lfwKe4m%lwgJxDh8& zifHLl>PR`m!-;xRHO~$>tb=2P8Z_mZr;kN;6X4^FUwT}uc1T0hd?}UXxoUz0j54X} z-{p}azkc+&FnUq4To@`XVnqffz+WF;HA@SS(Y?T0@1!FeZAOr9*`!gXulT*sQSrZ_ zv`W^=Gnm=mhlJhI4*_JEPl&iL0((oFEV+n0>@AP6+oNfgS7z~cY@4K7`@oet8T#*ViDeM(py6A1kDcTFPltJ zE~m~~^I;f9zY2nug>qzFK=a`(AKx~z6h>#Morz7Em8x~%S%C4et*VjxD6Ce}=;cS)sG|a|B<_3sj5(cY8Lq4bO%{T4fJdDEd zTYAovC}c#iC((Mu?}TrpeQ^F9)puF?`BpDmobg!`dvqF&7uTF@1%C9y4Bc9^NdN)?13P4Dzc6) zkYXG!w6_X_7!vY)ac*dxgIU?P{WWG0OA&6Dh5-(zLlSRi?6hNj#6Kt|~|K!S{FE1qMCV-n5 z(NxDqhMcyb<>*QQk!pzWD!P$%q8F*OuTPy!Unb7-+%mUBYSe?Pp4Mx zjE{yu?6xfS(c*a+4b!w>xtnYOjHLMd$_^+MwJ?TSR96rE5d2h?Im_a(a&0>N+iL42 zkybF)Ao1OI3-k-b_q)#%m_%OZ1x8@zYg&kRZ@$gqt~5`56|ofMi~p(?&ZLH-tNV|a zz6LLgVQp>@=%~o01mf}85!N-RfeA}J;PtK=~)3(`1QFy{MXdSbDiXFd4_ z@@d!_EA=EjO|(TztiF#lzYs~9C>}#$#bx?kMZi9rZu@*SkjevdGLJP&s5}$5fzkDFz|qdY(!6E%F?paONQ`^=>h3n6&4YPDoxnO9046QkW z_#Lk+$N`1}-9SEPd$vr`u&1F|K^MBMp7e_pQHVD)++?8|g9|0_`0E3({|My&R z0sn0{k0Y|cC>MIL%GJ+HqSO{u6e<^cX>rmM7WL&Bd8GIbej1(X&K%YT&?xN-VfD?1 z^ZKI#vG|6AdFj=$`;oq}LwWyAd6N<>>nD)16!9VbT5}Qm#G*|nNhu8F81Lyu%wu;N z-<}w~Xifn4P9ZdYyNnR^l9#>vxAO8=Nl(gXqQwDJW$nbhD1Q22(W>zY1rHtaKWlTd zA!)9as15(4RQW%D=3MD7H81~F-2A^6UVkR`G%oVrPSFKuJYqep{Mcd3%jRgH4A%et zLrHz2DEDtedh7?E3uucmg>#nuRw?k`4*DN8!)Y&3bl?5gf4uyk!^al} zdTt*Kv;1eTYwS;rlK+2?pD7Y0r~5svy_W(0ua0zX81SuNm+qVi1>3A!^|6C;4w(iQm6!HuyhF)&Gw< z=%E8zn@3sH{zqH@rX=OREujB+_wh&mzfLCL70^1;icB2oD*VZb;{RG1|8MOc=UY4y zgHmG+!wEx=|NmSXk|>w|Uk4O#d=2#N4ZTT#{9E&GQKb6k|7|Aza~%MaUJQ))KgI}A z{1PGp=}XmN{O9H2>-X-<`af6He=W~{eDuFB6**c7V9m+n)2afEyK#`}fg0%l+0B3c z^Y8urKa^c{TvS`v2Bble7HOnARJuE)yStR`E&-)GM7q1XMH)f6LAtwR;G41a`rhmJ zkH5n>`|P#WUeB{)?>XoD#K4PIrR4=q(!Yb{*R}tGAaXE}%Ym6Q2L1-Z`$=H@qQ4^K z%fN450-Vh2ng^)z#R4ru=yzdqB|M!T{hc7zzF_E1)=*4zpD8I7_G9v2BL!&CS0TXB zX-iRWO5>=0S+1|f{!9e_TLZ5bAdo}z2L3*$w}__!(Z4GIe3^Gn7wTJ=?w=^la~?7d z@2?h(LI7wq3R|W)bdcDv7~nB}zsmr!^#5QuZcxf0^+uwX8$#ms|6?3*wLr|M2LflD zB?IF>MCHl5ug&$+}{{p4U>>ztPR0m;wanmjr zh(7Rt2mu1|FQEE5QP>sG;*nQES#4o3NVuMSWpKFel^ko@+Xy_&G-Gf zm8=Ejw{Vp~35+Hgn~6G2A_Uwh+DZT0oqx5(7YK+v^oo*M7FC6dr~jj&d+uus{o8`S zN(Sn^Xw=E{+qBlPV8y>CeqVL?nb6UUf$L52u{zoJI~@vm8j$=+&VR1lAu}j`?_G7y z00Yi42DqmGFQ)%0^e=|~>Be1pAXoFnZV~?jhl0WV7OuYA|7$!VP5^2h@hooR_gDwa z5QF|RjtPLm*Q-|#ynYBQn+SF!{1*rba!hv;7n?JefH@*j-YLI#}t0FF5F z=hRRH8azw%Zxr_gk`kVv_tVc_%Ciq!0<_{E#{Efe9#2^5a55g|NWp_xFuA`BJU{&B zP!2A)D>8W(<4Eiu6NZUF#$o;|F%T#L$(T&Z>PYt&c~f9N{!J*>eFR+X2}g!6YJ