feat(table): route merge_insert through the MemWAL LSM write path#3354
Conversation
|
@jackye1995 PTAL — this is the LanceDB-side companion to lance-format/lance#6706 (now released as v7.0.0-beta.6). |
jackye1995
left a comment
There was a problem hiding this comment.
thanks for the work! Added some comment for the initial pass. Would be great to also get some performance numbers from this.
| pub num_updated_rows: i64, | ||
| pub num_deleted_rows: i64, | ||
| pub num_attempts: i64, | ||
| pub num_inserted_or_updated_rows: i64, |
There was a problem hiding this comment.
probably better to just do num_rows, becuase we will eventually also allow deletion
There was a problem hiding this comment.
Renamed to num_rows so the same field can carry delete counts in the future.
| def drop_columns(self, columns: Iterable[str]) -> DropColumnsResult: | ||
| return LOOP.run(self._table.drop_columns(columns)) | ||
|
|
||
| def set_unenforced_primary_key(self, column: str) -> None: |
There was a problem hiding this comment.
we should support multiple columns for this API. it is okay to check in merge_insert and say that for initial implementation of lsm write, we only support single column primary key table.
There was a problem hiding this comment.
set_unenforced_primary_key now accepts an ordered list of columns; the bucket merge_insert spec still rejects multi-column PKs but the table-level API does not.
|
|
||
| impl From<MergeInsertSpec> for lancedb::table::MergeInsertSpec { | ||
| fn from(spec: MergeInsertSpec) -> Self { | ||
| Self::Bucket { |
There was a problem hiding this comment.
I think we still want to also surface this to python, so this is of type bucket. We should not just omit this in python layer just because it is the only supported one right now.
There was a problem hiding this comment.
Added spec_type discriminator getter ("bucket" or "unsharded"); column and num_buckets are now Optional and only populated for bucket.
| /// `Some((bucket, entry))` once a writer has been opened. Only one | ||
| /// bucket is cached at a time. Switching buckets requires draining | ||
| /// this slot via `drain_and_close()` first. | ||
| slot: RwLock<Option<(u32, Arc<ShardWriterEntry>)>>, |
There was a problem hiding this comment.
I think we should only support a single ShardWriter to cache. we should not auto split data, instead we should just ask user to split record batches upstream and then use multiple lancedb processes to write each shard. This probably fits the actual application better anyway right? Because typically the upstream engine e.g. Spark/Flink already does this for you.
There was a problem hiding this comment.
Confirmed: single ShardWriter cache, no auto-split, caller pre-shards. Already implemented this way.
| /// the primary key column is missing, or any primary key value is null. | ||
| /// | ||
| /// Empty batches return `None`; the caller should treat them as a no-op. | ||
| fn validate_single_bucket( |
There was a problem hiding this comment.
yeah, looks like you already are doing what I was saying above, so there is further no need to keep a map of shard writers.
There was a problem hiding this comment.
Right — there's no shard-writer map, just a single-slot cache.
There was a problem hiding this comment.
should probably call this module lsm.rs
There was a problem hiding this comment.
Renamed module merge/shard.rs → merge/lsm.rs (preserved git history via rename).
| /// | ||
| /// `column` must equal the table's currently-set unenforced primary key. | ||
| /// `num_buckets` must be in `[1, 1024]`. | ||
| Bucket { column: String, num_buckets: u32 }, |
There was a problem hiding this comment.
looks like we are missing the idea of no partitioning in https://lance.org/format/table/mem_wal/#shard-expression. Or I guess we could use an expression that is basically a constant like 1. I would suggest we add this to the MemWAL spec as unsharded.
There was a problem hiding this comment.
Added MergeInsertSpec::Unsharded variant. Persists a ShardSpec with transform="unsharded" and num_shards=1; merge_insert dispatches all rows to bucket 0 and skips per-row hashing. Worth proposing the named transform on the Lance side too.
- Rename module merge::shard → merge::lsm. - Rename MergeResult.num_inserted_or_updated_rows → num_rows so the field can later cover deletions too. - set_unenforced_primary_key now accepts an ordered list of columns for composite primary keys; the bucket merge_insert spec still requires a single-column key, but the table-level API does not. Position metadata is 1-indexed so caller order is preserved by Lance's Schema::unenforced_primary_key sort. - Add MergeInsertSpec::Unsharded variant for tables that should write through MemWAL with no per-row hashing (single shard). - Expose MergeInsertSpec.spec_type discriminator in Python; column / num_buckets are now Optional and only populated for the bucket variant.
Benchmark — c7i.12xlarge / S3 us-east-1Bench script: large (batch_rows=10000, calls=20)
medium (batch_rows=1000, calls=50)
small (batch_rows=100, calls=50)
Headline: the LSM path is 2.3× to 7.2× faster than baseline merge_insert across batch sizes, with much tighter per-call latency (~120-170 ms p50 vs. 600-880 ms baseline). The win comes from skipping the hash-join scan against the target table — the LSM path is just a WAL append and one durable-write round-trip per call. bucket(N=4) trails unsharded slightly (per-row Murmur3 hash + single-bucket validation overhead), but enables the multi-process scale-out story by letting independent processes own different buckets. Caveats: single-process measurement, modest seed size — the gap should widen with larger seeds because baseline scales with target size while LSM append cost is independent of it. Full report + raw JSON: |
Bench addendum — 1024-dim vector schema + HNSW maintainedRe-ran the same workload against an (1) Vector schema, no maintained index
The LSM advantage is ~25-30% smaller than on the string-only schema (was 4-7×, now 3-6×) — both paths now spend more time on vector payload I/O so the relative win shrinks. (2) Vector schema + IVF_HNSW_PQ maintained on
|
| sweep | lsm_unsharded+hnsw | lsm_bucket(N=4)+hnsw |
|---|---|---|
| medium (1k rows/call) | 646 rows/s | 659 rows/s |
| small (100 rows/call) | 628 rows/s | 608 rows/s |
Throughput plateaus at ~600-650 rows/s once HNSW maintenance kicks in — consistent with ~1.5 ms/row HNSW insertion cost on 1024-dim with M=20 and a 100k-vertex graph (~M·log₂N ≈ 240 distance comps per insertion).
Cost of maintaining HNSW vs. not (lsm_unsharded only)
| sweep | no maintained index | +hnsw | overhead |
|---|---|---|---|
| small | 599 rows/s | 628 rows/s | ~0 (WAL flush dominates) |
| medium | 4,781 rows/s | 646 rows/s | 86% slower |
For small batches the HNSW work fits within the WAL-flush window. For larger batches HNSW becomes the bottleneck. The trade-off is intentional: in exchange for a 7× write slowdown at medium batches, the index stays queryable immediately — no separate optimize / index rebuild needed for the freshly-written rows.
Full numbers + raw JSON in this branch's analysis folder (bench/REPORT-vec.md, bench/results-vec/). EC2 host (lsm-bench-vec, i-0f906316...) terminated.
Bench addendum #2 — small batches + checked-vs-unchecked + HNSWSweep at
Throughput (rows/s) — no maintained index
Throughput (rows/s) — HNSW maintained
Three things to call out
Full report + raw JSON: |
- Rename module merge::shard → merge::lsm. - Rename MergeResult.num_inserted_or_updated_rows → num_rows so the field can later cover deletions too. - set_unenforced_primary_key now accepts an ordered list of columns for composite primary keys; the bucket merge_insert spec still requires a single-column key, but the table-level API does not. Position metadata is 1-indexed so caller order is preserved by Lance's Schema::unenforced_primary_key sort. - Add MergeInsertSpec::Unsharded variant for tables that should write through MemWAL with no per-row hashing (single shard). - Expose MergeInsertSpec.spec_type discriminator in Python; column / num_buckets are now Optional and only populated for the bucket variant.
8989d09 to
62f3062
Compare
|
To reduce complexity, let's split the unenforced primary key part and sharding spec part so that they are separated PRs. |
## Summary Adds `Table::set_unenforced_primary_key` — records a single column as the table's unenforced primary key in Lance schema field metadata. "Unenforced" means LanceDB does not check uniqueness on write; the key is metadata that `merge_insert` consumes. - Single-column only; the column must exist and have a supported dtype (Int32, Int64, Utf8, LargeUtf8, Binary, LargeBinary, FixedSizeBinary). The API accepts an iterable for binding ergonomics but requires exactly one column — compound keys are rejected. - The primary key is immutable: calling this on a table that already has an unenforced primary key is rejected. Concurrent writers racing to set the key fail at commit time rather than silently overriding it. - `RemoteTable` returns `NotSupported`. - Bindings: Python (`AsyncTable`, `LanceTable`, `RemoteTable`) and TypeScript (`Table.setUnenforcedPrimaryKey`). ## Context Split out from #3354 per review feedback, so the unenforced primary key and the `merge_insert` sharding spec land as separate reviewable PRs. No Lance dependency bump — `main` is already on v7.0.0-beta.10, which includes the field-metadata round-trip fix the API relies on. Enforcing primary-key immutability at the Lance commit layer (so the cross-column concurrent race is also rejected) is a companion Lance change: lance-format/lance#6810.
Add LsmWriteSpec (Bucket / Unsharded) and Table::set_lsm_write_spec / unset_lsm_write_spec to install and clear the spec that selects Lance's MemWAL LSM write path. The actual merge_insert dispatch and writer are a follow-up. Python and TypeScript bindings included. Split out from lancedb#3354; the unenforced primary key half landed in lancedb#3394.
## Summary Split out from #3354 Adds `LsmWriteSpec` and `Table::set_lsm_write_spec` / `unset_lsm_write_spec` to install and clear the spec that selects Lance's MemWAL LSM-style write path for `merge_insert`. `LsmWriteSpec` offers three sharding strategies, all built on Lance's `InitializeMemWalBuilder`: - `LsmWriteSpec::bucket(column, num_buckets)` — hash-bucket sharding by the single-column unenforced primary key. - `LsmWriteSpec::identity(column)` — identity sharding by the raw value of a scalar column. - `LsmWriteSpec::unsharded()` — a single MemWAL shard. Each can be refined with `with_maintained_indexes(...)` (indexes the MemWAL keeps up to date as rows are appended) and `with_writer_config_defaults(...)` (default `ShardWriter` configuration recorded in the MemWAL index, so every writer starts from the same defaults). All variants require the table to have an unenforced primary key. - `set_lsm_write_spec` installs the spec by initializing the MemWAL index; `unset_lsm_write_spec` removes it (dropping the MemWAL index), reverting to the standard `merge_insert` path. `unset` is idempotent. - Bindings: Python (`LsmWriteSpec.bucket` / `.identity` / `.unsharded`, `set_lsm_write_spec` / `unset_lsm_write_spec`) and TypeScript (`setLsmWriteSpec` with `specType` `"bucket"` / `"identity"` / `"unsharded"`). `RemoteTable` returns `NotSupported`. The actual `merge_insert` LSM dispatch and `ShardWriter` write path are a follow-up — this PR only installs and clears the spec.
251492b to
8878ec6
Compare
| /// Controls whether `merge_insert` uses the MemWAL LSM write path when an | ||
| /// LSM write spec is installed on the table. | ||
| /// | ||
| /// When `true` (the default), a `merge_insert` on a table with an |
There was a problem hiding this comment.
this does not really default to true, it should default to None, which means the table that has the LsmWriteSpec set will use lsm write, ones without will not use it.
setting it to true for a table without LsmWriteSpec is not meaningful, and we should return an error saying to set it first.
There was a problem hiding this comment.
Done — use_lsm_write is now Option<bool> defaulting to None: unset routes through the LSM path only when a spec is set, Some(false) forces the standard path, and Some(true) errors when no LSM write spec is installed.
| /// | ||
| /// When a table has an LSM write spec, every row in a single `merge_insert` | ||
| /// call must route to the same shard. By default every row is inspected to | ||
| /// verify this. When set to `true`, only the first row of each batch is |
There was a problem hiding this comment.
we should not even inspect the first row of a batch, we should just inspect the first row to know the shard, and then just keep using it.
I think the term "validate_single_shard" is better than "assume_pre_sharded". the default should be validate_single_shard=true.
In addition, we should allow
for_shard: setting the shard field values to avoid the initial row probeusing_shard_writer: setting the actual shard writer cache if caller want to fully reuse a shard writer.
2 is not necessary to be exposed in python and typescript.
There was a problem hiding this comment.
Renaming assume_pre_sharded → validate_single_shard (default true) in this PR. for_shard and using_shard_writer will be added as separate follow-up PRs.
| #[derive(Default)] | ||
| #[allow(clippy::redundant_pub_crate)] | ||
| pub(crate) struct ShardWriterCache { | ||
| writers: RwLock<HashMap<Uuid, Arc<ShardWriterEntry>>>, |
There was a problem hiding this comment.
we are only handling a single shard, it should not need a map.
There was a problem hiding this comment.
Done — ShardWriterCache now holds a single writer slot instead of a map. A dataset writes to one shard at a time; routing a merge_insert to a different shard errors until the current writer is closed via close_lsm_writers.
| /// MemWAL `ShardWriter` cache, co-located with the dataset so open writers | ||
| /// are cached for the session and share the dataset's lifecycle. Shared by | ||
| /// `Arc` across clones of the wrapper. | ||
| shard_writers: Arc<ShardWriterCache>, |
There was a problem hiding this comment.
As said in the comment above, there should only be a single shard writer for this dataset writing to a single shard. It should not allow multiple writers to multiple shards.
There was a problem hiding this comment.
Done — the wrapper holds a single shard_writer (renamed from shard_writers), backed by the single-slot cache. Multiple writers to multiple shards are not allowed.
8d15a79 to
bc6774a
Compare
When an LsmWriteSpec is installed on a table, merge_insert upsert calls are dispatched through Lance's MemWAL ShardWriter (LSM-style append) instead of the standard merge path. - New MergeInsertBuilder options: `use_lsm_write` (routes through the LSM path when a spec is set; pass false to force the standard path, true to require a spec) and `validate_single_shard` (default true; pass false to route by the first row only). - `close_lsm_writers` drains and closes the cached shard writer. - The `on` columns default to, and are validated against, the table's unenforced primary key. - A single shard writer is cached alongside the dataset for the session. - `MergeResult` gains `num_rows`. Bindings and tests updated for Python and TypeScript. Also serializes the `remote/client.rs` env-var tests with a process mutex to fix a pre-existing race that flaked on Windows CI.
bc6774a to
1f80251
Compare
jackye1995
left a comment
There was a problem hiding this comment.
I think we want to actually move shard routing to lance, but that can be done separately. thanks for the work @touch-of-grey !
* feat(rust): support DataFusion Expr for table row deletions (#3415) Modified the parameter of delete to a Predicate that could be constructed from either datafusion Expr, from str (to support SQL predicate), or from String to support python and javascript bindings. When a datafusion Expr is used, it avoids the overhead of serializing to SQL and re-parsing when callers already have an Expr (e.g. from query planning). The native implementation uses lance's `DeleteBuilder::from_expr`. The remote implementation converts the Expr to SQL via `expr_to_sql_string` before sending to the server, consistent with the existing query and count_rows paths. Closes #3204 Signed-off-by: Yuval Lifshitz <[email protected]> Co-authored-by: Claude Code <[email protected]> * feat(remote): send read freshness headers for remote table consistency (#3439) Closes client side work of #3370 ### Summary - Plumbs `read_consistency_interval` from `ConnectBuilder` through `RestfulLanceDbClient` so remote reads attach an `x-lancedb-min-timestamp` freshness header. None = no header (default), zero = "now", positive = `now - interval`. - Adds per-table `FreshnessState` on `RemoteTable`: write responses (`update`, `delete`, `merge_insert`, `add_columns`, `alter_columns`, `drop_columns`) track the committed version, and the next read sends `x-lancedb-min-version` so the server's cache honors read-your-write. - `checkout(v)` / `checkout_tag(t)` / `checkout_latest()` / `restore()` reset the freshness state appropriately; the validating `/describe/` and tag-resolve requests are sent without freshness headers so they don't carry stale state. - Updates Rust, Python, and Node docstrings and calls out that stronger consistency raises per-read latency and cost. ### Testing - Unit tests cover default behavior, interval=0, positive interval, checkout_latest baseline, min_version-after-write, checkout clears state, and the two no-stale-header invariants on `checkout(v)` and `checkout_tag(t)`. - Ran smoke tests against local remote table to verify functionality * chore(deps): bump the rust-minor-patch group across 1 directory with 2 updates (#3440) Bumps the rust-minor-patch group with 2 updates in the / directory: [serde_json](https://github.com/serde-rs/json) and [aws-smithy-runtime](https://github.com/smithy-lang/smithy-rs). Updates `serde_json` from 1.0.149 to 1.0.150 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/serde-rs/json/releases">serde_json's releases</a>.</em></p> <blockquote> <h2>v1.0.150</h2> <ul> <li>Reject non-string enum object keys (<a href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>, thanks <a href="https://github.com/puneetdixit200"><code>@puneetdixit200</code></a>)</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/serde-rs/json/commit/a1ae73ac6a6940a4a57c673aebaa13ed4dfe3e8c"><code>a1ae73a</code></a> Release 1.0.150</li> <li><a href="https://github.com/serde-rs/json/commit/1a360b0a6c003912afc3503c834b0edd798bca28"><code>1a360b0</code></a> Merge pull request <a href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a> from puneetdixit200/reject-non-string-enum-keys</li> <li><a href="https://github.com/serde-rs/json/commit/2037b634f9dccbddc11cff189ebeb5854fa0e01c"><code>2037b63</code></a> Reject non-string enum object keys</li> <li><a href="https://github.com/serde-rs/json/commit/5d30df60e916e9b8fc46c74794007ff271fdfbbf"><code>5d30df6</code></a> Resolve manual_assert_eq pedantic clippy lint</li> <li><a href="https://github.com/serde-rs/json/commit/dc8003a88e7142529cf4a7429c4778af31dadf50"><code>dc8003a</code></a> Raise required compiler for preserve_order feature to 1.85</li> <li><a href="https://github.com/serde-rs/json/commit/a42fa980f8556cda36d896fa3713544b2e5eaa2c"><code>a42fa98</code></a> Unpin CI miri toolchain</li> <li><a href="https://github.com/serde-rs/json/commit/684a60eba18abfc0e0f7ddb0c2cd39f8f60249cf"><code>684a60e</code></a> Pin CI miri to nightly-2026-02-11</li> <li><a href="https://github.com/serde-rs/json/commit/7c7da3302b6b1cdab7f11ea49ca1a74422ab4551"><code>7c7da33</code></a> Raise required compiler to Rust 1.71</li> <li><a href="https://github.com/serde-rs/json/commit/acf4850e2969f1caccab2c4727a90ed006ba35bb"><code>acf4850</code></a> Simplify Number::is_f64</li> <li><a href="https://github.com/serde-rs/json/commit/6b8ceab565dcfe4f83dfaacd287d11c8bd8f306c"><code>6b8ceab</code></a> Resolve unnecessary_map_or clippy lint</li> <li>Additional commits viewable in <a href="https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150">compare view</a></li> </ul> </details> <br /> Updates `aws-smithy-runtime` from 1.11.1 to 1.11.3 <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/smithy-lang/smithy-rs/commits">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(rerankers): inverted scores and incorrect missing-FTS penalty in LinearCombinationReranker (#3437) ## Problem `LinearCombinationReranker.merge_results` has two related bugs that make it return **inverted relevance rankings** — the least relevant document ranks first (closes #3154). ### Bug 1 — `_combine_score` subtracts from 1, inverting the final ranking ```python def _combine_score(self, vector_score, fts_score): return 1 - (self.weight * vector_score + (1 - self.weight) * fts_score) ``` Both `vector_score` (already converted via `_invert_score`) and `fts_score` (BM25 relevance) are in **higher-is-better** space. Wrapping the weighted average in `1 - (...)` flips the direction: a perfectly matching document (`vector_score=1, fts_score=1`) gets `_relevance_score = 0.0`, while a non-matching document gets a high score. ### Bug 2 — Documents missing an FTS score are rewarded, not penalised ```python fts_score = result.get("_score", fill) # fill=1.0 by default ``` When a document has no FTS match, `fts_score = fill = 1.0`. In `_combine_score` (with the bug-1 formula), this large value becomes a **negative penalty** via `1 - (... + 0.3 * 1.0)`, counterintuitively *boosting* the document's score. By contrast, missing vector results correctly receive `_invert_score(fill) = 0.0` (penalised). ## Fix **Bug 1** — remove the `1 -` inversion from `_combine_score`: ```python def _combine_score(self, vector_score, fts_score): return self.weight * vector_score + (1 - self.weight) * fts_score ``` **Bug 2** — use `1 - fill` for missing FTS scores so both penalties are symmetric (mirror of what `_invert_score(fill)` already does for missing vector scores): ```python fts_score = result.get("_score", 1 - fill) # was: fill ``` With `fill=1.0` (default): `1 - 1.0 = 0.0` — missing-FTS entries contribute `0` to the FTS term, identical to how missing-vector entries contribute `0` to the vector term. ## Verification Concrete example from the issue. With `weight=0.7`, `fill=1.0`: | Document | `_distance` | `_score` | Old `_relevance_score` | New `_relevance_score` | |----------|-------------|----------|------------------------|------------------------| | `apple orange` | 0.0 (best) | 2.41 (only FTS) | 0.30 (**wrong: ranked 2nd**) | 1.42 (**correct: ranked 1st**) | | `banana grape` | 0.9999 (worst) | — | 0.70 (**wrong: ranked 1st**) | 0.00 (**correct: ranked last**) | ## Tests Two regression tests added to `python/python/tests/test_rerankers.py`: - `test_linear_combination_best_match_ranks_first` — the document with the smallest distance **and** an FTS match must have the highest `_relevance_score`. - `test_linear_combination_missing_fts_is_penalised` — a document with any FTS score must beat an otherwise-equal document with no FTS match. --------- Co-authored-by: Will Jones <[email protected]> * fix: remove primary key constraint from MemWAL bucket sharding (#3435) ## Summary - Bump lance dependency from `v7.0.0-beta.13` to `v7.0.0-rc.1` - Remove PK constraint from `LsmWriteSpec::Bucket` docs and `Table::set_lsm_write_spec` docs - Remove test assertions that expected rejection when no PK is set or when bucket column != PK Closes https://github.com/lance-format/lance/issues/6917 * fix: allow appending arrow.json data into lance.json tables (#3429) When a table is created with `pa.json_()` (PyArrow's JSON extension type), it is stored internally as `lance.json` (LargeBinary with `lance.json` extension metadata). Calling `table.add()` with `pa.json_()` data failed with: ``` RuntimeError: lance error: Append with different schema: `data` should have type json but type was large_binary ``` `build_field_exprs` in `rust/lancedb/src/table/datafusion/cast.rs` saw that the input field (`Utf8` with `arrow.json` metadata) differed from the table field (`LargeBinary` with `lance.json` metadata). Since `can_cast_types(Utf8, LargeBinary)` is true, it inserted a DataFusion `Utf8 → LargeBinary` cast. That cast preserved the input field's `arrow.json` extension metadata instead of adopting the table's `lance.json` metadata, so lance-core detected a schema mismatch and rejected the append. This adds a special case in `build_field_exprs`: when the input is `arrow.json` and the table field is `lance.json`, the expression is passed through unchanged. Lance-core's write path already handles the `arrow.json → lance.json` conversion (including JSONB encoding), so no DataFusion cast is needed. Fixes #3144 Continues #3291 from a fork (the original author's branch could not be pushed to). The original commits are preserved; an additional commit fixes the CI failures on that PR — formatting, a missing trait import, and read-back assertions that assumed binary storage when a lance.json column is read back as `Utf8`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: yunju.lly <[email protected]> Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> * chore: update lance dependency to v7.1.0-beta.4 (#3450) ## Summary - Updates Lance Rust workspace dependencies to `v7.1.0-beta.4` using `ci/set_lance_version.py`. - Updates the Java `lance-core` dependency property to `7.1.0-beta.4`. - Triggering Lance tag: https://github.com/lance-format/lance/releases/tag/v7.1.0-beta.4 ## Verification - `cargo clippy --workspace --tests --all-features -- -D warnings` - `cargo fmt --all` Co-authored-by: Daniel Rammer <[email protected]> * ci: fix pypi publish on mac/windows/arm (#3449) The python-v0.32.0 publish run failed on every build matrix entry. Three independent issues: 1. **Mac and Windows**: `pypa/gh-action-pypi-publish` only runs on Linux, but was being called inline from each build job. 2. **Linux (all arches)**: `pypa/gh-action-pypi-publish` derives its docker image name from `github.action_repository`, which is empty when the action is invoked from inside a composite action (actions/runner#2473 — pypa's own `action.yml` references this bug). It falls back to `github.repository`, generating `docker://ghcr.io/lancedb/lancedb:<tag>`, which doesn't exist → `denied`. Only the ARM matrix entry surfaced this because it failed first and cancel-cascaded the rest. 3. **Windows**: `upload-artifact` in `build_windows_wheel` pointed at `python\target\wheels`, but maturin writes to the workspace-root `target/wheels`. The artifact was always empty. Also, `pypi-publish.yml` passed a `vcpkg_token` input that the composite doesn't declare. ## Changes - Build jobs (linux/mac/windows) now upload their wheels as `actions/upload-artifact` artifacts. - New Linux `publish` job downloads all wheel artifacts and runs the Fury or PyPA publish step directly (not via a composite), so `github.action_repository` resolves correctly. - Delete the unused `upload_wheel` composite action. - Drop the broken upload-artifact step inside `build_windows_wheel`. - Remove the bogus `vcpkg_token` input. - Fury upload now loops over all wheels instead of just the first. - Bump `actions/checkout`, `actions/upload-artifact`, `actions/download-artifact` to current major versions (Node 24) to clear deprecation warnings. - Bump Windows job timeout 60 → 90 minutes; previous run was cancel-timing-out on a 60m cap. - Use `rust-lld` as the Windows MSVC linker via `CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER`. `link.exe` is single-threaded and the long pole on Windows builds. Fixes #3445 ## Test plan - [x] Open this PR — `paths` filter triggers a dry-run build on all three platforms. - [x] Verify all three builds produce wheels. - [x] Confirm the `pypa/gh-action-pypi-publish` container actually starts (the actions/runner#2473 bug) via the `publish-dry-run` job pointed at TestPyPI. - [x] **REMOVE BEFORE MERGE**: drop the `publish-dry-run` job and the now-redundant `actions/upload-artifact` runs on PRs (currently always-on so the dry-run has wheels to publish). - [ ] After merge, cherry-pick onto `python-v0.32.0` and force-push the tag to re-trigger the publish. * ci: drop manylinux2_17 wheel builds (#3455) manylinux2_17 reached EOL in 2024 and pyarrow stopped publishing 2_17 wheels long ago. We already build manylinux2_28 wheels, so drop the 2_17 matrix entries. Fixes #3452 Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> * perf: migrate list_indices to use Lance's describe_indices (#3108) This needs https://github.com/lance-format/lance/pull/6099 to work. Closes #3140 --------- Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> * feat(python): unify sync create_index API to match async API (#2882) ## Summary - Transitions `LanceTable` and `RemoteTable` to use the unified `create_index()` API matching `AsyncTable` - Deprecates `create_scalar_index()` and `create_fts_index()` with deprecation warnings - Adds detection logic to distinguish legacy vs new API calls - Adds `@overload` decorators for type checker compatibility - Adds `accelerator` parameter to IVF config classes for GPU support **New API:** ```python table.create_index("vec", config=IvfPq(distance_type="l2")) table.create_index("col", config=BTree()) table.create_index("text_col", config=FTS(with_position=True)) ``` **Legacy API (deprecated):** ```python table.create_index("l2", vector_column_name="vec") # emits DeprecationWarning table.create_scalar_index("col", index_type="BTREE") # deprecated table.create_fts_index("text_col") # deprecated ``` Fixes #2879 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <[email protected]> * test(python): fix remote create_index schema fixture (#3462) The latest main Python workflow fails across multiple matrix jobs because `test_remote_create_index_new_api` opens a remote table whose mocked schema only exposes `id`, while the new `create_index(..., config=...)` path validates the requested indexed columns. This updates the remote-table fixture to include the indexed columns used by the smoke test and checks the emitted column payloads, keeping the test aligned with the schema-aware API path. * chore: upgrade Rust toolchain to 1.95.0 (#3390) Bumps the pinned toolchain in `rust-toolchain.toml` from 1.94.0 to 1.95.0. Fixes new lints surfaced by clippy on 1.95.0: - `manual_checked_ops` — fragment size mean in `table.rs` uses `checked_div` - `explicit_counter_loop` — shuffle test loop in `shuffle.rs` No rustc warnings were introduced. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> * feat(table): route merge_insert through the MemWAL LSM write path (#3354) ## Summary When an `LsmWriteSpec` is installed on a table (#3396), `merge_insert` upsert calls are dispatched through Lance's MemWAL `ShardWriter` (LSM-style append) instead of the standard merge path. - **`use_lsm_write`** — a `merge_insert` builder option, default `true`; set it `false` to use the standard path for a call even when a spec is set. - **`assume_pre_sharded`** — a `merge_insert` builder option, default `false`; skips the per-row shard check and routes by the first row only. - **`close_lsm_writers`** — drains and closes the table's cached MemWAL shard writers. - The `merge_insert` **`on`** columns default to, and are validated against, the table's unenforced primary key. - Shard writers are cached alongside the dataset (in `DatasetConsistencyWrapper`) and reused for the session. - `MergeResult` gains **`num_rows`** — on the LSM path the insert/update breakdown is unknown until compaction, so only the total is reported. Routing covers all three sharding strategies — bucket (murmur3, Iceberg-compatible), identity, and unsharded. Each `merge_insert` call targets a single shard; the whole input is collected and validated before a single atomic `ShardWriter::put`, so a validation failure leaves the MemWAL untouched. Bindings: Python (`merge_insert(...).use_lsm_write(...)` / `.assume_pre_sharded(...)`, `Table.close_lsm_writers`) and TypeScript (`mergeInsert(...).useLsmWrite(...)` / `.assumePreSharded(...)`, `Table.closeLsmWriters`). ## Context Reconstructed from the original #3354 branch onto current `main`: the branch predated the #3394 (unenforced primary key) / #3396 (`LsmWriteSpec`) split and has been rebuilt on that merged foundation. Depends on Lance `v7.0.0-beta.13`. The MemWAL read path (reading un-flushed shard data back into queries) and remote (LanceDB Cloud) LSM support are follow-ups. --------- Co-authored-by: Jack Ye <[email protected]> * chore: update Lance to v7.2.0-beta.1 (#3461) Update the Rust workspace Lance git dependencies and Java lance-core dependency to v7.2.0-beta.1. This keeps LanceDB aligned with the latest Lance beta release and refreshes the Cargo lockfile for the new Lance dependency graph. * chore(deps): bump the rust-minor-patch group with 5 updates (#3465) Bumps the rust-minor-patch group with 5 updates: | Package | From | To | | --- | --- | --- | | [log](https://github.com/rust-lang/log) | `0.4.29` | `0.4.30` | | [serde_json](https://github.com/serde-rs/json) | `1.0.149` | `1.0.150` | | [http](https://github.com/hyperium/http) | `1.4.0` | `1.4.1` | | [uuid](https://github.com/uuid-rs/uuid) | `1.23.1` | `1.23.2` | | [aws-smithy-runtime](https://github.com/smithy-lang/smithy-rs) | `1.11.1` | `1.11.3` | Updates `log` from 0.4.29 to 0.4.30 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/rust-lang/log/releases">log's releases</a>.</em></p> <blockquote> <h2>0.4.30</h2> <h3>What's Changed</h3> <ul> <li>Support capturing of <code>std::net</code> types by <a href="https://github.com/KodrAus"><code>@KodrAus</code></a> in <a href="https://redirect.github.com/rust-lang/log/pull/724">rust-lang/log#724</a></li> </ul> <h3>New Contributors</h3> <ul> <li><a href="https://github.com/V0ldek"><code>@V0ldek</code></a> made their first contribution in <a href="https://redirect.github.com/rust-lang/log/pull/720">rust-lang/log#720</a></li> <li><a href="https://github.com/woodruffw"><code>@woodruffw</code></a> made their first contribution in <a href="https://redirect.github.com/rust-lang/log/pull/723">rust-lang/log#723</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/rust-lang/log/compare/0.4.29...0.4.30">https://github.com/rust-lang/log/compare/0.4.29...0.4.30</a></p> <h3>Notable Changes</h3> <ul> <li>MSRV is bumped to 1.71.0 in <a href="https://redirect.github.com/rust-lang/log/pull/723">rust-lang/log#723</a></li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/rust-lang/log/blob/master/CHANGELOG.md">log's changelog</a>.</em></p> <blockquote> <h2>[0.4.30] - 2026-05-21</h2> <h3>What's Changed</h3> <ul> <li>Support capturing of <code>std::net</code> types by <a href="https://github.com/KodrAus"><code>@KodrAus</code></a> in <a href="https://redirect.github.com/rust-lang/log/pull/724">rust-lang/log#724</a></li> </ul> <h3>New Contributors</h3> <ul> <li><a href="https://github.com/V0ldek"><code>@V0ldek</code></a> made their first contribution in <a href="https://redirect.github.com/rust-lang/log/pull/720">rust-lang/log#720</a></li> <li><a href="https://github.com/woodruffw"><code>@woodruffw</code></a> made their first contribution in <a href="https://redirect.github.com/rust-lang/log/pull/723">rust-lang/log#723</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/rust-lang/log/compare/0.4.29...0.4.30">https://github.com/rust-lang/log/compare/0.4.29...0.4.30</a></p> <h3>Notable Changes</h3> <ul> <li>MSRV is bumped to 1.71.0 in <a href="https://redirect.github.com/rust-lang/log/pull/723">rust-lang/log#723</a></li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/rust-lang/log/commit/9c55760b499b18e81de7df5f3c13a67d5661131d"><code>9c55760</code></a> Merge pull request <a href="https://redirect.github.com/rust-lang/log/issues/725">#725</a> from rust-lang/cargo/0.4.30</li> <li><a href="https://github.com/rust-lang/log/commit/d1acb0585c0f6af5dc466eb255187cd6d3b7359e"><code>d1acb05</code></a> update docs on current MSRV and note latest bump in changelog</li> <li><a href="https://github.com/rust-lang/log/commit/50682937b0d9ec9a18c4c9b0510d889762e20e34"><code>5068293</code></a> prepare for 0.4.30 release</li> <li><a href="https://github.com/rust-lang/log/commit/7ccd873cb50de97690d46f69d8744a61f0b87c46"><code>7ccd873</code></a> Merge pull request <a href="https://redirect.github.com/rust-lang/log/issues/724">#724</a> from rust-lang/feat/net-to-value</li> <li><a href="https://github.com/rust-lang/log/commit/923dfaaf00dca352efe45930ae009d9a22526597"><code>923dfaa</code></a> fix up test cfgs</li> <li><a href="https://github.com/rust-lang/log/commit/ecb7de8daf7feec9dcf0d31cecc8523b31a8d104"><code>ecb7de8</code></a> gate net value impls on std</li> <li><a href="https://github.com/rust-lang/log/commit/67bb4f6d2e377b0008b740631124f292e80d4e5d"><code>67bb4f6</code></a> run fmt</li> <li><a href="https://github.com/rust-lang/log/commit/25f49fe3d31e7a0797652ad4bacaff633f7237cd"><code>25f49fe</code></a> rework net type capturing</li> <li><a href="https://github.com/rust-lang/log/commit/7087dcb95cb925364b4ba1da0d7c0eead9356dfc"><code>7087dcb</code></a> feat: impl ToValue for core::net types</li> <li><a href="https://github.com/rust-lang/log/commit/67bc7e32c68a4a8908d1016693418f12b43bab90"><code>67bc7e3</code></a> Merge pull request <a href="https://redirect.github.com/rust-lang/log/issues/723">#723</a> from woodruffw-forks/ww/ci</li> <li>Additional commits viewable in <a href="https://github.com/rust-lang/log/compare/0.4.29...0.4.30">compare view</a></li> </ul> </details> <br /> Updates `serde_json` from 1.0.149 to 1.0.150 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/serde-rs/json/releases">serde_json's releases</a>.</em></p> <blockquote> <h2>v1.0.150</h2> <ul> <li>Reject non-string enum object keys (<a href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a>, thanks <a href="https://github.com/puneetdixit200"><code>@puneetdixit200</code></a>)</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/serde-rs/json/commit/a1ae73ac6a6940a4a57c673aebaa13ed4dfe3e8c"><code>a1ae73a</code></a> Release 1.0.150</li> <li><a href="https://github.com/serde-rs/json/commit/1a360b0a6c003912afc3503c834b0edd798bca28"><code>1a360b0</code></a> Merge pull request <a href="https://redirect.github.com/serde-rs/json/issues/1324">#1324</a> from puneetdixit200/reject-non-string-enum-keys</li> <li><a href="https://github.com/serde-rs/json/commit/2037b634f9dccbddc11cff189ebeb5854fa0e01c"><code>2037b63</code></a> Reject non-string enum object keys</li> <li><a href="https://github.com/serde-rs/json/commit/5d30df60e916e9b8fc46c74794007ff271fdfbbf"><code>5d30df6</code></a> Resolve manual_assert_eq pedantic clippy lint</li> <li><a href="https://github.com/serde-rs/json/commit/dc8003a88e7142529cf4a7429c4778af31dadf50"><code>dc8003a</code></a> Raise required compiler for preserve_order feature to 1.85</li> <li><a href="https://github.com/serde-rs/json/commit/a42fa980f8556cda36d896fa3713544b2e5eaa2c"><code>a42fa98</code></a> Unpin CI miri toolchain</li> <li><a href="https://github.com/serde-rs/json/commit/684a60eba18abfc0e0f7ddb0c2cd39f8f60249cf"><code>684a60e</code></a> Pin CI miri to nightly-2026-02-11</li> <li><a href="https://github.com/serde-rs/json/commit/7c7da3302b6b1cdab7f11ea49ca1a74422ab4551"><code>7c7da33</code></a> Raise required compiler to Rust 1.71</li> <li><a href="https://github.com/serde-rs/json/commit/acf4850e2969f1caccab2c4727a90ed006ba35bb"><code>acf4850</code></a> Simplify Number::is_f64</li> <li><a href="https://github.com/serde-rs/json/commit/6b8ceab565dcfe4f83dfaacd287d11c8bd8f306c"><code>6b8ceab</code></a> Resolve unnecessary_map_or clippy lint</li> <li>Additional commits viewable in <a href="https://github.com/serde-rs/json/compare/v1.0.149...v1.0.150">compare view</a></li> </ul> </details> <br /> Updates `http` from 1.4.0 to 1.4.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/hyperium/http/releases">http's releases</a>.</em></p> <blockquote> <h2>v1.4.1</h2> <h2>tl;dr</h2> <ul> <li>Fix <code>PathAndQuery::from_static()</code> and <code>from_shared()</code> to reject inputs that do not start with <code>/</code>.</li> <li>Fix <code>Extend</code> for <code>HeaderMap</code> to clamp max size hint and not overflow.</li> <li>Fix <code>header::IntoIter</code> that could use-after-free if the generic value type could panic on drop.</li> <li>Fix <code>header::{IterMut, ValuesIterMut}</code> to not violate stacked borrows.</li> </ul> <h2>What's Changed</h2> <ul> <li>chore(header): fix clippy::assign_op_pattern by <a href="https://github.com/rxc-amzn"><code>@rxc-amzn</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/806">hyperium/http#806</a></li> <li>ci: pin itoa in msrv job by <a href="https://github.com/seanmonstar"><code>@seanmonstar</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/813">hyperium/http#813</a></li> <li>Remove unnecessary explicit lifetimes by <a href="https://github.com/jplatte"><code>@jplatte</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/815">hyperium/http#815</a></li> <li>chore(ci): update to actions/checkout@v6 by <a href="https://github.com/tottoto"><code>@tottoto</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/819">hyperium/http#819</a></li> <li>tests: update to rand 0.10 by <a href="https://github.com/tottoto"><code>@tottoto</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/818">hyperium/http#818</a></li> <li>refactor: Remove usage of float instruction by <a href="https://github.com/AurelienFT"><code>@AurelienFT</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/823">hyperium/http#823</a></li> <li>refactor(uri): consolidate PathAndQuery::from_shared and from_static by <a href="https://github.com/seanmonstar"><code>@seanmonstar</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/825">hyperium/http#825</a></li> <li>fix(uri): reject Path::from_shared/from_static if doesn't start with slash by <a href="https://github.com/seanmonstar"><code>@seanmonstar</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/826">hyperium/http#826</a></li> <li>Rephrase comment by <a href="https://github.com/daalfox"><code>@daalfox</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/827">hyperium/http#827</a></li> <li>Fix typo in request builder docs by <a href="https://github.com/vleksis"><code>@vleksis</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/831">hyperium/http#831</a></li> <li>fix: clamp Extend size hint so HeaderMap reserve cannot overflow by <a href="https://github.com/SAY-5"><code>@SAY-5</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/833">hyperium/http#833</a></li> <li>fix(headers): fix stacked borrows for IterMut/ValuesIterMut by <a href="https://github.com/seanmonstar"><code>@seanmonstar</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/837">hyperium/http#837</a></li> <li>fix(header): use a set_len guard in IntoIter drop by <a href="https://github.com/seanmonstar"><code>@seanmonstar</code></a> in <a href="https://redirect.github.com/hyperium/http/pull/838">hyperium/http#838</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/rxc-amzn"><code>@rxc-amzn</code></a> made their first contribution in <a href="https://redirect.github.com/hyperium/http/pull/806">hyperium/http#806</a></li> <li><a href="https://github.com/AurelienFT"><code>@AurelienFT</code></a> made their first contribution in <a href="https://redirect.github.com/hyperium/http/pull/823">hyperium/http#823</a></li> <li><a href="https://github.com/daalfox"><code>@daalfox</code></a> made their first contribution in <a href="https://redirect.github.com/hyperium/http/pull/827">hyperium/http#827</a></li> <li><a href="https://github.com/vleksis"><code>@vleksis</code></a> made their first contribution in <a href="https://redirect.github.com/hyperium/http/pull/831">hyperium/http#831</a></li> <li><a href="https://github.com/SAY-5"><code>@SAY-5</code></a> made their first contribution in <a href="https://redirect.github.com/hyperium/http/pull/833">hyperium/http#833</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/hyperium/http/compare/v1.4.0...v1.4.1">https://github.com/hyperium/http/compare/v1.4.0...v1.4.1</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/hyperium/http/blob/master/CHANGELOG.md">http's changelog</a>.</em></p> <blockquote> <h1>1.4.1 (May 25, 2026)</h1> <ul> <li>Fix <code>PathAndQuery::from_static()</code> and <code>from_shared()</code> to reject inputs that do not start with <code>/</code>.</li> <li>Fix <code>Extend</code> for <code>HeaderMap</code> to clamp max size hint and not overflow.</li> <li>Fix <code>header::IntoIter</code> that could use-after-free if the generic value type could panic on drop.</li> <li>Fix <code>header::{IterMut, ValuesIterMut}</code> to not violate stacked borrows.</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/hyperium/http/commit/a24c968ba3b53c4c9953164235664cab9e8fa315"><code>a24c968</code></a> v1.4.1</li> <li><a href="https://github.com/hyperium/http/commit/bc3b0441be3065fc2653e9b3b1392c0fed873482"><code>bc3b044</code></a> fix(header): use a set_len guard in IntoIter drop (<a href="https://redirect.github.com/hyperium/http/issues/838">#838</a>)</li> <li><a href="https://github.com/hyperium/http/commit/1b968dc519c49b1922bc546c95f33900e684f4ab"><code>1b968dc</code></a> fix(header): fix stacked borrows for IterMut/ValuesIterMut (<a href="https://redirect.github.com/hyperium/http/issues/837">#837</a>)</li> <li><a href="https://github.com/hyperium/http/commit/6e2dd42a15d4c1711baa2191bd1d15022e1e2e9c"><code>6e2dd42</code></a> fix: clamp Extend size hint so HeaderMap reserve cannot overflow (<a href="https://redirect.github.com/hyperium/http/issues/833">#833</a>)</li> <li><a href="https://github.com/hyperium/http/commit/68e0abb052a243a5530ad4c404cb0b169a7ecb4a"><code>68e0abb</code></a> docs: fix typo in request builder docs (<a href="https://redirect.github.com/hyperium/http/issues/831">#831</a>)</li> <li><a href="https://github.com/hyperium/http/commit/29dd307b3e382a4343fc917fa3c41125ac50dfb8"><code>29dd307</code></a> docs(extensions): rephrase internal comment (<a href="https://redirect.github.com/hyperium/http/issues/827">#827</a>)</li> <li><a href="https://github.com/hyperium/http/commit/ae48fb55b090b4859d38a3a49a8332b83492d7c1"><code>ae48fb5</code></a> fix(uri): reject Path::from_shared/from_static if doesn't start with slash (#...</li> <li><a href="https://github.com/hyperium/http/commit/1ad200ec4ce5ec714005d500f8b0cea39c6c16f5"><code>1ad200e</code></a> refactor(uri): consolidate PathAndQuery::from_shared and from_static (<a href="https://redirect.github.com/hyperium/http/issues/825">#825</a>)</li> <li><a href="https://github.com/hyperium/http/commit/d59d939f928c6d836f5c87940f01399cb45cddb9"><code>d59d939</code></a> refactor: Remove usage of float instruction (<a href="https://redirect.github.com/hyperium/http/issues/823">#823</a>)</li> <li><a href="https://github.com/hyperium/http/commit/ed680c4d90a514b7f427efc99b61e60632811d2f"><code>ed680c4</code></a> tests: update to rand 0.10 (<a href="https://redirect.github.com/hyperium/http/issues/818">#818</a>)</li> <li>Additional commits viewable in <a href="https://github.com/hyperium/http/compare/v1.4.0...v1.4.1">compare view</a></li> </ul> </details> <br /> Updates `uuid` from 1.23.1 to 1.23.2 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/uuid-rs/uuid/releases">uuid's releases</a>.</em></p> <blockquote> <h2>v1.23.2</h2> <h2>What's Changed</h2> <ul> <li>Improve error messages for ambiguous formats by <a href="https://github.com/KodrAus"><code>@KodrAus</code></a> in <a href="https://redirect.github.com/uuid-rs/uuid/pull/882">uuid-rs/uuid#882</a></li> <li>Prepare for 1.23.2 release by <a href="https://github.com/KodrAus"><code>@KodrAus</code></a> in <a href="https://redirect.github.com/uuid-rs/uuid/pull/883">uuid-rs/uuid#883</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/uuid-rs/uuid/compare/v1.23.1...v1.23.2">https://github.com/uuid-rs/uuid/compare/v1.23.1...v1.23.2</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/uuid-rs/uuid/commit/d11965705f88ae2546e0d277dac8f52f47e5694f"><code>d119657</code></a> Merge pull request <a href="https://redirect.github.com/uuid-rs/uuid/issues/883">#883</a> from uuid-rs/cargo/v1.23.2</li> <li><a href="https://github.com/uuid-rs/uuid/commit/0651cfcb895d5d0b7e21edba621422bf446d585f"><code>0651cfc</code></a> prepare for 1.23.2 release</li> <li><a href="https://github.com/uuid-rs/uuid/commit/e8dea0c1fdc69e066cff93957e441022acfcb90f"><code>e8dea0c</code></a> Merge pull request <a href="https://redirect.github.com/uuid-rs/uuid/issues/882">#882</a> from uuid-rs/fix/error-msgs</li> <li><a href="https://github.com/uuid-rs/uuid/commit/bdc429a8c731a067b0d49c8890c6209dbb9f02db"><code>bdc429a</code></a> fix up serde messages</li> <li><a href="https://github.com/uuid-rs/uuid/commit/d4342e400df7adb17028b499a53a96228951baec"><code>d4342e4</code></a> make indexes 0 based and fix up more error messages</li> <li><a href="https://github.com/uuid-rs/uuid/commit/4ad479fc20fd09f34467e00adf176d4fdbdf9161"><code>4ad479f</code></a> work on more accurate parser errors</li> <li>See full diff in <a href="https://github.com/uuid-rs/uuid/compare/v1.23.1...v1.23.2">compare view</a></li> </ul> </details> <br /> Updates `aws-smithy-runtime` from 1.11.1 to 1.11.3 <details> <summary>Commits</summary> <ul> <li>See full diff in <a href="https://github.com/smithy-lang/smithy-rs/commits">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore: bump lance to 7.2.0-beta.3 (#3471) This updates the workspace Lance dependencies from `v7.1.0-beta.4` to `v7.2.0-beta.3` and refreshes `Cargo.lock`. The lockfile now points at Lance commit `7c070f760fa8e24c8015cb2afbd22c5e6b7898e8` and includes the transitive dependency updates required by the new beta. * Bump version: 0.33.0-beta.1 → 0.33.1-beta.0 * Bump version: 0.30.0-beta.1 → 0.30.1-beta.0 * feat(python): support remote tables in PyTorch dataloaders (#3432) This PR makes remote LanceDB tables usable from PyTorch multiprocessing workers. Remote tables now carry enough safe JSON connection state to reopen themselves after pickle/spawn or fork, and permutations lazily rebuild their reader from restored tables instead of trying to reuse process-local handles. This addresses the remote-table gap in the PyTorch dataset path while preserving the explicit connection factory escape hatch for custom worker-side credential loading or non-serializable header providers. Validated with targeted remote table, permutation, and PyTorch DataLoader tests. * ci: move Lance dependency bump flow into skill (#3475) Moves the Lance dependency bump process into an in-repository skill so local agents and GitHub Actions share the same workflow definition. The update workflow is now an explicit, optional-tag entrypoint; latest-release resolution, duplicate PR handling, Java/Rust dependency updates, and Sophon follow-up are documented in the skill and backed by a small deterministic helper. * feat: add update_field_metadata to edit per-field metadata (#3482) ### Summary Adds update_field_metadata to the client SDK (Rust core, Python, and TypeScript) so clients can edit per-field (column) Arrow metadata (schema.fields[].metadata) ### Testing - added unit tests - ran E2E against a local server on both local and remote tables (set → merge → delete), across Python sync/async and TypeScript ### Next steps - deprecate replace_field_metadata in the python lancedb favor of this (typescript didn't have replace_field_metadata method). This matches Lance's API direction (Lance already deprecated replace_field_metadata for update_field_metadata) * feat: deprecate replace_field_metadata for update_field_metadata (#3484) ### Summary Deprecates the Python replace_field_metadata (on Table and AsyncTable) in favor of update_field_metadata. Mirrors Lance, which already deprecated Dataset.replace_field_metadata for update_field_metadata. Stacked on top of #3482 as this was a follow-up task after adding update_field_metadata * feat(python): support blob modes in query to_pandas (#3487) ## Feature - What is the new feature? - Adds `blob_mode` support to sync and async Python query `to_pandas()` APIs. - Enables plain scan queries to return blob columns as lazy `BlobFile` objects, raw bytes, or blob descriptions. - Lets namespace-backed local tables use Lance native blob-aware pandas conversion for lazy blobs. - Why do we need this feature? - Table and Lance dataset/scanner APIs already support blob-aware pandas conversion, but LanceDB query builders did not expose that capability. - Geneva and other callers should be able to use query-level `to_pandas(blob_mode=...)` without manually constructing Lance scanners. - How does it work? - Plain scan queries route through Lance scanner native `to_pandas(blob_mode=...)`, preserving filter, projection, limit, offset, row id, and alias/expression projection behavior. - Non-native query shapes keep existing Arrow fallback semantics and raise a clear error when they return blob columns with `blob_mode="lazy"` or `blob_mode="bytes"`. - Focused tests cover table/query blob modes, filter/select/limit/offset/alias query cases, async query behavior, vector-query error boundaries, and namespace-backed lazy blobs. ## Validation - `cd python && .venv/bin/maturin develop --uv --extras tests,dev --profile dev` - `cd python && uv run --frozen --no-sync pytest python/tests/test_table.py::test_table_to_pandas_blob_modes python/tests/test_table.py::test_async_table_to_pandas_blob_bytes python/tests/test_query.py::test_plain_scan_query_to_pandas_blob_modes python/tests/test_query.py::test_plain_scan_query_to_pandas_blob_projection python/tests/test_query.py::test_async_plain_scan_query_to_pandas_blob_projection python/tests/test_query.py::test_vector_query_to_pandas_blob_mode_requires_native_path python/tests/test_namespace.py::TestNamespaceConnection::test_table_to_pandas_blob_lazy_through_namespace -q` - `cd python && uv run --frozen --no-sync ruff format --check .` - `cd python && uv run --frozen --no-sync ruff check .` - `git diff --check` * Bump version: 0.33.1-beta.0 → 0.33.1-beta.1 * Bump version: 0.30.1-beta.0 → 0.30.1-beta.1 * fix(rerankers/mrr): raise ValueError on empty vector_results list (#3469) ## What's broken `MRRReranker.rerank_multivector([])` raises `IndexError: list index out of range`. The crash happens on line 128 (the `all()` type-homogeneity check passes vacuously on an empty iterable) and on line 134 which accesses `vector_results[0]` unconditionally, with no prior guard for an empty list. ## Why it happens `all()` over an empty iterable returns `True`, so the type check silently passes and execution falls through to `vector_results[0]` which crashes. ## Fix Added a two-line guard at the top of `rerank_multivector` that raises a clear `ValueError("vector_results must not be empty")` before any indexing occurs. ## Test Added `test_mrr_reranker_empty_input` in `test_rerankers.py` which calls `rerank_multivector([])` and asserts that a `ValueError` with the message "must not be empty" is raised. Fixes #3468 Co-authored-by: Aegis Dev <[email protected]> * fix(rerankers): guard against empty vector_results in RRFReranker.rerank_multivector (#3467) ## What's broken Calling `RRFReranker().rerank_multivector([])` crashes with `IndexError: list index out of range` because the method accesses `vector_results[0]` for the type-homogeneity check before verifying the list is non-empty. The `all()` call passes vacuously on an empty iterable so the crash hits the next lines. ```python from lancedb.rerankers import RRFReranker RRFReranker().rerank_multivector([]) # IndexError: list index out of range ``` ## Why it happens The type check uses `vector_results[0]` as the reference type but never guards against an empty list. `all(...)` short-circuits to `True` when the iterable is empty, so the bad index access on the lines that follow is never reached by the existing guard logic. ## Fix Add an explicit empty-list check before any indexing. * docs: add cross-SDK parity guidance for code review (#3464) Adds a REVIEW.md at the repo root with cross-SDK parity guidance for automated code review. The Claude Code review feature automatically loads `REVIEW.md` as review-only context. This is intentionally a semantic nudge, not a deterministic check, it relies on the reviewer reading the sibling SDK, so it will catch most gaps. * test(python): add regression test for nullable struct with None (#2654) (#3483) ## Summary Regression test for [issue #2654](https://github.com/lancedb/lancedb/issues/2654) — a nullable struct column whose first batch contains only `None` values crashed in `_align_field_types` with `AttributeError: 'pyarrow.lib.DataType' object has no attribute 'fields'`. The actual fix landed in #3394, but no test was added. This PR adds the reproducer from the issue as a test. ## Test plan - `test_add_nullable_struct_with_none`: creates a table with a nullable struct column, adds a row with a non-null struct value, then a row with `None` for the struct field. Verifies both rows land correctly. - Uses Lance file format v2.1 (`new_table_data_storage_version="2.1"`) because nullable structs aren't supported on v2.0. ## Related - #3028 (the original fix attempt, now superseded) * ci: update python lockfile weekly (#3498) Make sure we are getting security fixes in there regularly, and other useful bumps. * feat(rust): support datafusion expressions for merge insert predicates (#3444) ### Description This PR exposes native DataFusion expression support in the Rust SDK's `MergeInsertBuilder` via two new builder methods: `when_matched_update_all_expr` and `when_not_matched_by_source_delete_expr`. For remote LanceDB tables (where operations are serialized over HTTP/JSON to the SaaS backend), native DataFusion expression trees cannot be executed directly. The SDK handles this gracefully by returning a `NotSupported` error. ### Key Changes - **`MergeFilter` Enum**: Introduced a helper enum to store either a SQL string or a native `datafusion_expr::Expr`. - **`MergeInsertBuilder`**: Updated `when_matched_update_all_filt` and `when_not_matched_by_source_delete_filt` fields to store the new enum, and added `when_matched_update_all_expr` and `when_not_matched_by_source_delete_expr` builder methods. - **Execution & Remote Dispatch**: Dispatched the filter variants during local execution, and rejected expression filters with a clean `NotSupported` error in remote table request conversion. - **Testing**: Added a `test_merge_insert_expr` unit test covering conditional updates and deletes with programmatically built DataFusion expressions. ### Verification - Added integration test `test_merge_insert_expr` which successfully compiles and passes. - Formatted and linted the code. Closes #3416 * fix(python): route blob query pandas through scanner (#3491) ## Bug Fix ### What is the bug? `QueryBuilder.to_pandas(blob_mode="descriptions")` could still fall back to `self.to_arrow()` for query outputs with blob columns. Custom query subclasses or wrappers can have `to_arrow()` behavior that is not compatible with pandas blob-description conversion, which can surface as low-level Arrow/list-batch conversion failures. ### What issues or incorrect behavior does the bug cause? Callers need to carry local `to_pandas` or plain-scan adapter special casing for blob descriptions, and scanner-only kwargs such as row addresses and fragment selection are not represented in LanceDB query state. ### How does this PR fix the problem? This PR routes blob-output query `to_pandas()` through the Lance scanner path for `lazy`, `bytes`, and `descriptions` modes when the query is a scanner-backed plain scan. For `blob_mode="descriptions"` with `flatten`, it collects scanner Arrow/table output, applies LanceDB `flatten_columns`, and converts to pandas from there. Non-plain blob query shapes now fail with a clear unsupported error instead of falling into subclass `to_arrow()` behavior. It also adds Python query state and builder methods for scanner-only plain-scan parameters: - `with_row_address()` for `_rowaddr` - `with_fragments(...)` for Lance fragment objects - `fragment_ids([...])` as a convenience wrapper that resolves IDs to Lance fragments ## Validation - `cd python && uv run --no-sync ruff format --check python/lancedb/query.py python/tests/test_query.py` - `cd python && uv run --no-sync ruff check python/lancedb/query.py python/tests/test_query.py` Targeted pytest was intentionally not run locally per maintainer request. * Bump version: 0.33.1-beta.1 → 0.33.1-beta.2 * Bump version: 0.30.1-beta.1 → 0.30.1-beta.2 * chore(deps): bump the rust-minor-patch group with 3 updates (#3499) Bumps the rust-minor-patch group with 3 updates: [log](https://github.com/rust-lang/log), [test-log](https://github.com/d-e-s-o/test-log) and [serial_test](https://github.com/palfrey/serial_test). Updates `log` from 0.4.30 to 0.4.31 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/rust-lang/log/releases">log's releases</a>.</em></p> <blockquote> <h2>0.4.31</h2> <h2>What's Changed</h2> <ul> <li>fix typos in kv compile errors and log documentation by <a href="https://github.com/Isvane"><code>@Isvane</code></a> in <a href="https://redirect.github.com/rust-lang/log/pull/726">rust-lang/log#726</a></li> <li>Leverage static str key when possible by <a href="https://github.com/tisonkun"><code>@tisonkun</code></a> in <a href="https://redirect.github.com/rust-lang/log/pull/727">rust-lang/log#727</a></li> <li>Prepare for 0.4.31 release by <a href="https://github.com/KodrAus"><code>@KodrAus</code></a> in <a href="https://redirect.github.com/rust-lang/log/pull/728">rust-lang/log#728</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/Isvane"><code>@Isvane</code></a> made their first contribution in <a href="https://redirect.github.com/rust-lang/log/pull/726">rust-lang/log#726</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/rust-lang/log/compare/0.4.30...0.4.31">https://github.com/rust-lang/log/compare/0.4.30...0.4.31</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/rust-lang/log/blob/master/CHANGELOG.md">log's changelog</a>.</em></p> <blockquote> <h2>[0.4.31] - 2026-06-02</h2> <h2>What's Changed</h2> <ul> <li>Leverage static str key when possible by <a href="https://github.com/tisonkun"><code>@tisonkun</code></a> in <a href="https://redirect.github.com/rust-lang/log/pull/727">rust-lang/log#727</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/Isvane"><code>@Isvane</code></a> made their first contribution in <a href="https://redirect.github.com/rust-lang/log/pull/726">rust-lang/log#726</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/rust-lang/log/compare/0.4.30...0.4.31">https://github.com/rust-lang/log/compare/0.4.30...0.4.31</a></p> <h2>[Unreleased]</h2> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/rust-lang/log/commit/580839288e5f2babc17e6c36f7d56e60082a47ef"><code>5808392</code></a> Merge pull request <a href="https://redirect.github.com/rust-lang/log/issues/728">#728</a> from rust-lang/cargo/0.4.31</li> <li><a href="https://github.com/rust-lang/log/commit/86d739f51a9c59a3cb66a79e695639e6fb41465b"><code>86d739f</code></a> prepare for 0.4.31 release</li> <li><a href="https://github.com/rust-lang/log/commit/c906cfb02e351b59cfe35c0f0be22093086aabb1"><code>c906cfb</code></a> Merge pull request <a href="https://redirect.github.com/rust-lang/log/issues/727">#727</a> from tisonkun/leverage-static-str-key-when-possible</li> <li><a href="https://github.com/rust-lang/log/commit/756c279649f79ce0ef8dccf952c5df4017791d1c"><code>756c279</code></a> leverage str literal as well</li> <li><a href="https://github.com/rust-lang/log/commit/3dd250d1537fd7e5974e0802b1025cc3e4561503"><code>3dd250d</code></a> rename Key::from_static_str to from_str_static</li> <li><a href="https://github.com/rust-lang/log/commit/db145979e229549215300f2696fa89b215cb1cab"><code>db14597</code></a> Leverage static str key when possible</li> <li><a href="https://github.com/rust-lang/log/commit/761461a5d0c8ea3d483d79b1de0205c2897318d2"><code>761461a</code></a> Merge pull request <a href="https://redirect.github.com/rust-lang/log/issues/726">#726</a> from Isvane/fix/typos</li> <li><a href="https://github.com/rust-lang/log/commit/48ce372edd343179cb9f4837381bf34c7679db3e"><code>48ce372</code></a> fix typos in kv compile errors and log documentation</li> <li>See full diff in <a href="https://github.com/rust-lang/log/compare/0.4.30...0.4.31">compare view</a></li> </ul> </details> <br /> Updates `test-log` from 0.2.20 to 0.2.21 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/d-e-s-o/test-log/releases">test-log's releases</a>.</em></p> <blockquote> <h2>v0.2.21</h2> <ul> <li>Fixed spans in generated code, improving <code>rust-analyzer</code> interaction</li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/jorendorff"><code>@jorendorff</code></a> made their first contribution in <a href="https://redirect.github.com/d-e-s-o/test-log/pull/68">d-e-s-o/test-log#68</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/d-e-s-o/test-log/compare/v0.2.20...v0.2.21">https://github.com/d-e-s-o/test-log/compare/v0.2.20...v0.2.21</a></p> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/d-e-s-o/test-log/blob/main/CHANGELOG.md">test-log's changelog</a>.</em></p> <blockquote> <h2>0.2.21</h2> <ul> <li>Fixed spans in generated code, improving <code>rust-analyzer</code> interaction</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/d-e-s-o/test-log/commit/b7b9da034578877997e0cbfb59ea11507e0a3da8"><code>b7b9da0</code></a> Bump version to 0.2.21</li> <li><a href="https://github.com/d-e-s-o/test-log/commit/db522dc408e1ac6f04b2d0c89dda7e4b48be0584"><code>db522dc</code></a> Add CHANGELOG entry for <a href="https://redirect.github.com/d-e-s-o/test-log/issues/68">#68</a></li> <li><a href="https://github.com/d-e-s-o/test-log/commit/5e996d9ac66882e6258d1d86df91417336436d14"><code>5e996d9</code></a> Wrap the injected init code, not the original test body</li> <li><a href="https://github.com/d-e-s-o/test-log/commit/c78563c1ca76720571dc5ffe731217adc7e781ed"><code>c78563c</code></a> Retain existing spans for test code</li> <li>See full diff in <a href="https://github.com/d-e-s-o/test-log/compare/v0.2.20...v0.2.21">compare view</a></li> </ul> </details> <br /> Updates `serial_test` from 3.4.0 to 3.5.0 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/palfrey/serial_test/releases">serial_test's releases</a>.</em></p> <blockquote> <h2>v3.5.0</h2> <h2>What's Changed</h2> <ul> <li>Replace scc/sdd with std::sync::Mutex for Miri strict provenance compatibility by <a href="https://github.com/justanotheranonymoususer"><code>@justanotheranonymoususer</code></a> in <a href="https://redirect.github.com/palfrey/serial_test/pull/157">palfrey/serial_test#157</a></li> </ul> <h2>New Contributors</h2> <ul> <li><a href="https://github.com/justanotheranonymoususer"><code>@justanotheranonymoususer</code></a> made their first contribution in <a href="https://redirect.github.com/palfrey/serial_test/pull/157">palfrey/serial_test#157</a></li> </ul> <p><strong>Full Changelog</strong>: <a href="https://github.com/palfrey/serial_test/compare/v3.4.0...v3.5.0">https://github.com/palfrey/serial_test/compare/v3.4.0...v3.5.0</a></p> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/palfrey/serial_test/commit/6181f64de942180231fdb9098dd0894bbd9e7472"><code>6181f64</code></a> 3.5.0</li> <li><a href="https://github.com/palfrey/serial_test/commit/480bead2f697707cd2e61214287d5f0b8518d44d"><code>480bead</code></a> Merge pull request <a href="https://redirect.github.com/palfrey/serial_test/issues/157">#157</a> from justanotheranonymoususer/remove-scc-dep</li> <li><a href="https://github.com/palfrey/serial_test/commit/e03019e3cdc79b65daeaa913c99376aed69d4101"><code>e03019e</code></a> Update ci.yml</li> <li><a href="https://github.com/palfrey/serial_test/commit/820c0f3de967d55c52c59cac9ae55345511bf468"><code>820c0f3</code></a> Update ci.yml</li> <li><a href="https://github.com/palfrey/serial_test/commit/62a89b055fb923159c428269c5be999509344cb1"><code>62a89b0</code></a> Only skip file_lock with filesystem access</li> <li><a href="https://github.com/palfrey/serial_test/commit/5ff550164ed6f149fc80230faa8d5b5ded234190"><code>5ff5501</code></a> Update ci.yml</li> <li><a href="https://github.com/palfrey/serial_test/commit/0bd996de9eb044293e149095465701175309942e"><code>0bd996d</code></a> Let's try --all-features</li> <li><a href="https://github.com/palfrey/serial_test/commit/338e4ed891a095e2bfda01572c150449e5f26e73"><code>338e4ed</code></a> Fix formatting</li> <li><a href="https://github.com/palfrey/serial_test/commit/a55cde5d1d1572db2a8e5930d361d95df9796ea0"><code>a55cde5</code></a> Cleanup code_lock.rs</li> <li><a href="https://github.com/palfrey/serial_test/commit/9ad7a8f18c9a598109df5197214669e8680ccb96"><code>9ad7a8f</code></a> Remove unnecessary test leftover changes</li> <li>Additional commits viewable in <a href="https://github.com/palfrey/serial_test/compare/v3.4.0...v3.5.0">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(python): make LanceDBClientError pickleable (#3470) ## Summary - Add `__reduce__` methods to `LanceDBClientError` and `RetryError` so that instances can be pickled and unpickled correctly - `HttpError` inherits the fix from `LanceDBClientError` since it has no additional `__init__` parameters - Add tests verifying pickle roundtrip for all three exception classes Fixes #3447 ## Test plan - [x] Verified pickle roundtrip for `LanceDBClientError` with and without `status_code` - [x] Verified pickle roundtrip for `HttpError` (subclass, no extra init params) - [x] Verified pickle roundtrip for `RetryError` (subclass with many extra params) - [ ] CI tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <[email protected]> Co-authored-by: Will Jones <[email protected]> * chore: update lance dependency to v8.0.0-beta.2 (#3500) Updates Lance dependencies to v8.0.0-beta.2 across the Rust workspace and Java lance-core metadata. The update was generated with ci/update_lance_dependency.py and required no compatibility code changes. Lance tag: https://github.com/lance-format/lance/releases/tag/v8.0.0-beta.2 ## ⛔ Merge blocker: legal review required This bump pulls in a new transitive **dev/profiling** dependency chain `inferno v0.11.21` → `pprof v0.15.0` → `lance-testing`, and `inferno` is licensed **CDDL-1.0** (copyleft). To get `cargo-deny` green, `CDDL-1.0` was added to the `deny.toml` allow list. **Do not merge until legal has reviewed and signed off on allowing CDDL-1.0.** The dependency is dev/test-only and not distributed, but the allow-list addition still requires legal approval per our policy. --------- Co-authored-by: Daniel Rammer <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> * feat(remote): implement set/unset_lsm_write_spec REST variant (#3501) ## Summary Wires `RemoteTable::set_lsm_write_spec` / `unset_lsm_write_spec` to the sophon REST endpoints added in [lancedb/sophon#6181](https://github.com/lancedb/sophon/pull/6181), replacing the previous `NotSupported` stubs. - `set_lsm_write_spec` maps the `LsmWriteSpec` onto sophon's request DTO — mode-tagged `sharding` (`unsharded` / `bucket` / `identity`), `maintained_indexes`, and `writer_config_defaults` — and POSTs to `/v1/table/{name}/set_lsm_write_spec/`. - `unset_lsm_write_spec` POSTs to `/v1/table/{name}/unset_lsm_write_spec/`. - Both call `check_mutable` first, matching the other remote mutations. - `maintained_indexes` is sent verbatim (an empty list means "no maintained indexes", matching native semantics). ## Testing - Added mocked-endpoint unit tests for unsharded / bucket / identity set and for unset. - `cargo check --features remote --tests` passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> * fix(python): run AsyncTable.search embeddings on a dedicated executor (#3459) ## Summary `AsyncTable.search()` computes the query embedding with `loop.run_in_executor(None, ...)`, which uses asyncio's **default** `ThreadPoolExecutor`. That pool is shared with all other `run_in_executor(None, ...)` work, so a slow embedding call — a heavy local model or an HTTP request to an embeddings API — ties up those threads and starves unrelated async I/O under concurrent load. This moves the (potentially blocking) embedding call onto a **dedicated executor**, isolating it from the default pool. Closes #3310. ## Problem `python/lancedb/table.py`, `AsyncTable.search()`: ```python return ( await loop.run_in_executor( None, # asyncio's default executor, shared with other blocking I/O embedding.function.compute_query_embeddings_with_retry, query, ) )[0] ``` Under load, concurrent searches whose embeddings block (or any other code using the default executor) contend for the same small thread pool. ## Change - Add a dedicated `ThreadPoolExecutor(thread_name_prefix="lancedb-embedding")` in `background_loop.py`, exposed via `embedding_executor()`. - Use it in `AsyncTable.search()`'s `make_embedding` instead of the default executor. - Reset the executor in the existing `_reset_after_fork` hook — its worker threads don't survive `fork()`, same as the background even…
Summary
When an
LsmWriteSpecis installed on a table (#3396),merge_insertupsertcalls are dispatched through Lance's MemWAL
ShardWriter(LSM-style append)instead of the standard merge path.
use_lsm_write— amerge_insertbuilder option, defaulttrue; set itfalseto use the standard path for a call even when a spec is set.assume_pre_sharded— amerge_insertbuilder option, defaultfalse;skips the per-row shard check and routes by the first row only.
close_lsm_writers— drains and closes the table's cached MemWAL shardwriters.
merge_insertoncolumns default to, and are validated against,the table's unenforced primary key.
DatasetConsistencyWrapper) and reused for the session.MergeResultgainsnum_rows— on the LSM path the insert/updatebreakdown is unknown until compaction, so only the total is reported.
Routing covers all three sharding strategies — bucket (murmur3,
Iceberg-compatible), identity, and unsharded. Each
merge_insertcall targetsa single shard; the whole input is collected and validated before a single
atomic
ShardWriter::put, so a validation failure leaves the MemWAL untouched.Bindings: Python (
merge_insert(...).use_lsm_write(...)/.assume_pre_sharded(...),Table.close_lsm_writers) and TypeScript(
mergeInsert(...).useLsmWrite(...)/.assumePreSharded(...),Table.closeLsmWriters).Context
Reconstructed from the original #3354 branch onto current
main: the branchpredated the #3394 (unenforced primary key) / #3396 (
LsmWriteSpec) split andhas been rebuilt on that merged foundation. Depends on Lance
v7.0.0-beta.13.The MemWAL read path (reading un-flushed shard data back into queries) and
remote (LanceDB Cloud) LSM support are follow-ups.