Commit Graph

829 Commits

Author SHA1 Message Date
lancedb-gatefixer[bot] ec21e37040 test(rust): cover Hugging Face table symlinks (#3887)
## Summary

- cover Hugging Face cache layouts where both manifests and Lance data
files are relative symlinks into a blob directory
- reconnect with a fresh session before opening so the test exercises
filesystem discovery instead of cached manifest metadata
- scan the reopened table to verify both manifest recovery and data-file
reads

## Root cause

Lance 3.0.1 recorded Unix symlink metadata as the known manifest size,
so the short link length caused a file size is too small error. The
current Lance v11.0.0-beta.2 dependency repairs this by detecting an
invalid footer from a stale known size and retrying with the target file
metadata. This regression test locks that behavior into the LanceDB
open-table path used by Node.

## Validation

- cargo fmt --all
- cargo test --quiet --features remote -p lancedb --lib
test_open_table_follows_hugging_face_symlinks -- --nocapture
- cargo test --quiet --features remote -p lancedb --lib
database::listing::tests
- cargo clippy --quiet --features remote -p lancedb --lib --tests -- -D
warnings
- cargo check --quiet --features remote --tests --examples

Fixes #3197

<!-- lance-gatekeeper-fix:v1 agent=4aadcf04e9ac93b97d499d7448b67e19
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:37:37 +08:00
lancedb-gatefixer[bot] 11f24b1df4 fix: explain unsupported object storage mounts (#3823)
## Summary

- classify unsupported local-filesystem operations from Lance as a
NotSupported error
- explain that object-storage mounts cannot provide the safe commit
operations Lance requires and direct users to native object-store URIs
- preserve existing error behavior for other local I/O failures and
non-local backends

## Root cause

Mountpoint for Amazon S3 exposes an S3 bucket as a local path but does
not implement atomic rename. Lance uses atomic rename for safe local
commits, and the resulting unsupported I/O error was previously passed
through as a generic Lance error, leaving Python users with an opaque
low-level failure. Transparent support for such mounts is not safe;
direct s3:// access remains the supported path.

## Validation

- cargo test --quiet --features remote -p lancedb error::tests
- cargo test --quiet --features remote -p lancedb --lib (807 passed, 1
ignored)
- cargo check --quiet --features remote --tests --examples
- cargo clippy --quiet --features remote --tests --examples
- cargo fmt --all -- --check

Fixes #2016

<!-- lance-gatekeeper-fix:v1 agent=d53283c18fdb00a3a1b69448b1f40529
generation=1 -->

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:33:02 +08:00
lancedb-gatefixer[bot] 2922c171f7 test(rust): cover Azure table URI separators (#3837)
## Root cause

The former listing-database table URI builder used OS-native
`Path::join` for object-store URIs. On Windows this inserted backslashes
into `az://` table paths, so `table_names` found slash-delimited objects
while `open_table` addressed a different key. The production path now
builds URI paths with forward slashes after the equivalent S3 report was
fixed in #2575, but #1072 remained open without Azure-specific
regression coverage.

## Fix

- Add Azure URI regression assertions at the Rust table URI construction
boundary.
- Cover connection bases both with and without a trailing slash,
matching the behavior reported in #1072.
- Verify the resulting table URI always uses forward slashes on every
platform.

## Validation

- `cargo fmt --all -- --check`
- `cargo test --quiet -p lancedb --lib
database::listing::tests::test_table_uri`
- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`
- `cargo test --quiet --features remote --tests` (866 passed, 1 ignored)

Fixes #1072

<!-- lance-gatekeeper-fix:v1 agent=7d385255a072ed89ddc3ff4d08f82218
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:31:08 +08:00
LanceDB Robot 001237c7a4 chore: update lance dependency to v11.0.0-beta.2 (#3886)
Updates the Lance dependencies and Java lance-core to v11.0.0-beta.2.
Includes required compatibility fixes for the LanceFileVersion module
move and the updated GooseFS/OpenDAL dependency. Lance tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.2

---------

Co-authored-by: Daniel Rammer <hamersaw@protonmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:00:22 -05:00
lancedb-gatefixer[bot] 369b10a377 test(rust): cover object store reuse on table open (#3831)
## Summary

- add regression coverage for repeated table opens through one database
connection
- assert that each open reuses the connection object-store client
without another registry miss
- exercise the table after every open so the test covers the complete
dataset-loading path

## Root cause

At the commit reported in #1600, opening a table constructed a separate
object-store client rather than reusing the client that had already
connected to the database. On S3 this repeated credential discovery,
which could fail intermittently in AWS Lambda and surface as
TableNotFound. The connection-owned Session reuse added later fixed the
runtime path, but no focused test protected the open-table invariant.

## Fix

Add a regression test backed by ObjectStoreRegistry statistics. Three
successive opens must add cache hits while leaving the miss count
unchanged, proving that open_table uses the connection Session and its
authenticated object-store client.

## Validation

- cargo fmt --all
- cargo test --quiet --features remote -p lancedb
database::listing::tests::test_open_table_reuses_connection_object_store
- cargo check --quiet --features remote --tests --examples
- cargo clippy --quiet --features remote --tests --examples
- cargo test --quiet --features remote --tests

Fixes #1600

<!-- lance-gatekeeper-fix:v1 agent=974491978c3e42840f32dbc35492d856
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:54:59 +08:00
lancedb-gatefixer[bot] 9707966943 test(rust): cover named memory databases on Windows (#3839)
## Summary\n\n- add a create-table regression for a named database\n-
assert that the derived table URI uses URL separators\n- restore the
four query tests that were moved to temporary files for #1051\n\n## Root
cause\n\n historically joined table names with . On Windows this
inserted a backslash into , so Lance interpreted the URI as an invalid
local filename. The production URI builder now preserves forward slashes
for URI schemes; this change restores the issue-specific tests and adds
direct regression coverage for table creation and the derived URI.\n\n##
Validation\n\n- \n- \n- (passes with four pre-existing warnings in
unrelated remote-table code)\n-
running 814 tests

.......................................................................................
87/814

.....................................i.................................................
174/814

.......................................................................................
261/814

.......................................................................................
348/814

.......................................................................................
435/814

.......................................................................................
522/814

.......................................................................................
609/814

.......................................................................................
696/814

.......................................................................................
783/814
...............................
test result: ok. 813 passed; 0 failed; 1 ignored; 0 measured; 0 filtered
out; finished in 7.76s


running 39 tests
.......................................
test result: ok. 39 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.23s


running 6 tests
......
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.03s


running 5 tests
.....
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.10s


running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.00s


running 2 tests
..
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.00s


running 2 tests
..
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered
out; finished in 0.00s (867 passed, 1 ignored)\n- focused named-memory
create and restored query tests\n\nFixes #1051\n\n<!--
lance-gatekeeper-fix:v1 agent=5ddf7a9520292b4cbaa58b9ea5a1fe76
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:53:32 +08:00
Wyatt Alt 62fe413a52 fix: percent-encode index names in per-index remote REST paths (#3840)
Nothing validates index names, so a `/` in one is reachable, and the
remote client interpolates it straight into the URL, splitting the path
so the router 404s. The index then reads back as missing and cannot be
dropped, while `create_index` keeps succeeding because it sends the name
in the body.

Encode at the three affected sites, mirroring `fetch_blob_files`. The
shared Rust client covers all bindings.
2026-08-06 16:53:09 +08:00
lancedb-gatefixer[bot] e6444ecc05 fix(rust): handle missing mirrored copy sources (#3843)
## Summary

- treat `NotFound` from the mirrored secondary copy as a cache miss
while preserving every other secondary error
- perform the durable primary copy after either a successful secondary
copy or a secondary cache miss
- cover both an initially missing secondary manifest and eviction
immediately before the secondary copy

## Root cause

Readers can use process-local secondary stores that do not contain a
staging manifest written by another process, or that evict it before
finalization. `MirroringObjectStore::copy_opts` propagated that
secondary `NotFound`, so older object_store versions could loop
indefinitely and the locked version aborted before performing the
durable primary copy.

## Validation

- `cargo fmt --all -- --check`
- `cargo test --quiet --features remote -p lancedb
io::object_store::test::test_copy_when -- --nocapture`
- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`
- `cargo test --quiet --features remote --tests`

Fixes #1176

<!-- lance-gatekeeper-fix:v1 agent=636210af9dcd25b6dceadebd2fcafc6f
generation=1 -->

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:49:49 +08:00
lancedb-gatefixer[bot] b20696ef9c fix(remote): validate cloud DNS hostnames (#3845)
## Summary

- validate the generated LanceDB Cloud hostname during connection setup
- return a clear invalid-input error for empty, overlong, or oversized
DNS names before network resolution
- add Rust and Python regression coverage for malformed `db://`
authorities

## Root cause

The `db://` authority and region were interpolated into the Cloud API
hostname without DNS length validation. Empty or overlong labels
therefore reached the resolver and surfaced as an opaque IDNA
`UnicodeError` instead of a useful connection error.

## Validation

- `cargo test --quiet --features remote -p lancedb
test_rejects_invalid_cloud_dns_hostname --lib`
- `cargo check --quiet --features remote --tests --examples`
- `uv run --no-sync --extra tests pytest
python/tests/test_remote_db.py::test_async_remote_db
python/tests/test_remote_db.py::test_connect_rejects_invalid_cloud_dns_hostname
-q`
- `cargo fmt --all -- --check`
- `ruff check .`
- `ruff format --check python/python/tests/test_remote_db.py`

Fixes #799

<!-- lance-gatekeeper-fix:v1 agent=4d1597b3d244b58f0603ed40a8a59cf9
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:48:56 +08:00
lancedb-gatefixer[bot] 772bdeced8 fix(rust): prevent vector optimize regression after deletes (#3848)
## Summary

- Adds a LanceDB regression for stable row IDs, scattered deletes,
IVF_RQ, and default index optimization.
- Verifies optimization completes and preserves the expected live-row
count.

## Root cause

Lance 3.0.1 built the stable-row-ID address list by dropping deleted IDs
while retaining the original ID list. The subsequent positional zip
misaligned IDs and addresses, so vector partition joins requested
deleted rows and failed with batch.num_rows() != chunk.len(). Lance PR
https://github.com/lance-format/lance/pull/7704 corrected the generic
filter, and the LanceDB dependency currently pinned on main contains
that correction.

## Fix

Add regression coverage at the Rust Table optimize surface using the
IVF_RQ configuration from the report. This locks the upstream correction
into the LanceDB workflow that originally crashed.

## Validation

- cargo fmt --all -- --check
- cargo test --quiet --features remote -p lancedb table::optimize::tests
(14 passed)
- cargo check --quiet --features remote --tests --examples
- cargo clippy --quiet --features remote --tests --examples -p lancedb

Fixes #3330

<!-- lance-gatekeeper-fix:v1 agent=4c2c25373942aab9ba9f7444977de7e3
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:48:24 +08:00
lancedb-gatefixer[bot] 3af51541a0 test(rust): cover fixed-size binary merge insert regression (#3854)
## Summary

- add a LanceDB regression for `merge_insert` with a non-nullable
`FixedSizeBinary` column
- exercise matched updates, unmatched inserts, and source-missing
deletes
- assert the exact merge statistics and final row count

## Root cause

The Arrow `take` kernel previously ignored nulls in the index array for
`FixedSizeBinary`. DataFusion uses that kernel while constructing
outer-join results, so the join behind
`when_not_matched_by_source_delete` could place invalid values into
non-nullable columns. The current Arrow dependency contains the upstream
fix; this test locks the corrected behavior at the LanceDB API boundary.

## Validation

- `cargo fmt --all -- --check`
- `cargo test --quiet --features remote --tests`
- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`

Fixes #2869

<!-- lance-gatekeeper-fix:v1 agent=e275446044185ef4e8cf88da6af3e70b
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:46:49 +08:00
lancedb-gatefixer[bot] 99a68db78c test(rust): cover concurrent appends during compaction (#3878)
## Summary

- add a LanceDB core regression for compaction overlapping appends
through separate table handles
- verify concurrent commits preserve fragment ID order on an indexed
table
- run the follow-up compaction that exposed the original row-ID ordering
failure and verify all rows remain

## Root cause

Older Lance versions could reserve fragment IDs for compaction, allow
concurrent appends to commit later IDs, and then commit the reserved
compaction fragments at the end of the manifest. A later compaction
could consequently receive row IDs out of order. Current Lance sorts
fragments at the transaction boundary; this adds the missing
LanceDB-level regression coverage for the Node-visible concurrency
contract.

## Validation

- `cargo fmt --all`
- focused regression passed once with output and 20 repeated runs
- `cargo test --quiet --features remote -p lancedb
table::optimize::tests` (14 passed)
- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`
- `cargo test --quiet --features remote --tests` (867 passed, 1 ignored)

Fixes #1498

<!-- lance-gatekeeper-fix:v1 agent=93aaefb15507dca52d064e15388773d7
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:40:01 +08:00
lancedb-gatefixer[bot] 3956d9dbfa fix(python): prevent OpenSSL linkage in Linux wheels (#3877)
## Summary

- select rustls with native certificate roots explicitly for LanceDB's
remote HTTP client
- add a Linux regression test that rejects `libssl` or `libcrypto`
dependencies in the built Python extension

## Root cause

The Python remote client originally enabled reqwest's native TLS
backend. During manylinux wheel repair, that caused OpenSSL 1.1
libraries to be bundled into the wheel. Loading those libraries on RHEL
9 with FIPS enabled aborts during the OpenSSL self-test before `import
lancedb` can complete.

LanceDB has since moved away from native TLS, but its own reqwest
dependency relied on transitive rustls feature selection and the built
extension had no regression guard. This change makes rustls selection
explicit and tests the produced Linux native module's dynamic
dependencies.

## Validation

- `uv run --no-sync pytest python/tests/test_import.py -q`
- `ruff format --check python`
- `ruff check .`
- `cargo fmt --all -- --check`
- `cargo check --quiet --features remote --tests --examples`
- `ldd python/lancedb/_lancedb.abi3.so` (no `libssl` or `libcrypto`
dependency)
- verified the resolved Python Rust dependency graph contains rustls and
no `openssl-sys` or `native-tls`

Fixes #1884

<!-- lance-gatekeeper-fix:v1 agent=31f916c7ac5c072bbbd54f3539d24f71
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:38:27 +08:00
Justin Miller c7ea91f3ea test: cover blob null/empty preservation across Table::optimize (#3774)
## Description

`Table::optimize()` compacts through
`lance::dataset::optimize::compact_files`
(`rust/lancedb/src/table/optimize.rs:155`). Until
lance-format/lance#7965 that rewrite corrupted blob columns holding null
or empty values, which is what #3744 reports:

- **storage 2.0** (legacy v1 `lance-encoding:blob` descriptors): every
payload following a null or empty row in the same fragment was rewritten
as `{position: 0, size: 0}`, so it read back as `b""` and the new
fragment no longer referenced the bytes — silent payload loss,
unrecoverable once the pre-optimize versions are pruned.
- **storage 2.2** (blob v2): a valid empty value was rewritten as null,
destroying the null-vs-empty distinction.

Both manifestations share one root cause: `is_inline_null_blob`
classified any inline blob with `position == 0 && size == 0` as null,
which is also exactly what a *valid empty value* looks like. Such rows
were dropped from `blob_read_addrs`, misaligning every payload that
followed.

The behaviour is already correct on `main`: the vendored lance crate
first carried the fix at `v10.0.0-beta.3` (#3710) and is now
`v10.1.0-beta.1` (#3757). What was missing is coverage — nothing in this
repo exercised a blob column containing a null or empty value through
`optimize()`, which is why this shipped unnoticed. This PR adds that
guard.

## Tests

Two tests in `rust/lancedb/tests/blob_integration.rs`, reusing the
file's existing 64 KiB dedicated-blob helpers and a delete-triggered
fragment rewrite. After `id IN (1, 4)` is deleted the surviving rows are
`2` (null), `3` (valid empty), `5` and `6` (payloads) — payloads sit
immediately after the null/empty, which is where the misalignment
landed.

- `optimize_preserves_v1_blob_payloads_with_null_and_empty` — storage
2.0; asserts the **payload bytes** are unchanged across
`OptimizeAction::All` (what the Python/Node `optimize()` bindings
invoke). Payloads are read through `lance::Dataset::take_blobs`, since
`Table::fetch_blobs` rejects legacy v1 columns. The before/after
descriptors are reported on failure but deliberately *not* asserted:
compaction repacks the blob file, so they shift legitimately (id 5
`(131072, 65536)` → `(0, 65536)`, id 6 `(196608, 65536)` → `(65536,
65536)`). Note that a post-compaction `position: 0` is both the
legitimate first-payload offset and the bug's signature, so asserting
descriptors would be actively misleading.
- `optimize_preserves_blob_v2_null_and_empty_distinction` — storage >=
2.2; asserts a null stays null and a valid empty value stays non-null
empty.

Both assert the pre-optimize state first, so a setup change that stops
producing the null/empty/payload mix fails loudly instead of passing
vacuously.

Both also assert the returned `CompactionMetrics` show a fragment was
actually rewritten. These tests depend on `delete("id IN (1, 4)")`
pushing the fragment past lance's `materialize_deletions_threshold` (0.1
by default; 2 of 6 rows here). That coupling is invisible and unasserted
otherwise: against a forced no-op (`materialize_deletions_threshold:
1.5`) the metrics come back all zeroes and *every payload assertion
still passes*. Since the whole point of these tests is to survive
dependency changes, they check that the rewrite happened rather than
trusting the planner to keep selecting the fragment.

Guard verified against a pre-fix lance: with the published
`lancedb==0.36.0` wheel (vendors lance 9.0.0), `Table.optimize()` on the
same data rewrites the descriptors of the two rows following the
null/empty from `(131072, 65536)` and `(196608, 65536)` to `(0, 0)`, and
the payloads read back empty. Against the pinned `v10.1.0-beta.1`, all
39 tests in the file pass, adding roughly 10–20 ms to the file's
runtime.

## Not addressed here

- **No released artifact has the fix yet.** PyPI `lancedb` 0.36.0
(2026-07-29) vendors lance 9.0.0; npm `@lancedb/lancedb` 0.37.1-beta.0
predates the bump. No 9.x lance tag carries the fix: `v10.0.0-beta.3` is
the first tag containing it, every `v9.1.0-beta.1`…`beta.8` is behind
it, and `v9.0.0` / `v9.0.1-rc.1` sit on a diverged branch without it. A
stable lancedb release needs a stable lance >= 10.
- **The version skew #3744 flagged is still live.**
`python/pyproject.toml` pins `pylance==9.0.0rc1` for the `tests` extra
against a vendored `10.1.0-beta.1`, so Python CI still cannot observe
this class of divergence.
- **Only the single-fragment rewrite shape is covered.** Both tests
rewrite one fragment by materializing deletions. lance's own
`test_compact_blob_v1/v2_preserves_null_empty_and_payload_order` cover
the multi-fragment merge shape (3 fragments → 1) at unit level, so this
PR is complementary rather than redundant — it covers the binding-level
path through `Table::optimize` — but it would not catch a regression
that only appears when *merging* fragments.
`multi_fragment_dedicated_blob_table` in the same file makes that a
cheap follow-up.

Closes #3744

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:23:03 -07:00
Wyatt Alt 8e24dd3828 feat(rust)!: make add_columns a builder (#3778)
Table::add_columns now takes no arguments and returns AddColumnsBuilder,
so calls become .add_columns().transform(t).execute().

read_columns was the second positional argument but reaches only one of
the five transform variants. In lance's add_columns_to_fragments only
BatchUDF receives the caller's value: SqlExpressions replaces it with
the columns its expressions reference, Stream and Reader pass None, and
AllNulls reads nothing. So it was mandatory on every call -- all
eighteen call sites here passed None -- and silently discarded four
times out of five. As a builder method it is optional, and setting it
where lance would discard it is now an error, which does reject a call
that previously succeeded while ignoring the argument.

Matches the builders add, update, and merge_insert already use.
2026-08-04 11:18:22 -07:00
Drew Gallardo 3dd9c598e9 feat(remote): add seekable blob range reads (#3750)
## Summary

- Implements Cloud `fetch_blob_files`: returns real seekable `BlobFile`
handles over HTTP Range instead of `NotSupported`.
- Completes the second Cloud blob read verb after #3684 (`fetch_blobs` =
eager whole bytes; this = lazy / partial / sequential reads).
- Same public handle API as local (`read_range`, `read_up_to`, `seek`,
`tell`, `close`), so one code path works for local and Cloud.

Large blobs (video, audio, PDFs) should not require downloading the
whole object to inspect a header or stream a slice. After search,
callers open a handle and read only what they need:

```python
hits = table.search(vec).select(["id", "video"]).limit(5).to_arrow()

with table.fetch_blob_files("video", hits)[0] as f:
    header = f.read_range(0, 256)
    f.seek(keyframe_offset)
    chunk = f.read_up_to(1 << 20)
```

### Behavior

- Handle creation probes size with `bytes=0-0` (bounded concurrency,
input order preserved).
- `204` → null (`None`); `416` with `bytes */0` → valid empty blob;
other `416` → error.
- `read_range` validates `Content-Range` and body length; OOB ranges
fail with `invalid_input` before the request (aligned with Lance).
- `read_up_to` reuses one open-ended Range response across sequential
reads; `seek` drops it.
- Servers older than 0.5.0 get a clear `NotSupported` (does not suggest
`fetch_blobs`, which they also lack).

## Testing

- `cargo test --features remote -p lancedb remote_blob`
- `cargo test --features remote -p lancedb test_blob`
- `cargo clippy --features remote --tests --examples` (no new warnings
from this change)

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 08:38:08 -07:00
Will Jones 93354baf34 chore: upgrade rust toolchain to 1.97.0 (#3643)
Bumps the pinned Rust toolchain from 1.95.0 to the latest stable
(1.97.0).

Rust 1.97's clippy adds `useless_borrows_in_formatting`, which flags a
redundant `&` in `format!`/`debug!` arguments in a few places. This PR
removes those to keep `cargo clippy` clean.

No behavior change; the MSRV (`rust-version = "1.91.0"`) is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-31 16:40:58 -07:00
LuQQiu 05602ec7d5 chore: update lance dependency to v10.1.0-beta.1 (#3757)
Updates the Lance Rust workspace dependencies and Java lance-core
version to v10.1.0-beta.1.

Includes a compatibility fix for the Lance file writer API by using the
explicit V2_1 writer creation path for permutation shuffle spill files.

Triggered by
https://github.com/lance-format/lance/releases/tag/v10.1.0-beta.1
2026-07-31 16:16:17 -07:00
Wyatt Alt e3b472c212 feat: connection-level job operations (#3755)
Adds job operations to the connection surface, building on the Job
handle from #3742: job(id), list_jobs, get_job, cancel_job, and
job_history, plus a non-blocking Job.status(). Implemented on the
Database trait (defaulting to NotSupported), the remote backend
(/v1/jobs), and the Python and Node bindings; job_history returns Arrow
batches.

errors() and progress() are not included.

Tested with mocked endpoints in all three languages.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 12:51:43 -07:00
Wyatt Alt a6418b6cb9 feat: create_index returns a Job handle (#3742)
IndexBuilder::execute now returns a Job with wait and cancel methods.
Local tables build the index synchronously and return an already-done
job. Remote tables read the job id the server returns from create_index
and track it through the /v1/jobs API: wait polls describe until the job
reaches a terminal state and cancel posts a cancellation. Servers that
return no job id yield a done job, so behavior against older servers is
unchanged. The job id is not exposed on the handle.

The Python and TypeScript bindings keep their current signatures and
discard the handle; exposing Job there is left to follow-ups.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 07:32:28 -07:00
Drew Gallardo 77208fd464 feat(remote): add RemoteTable fetch_blobs HTTP client (#3684)
Remote half of the blob read path. #3578 did local Python. This makes
`RemoteTable` hit the server.

- `fetch_blobs(column, row_ids or hits)` → bytes over `POST
/v1/table/{id}/fetch_blobs/`
- `blob_columns()` from the cached schema (describe already has the
metadata, no extra route)
- search then `fetch_blobs` works. row identity rides inside the blob
descriptor so you do not need a public `_rowid`
- `fetch_blob_files` still `NotSupported` on remote. use `fetch_blobs`
for full bytes for now. Range is a follow up

Accepts Binary / LargeBinary / BinaryView on the way back. Empty
`row_ids` short-circuits. Version + branch go in the request body same
as other read calls.

### Example

```python
db = lancedb.connect(uri="db://my-project", api_key=...)
table = db.open_table("clips")

hits = table.search(query_vec).select(["id", "video"]).limit(10).to_arrow()
# hits is just id + video. row ids are stashed on the descriptor
blobs = table.fetch_blobs("video", hits)  # null-aligned, same length as hits
```

Or pass ids yourself:

```python
blobs = table.fetch_blobs("video", [10, 20, 30])
```

### Testing

- `cargo test -p lancedb --features remote --lib`
- `cargo test -p lancedb --features remote --test blob_integration`
- `pytest python/tests/test_remote_db.py -k remote_blob`
- live e2e against a local 0.5.0 remote server (search → fetch, nulls,
nested path, old server gate)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:16:01 -07:00
Joaquin Hui b505dc1315 fix: distinguish corrupt table from missing in open_table (#3731)
`table_names()` lists any `*.lance` directory, but `open_table()` maps
every `DatasetNotFound` to `TableNotFound`, so a corrupt or
partially-written table looks identical to one that never existed
(#3127). This takes the issue's Option 2: on `DatasetNotFound`, check
the parent listing for the table's `.lance` entry — the same predicate
`table_names()` uses — and return a new `TableCorrupted` error when the
directory is present. The check runs only on the error path, and any
failure in the recheck falls back to the previous `TableNotFound`
behavior.

Tests cover the reporter's empty-dir repro, a deleted-manifest case,
true absence (still `TableNotFound`), and an end-to-end list-then-open
assertion; the three new corrupt-case tests fail without the src change.
`cargo test -p lancedb --lib` 732 passed, clippy/fmt clean, `cargo check
--workspace --all-targets` clean (both language bindings end in wildcard
error arms).

Two notes for review: `Error` isn't `#[non_exhaustive]`, so the new
variant is technically semver-breaking for exhaustive matchers (pre-1.0,
and the alternative — changing `TableNotFound`'s shape — breaks more);
and on the Python side corrupt tables now surface as `RuntimeError`
rather than `ValueError`, which is the intended distinction but worth a
maintainer's eye. `open_from_namespace` was left unchanged since
namespace listings come from a server-side registry, not directory
globbing.

Closes #3127
2026-07-30 07:56:43 -07:00
sanskar singh bhardwaj 7dfdfe6401 fix(remote): surface masked merge_insert stream errors under HTTP2 (#2339) (#3711)
## Summary

Fixes #2339. `merge_insert()` on the remote client could mask the real
cause of a mid-stream input error, reporting only:

> stream error sent by user: unexpected internal error

## Root cause

There were two divergent streaming-write code paths in the remote
client:

- `add()` uses `RemoteInsertExec`, which streams the request body
through a `tokio::sync::oneshot` error side-channel and drains it before
reporting the HTTP result. If the input stream errors mid-body, the
original error is recovered.
- `merge_insert()` used a legacy path (`send_streaming` ->
`reader_as_body`) that piped arrow `Some(Err(e))` straight into the
HTTP2 request body. Hyper swallows body-stream errors under HTTP2 (see
hyperium/hyper#2547), so the original error was lost and only the
generic transport error surfaced.

## Fix

Consolidate both write paths onto the side-channel mechanism instead of
patching the legacy path:

- Generalize `RemoteInsertExec` into `RemoteWriteExec`, carrying a
`WriteOp` enum (`Insert { overwrite }` | `MergeInsert { query, timeout
}`) that selects the endpoint, query params, request-timeout header, and
response parsing. The executor returns a `WriteResult` enum (`Add` |
`Merge`) with typed accessors, and `with_new_children` still resets the
result so the rescannable retry loop is unaffected.
- Route `merge_insert()` through `RemoteWriteExec`. The public API only
accepts a `RecordBatchReader` (not rescannable), so the reader is
buffered into a `Vec<RecordBatch>` before the retry loop to preserve the
previous retry-on-retryable-status behaviour. This mirrors what the old
`send_streaming(with_retry=true)` path already did.
- Remove the now-unused `send_streaming` / `reader_as_body` /
`buffer_reader` / `make_reader` helpers. Multipart stays insert-only
(the server has no multipart merge_insert endpoint), so that hot path is
behaviorally unchanged.

## Testing

- Added `test_merge_insert_input_error_surfaces_original`, which drives
an erroring input through the single-request `merge_insert` path and
asserts the original error (`boom`) is surfaced rather than the masked
HTTP error. Confirmed it fails without the side-channel drain (it then
reports a masked `500 ... request or response body error`).
- Full suite green: `cargo test -p lancedb --lib --features remote` ->
694 passed, 0 failed. Includes the existing
`test_merge_insert_retries_on_409`, confirming retry behaviour is
preserved.
2026-07-30 07:56:25 -07:00
LanceDB Robot 1ad6ce3a4e chore: update lance dependency to v10.0.0-beta.7 (#3745)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v10.0.0-beta.7. No compatibility fixes were required; full
workspace clippy passed with warnings denied. Lance tag:
https://github.com/lance-format/lance/releases/tag/v10.0.0-beta.7

---------

Co-authored-by: Lu Qiu <luqiujob@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:14:27 -07:00
Will Jones 03b26d585b fix: deflake test_read_consistency_interval (#3713)
`test_read_consistency_interval` asserted that a table opened with a
100ms `read_consistency_interval` still read stale data immediately
after a concurrent write. The cache timestamp is set when the table is
opened and reads within the interval do not refresh it, so that
assertion only held if the intervening open/count/commit/count sequence
finished within 100ms of real wall-clock time. On a loaded CI runner it
did not: the TTL expired, `count_rows` refreshed synchronously, and the
test failed with `left: 1, right: 0`. This broke the Rust workflow on
`main` at 0bc08160 (a Python-only commit).

This pins the `background_cache` mock clock once `table2` has seeded its
cache, and advances it explicitly in place of `tokio::time::sleep`, so
the test controls when the interval elapses. Same approach as #3547.
With the clock pinned there is no real sleep left to be imprecise, so
the `cfg(not(target_os = "windows"))` guard is dropped and the test now
runs on Windows too.

Verified by inserting a stall before the write: 120ms reproduces the
original failure deterministically, and with this change the test still
passes with a 500ms stall.

Fixes #3712
2026-07-29 13:06:41 -07:00
Yang Cen f7feed48c3 feat(fts): support custom stop-word lists (#3734)
## What

Expose custom FTS stop-word lists in the Python and TypeScript public
APIs, including their standalone tokenize helpers and remote index
creation.

This PR supports concrete string lists only. It does not add file or
LanceDB-table stop-word sources.

## Why

Rust already exposes Lance's custom stop-word list option. The Python
and TypeScript APIs did not pass it through, and local index details did
not retain the full tokenizer parameters needed by index-backed
tokenization after reopening a table.

## How

- Add `custom_stop_words` / `customStopWords` to the Python and
TypeScript FTS and tokenize options.
- Preserve `None` / `undefined`, empty lists, and list contents without
normalization.
- Load the persisted FTS segment parameters when returning local index
details.
- Serialize the concrete list in remote create-index requests.
- Keep Python and TypeScript tests thin; behavior, persistence, query
tokenization, and remote JSON coverage live primarily in Rust.

## Validation

- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`
- `cargo test --quiet --features remote --tests`
- Python extension rebuild with `uv` and `maturin`
- Targeted Python tests: 4 passed
- Python `ruff format --check` and `ruff check`
- TypeScript build, typecheck, Biome lint, generated docs, and targeted
tests

---------

Co-authored-by: Yang Cen <yangcen@Yangs-Mac-mini.local>
2026-07-29 17:40:12 +08:00
Lance Release e5f489818b Bump version: 0.37.0-beta.0 → 0.37.1-beta.0 2026-07-29 07:12:34 +00:00
Dan Tasse 29c030f865 fix: accept either timestamp or timestamp_millis for versions (#3733)
`list_versions()` against a remote table on a server that uses
lance-namespace was failing. The server was returning
`timestamp_millis`, while db-catalog deployments were returning
`timestamp`, and the client was only accepting `timestamp`. So, updated
the client to accept both. (assuming we're migrating over time;
eventually we can turn off the `timestamp` code path I suppose.)
2026-07-28 10:44:49 -04:00
Xuanwo ff6ff09998 feat: support batched blob range reads (#3703)
## Summary

Lance can now plan multiple byte ranges for the same blob in one
`read_blob_ranges` operation, but LanceDB users currently cannot expose
a complete set of logical ranges to that planner.

This complements `BlobFile`: file-like consumers such as PyAV can
continue to discover ranges dynamically, while callers that already know
the ranges for a batch can submit them together.

## Motivating example

A training table may store a large video blob together with a small
application-level clip index:

```text
video: blob
clips: [{offset, length}, ...]
```

The caller can select the videos and clips for a batch, obtain their row
IDs from the query, and read all of the selected windows together:

```python
rows = (
    table.search()
    .select(["clips"])
    .with_row_id(True)
    .limit(64)
    .to_arrow()
    .to_pylist()
)

requests = []
for row in rows:
    clip = sample_clip(row["clips"])
    requests.append(
        (row["_rowid"], clip["offset"], clip["length"])
    )

chunks = table.fetch_blob_ranges("video", requests)
```

Here, `_rowid` comes from the LanceDB query, while `offset` and `length`
come from the application's clip index and are relative to that row's
video blob. The caller describes only the logical reads; Lance still
handles validation, source grouping, coalescing, scheduling, and byte
backpressure.

Lance v10.0.0-beta.5 returns one logical result per blob selector or
range request and explicitly distinguishes null blobs from valid empty
values. LanceDB consumes that aligned result contract directly and only
adds a cardinality check for unresolved row IDs.

This PR exposes batched blob-range reads on local Rust and Python
tables. Results preserve request identity, duplicates, null slots, and
valid empty ranges while allowing Lance to execute the physical reads
out of order. Scheduler buffer sizing remains an internal Lance concern,
so the LanceDB API does not expose `io_buffer_size`.

Cloud tables continue to report this operation as unsupported until
there is a corresponding remote API.
2026-07-27 15:08:47 -07:00
Vivek 119b9baf90 fix: preserve row count in MetadataEraserExec for zero-column batches (#3717)
SELECT COUNT(*) FROM t WHERE <predicate> — and any query that plans an
empty-projection scan — panics the executing query task:

InvalidArgumentError("must either specify a row count or at least one
column")

Root cause

MetadataEraserExec wraps every LanceDB table scan to strip schema-level
metadata, rebuilding each batch in execute():

RecordBatch::try_new(schema.clone(), batch.columns().to_vec()).unwrap()

RecordBatch::try_new infers the row count from the columns. COUNT(*)
with a filter is planned with an empty projection, so the scan emits
zero-column batches — there are no columns to infer a length from,
try_new returns Err, and the .unwrap() panics.

(This is specific to the empty-projection case: COUNT(*) with no filter
is answered from statistics and never scans, and COUNT(<col>) projects a
column — both already work.)
2026-07-27 12:25:11 -07:00
Heng Ge f655f62e09 feat(query): add use_lsm to read MemWAL LSM data (#3489)
## What

MemWAL LSM **read** support. When a table has an LSM write spec
(`set_lsm_write_spec`), `merge_insert` upserts live in the MemWAL
active/frozen memtables and flushed SSTables until an external
compaction merges them into the base table, so a normal scan returns
**stale** data. This routes reads through Lance's `LsmScanner` so
queries also surface that in-flight data, deduplicated by primary key
(newest generation wins).

## How

- Adds a **`use_lsm: Option<bool>`** query flag, symmetric with the
`merge_insert` flag:
- **unset** — auto-route through the LSM scanner when the table carries
a write spec
- **`use_lsm(true)`** — force the LSM path; error if there is no spec
    - **`use_lsm(false)`** — read the base table only (the escape hatch)
- Plain scan, single-column full-text search, and single-vector ANN all
run through one `LsmScanner` (assembled from on-disk shard manifests
plus the cached writer's in-memory memtables), so a `where` predicate is
honored as a **prefilter** uniformly — including for vector search.
- **Compaction-aware snapshots:** an SSTable generation is dropped only
once it is both compacted into the base table and covered by the arm's
base-index catch-up (`index_catchup`); plain scans use the compaction
watermark alone.
- Query shapes the scanner cannot honor hard-error with guidance to set
`use_lsm(false)`: hybrid, multi/binary vectors, `with_row_id`,
reranking, `order_by`, dynamic/Substrait projection or filters,
`distance_range`, `use_index(false)`, postfilter, take-by-row-id/offset,
reads from a time-traveled version, and an unmaintained or ambiguous
(multiple) FTS/vector index. Namespace-pushdown queries fall back to
local execution when a spec is present; WAL-only writers are handled.
- Exposed across the Rust core and the Python (`use_lsm`) and TypeScript
(`useLsm`) bindings, including `TakeQuery`.

Rebased from Lance `7.2.0-beta.3` to `10.0.0-beta.3`.
2026-07-25 23:45:27 -07:00
Will Jones bf15655c83 chore: unify SDK versions and release tags on a single line (#3714)
Python was versioned and tagged separately from the Rust, Java, and
Node.js SDKs, and had drifted three minor versions ahead (0.36 vs 0.33).
Users had no way to tell which Python version corresponded to which Rust
or Node release, and the gap had no meaning behind it.

This unifies the two tracks so there is one version and one tag for all
four SDKs.

## Version

The shared version is set to `0.37.0-beta.0`. Python continues its own
sequence (highest published: 0.36 → 0.37) while Rust, Java, and Node.js
jump 0.33 → 0.37 to meet it. Picking Python's next minor means Python
users see no discontinuity at all, and only the other SDKs skip forward.

Note that `main` trails the `release/v0.32` branch on both lines (main
is at 0.32.0-beta.3 / 0.35.0-beta.3; the release branch carries
0.33.0-beta.0 / 0.36.0-beta.0), so 0.37 is chosen to clear the highest
tag on either branch. Every index stays monotonic:

| index | publishes | last published | next |
|---|---|---|---|
| PyPI | stable only | 0.34.0 | 0.37.0 |
| Fury | previews | 0.36.0b0 | 0.37.0-beta.1 |
| npm | both | 0.33.0-beta.0 | 0.37.0-beta.1 |
| crates.io | stable only | 0.31.0 | 0.37.0 |
| Maven | both | 0.33.0-beta.0 | 0.37.0-beta.1 |

A one-time jump for three SDKs, versus explaining the offset
indefinitely.

## Mechanism

* `python/.bumpversion.toml` is removed. `python/Cargo.toml` — the
source of the Python package version, since `pyproject.toml` declares
`dynamic = ["version"]` — becomes a tracked file of the root config. Its
`cargo update -p lancedb-python` pre-commit hook is dropped as
redundant: `ci/update_lockfiles.sh` already refreshes every workspace
member version in `Cargo.lock`.
* `pypi-publish.yml` triggers on `v*` instead of `python-v*`, so one tag
releases all four packages. `ci/bump_version.sh` and
`make-release-commit.yml` lose their now-dead tag-prefix and
per-language plumbing, including the `python` / `other` dispatch inputs.
* The two byte-identical GH release jobs in `npm-publish.yml` and
`pypi-publish.yml` are replaced by a single `gh-release.yml`. One
release per tag, named `LanceDB vX.Y.Z`, instead of separate "Python
LanceDB" and "Node/Rust LanceDB" releases for the same commit.

The trade-off: there is no longer a way to ship a Python-only patch
without also releasing crates.io, Maven, and npm. That is the cost of
making drift structurally impossible.

## Beta releases marked "Latest" (#3666)

Both GH release jobs used:

```yaml
prerelease: ${{ contains('beta', github.ref) }}
```

The arguments are reversed. `contains(search, item)` asks whether
*`search`* contains *`item`*, so this evaluated "does the literal string
`'beta'` contain `refs/tags/python-v0.35.0-beta.2`?" — always `false`.
Every beta was published as a full release, and GitHub awards "Latest"
to the newest non-prerelease.

The new workflow derives the flag from the parsed version rather than
the raw ref, and sets `make_latest` explicitly:

```yaml
prerelease: ${{ steps.extract_version.outputs.prerelease }}
make_latest: ${{ steps.extract_version.outputs.prerelease == 'false' }}
```

npm was never affected (`--tag preview` uses correct bash), and PyPI
already excludes pre-releases from resolution.

This only fixes releases published from here on. Already-published betas
need a one-time backfill:

```shell
gh api --paginate /repos/lancedb/lancedb/releases \
  --jq '.[] | select(.prerelease == false) | select(.tag_name | test("beta")) | .id' \
  | xargs -I{} gh api -X PATCH /repos/lancedb/lancedb/releases/{} -F prerelease=true
```

## Verification

Ran `ci/bump_version.sh` end-to-end against this branch with the release
tooling installed:

* `preview` → tags `v0.37.0-beta.1` (previous tag `v0.33.0-beta.0`
detected, `pre_n` bump)
* `stable` → tags `v0.37.0`
* Both paths update `.bumpversion.toml`, `rust/lancedb/Cargo.toml`,
`nodejs/Cargo.toml`, `python/Cargo.toml`, `nodejs/package.json`, the 7
`nodejs/npm/*/package.json` files, both Java poms, and
`docs/src/java/java.md` together
* `check_breaking_changes.py` resolves the last stable as `v0.31.0`, so
the minor-version gate passes

All five touched workflows parse as valid YAML and the pre-commit hooks
pass.

## Notes for review

* This targets `main` only, so it takes effect at the next
release-branch cut. The in-flight `release/v0.32` branch still carries
`v0.33.0-beta.0` / `python-v0.36.0-beta.0`; if we want the imminent
stable to be 0.37.0, this needs to be applied there too.
* Historical `python-v*` tags are left alone. The changelog builder
scans `^v`, which does not match them, so the first unified release's
notes will compute `fromTag` from the Rust/Node line only — a one-time
gap in the Python-side changelog.
* Pre-existing and not addressed here: `ci/update_lockfiles.sh --amend`
amends the commit that `bump-my-version` has already tagged, so the
lockfile update lands outside the tag on stable releases.

Fixes #3666
2026-07-25 09:22:35 -07:00
Lance Release a00edef0e6 Bump version: 0.32.0-beta.2 → 0.32.0-beta.3 2026-07-24 22:04:02 +00:00
Yang Cen 9dc5ec03aa feat(fts): add block size configuration (#3691)
## What changed

- add `block_size` to Python FTS configuration and the deprecated
local/remote helpers
- add `blockSize` to the TypeScript FTS options and propagate it through
the NAPI binding
- serialize the value as `block_size` for remote index creation
- document the existing Rust builder API and generate the TypeScript API
reference
- add local, remote, metadata, search, and invalid-value regression
coverage

## Why

Lance supports configuring the number of documents per compressed FTS
posting block, but LanceDB's Python and TypeScript APIs did not expose
the setting. This made the experimental FTS V3 layout unavailable
through those clients and allowed the value to be dropped before index
creation.

## How it works

The default remains `128`. Supported values are `128` and `256`;
selecting `256` uses the experimental FTS V3 format. Invalid values are
rejected by the Lance builder and surfaced as Python or JavaScript
errors.

## Validation

- `cargo check --quiet --features remote --tests --examples`
- `cargo +1.94.0 clippy --quiet --features remote --tests --examples --
-D warnings`
- targeted Rust local and remote index tests
- Rust doctests: 34 passed
- Python Ruff checks, doctest, and targeted local/remote tests: 5 passed
- TypeScript build, Biome lint, generated docs, and targeted Jest tests:
9 passed
- `git diff --check`

## Limitations

The Java client remains unchanged because its external remote REST model
does not currently expose `block_size`.

Co-authored-by: Yang Cen <yangcen@Yangs-Mac-mini.local>
2026-07-24 15:02:38 -07:00
LanceDB Robot c9d07ef6fc chore: update lance dependency to v10.0.0-beta.3 (#3710)
Updates the Rust workspace and Java lance-core dependencies to [Lance
v10.0.0-beta.3](https://github.com/lance-format/lance/releases/tag/v10.0.0-beta.3).

Includes compatibility updates for Lance’s nullable blob payload and
handle APIs.
2026-07-24 15:01:30 -07:00
LanceDB Robot 2ce88f8e02 chore: update lance dependency to v9.1.0-beta.8 (#3702)
Updates Rust workspace Lance dependencies and Java lance-core to
v9.1.0-beta.8. Removes MemWAL writer settings that are no longer exposed
by Lance.

Lance tag:
https://github.com/lance-format/lance/releases/tag/v9.1.0-beta.8
2026-07-21 23:37:32 -05:00
Expyron 82231bf66d chore: replace lazy_static with LazyLock (#3679) 2026-07-21 09:28:37 -07:00
Drew Gallardo 65cd142c7e feat: add remote branch diff and merge client APIs (#3686)
This PR adds some support for `diff` / `merge` in the remote client as
for local tables we stay `NotSupported` until
https://github.com/lance-format/lance/issues/7263.


This wires the two review-and-land calls against the remote REST API:
- `POST /v1/table/{id}/branches/diff`
- `POST /v1/table/{id}/branches/merge`

Rust gets typed results (`BranchDiff`, `MergeBranchResult`). Python
returns the wire JSON, same shape as the REST response.

Merge here means promoting a branch's added columns onto `main`.

### Behavior
- Remote only. Local raises `NotSupported`.
- A rejected merge is not an exception. HTTP 409 still returns `Ok` / a
dict with `status="rejected"` and blockers in `diff.mergeBlockers`.
- Unknown blocker / status codes parse as `Unknown` so a newer server
does not break older clients.
- `MergePreview` tolerates missing fields for the same reason.
- Merge requests are not retried. 409 is final and carries the body you
need.

### Example
```python

table = db.open_table("images")

table.branches.create("exp")
exp = table.branches.checkout("exp")

exp.add_columns({"tag": "cast('draft' as string)"})

diff = table.branches.diff("exp")
preview = table.branches.merge("exp", dry_run=True)
result = table.branches.merge("exp", dry_run=False)

if result["status"] == "merged":
    print("landed at", result["mainVersionAfter"])
elif result["status"] == "rejected":
    print(result["diff"]["mergeBlockers"])
```

### Testing
cargo test -p lancedb --features remote diff_branch
cargo test -p lancedb --features remote merge_branch

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 12:38:05 -07:00
Will Jones 5d0a1ef66c fix(rust): bound remote insert request size to avoid ingestion timeouts (#3630)
## Problem

On the remote (LanceDB Cloud) write path, each write partition is
uploaded as a **single** `/insert?upload_id=...` request that stays open
until the whole partition has been streamed and the server has written
it to object storage. For large bulk ingests a partition can be many GB,
so a single request can run longer than the client read timeout (default
300s), surfacing as:

```
lancedb.remote.errors.HttpError: operation timed out
```

The server already supports staging **multiple** parts under one
`upload_id` (each `/insert` writes a separate transaction that
`complete` merges atomically), but the client never used that — it sent
one part per partition.

## Change

Split each partition into multiple parts of at most
`max_bytes_per_request` (Arrow IPC, LZ4-compressed) bytes, each uploaded
as its own `/insert?upload_id=...&upload_part_id=...` request. This
bounds how long any single request stays open, independent of total data
size or write parallelism.

Key properties:
- **Still streamed, not buffered.** Each part's body is driven through a
bounded channel while the request is in flight (`futures::join!` of a
producer + the send), so peak memory stays at a couple of batches per
partition regardless of the part size. Backpressure from a
slow/throttled server still propagates upstream.
- **Correct part accounting.** An empty partition still sends exactly
one (schema-only) part so `complete` has a transaction to commit; a size
cut landing exactly on the end of input does not emit a trailing empty
part.
- **Multipart only.** The single-request (non-multipart) path is
unchanged.

## Config

New `ClientConfig::max_bytes_per_request: Option<usize>`, also settable
via the `LANCE_CLIENT_MAX_BYTES_PER_REQUEST` environment variable.
**Default 1 GiB** (`Some(0)` disables splitting → one request per
partition). Python users pick up the default/env automatically through
the remote client.

## Tests

- `test_multipart_chunked_splits_into_parts`: a 1-byte budget puts each
batch in its own part → N requests, each carrying the shared `upload_id`
and a distinct `upload_part_id`.
- `test_multipart_single_part_when_under_budget`: a large budget keeps
the partition in a single request.
- Verified end-to-end against a live remote table: a forced-chunked
multipart add (many parts) assembles to the correct row count.

Related to ENT-1883.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:37:41 -07:00
LanceDB Robot bc8674ab22 chore!: update lance dependency to v9.0.0-rc.1 (#3673)
BREAKING CHANGE: splits generated by the permutation data loader will
not be the same, due to a change in hash function.

Updates the Lance dependencies and Java lance-core to
[v9.0.0-rc.1](https://github.com/lance-format/lance/releases/tag/v9.0.0-rc.1).
Includes the required DataFusion 54 and Lance file-reader compatibility
updates.

---------

Co-authored-by: Will Jones <willjones127@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 10:31:30 -07:00
Jack Ye 37032151d3 feat: support distributed analyze plan metrics in clients (#3675)
Adds client-side support for analyze_plan distributed metrics modes
across Rust, Python, and TypeScript clients. Defaults to aggregate for
backward compatibility and sends the remote distributed_metrics
parameter only when a non-default mode is requested.
2026-07-15 21:21:40 -07:00
Lance Release 8a4eaaa8b9 Bump version: 0.32.0-beta.1 → 0.32.0-beta.2 2026-07-14 23:28:32 +00:00
kid 1918d1a3b6 fix(rust): skip embedding functions for empty batches (#3646)
Fixes #3174
Also fixes #3645

Empty record batches now append correctly typed empty embedding arrays
without invoking embedding providers. This avoids OpenAI requests with
an invalid empty input while preserving source-column validation and
the non-empty execution paths.

As a small cleanup, the single- and multi-embedding code paths now share
a single upfront lookup of their source columns ("input_columns")
instead
of each path looking them up independently. Also moves `lance-testing`
from regular dependencies to dev-dependencies where it belongs.

Tests run:
- `cargo fmt --all -- --check`
- `cargo test --quiet -p lancedb --lib
empty_batch_skips_embedding_functions`
- `cargo test --quiet -p lancedb --lib
empty_batch_still_validates_source_column`
- `cargo test --quiet -p lancedb --lib
test_create_empty_table_with_embeddings`
- `cargo check --quiet -p lancedb --features remote --tests --examples`
- `cargo clippy --quiet -p lancedb --features remote --tests --examples`
- `cargo test --quiet -p lancedb --lib`
- `cargo test --quiet --features remote --tests`
2026-07-14 14:46:31 -07:00
Jack Ye 06b53c97d6 feat: add table FTS query tokenization (#3659)
## Summary
- add table-level FTS query tokenization returning token text and
position
- use the native index tokenizer for local tables and remote index
metadata for remote tables
- expose sync and async Python table wrappers with focused coverage
2026-07-14 10:59:33 -07:00
Will Jones 711e05619b perf: skip Dataset::index_statistics() for all index types (#3346)
`Dataset::index_statistics()` loads index files and does meaningful CPU
work to serialize low-level info. Most fields
`NativeTable::index_stats()` needs are available from manifest metadata
via `Dataset::describe_indices()`, which is much cheaper.

`NativeTable::index_stats()` now:

- Calls `describe_indices()` filtered by name; returns `Ok(None)` if no
match.
- Parses `distance_type` from `description.details()` JSON (the
`VectorIndexDetails` proto stored in the manifest by recent Lance
versions).
- Falls back to `index_statistics()` only for vector indices where
`details()` returns no `distance_type` — this handles older Lance
datasets that didn't write `VectorIndexDetails`.
- `Unknown` index types (e.g. Lance's internal `FragReuseIndex`) are
explicitly filtered out of `list_indices` rather than erroring.

## Test plan
- [x] `test_create_scalar_index` — asserts `index_type`,
`distance_type`, and `num_unindexed_rows > 0` after adding rows
post-index
- [x] `test_create_fm_index`, `test_create_bitmap_index`,
`test_create_label_list_index` — added `index_stats` assertions
- [x] IvfPq, IvfHnswPq, IvfHnswSq, IvfHnswFlat tests assert
`distance_type == Some(L2)`
- [x] `test_list_indices_skip_frag_reuse` — FragReuseIndex is filtered
by the Unknown guard in `list_indices`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-07-14 09:45:50 -07:00
Lance Release 104fc5a08e Bump version: 0.32.0-beta.0 → 0.32.0-beta.1 2026-07-10 16:13:35 +00:00
Lance Release 8e364e6812 Bump version: 0.31.0-beta.6 → 0.32.0-beta.0 2026-07-10 05:26:01 +00:00
Will Jones 285add40dd feat: expose Lance metrics via OpenTelemetry in Python and Node (#3609)
Bridges Lance's internal `metrics`-crate instrumentation (object store
request counts, bytes, latency, errors, and throttles) into
OpenTelemetry, in both the Python and Node bindings, with a shared
adapter in the Rust core. This is the LanceDB counterpart to
lance-format/lance#7537.

## Rust core (`rust/lancedb`)
Two new, **off-by-default** features:
- `metrics` — re-exports the [`metrics`](https://docs.rs/metrics) crate
as `lancedb::metrics` and turns on Lance's object-store instrumentation.
Install any `metrics`-compatible recorder to collect them.
- `metrics-otel` — adds `lancedb::metrics_otel`, a pull-based adapter
that installs a process-global recorder aggregating into lock-free
cumulative storage and exposes a snapshot/catalog API
(`register_metrics_recorder`, `metrics_catalog`, `snapshot_metrics`,
`MetricPoint`/`MetricValue`/`MetricKind`/`MetricDescription`). Both
bindings build on this.

## Python
`lancedb.otel.instrument_lancedb_metrics()` registers each metric as an
OpenTelemetry observable instrument on the given (or global)
`MeterProvider`. Available via the `otel` extra (`pip install
lancedb[otel]`), which pulls in only `opentelemetry-api` — the
application supplies and configures the SDK.

## Node
`instrumentLanceDbMetrics()` provides the equivalent wiring against
`@opentelemetry/api`. This is the only public entry point; the
underlying recorder/catalog/snapshot functions stay internal.

Because OpenTelemetry has no asynchronous histogram instrument,
histograms are exported Prometheus-style as `<name>_bucket` (with an
`le` attribute), `<name>_count`, and `<name>_sum`. Only `_sum` carries
the histogram's unit; `_bucket` and `_count` observe cumulative counts
and are unitless. The adapter is enabled by default in the Python and
Node builds, and off by default in the Rust crate.

## Notes
- Requires Lance ≥ `v9.0.0-beta.19`, which ships the object-store
metrics APIs (upstream lance-format/lance#7537, now merged). `main` is
already on beta.19, so this is a single feature commit with no
dependency bump.
- Tests: 8 Rust unit tests, 3 Python tests, 2 Node tests, all covering
the end-to-end object-store-metrics → OpenTelemetry path.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:36:03 -07:00
Xuanwo 22bf091de1 fix: avoid manifest writes for read-only directory namespace opens (#3635)
Bumps Lance to v9.0.0-beta.19, which includes lance-format/lance#7687
for side-effect-free DirectoryNamespace read paths.

This fixes root-level read-only table opens that previously could
trigger `__manifest` creation through directory namespace construction,
including Hugging Face bucket reads with read-only tokens. A LanceDB
regression test now covers root listing operations without creating
`__manifest`.

Fixes #3633.
2026-07-09 12:39:34 -07:00
ForwardXu 291e9e37be feat: add Tencent COS and GooseFS object store support via new feature flags (#3526)
## Summary

Closes #3525

This PR wires up two new optional object-store backends at the LanceDB
layer, exposing capabilities that already exist upstream in `lance` /
`lance-io`:

| Backend | Cargo feature | Default in Rust crate | Default in Python
wheel | Default in Node binding |
| --- | --- | --- | --- | --- |
| **Tencent COS** | `cos`     |  off |  on |  off |
| **GooseFS**     | `goosefs` |  off |  on |  on |

Both backends are additive and do not affect existing users who don't
opt in.

## Motivation

- **Tencent COS** is the dominant object storage in the China region.
Tencent Cloud users currently need an S3-compatible proxy or a private
fork to use LanceDB against COS buckets.
- **GooseFS** is Tencent Cloud's distributed cache acceleration layer
that sits in front of COS/S3, a common pattern for vector search / AI
training where the same hot dataset is read repeatedly.
- This brings COS / GooseFS to feature parity with the existing
first-class backends (`aws`, `gcs`, `azure`, `oss`, `huggingface`).

See the linked issue #3525 for the full discussion.

## Changes

### `rust/lancedb/Cargo.toml`

Add two new optional features that pull through the corresponding
upstream feature flags:

```toml
cos = ["lance/tencent", "lance-io/tencent"]
goosefs = [
    "lance/goosefs",
    "lance-io/goosefs",
    "lance-namespace-impls/dir-goosefs",
]
```

### `python/Cargo.toml`

Enable both `cos` and `goosefs` by default for the Python wheels, so
`pip install lancedb` works against COS / GooseFS out of the box
(consistent with how `aws` / `gcs` / `azure` / `oss` are bundled today):

```diff
-default = ["remote",  "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface"]
+default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface", "lancedb/cos", "lancedb/goosefs"]
```

### `nodejs/Cargo.toml`

Enable `goosefs` by default for the Node binding (COS kept opt-in to
limit the default native binary size; can be revisited based on demand):

```diff
-default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface"]
+default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface", "lancedb/goosefs"]
```

### `Cargo.lock`

Regenerated to reflect the transitive dependencies brought in by the new
upstream features. No manual edits.

## Example Usage

### Rust

```toml
# Cargo.toml
lancedb = { version = "0.30", features = ["cos", "goosefs"] }
```

```rust
// Tencent COS
let db = lancedb::connect("cos://my-bucket/my-db").execute().await?;

// GooseFS
let db = lancedb::connect("goosefs://my-namespace/my-db").execute().await?;
```

### Python

```python
import lancedb

db = lancedb.connect(
    "cos://my-bucket/my-db",
    storage_options={
        "secret_id": "...",
        "secret_key": "...",
        "region": "ap-guangzhou",
    },
)
```

## Backwards Compatibility

- All new features are **opt-in** at the Rust crate level (`default =
[]` for `lancedb` itself is unchanged).
- The Python wheel gains both backends by default, increasing wheel size
slightly but matching the existing pattern of bundling all major cloud
backends.
- Node binding only adds `goosefs` to defaults; existing users see no
behavior change.

## Testing

- `cargo check --all-features` 
- `cargo check -p lancedb --features cos` 
- `cargo check -p lancedb --features goosefs` 
- End-to-end COS / GooseFS smoke tests require Tencent Cloud credentials
and are intentionally not added to CI in this PR (same approach used for
`s3-test`). Happy to add a gated test feature in a follow-up if
reviewers prefer.

## Checklist

- [x] Added `cos` and `goosefs` features to `rust/lancedb/Cargo.toml`
- [x] Updated `python/Cargo.toml` default features
- [x] Updated `nodejs/Cargo.toml` default features
- [x] Regenerated `Cargo.lock`
- [x] Verified build with `--all-features`
- [ ] Documentation update (can be done in a follow-up PR once API
stabilizes)

## Related

- Issue: #3525
- Upstream support:
[`lance/tencent`](https://github.com/lance-format/lance),
[`lance/goosefs`](https://github.com/lance-format/lance)
2026-07-08 14:14:39 -07:00