## Summary
- document that Azure Blob Storage credentials can be passed directly
through `storage_options`
- provide valid quoted `account_name` and `account_key` examples for
both sync and async Python connections
- execute the option dictionaries during doctests so the original
unquoted-key mistake is caught
## Root cause
The historical Python storage guide used `account_name` and
`account_key` as bare identifiers in dictionary literals. Following that
example either raised `NameError` or, when those names were predefined,
produced incorrect option keys. The runtime already accepts direct Azure
credentials, but the current Python API reference did not contain a
corrected Azure example.
## Validation
- `python/.venv/bin/ruff format --check
python/python/lancedb/__init__.py`
- `python/.venv/bin/ruff check .`
- `cd python && uv run --no-sync pytest --doctest-modules
python/lancedb/__init__.py -q`
- `cd python && uv run --no-sync pytest python/tests/test_import.py -q`
Fixes#2236
<!-- lance-gatekeeper-fix:v1 agent=7a7a9e009eb2ebe52ac9b1adf2e8afb2
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
#3528 added blob declarations and binary coercion. String values were
still rejected. They now coerce to the blob `uri` child.
```python
table.add([{"id": 1, "image": "s3://bucket/media/cat.jpg"}])
payload = table.fetch_blobs("image", table.search().to_arrow())
```
A URI under a registered base writes with no extra options. An
unregistered URI fails. `allow_external_blob_outside_bases` is a local
escape hatch that stores an absolute URI. It does not register a base.
Remote `add` rejects that flag before making a request. String input
still coerces and is sent as a `uri` struct.
`add_bases` is a follow-up. `merge_insert` does not coerce string blob
input.
### Testing
- `cargo test -p lancedb --test blob_integration`
- `cargo test -p lancedb blob_coerce`
- `cargo test -p lancedb --features remote --lib
add_rejects_external_blob_flag add_string_blob_becomes_uri_struct`
- `cd python && uv run --extra tests pytest python/tests/test_blob.py -k
uri -q`
Co-authored-by: Xuanwo <github@xuanwo.io>
## Problem
`uv run ... maturin develop` synchronizes the project as an editable
package before running the command. Maturin's editable build otherwise
uses the release profile, which enables the repository's fat LTO
configuration during local bootstrap.
## Behavior
Editable Python builds now explicitly use Cargo's dev profile. The
minimum maturin version is raised to 1.10, where `editable-profile`
support was introduced.
## Problem
The unreleased First-Class Function authoring API exposed
`secrets=[...]` and serialized `required_secrets`, promising runtime
resolution and injection that Sophon does not implement.
## Behavior
Remove the secrets dimension from the public Python decorator, Python
and Rust registration/version models, and shared wire fixtures. Existing
successfully registered Functions retain stable identity: the field
could only be empty and empty values were already omitted from canonical
serialization.
A stable LanceDB release has not shipped this Function API, so this
contracts the surface before it becomes a published compatibility
commitment.
## Validation gap
The Rust shared-golden Function suites and Python formatting/lint checks
pass locally. Python pytest was not run because the local environment
lacks its runtime dependencies and the frozen native editable build did
not complete in practical time.
Function applications and bindings currently encode `group_id` and
binding `revision` even though `binding_id` already owns the complete
immutable binding lifecycle and `outputs` already defines the atomic
multi-output set.
Make `binding_id` the sole binding identity, remove the redundant fields
from the Rust and Python client contracts, and describe multi-output
declarations directly. This intentionally replaces the removed wire
fields without a compatibility path.
Registering a real (embedding) Function failed on the client for three
reasons:
- `_package_source` treated `inspect.getclosurevars().unbound` as
"unresolved globals"; CPython puts attribute names there, so any body
with `np.linalg.norm(...)` or `body.split()` was rejected. Module-scope
references now come from Python's own scope analysis (`symtable`) over
the function source, recursively, and each is resolved the way the
interpreter would: the function's globals first (a module global may
shadow a builtin), then builtins. Free variables of nested scopes stay
lexical; postponed annotations are not runtime loads. A genuinely
missing global still fails.
- `_canonical_arrow_type` emitted spellings the server's frozen grammar
rejects (`fixed_size_list<T>[n]`, `timestamp[us]`, `struct<...>`,
zero-sized lists). It now emits exactly the grammar, with the server's
`fixed_size_list<item, size>` form, and the Rust declaration planner
parses that form too.
A shared golden
(`tests/fixtures/first_class_functions/v1/arrow_types.json`)
enumerates every grammar type, nested forms and rejected spellings; the
Python emitter and Rust parser are tested against it, and the same file
is under test in sophon. Packaging tests execute the shipped artifact in
a fresh namespace.
Contract changes (hence `breaking-change`):
- `@udf` now rejects namespace acquisition structurally
(`globals()`/`eval`/... by name, plus `import
sys`/`builtins`/`importlib`/`inspect` inside the body), requires the
function's captured `__builtins__` to be the standard mapping itself
(identity, so neither lookups nor implicit hooks such as `__import__`
can differ), rejects module globals that are namespace-bearing modules
(`builtins`, `sys`, ...), and treats the function's own name as
recursion only when the module binds it to the function or to the exact
`UdfDefinition` the decorator produced; it resolves module globals
through the function's real namespace (a module global may shadow a
builtin) and ships importable classes/functions as imports.
- List outputs must declare a non-nullable, metadata-free child named
`item` (`pa.list_(pa.field("item", t, nullable=False))`); that is what
the grammar means, and pyarrow's default nullable child was being
silently collapsed into it.
Contract, stated in the `udf` docstring: the artifact is a snapshot of
the function source plus exactly the module names it references.
Reaching the module namespace by another route is rejected where a
static packager can see it and is otherwise unsupported; there is no
dynamic-access detection beyond that.
A permutation stores `_rowid`s, which are row addresses unless stable
row ids are enabled. Nothing in the data loader pinned a table version,
so a compaction between building a permutation and reading it can
resolve those ids to different rows.
The exposure differs by backend but exists on both:
- Remote never pins. `prepare_query_bodies` stamps `"version":
current_version()` on every request, but `current_version()` is `None`
unless `checkout` was called, so every request means "latest".
- Native pins implicitly by holding an `Arc<Dataset>` under
`ConsistencyMode::Lazy`, but `StreamingDataset.__setstate__` reopens the
table in each DataLoader worker, so each worker pins to whatever is
latest at fork time.
## Changes
`Table::at_version` returns an independent handle pinned to a version
without mutating the receiver. `checkout` cannot serve this: on remote
the version cell is an `Arc<RwLock<Option<u64>>>` shared across clones,
so pinning through it would silently pin the caller's table too.
`PermutationBuilder::build` pins for the whole build and records the
version in the permutation table's schema metadata, alongside the
existing split names. `PermutationReader` pins the base table to that
version before any take.
Because the reader pins on construction, the Python worker fork is
covered without touching the pickle format — `Permutation.__setstate__`
drops the reader and `_ensure_open` rebuilds it, which re-pins.
## Behaviour change
A permutation is now bound to the version it was built against, so rows
appended to the base table afterwards are not visible through an
existing permutation. That is the intended semantics — the permutation
only addresses rows that existed when it was built — but it is a change
worth flagging.
Permutations written before this carry no version key and read exactly
as they did before.
## Summary
- compare inferred Arrow types by their semantic representation across
records
- throw the schema inference error when a later record has an
incompatible type
- cover compatible and incompatible multi-record inference across
supported Arrow versions
## Root cause
Schema inference compared newly allocated Arrow DataType objects by
identity, so equivalent inferred types did not compare equal. The
mismatch path also constructed an Error without throwing it, which
silently accepted incompatible values.
## Validation
- pnpm test __test__/arrow.test.ts --runInBand (176 tests passed)
- pnpm lint
- pnpm build
- pnpm run docs
Fixes#3781
<!-- lance-gatekeeper-fix:v1 agent=00ec4f61a3fd82694fa4fb9bb2b37aa8
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
`table_names` is being replaced by `list_tables` across the SDKs, but
TypeScript only had `tableNames`. This PR adds `listTables`, which
returns a page of table names together with the token that resumes after
it, and marks `tableNames` and `TableNamesOptions` deprecated in favor
of it.
It binds the `Connection::list_tables` that already exists, so nothing
in the Rust API changes and nothing existing breaks. `pageToken` is
documented as opaque rather than as a table name, since what resumes a
listing is the database's to decide — that keeps callers off a detail
that is going to change.
Stacked on #4040, which fixes a table being dropped at every page
boundary. The page-walking test here needs that fix to pass. Review the
last commit only until #4040 lands.
## Example
```ts
const names = [];
let pageToken = undefined;
do {
const page = await conn.listTables({ pageToken, limit: 100 });
names.push(...page.tables);
pageToken = page.pageToken;
} while (pageToken);
```
A namespace can be listed by passing its path first, mirroring
`tableNames`:
```ts
const page = await conn.listTables(["analytics"], { limit: 100 });
```
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes#4015
<!-- lance-gatekeeper-fix:v1 agent=1c6407eec60f0af2336ae2dee811e1be
generation=1 -->
## Root cause
PermutationBuilder issued count_rows and the projected row-ID scan
through an unpinned RemoteTable handle. Each request could independently
resolve latest, so a concurrent table update could make the count and
scanned rows come from different snapshots.
## Fix
- add a backend hook for obtaining an independent handle pinned to the
currently selected version
- resolve latest once for remote tables while preserving an explicit
checkout and leaving the caller handle unchanged
- build the count, filtered projection, and scan from that pinned handle
while retaining native-table behavior
- add a remote mock regression that advances latest between count and
scan and covers explicit checkout preservation
## Validation
- cargo test --quiet --features remote -p lancedb --lib
test_remote_permutation_builder_pins_snapshot
- cargo test --quiet --features remote -p lancedb --lib
dataloader::permutation::builder::tests
- cargo check --quiet --features remote --tests --examples
- cargo clippy --quiet --features remote --tests --examples
- cargo fmt --all -- --check
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
A caller that queues a refresh and executes it later can only tell the
view it captured from a drop-and-recreate by comparing the definition
and
version. A recreated view with the same definition and an equal or
higher
version passes that check, and the check runs before the refresh reloads
the view, so it never sees the state it commits against.
This mints an `mv.incarnation` token in the schema metadata at each
physical creation of a view table.
`RefreshMaterializedViewBuilder::expect_incarnation` carries the
captured
token into the refresh, which compares it against the latest stored
manifest before planning and again immediately before each commit
(publish, fragment swap, watermark stamp), refusing to land in a
different
incarnation. A view with no token -- declared before tokens existed, or
its metadata replaced wholesale -- is refused under a bound refresh with
its own wording and is minted one by its next unbound refresh. The token
is exposed through `MaterializedView::incarnation`; refreshes without an
expectation are unchanged.
This is best effort: the token is not part of lance's commit condition,
so a recreation landing between the final pre-commit read and the commit
itself is not caught. Closing that window needs a base-manifest
precondition in lance's commit path.
`table_names` paginates using a `start_after` table name. This works for
the `/v1/table` endpoint, which guarantees table-order. But the
`/v1/namespace/{id}/table/list` does not. We change that caller to
instead collect all table names, sort, and apply the pagination locally.
We are deprecating this API, so this is just an interim fix. For good
performance, users should move to the `list_tables` API instead, which
uses opaque tokens that don't rely on lexical sorting.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
- add `StreamingDataLoader`, which transports worker snapshots with
prefetched batches and commits them to the parent dataset only when the
trainer receives each batch
- preserve exact non-uniform per-split progress and resume lagging
splits without replaying already-consumed rows
- reject stale parent checkpoints after a standard multi-process
`DataLoader` has started, with guidance to use the consumer-aware loader
- document the new public loader and merge non-uniform state across
ranks
## Root cause
PyTorch runs `StreamingDataset.__iter__` in private worker-process
copies, while callers invoke `state_dict()` on the parent dataset.
Sharing producer counters would still be incorrect because DataLoader
prefetch can advance workers beyond batches returned to the trainer.
## Validation
- `uv run --extra tests pytest python/tests/test_elastic_dataloader.py
-q` (154 passed)
- focused non-uniform merge regression (1 passed)
- `uv run --project python --extra tests --extra dev ruff format .`
- `uv run --project python --extra tests --extra dev ruff check .`
- `cd docs && PYTHONPATH=. ../python/.venv/bin/mkdocs build`
Fixes#3967
<!-- lance-gatekeeper-fix:v1 agent=572be272619660b97e87fd5c85188341
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
Listing tables a page at a time against a local database silently
skipped one table at every page boundary. `ListingDatabase::list_tables`
returned the first name of the *next* page as that page's token, but
resuming from a token drops every name at or before it — so the table
the token named was never handed to the caller. Walking `[a, b, c, d,
e]` with a limit of 2 returned `[a, b, d, e]`.
This PR returns the last name of the page as the token instead, which is
what resuming after the token expects.
This is reachable from Python today through
`db.list_tables(page_token=...)` on a local connection; it also affects
`len(db)` and `name in db`, which walk the pages. Remote and
namespace-backed connections page on the server and were never affected.
## Example
```python
db = lancedb.connect(tmp_path)
for name in ["a", "b", "c", "d", "e"]:
db.create_table(name, [{"id": 1}])
names, token = [], None
while True:
page = db.list_tables(page_token=token, limit=2)
names += page.tables
token = page.page_token
if not token:
break
# before: ['a', 'b', 'd', 'e']
# after: ['a', 'b', 'c', 'd', 'e']
```
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Problem
`refresh_column_async` returned a unit-result job even though durable
refresh jobs carry a canonical terminal result. Python callers could not
obtain row counts or source and published versions through the public
`Job` API, and local and remote refresh jobs exposed different result
semantics.
## Behavior
`refresh_column_async` now returns `Job[RefreshColumnResult]` for local
and remote tables. The general typed-job bridge binds each endpoint to
its public result model while preserving unit-result jobs and existing
status, wait, cancel, and timeout behavior. A local no-op refresh
reports no published version.
The Node.js API continues to resolve `wait()` as `void`; its binding
erases the Rust result type internally to preserve the existing public
contract.
## Ownership and integration boundary
LanceDB owns the language-neutral `Job<T>` contract and language-binding
decode. Sophon owns production and durable persistence of terminal
payloads. Sophon #7348 and #7378 now publish the canonical refresh
result for Function-backed and expression-backed refresh jobs,
respectively. The remote client fixture matches the merged server
schema; live deployment and end-to-end demo acceptance remain separate
rollout checks.
## How packing works
Consider four tokenized documents:
[1]
[2]
[10, 11, 12, 13, 14, 15, 16, 17]
[20]
With:
```
StreamingDataset(
table,
shuffle=False,
columns=["tokens"],
num_splits=2,
pack_sequences=5,
eos_id=9,
pad_id=0,
blocks_per_epoch=6,
)
```
the documents are assigned to two fixed logical splits. Each split
maintains an independent token buffer, appends eos_id after every
document, and emits blocks of five tokens.
Because blocks_per_epoch=6, each split emits exactly three blocks:
Cycl/e 1:
Split 0: [1, 9, 2, 9, 0] # 9 is eos, 0 is padding
Split 1: [10, 11, 12, 13, 14]
Cycle 2:
Split 0: [0, 0, 0, 0, 0]
Split 1: [15, 16, 17, 9, 20]
Cycle 3:
Split 0: [0, 0, 0, 0, 0]
Split 1: [9, 0, 0, 0, 0]
If a split runs out of tokens early, it emits padded blocks through the
fixed budget. This prevents one rank from finishing before another.
Logical splits are independent of rank and worker ownership. A
checkpoint records each split’s consumed-document count, emitted-block
count, remaining tokens, and document boundaries. Merging
those per-split states allows the same packed stream to resume after the
topology changes.
doc_ids identifies document segments, including continuations across
block boundaries. It is not a padding mask: padding retains the
preceding document ID, so callers must mask padding using a
reserved pad_id.
blocks_per_epoch="auto" is also available. It estimates the budget from
a deterministic bounded sample and warns that the result is approximate.
WIP pre-training tests:
```
┌────────────────────────────────────┬────────────────────────┬─────────────────────────────────────┐
│ │ GPT-2 124M │ GPT-2 medium 354M │
├────────────────────────────────────┼────────────────────────┼─────────────────────────────────────┤
│ Corpus │ 2.4M docs / 12GB table │ 9.67M docs / 45GB table │
│ Tokens (Chinchilla) │ 2.43B │ 7.0B │
├────────────────────────────────────┼────────────────────────┼─────────────────────────────────────┤
│ Data prep (ingest→curate→tokenize) │ ~12 min │ ~51 min │
├────────────────────────────────────┼────────────────────────┼─────────────────────────────────────┤
│ Training wall time │ ~50 min │ 3h 06m │
├────────────────────────────────────┼────────────────────────┼─────────────────────────────────────┤
│ Throughput / MFU │ 1.60M tok/s / 35% │ 684k tok/s / 42.0%, │
├────────────────────────────────────┼────────────────────────┼─────────────────────────────────────┤
│ Final val loss │ 3.230 │ 2.840 │
└────────────────────────────────────┴────────────────────────┴─────────────────────────────────────┘
```
---------
Co-authored-by: OpenAI Codex <codex@openai.com>
Rename prefetch_batches → io_queue_depth and introduce
transform_queue_depth as a symmetric pair: both express "number of
batches to buffer per split at this pipeline stage." The old names are
still accepted as keyword arguments but log a deprecation warning
redirecting callers to the new names.
transform_queue_depth caps how many transform-result batches can
accumulate per split in the post-transform queue. Without this limit a
slow consumer (e.g. a GPU training step) causes cooked rows to pile up
unboundedly. The backpressure check in _try_submit_tx counts both
already-cooked rows and rows expected from in-flight transforms; it
skips proactive transform submission when the combined total reaches the
limit. The reactive _ensure_cooked path bypasses the check so the
consumer never stalls.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes materialized views to TypeScript: createMaterializedView,
openMaterializedView and listMaterializedViews on Connection, and a
MaterializedView handle carrying the parsed definition and
refresh({full, sourceVersion}), which returns the typed refresh result.
select accepts column names, [alias, expression] pairs, or a record of
the
same; the definition reads back off the stored schema, so a reopened
handle
needs no side channel. Remote connections surface the core's
not-supported
error up front.
The napi crate needed the same recursion-limit raise as the core crate:
the
refresh future's type graph overflows the default trait-recursion depth.
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
This PR is a **breaking** rename of #3686.
merge reads like git merge w/ three-way, replay history, combine two
lines of work. That is not this API.
This call takes one additive change on a branch and lands it on main.
New column, including a blob column. Main's existing columns are not
rewritten. If it cannot land, you get `status="failed"` and
`diff.errors`, not a merge conflict to resolve.
Cherry-pick is terminology that aligns more with that.
```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.cherry_pick("exp", dry_run=True)
result = table.branches.cherry_pick("exp")
if result["status"] == "cherryPicked":
print("landed at", result["mainVersionAfter"])
elif result["status"] == "failed":
print(result["diff"]["errors"])
```
### Behavior
- Remote / Enterprise only. Local still NotSupported.
- HTTP 409 is not an exception. It is Ok with status="failed" and
diff.errors (CherryPickError).
- Unknown error / status codes still parse as Unknown.
- Requests are not retried. 409 is final and carries the body.
- Endpoint is POST /v1/table/{id}/branches/cherry_pick/.
- merge_insert and Table.merge are unchanged.
### Testing
- `cargo test -p lancedb --features remote diff_branch`
- `cargo test -p lancedb --features remote cherry_pick`
- `pytest python/python/tests/test_remote_db.py -k cherry_pick`
- node `remote.test.ts` diffs / cherry-picks path
Exposes materialized views to Python in both the async and sync clients:
create_materialized_view / open_materialized_view /
list_materialized_views
on the connections, and MaterializedView / AsyncMaterializedView handles
carrying the parsed definition and refresh(full=, source_version=),
which
returns the typed refresh result. select accepts column names, (alias,
expression) pairs, or a dict of the same; the definition reads back off
the
stored schema, so a reopened handle needs no side channel. Remote
connections raise NotImplementedError up front rather than failing deep
in
a request, matching the computed-column convention.
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Example tests pin behaviors; the refresh contract is a property: after
any
sequence of source mutations, a view maintained by default refreshes
equals
the definition evaluated against the source directly, and so does a
forced
rebuild. This drives every mutation sequence up to length three --
appends,
deletes, updates crossing the filter, compactions, unrelated column adds
--
over an identity and a filtered view shape, checking against an oracle
that
shares nothing with the refresh path: a plain column scan with the
filter
applied in Rust. The oracle runs after every step because a later
rebuild-forcing mutation silently heals an incremental error; end-state
checks miss exactly the transient bugs that matter. A length-four sweep
runs behind
ignore.
Named regressions additionally assert the refresh mode, which value
comparison cannot: a wrongly rebuilding classifier still matches the
oracle, so the append, unrelated-column and compaction cases pin that
the
incremental path actually ran.
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
A declared view holds no rows; refresh computes them. It pins one source
version, brings the view to exactly the definition's result at that
version,
and records the version as a watermark in the view's schema metadata.
It is incremental when it can reconcile what changed: appended rows are
computed and appended, and rows the source deleted or updated are found
by
the lance delta and evicted by their __source_row_id provenance, the
updated
ones recomputed in the same commit. Compaction rearranges rows without
changing
them, so its outputs cost nothing -- which is what keeps routine
background
compaction from rebuilding the view. A vacuumed watermark, a
delta the transaction-log walk cannot classify, a Legacy-storage source,
or
more staged ids than a fixed cap all fall back to a rebuild; rebuilding
an
indexed view swaps every fragment in one Update, so readers never see it
unindexed or empty.
Concurrent refreshes serialize at commit -- each carries the
same sentinel row id in its inserted-rows filter, so the loser lands
nothing. On the append path the watermark moves in a follow-up commit,
so a
crash between the two re-appends those rows. Bumps lance
to v11.0.0-beta.19 for the delta reader.
## Summary
- reject whole-document `lance.json` fields during native BITMAP index
preparation
- preserve BITMAP support for raw `LargeBinary` fields
- return guidance to use a JSON-path scalar index or FTS instead
- add regression coverage for the logical JSON type while retaining the
existing raw binary coverage
## Root cause
Native scalar-index validation resolved the complete Arrow field but
checked BITMAP compatibility only against its physical data type.
Because `lance.json` is stored as `LargeBinary`, it was incorrectly
accepted under the raw binary compatibility rule.
The fix reuses Lance’s `lance_arrow::json::is_json_field` helper before
physical type validation. Remote serialization is unchanged, so remote
clients continue to send the requested BITMAP type for server-side
validation.
## Validation
- `cargo fmt --all -- --check`
- `cargo test --quiet --features remote -p lancedb
test_create_bitmap_index -- --nocapture`
- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`
- `cargo test --quiet --features remote --tests`
Fixes#3889
<!-- lance-gatekeeper-fix:v1 agent=e097fc02a548edc0d0be2e18c65c03a3
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add a merge-insert regression test whose fixed-size-list child count
crosses `u32::MAX`
- verify delete-by-source updates the matching row, deletes every other
row, and completes without an Arrow panic
- use a null child array so the boundary case avoids allocating a real
vector payload
## Root cause and fix
The affected Lance merge fallback carried the target payload through a
full outer hash join. Arrow's fixed-size-list take kernel uses `u32`
child indices, so taking a target row whose child offset crossed
`u32::MAX` wrapped the offset and produced child data shorter than the
parent array, triggering the reported `ArrayData::slice` assertion.
The projection-aware merge path in the Lance version now used by `main`
avoids materializing the target fixed-size-list payload in that join.
This regression test locks in that production behavior at the exact
child-index boundary.
## Validation
- `cargo fmt --all`
- `cargo test --quiet --features remote -p lancedb
test_merge_insert_fixed_size_list_above_u32_child_count`
- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`
Fixes#2874
<!-- lance-gatekeeper-fix:v1 agent=582e68bcad65739e189352cb3cbf144c
generation=3 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- expose `memory_limit` and `num_workers` on the Python FTS
configuration for local builds
- forward both build-only settings to the Lance inverted-index builder
- add an end-to-end regression proving the configured memory budget
reaches the native build
## Root cause
LanceDB 0.26.1 pinned Lance 1.0.1. That Lance version used an FTS
partition-merge path whose retained data made memory grow with merge
progress on very large indexes. Upstream Lance
[#5754](https://github.com/lance-format/lance/pull/5754) changed
partition merging to stream its inputs, reducing peak memory by about
25%. Lance [#6174](https://github.com/lance-format/lance/pull/6174) then
removed the old merge phase, compressed posting lists during
construction, reduced indexing memory by about 60%, and introduced a
total build `memory_limit` for bounded workers.
Current `main` pins Lance 11.0.0-beta.3, which contains those
architectural fixes. This PR does not duplicate or claim the upstream
leak fix; it addresses the remaining Python API gap.
## This repair
LanceDB Python did not expose the native FTS builder resource controls.
`memory_limit` now sets the total local-build budget in MiB, divided
among effective workers, and `num_workers` controls build parallelism.
Both are build-only settings and do not affect remote builds or
persisted index configuration.
## Validation
- `cargo check --quiet --features remote --tests --examples`
- `cargo fmt --all`
- `uv run --project python --extra tests --extra dev ruff check .`
- `uv run --project python --extra tests --extra dev ruff format --check
python/python/lancedb/index.py python/python/tests/test_fts.py`
- `uv run --project python --extra tests pytest python/tests/test_fts.py
-q` (51 passed)
Fixes#2923
<!-- lance-gatekeeper-fix:v1 agent=a1ceedf74531e0212cb6f1ebf9390a26
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
A materialized view is a table whose contents are defined by a query
over
one source table and maintained by refresh rather than by writes.
The declaration half: create_materialized_view(name, source) resolves a
projected, filtered and limited definition against the source schema --
output types come from the DataFusion planner, never the caller -- and
commits an empty table carrying it as kind-tagged JSON in schema
metadata.
The tag lets a kind added later read back as a view this version cannot
refresh rather than as a plain table. Views open and list as ordinary
tables.
Sources must have stable row ids, checked here because the property
cannot
be enabled later: each view row records its source row in
__source_row_id,
and that provenance survives compactions, updates and deletes only when
row
ids are stable.
A view inherits the metadata describing its columns and none governing
how a
table is written, so blob markers carry through while declarations its
always-nullable fields would contradict are stripped. Embedding
configuration is rewritten to the view's column names, and dropped where
it
does not project both ends of a function.
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
## Summary
- document that current LanceDB releases use one compute worker without
warning on two-vCPU containers
- distinguish compute-worker tuning from storage I/O concurrency
- direct users of affected LanceDB 0.21.1 installations to upgrade and
link the current threading guidance
## Root cause
The Lance version bundled with LanceDB 0.21.1 warned whenever the
detected CPU count was less than or equal to its default two-core I/O
reservation. A two-vCPU deployment therefore emitted the warning on
every query even though falling back to one compute worker was the
intended behavior. Lance fixed that warning condition upstream in
lance-format/lance#3710, and LanceDB current main already pins a version
containing the runtime fix; the Python package documentation did not
explain the corrected behavior or the distinct thread controls.
## Validation
- `git diff --check`
- verified the linked Lance threading-model documentation returns HTTP
200
Fixes#2326
<!-- lance-gatekeeper-fix:v1 agent=9f141242416a6dbeb43be0e80404dd4d
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add cross-platform regression coverage for Azure table URI
construction
- assert that az:// database paths always produce forward-slash blob
keys
## Root cause
ListingDatabase previously used the host filesystem Path join operation
for object-store URIs, which inserted a backslash on Windows. The URI
construction was corrected in #2575, but the original Azure report had
no regression coverage and remained open.
## Validation
- cargo fmt --all
- cargo test --quiet --features remote -p lancedb
test_table_uri_uses_forward_slashes_for_azure
- cargo check --quiet --features remote --tests --examples
- cargo clippy --quiet --features remote --tests --examples
Fixes#2283
<!-- lance-gatekeeper-fix:v1 agent=dd96adfd0fcf303c11e873300663d8f6
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
`StreamingDataset`, `PermutationBuilder`, and `Permutation` now work
against a `RemoteTable` (LanceDB Cloud and Enterprise), which unblocks
benchmarking the loader against the enterprise cluster cache.
```python
db = lancedb.connect("db://my-db", api_key=..., host_override=...)
ds = StreamingDataset(db.open_table("training"), world_size=8, rank=r)
```
Rows are addressed by `_rowid` exactly as before —
`PermutationReader::load_batch` already built the same `_rowid IN (...)`
filter that `Table::take_row_ids` sends, so the loader's fetch was
always the take path. It just was never allowed to run.
### The guard
`PermutationBuilder.__init__` rejected anything without `_inner`, so a
`RemoteTable` raised `TypeError` before reaching the PyO3 layer — which
already unwraps one via `_table._inner`.
### A bounded schema lookup
`PermutationReader::output_schema` reads the schema off a query plan,
and building a plan on a remote table *executes* the query
(`create_plan` → `execute_query`). With no limit that is `k =
isize::MAX`, so asking a remote table for its output schema pulled the
whole table over HTTP and threw it away — once per assigned split, on
every epoch, since `StreamingDataset.__iter__` constructs a
`Permutation` per split.
One row rather than zero, deliberately: lance gates its limit node on
`self.limit.unwrap_or(0) > 0`, so `Some(0)` means *no limit*.
### Tables with an LSM write spec are refused
A permutation references rows by row id, and rows that have not been
flushed to the base table do not have one yet. The loader could read
around them, but they would then be missing from training with nothing
said about it, so the build refuses such a table up front instead of
half supporting it.
### Fallible identity construction
`PermutationReader::identity` resolved `inner_new` with `unwrap`. That
was near total against a local dataset, but construction counts the base
table — an HTTP round trip for a remote one — so a transient network or
auth failure became a panic across the PyO3 boundary.
### Tests
End-to-end `permutation_builder` and `StreamingDataset` runs against a
mock server, the former torch-free so it runs wherever the suite does,
plus a test that a build succeeds without an LSM write spec and is
refused once one is installed.
A registered `FunctionVersion` has an exact identity and grouped output
contract, but the Python SDK cannot currently bind it to table columns
without manually constructing wire models.
Calling a `FunctionVersion` with named `col(...)` references now returns
one immutable `FunctionApplication` pinned to that exact version. The
application preserves named-struct outputs as one sibling group, while
`rename(columns=...)` defines the result-field to table-column mapping
consumed by `Table.add_columns`. Derived expressions and incomplete or
unknown input names fail before declaration.
A remote backfill submission validates its target column against a table
snapshot, but it did not carry the existing read-after-write freshness
headers. Immediately after `add_columns`, a stale query node could
therefore reject the newly committed column.
Route backfill submission through the remote table read fence so it
carries the version returned by the preceding write. The shared remote
submission path gives synchronous and asynchronous client surfaces the
same freshness guarantee.
The Windows wheel job is the slowest job in the PyPI release workflow.
Fat LTO of the cdylib is single-threaded and the peak-memory step of the
build, so it does not get faster with more cores — and it has already
caused rustc-LLVM OOM on the Windows runners for the nodejs builds.
Switch the job to thin LTO with 16 codegen units on a
`windows-2025-8x-x64` runner, trading some runtime performance on our
least performance-sensitive platform for build time. This matches what
the nodejs Windows builds in `npm-publish.yml` already do.
`pypi-publish.yml` is in this workflow's `pull_request` paths filter, so
this PR triggers a dry-run build that shows the new timing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The arrow-rs and datafusion crates are released in lockstep, but
Dependabot has been opening one PR per sub-crate for them — the 58.3.0
to 58.4.0 wave produced four separate PRs for `arrow`, `arrow-array`,
`arrow-schema`, and `arrow-buffer`. The existing `rust-minor-patch`
group did not catch them because it only filters on `update-types` and
declares no patterns.
This PR adds an explicit `arrow-datafusion` group matching `arrow*`,
`parquet*`, `datafusion*`, and `object_store`, so those bumps arrive as
a single PR. It is listed before `rust-minor-patch` because a dependency
joins the first group it matches, and it deliberately omits
`update-types` so major bumps are grouped too.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two bugs in Node's reading of the embedding_functions schema metadata.
First, parseFunctions keyed its result map by function name, so a table
whose metadata configures the same function for two vector columns came
back with only the last one. It now keys by the vector column, the
convention Python's parser already uses.
Second, Node could not read metadata written by the Python bindings at
all, which spell the keys snake_case: configs parsed with both columns
undefined, breaking embedding application on add() and leaving only
query-side embedding working. The parse now accepts both spellings.
Both fixes land in one shared parser used by every reader --
parseFunctions and the makeArrowTable schema validator, which had its
own private camelCase-only parse -- so the wire contract cannot fork
between entry points. A config naming no source or vector column is an
error at the boundary rather than a default downstream, as are two
configs claiming one column. The "vector" fallback remains only on the
optional field of user-supplied configs.
Breaking: parseFunctions is exported and its map keys change from
function name to vector column.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remote Function bindings can expose a nullable parameter schema even
when the source table column is non-nullable. Binding validation rebuilt
the exact input schema from table nullability and rejected this safe
widening.
Accept non-null table columns for nullable Function parameters while
continuing to reject nullable table columns for non-null parameters. All
other input schema fields remain exact, including named multi-input
ordering, names, types, and metadata.
Remote Function catalog requests used singular endpoints that are not
exposed by Phalanx. Route registration to `POST /v1/functions/create`
and exact-version lookup to `POST /v1/functions/get`, while preserving
the existing typed Job submission and wait behavior.
As in https://github.com/lancedb/lancedb/pull/3977, we're trying to
reduce anything in the lancedb skill that duplicates other docs. So this
shrinks the branch-ops logic down to a few lines that mostly just point
the agent to fetch the branching docs from lancedb.github.io.
Run stats (2 runs each):
<img width="1001" height="232" alt="Screenshot 2026-08-20 at 5 02 34 PM"
src="https://github.com/user-attachments/assets/a3f4d305-278e-4093-b153-07f0af57b251"
/>
This is out of order, rearranged:
|condition|time (sec)|cost|
|---|---|---|
|No branch_ops.md|250|1.33|
|No branch_ops.md|227|1.26|
|Old branch_ops.md|116|0.83|
|Old branch_ops.md|127|0.87|
|New branch_ops.md|135|0.86|
|New branch_ops.md|147|0.93|
Averaged between each of the two runs:
<img width="775" height="337" alt="Screenshot 2026-08-20 at 5 34 15 PM"
src="https://github.com/user-attachments/assets/4f2fb2a8-3112-4614-87d9-8dbf807f3b75"
/>
It seems helpful to have *some* doc about branching; otherwise the model
gets a little confused about our branch model and what methods to call.
But it looks like the new one (in this PR; all just references to
current docs) is basically as good as the old one (lots of duplicative
text).
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
## Problem
Generic job result propagation exposed the PyO3 representation of Rust's
unit value as `()` in Python. Unit jobs therefore returned an empty
tuple instead of `None`, breaking the documented `Job.wait()` contract
and the Python doctest workflow.
## Behavior
Unit job completion now converts explicitly to Python `None`. Typed job
results continue to pass through unchanged, with synchronous and
asynchronous regression coverage.
Follows lance-format/lance#8680, which removes
`FLAG_MEM_WAL_INDEX_CATCHUP`.
With one set of semantics there is no mode to switch into.
## Removed
`require_mem_wal_index_catchup` — the activation entry point — from the
trait,
from `Table`, and from the LSM merge module.
## The read path
`exclusion_watermarks` loses its `catchup_required` argument and keeps
the
conservative branch: an index with no entry is not known to hold these
rows, so
every generation stays readable from its SSTable. Nothing is excluded
until an
index records that it covers those generations, so a table that has
never
recorded catch-up reads every row from its SSTables rather than assuming
the
base covers them.
## One guard needed a replacement, not deletion
`refresh_column` and computed-column declaration refuse a table whose
rows sit
in un-compacted tiers, since refresh enumerates base fragments and would
silently omit them. They keyed on the feature bit because
`unset_lsm_write_spec` **drops the MemWAL index** — after an unset the
write
spec no longer describes such a table, and the bit was the only marker
that
outlived it. Two tests covered this, so deleting the term would have
dropped a
tested property.
Both guards now check for MemWAL shard directories on storage, which
outlive
the index. That is strictly wider than the bit ever was: the bit only
marked
tables where activation had run.
## Two tests conflated two different things
An index that is *caught up* and one that is *untracked* both fell back
to the
compaction watermark, because absence carried no information without the
bit.
Absence now means "not caught up", so untracked retains everything.
`an_untracked_index_does_not_widen_a_lagging_sibling` becomes
`an_untracked_index_retains_everything`, with the genuinely-caught-up
case
asserted separately.
## Testing
933 `lancedb` lib tests. `cargo fmt` clean. (The pre-existing
`Error::Http`
build failure in `job.rs` without the `remote` feature is unrelated and
untouched.)
The Linux Rust job can exhaust its disk after restoring a large fallback
target cache and compiling multiple feature graphs into one target
directory.
Run remote tests in an independent job with registry-only caching, and
run the simple example with all features so it reuses the preceding
build artifacts. This preserves remote coverage and fork behavior while
preventing all-features and remote-only artifacts from accumulating
together.
Failure evidence:
https://github.com/lancedb/lancedb/actions/runs/32467317540/job/96726650990
## Problem
The canonical Function wire values and typed remote Job contract do not
yet provide a Python authoring surface or catalog client, so users
cannot package a scalar callable, register it, or reopen the exact
immutable Function version.
## Behavior
This adds scalar-only `@udf` authoring with deterministic annotation or
explicit Arrow schema validation, content-addressed Python artifacts,
and an internal scalar-to-Arrow-batch adapter descriptor. Registration
payloads model non-secret environment values and secret names only.
Remote connections can submit `create_function_async` and receive a
typed `Job<FunctionVersion>`, then reopen that exact version by name and
version ID. Synchronous connections can call `create_function` to submit
and wait for the immutable version in one operation. Local Function
catalog operations return a stable `NotSupported` error. Shared
Rust/Python golden payloads and mocked catalog responses freeze the
request, typed terminal result, and exact lookup contract.
## Validation
- Rust formatting, remote check, clippy, and focused LDB-1/LDB-2 tests
- Python formatting, lint, and focused LDB-1/LDB-2 tests
- Python API documentation build
Function applications from the canonical remote contract cannot
currently declare scalar or grouped computed-column outputs atomically.
This adds the remote-only declaration contract for scalar,
struct-as-one-column, and expanded named-struct outputs. It validates
result mappings, fixes exact input/output Arrow schemas in the request,
persists grouped sibling metadata, and keeps local Function execution
unsupported. Unknown newer application or binding metadata remains
readable, while schema-changing mutations fail closed instead of
rewriting it.
Stable Lance field IDs are deliberately not a declaration prerequisite
in this slice. Inputs bind by parameter name and field path; Sophon
remains responsible for exact-version validation, atomic all-NULL
sibling creation, binding identity and revision allocation, and
persisted output identities.
LanceDB's Python SDK now requires Pydantic `>=2.7.4,<3` and uses the v2
APIs throughout. This removes dual-version behavior from schema
conversion, query serialization, embedding models, and Function wire
models while preserving their existing public and canonical-wire
behavior.
The minimum-dependencies CI job pins Pydantic 2.7.4 so the declared
compatibility floor remains covered.
Local benchmarks currently inherit the release profile's fat LTO and
single codegen unit, making local iteration pay release-artifact build
costs.
Provide repository-defined profiles for no-LTO local work and cheaper
benchmark builds, and document when each profile is appropriate. Release
artifacts continue to use fat LTO.
PyO3 exposed `LsmWriteSpec` with its default `builtins` module, causing
mkdocstrings to resolve the public `lancedb.LsmWriteSpec` re-export as
`builtins.LsmWriteSpec` and fail the documentation build. Declare the
native extension module and pin the public re-export with a regression
test.
This also applies the repository's current Ruff formatter to seven
previously unformatted Python scripts.
## Problem
Enterprise Function-backed computed columns need a stable SDK contract
before Sophon catalog and execution endpoints can be added. The existing
`Job` API can only represent unit terminal results, and there is no
shared Rust/Python wire definition for immutable Function versions,
applications, bindings, or refresh results.
## Behavior
This introduces remote-only canonical Function values in Rust and
Python, evolves `Job<T = ()>` to decode typed remote terminal results
while keeping local spawned operations unit-typed, and fixes the
cross-language contract with shared JSON golden fixtures. Unknown fields
and discriminator values remain forward-decodable, while canonical
output contains only fields known to the client. Function models contain
secret names only.
Sophon remains the sole owner of catalog persistence, environment bake,
secret resolution, execution, and publication. This PR does not add
authoring/catalog endpoints, local execution, refresh runners, or live
Sophon E2E coverage.
Background: if we keep adding stuff to the lancedb skill that repeats
other knowledge, we're basically creating a whole new docs site, which
means one more thing that can get out of date. Worse, if it gets out of
date, it will tell agents to do the wrong thing.
These files were added without a ton of analysis of whether they'd be
improving agent performance at all. It looks like they don't really:
<img width="644" height="90" alt="Screenshot 2026-08-20 at 5 21 03 PM"
src="https://github.com/user-attachments/assets/44e60436-b7ad-498b-8e73-0181385c7c60"
/>
(top run is without these docs, bottom run is with them - arguably these
docs might even make the agent a little slower! that's probably noise
though; I'd just say at least they're unnecessary.)
So this PR just removes them. We'll more judiciously add bits we need
and/or point to preexisting docs, to avoid duplication.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
`cargo deny` did not check crate-level dependency declarations against
`[workspace.dependencies]`, so a crate used by both the core crate and
the bindings could be declared independently in each one and drift. For
example `tokio` was pinned at `1.23` in `rust/lancedb` and `1.40` in
`python`, and `pin-project` at `1.0.7` in the workspace table but
`1.1.5` in `python`.
This PR turns on cargo-deny's `bans.workspace-dependencies` lint, which
fails when a dependency is used by more than one member without going
through `workspace = true`, and when a `[workspace.dependencies]` entry
is used by nobody.
Enabling it surfaced 12 violations. Fixing them means adding `bytes`,
`lancedb`, `serde`, `serde_json`, `tempfile`, `tokio`, and `uuid` to
`[workspace.dependencies]`, and pointing the `arrow`, `arrow-buffer`,
`async-trait`, `chrono`, and `pin-project` declarations at the entries
that already existed. `Cargo.lock` is unchanged, so resolution is the
same as before.
The shared `chrono` entry now carries `default-features = false,
features = ["clock"]`, matching what `nodejs` and `python` already asked
for — cargo ignores a member's `default-features = false` unless the
workspace entry sets it too. On the targets we build, `clock` covers
everything `rust/lancedb` was getting from chrono's defaults.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
<!-- lance-gatekeeper-fix:v1 agent=40e5cf476a59265c71653574eda834d2
generation=1 -->
## Summary
- preserve incoming PyArrow `arrow.json` fields while schema
sanitization aligns input to a stored `lance.json` schema
- let Lance perform the required JSONB encoding instead of relabeling
raw JSON bytes as encoded storage
- cover both merge insert and the conditional add sanitization path with
end-to-end regression tests
## Root cause
Python schema sanitization aligns incoming data to the table schema
before passing it to Lance. Merge insert always takes this path, while
add takes it conditionally for preprocessing such as non-default
bad-vector handling or embedding functions. For JSON columns, the cast
changed logical `arrow.json` strings into the table's JSONB-backed
`lance.json` storage type without encoding the bytes, so Lance treated
raw JSON text as JSONB.
## Validation
- `cd python && uv run --extra tests pytest python/tests/test_table.py
-k 'merge_insert or add_sanitization_encodes_json' -q`
- targeted schema-cast and JSON encoding tests
- `ruff check .`
- `ruff format --check python/python/lancedb/table.py
python/python/tests/test_table.py`
Fixes#3923
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Why
Four of the eight LSM methods are **remote-only in the core**. `impl
BaseTable for NativeTable` implements only
`set`/`unset`/`get_lsm_write_spec` and `close_lsm_writers`; `flush_lsm`,
`compact_lsm` and `get_lsm_stats` fall through to trait defaults
returning `NotSupported` (`rust/lancedb/src/table.rs:679,687,696`), and
`checkpoint_lsm` is built on all three.
That explains the state of the bindings: Node had bound the four that
work against a local table and stopped, so a Cloud user could install an
LSM write spec but had no way to observe fresh-tier state or drive a
checkpoint. Java had none of it at all.
| SDK | set/unset/get spec | closeWriters | flush | compact | getStats |
checkpoint |
|---|---|---|---|---|---|---|
| Rust core | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Python | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Node *(before)* | ✅ | ✅ | — | — | — | — |
| **Node (after)** | ✅ | ✅ | **new** | **new** | **new** | **new** |
| Java *(before)* | — | — | — | — | — | — |
| **Java (after)** | **new** | n/a | **new** | **new** | **new** |
**new** |
Go and C are separate repos and are out of scope here. `closeLsmWriters`
drains cached in-process shard writers, so it has no meaning for Java,
which is a pure REST client.
## Node
Adds napi bindings for `flushLsm`, `compactLsm`, `checkpointLsm` and
`getLsmStats`, plus typed `LsmStats` / `BucketStats` / `GenerationStats`
/ `MemtableStats` objects — typed rather than a JSON blob, matching the
existing `LsmWriteSpec` object in the same file, with `u64` cast to
`i64` per that file's convention.
Because these four are remote-only, the new tests assert each binding
reaches the core and surfaces `NotSupported` against a local table. That
covers the wiring; behavior against a real endpoint stays covered by the
mocked-endpoint tests in `rust/lancedb/src/remote/table.rs`.
## Python
No new methods. All eight are on `LanceTable`, `AsyncTable` and
`RemoteTable` — the last four landed on the sync `RemoteTable` in #3961,
which is merged into this branch.
What was missing here was reachability. `LsmWriteSpec` was importable
only from the private `lancedb._lancedb`, appearing in `table.py` solely
under `if TYPE_CHECKING:`, and `docs/src/python/python.md` had no
mention of it, which per the repo's docs guidance means it rendered
nowhere in the API reference. It is now `lancedb.LsmWriteSpec`, in
`__all__`, and documented.
## Java
Java reaches LanceDB purely over REST through the generated Lance
Namespace client, and these routes are not in that spec, so they are
issued through a small dedicated client rather than added to the spec.
That call is revisitable — LSM is one of four unspecified route families
alongside `multipart_write`, `page_cache/prewarm` and
`branches/diff|merge`. If those are ever regularized into the spec as a
group, `LanceDbTableLsm` is one file that gets deleted.
`LsmWriteSpec` here is deliberately **not**
`org.lance.memwal.InitializeMemWalParams`. That type defaults to
maintaining *no* indexes where a spec here defaults to maintaining
*every* index, and it cannot express the `null` that asks the server to
resolve the set:
| Value | On the wire | Meaning |
|---|---|---|
| unset (null) | `null` | Server resolves **every** maintainable index |
| `Collections.emptyList()` | `[]` | Maintain **none** |
| `Arrays.asList("id_idx")` | `["id_idx"]` | Exactly those |
A dedicated test pins null and `[]` as distinct on the wire, since
collapsing them is the failure mode that motivated a LanceDB-owned type.
`checkpointLsm` is ported from `rust/lancedb/src/table/checkpoint.rs`
with its constants and status semantics intact: 429/503 retried in place
against an 8-budget, 421 restarting from flush against a 3-budget, 5s
poll, and a target watermark fixed after the seal so it terminates under
write load.
`getLsmStats` returns typed `LsmStats` / `BucketStats` /
`GenerationStats` / `MemtableStats`, mirroring the Rust structs in
`rust/lancedb/src/table/lsm_stats.rs` and the objects Node exposes.
Decoding is strict — see below.
## Review feedback
Both gatekeeper findings were real. Each was reproduced against the
scripted test server first, and each fix ships with the reproducer as a
regression test.
**The transport was doubling every checkpoint retry budget.**
`HttpClients.createDefault()` installs Apache's default response retry
strategy, whose retryable-status list is exactly 429 and 503 — the two
statuses `isRetryable` owns. A 429 held against `flush_lsm` issued
**18** wire requests where the loop intends 9, and `compact_lsm` was
retried in place despite the loop being built to fall through to a fresh
stats poll instead. Timing confirmed the mechanism: that run took 25.4s
≈ 16.3s of the loop's own backoff plus 9 × the transport's 1s retry
interval.
Automatic retries are now disabled, so the checkpoint loop is the sole
owner of the 421/429/503 transitions. A side effect worth noting:
`testCheckpointRetriesRetryableStatusInPlace` was passing on a
transport-absorbed 429 and never reaching `issue()`'s retry branch at
all. It now exercises the real path.
**Stats decoding failed open.** `getLsmStats` read the response with
Jackson's `path()`, which yields a missing node that iterates as an
empty array — making "malformed" indistinguishable from "no buckets",
which is indistinguishable from "drained". Four separate payloads made
`checkpointLsm()` report convergence for a checkpoint that never ran:
| Response | Before | Now |
|---|---|---|
| `{"lsm_stats": null}` or absent key | disabled ✓ | disabled ✓ |
| `{"lsm_stats": {}}` | **reported success** | `IllegalStateException` |
| empty response body | **reported success** | `IllegalStateException` |
| bucket missing required fields | **reported success** |
`IllegalStateException` |
The empty-body row is the one to weight: a proxy 200 with no body is a
realistic production event, and it silently reported a checkpoint that
never happened.
Decoding is now strict and fails closed, matching the serde contract on
the Rust side exactly. One deliberate deviation from the review comment,
which asked that *only* explicit JSON `null` count as disabled: Rust has
`#[serde(default)]` on `lsm_stats`, so an **absent key** decodes to
`None` there too. Java now matches that. It is an absent-or-malformed
**`buckets`** that fails closed, which is the case the comment was
actually protecting.
## Testing
- Java: **33 passing** (8 existing + 25 LSM) against a scripted
`com.sun.net.httpserver.HttpServer` — no new test dependency. Wire
assertions mirror `rust/lancedb/src/remote/table.rs:6581-6748`;
checkpoint tests cover convergence, not piling onto a latched bucket,
421 restart-from-flush, 429 retry-in-place, terminal-status propagation,
reissue exhaustion, the exact wire-request count against the retry
budget, and five malformed stats payloads.
- Node: **19 LSM tests passing**; `cargo check`, `npm run build`, `npm
run tsc`, `npm run lint`, `npm run docs` all clean.
- Python: `ruff format --check` and `ruff check` clean.
- Java formatting: `./mvnw -pl lancedb-core spotless:apply` and
`spotless:check` both clean under a JDK 11 toolchain.
## Note: spotless needs a pre-16 JDK
`./mvnw spotless:apply` fails on JDK 16+ with
`JCTree$JCImport.getQualifiedIdentifier()` — google-java-format 1.7,
pinned at `java/pom.xml:34`, predates JDK 16's compiler API change.
**This is pre-existing** and reproduces on a pristine `main` checkout.
It is not a blocker, just a toolchain requirement. Spotless was run
against these sources under JDK 11 and both `spotless:apply` and
`spotless:check` pass on the whole module:
```shell
JAVA_HOME=/path/to/jdk11 ./mvnw -pl lancedb-core spotless:apply
```
Bumping the plugin so it works on modern JDKs is still worth doing, but
separately from this PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Summary
The sync `RemoteTable` carried `set_lsm_write_spec`,
`unset_lsm_write_spec`, `get_lsm_write_spec`, and `close_lsm_writers`,
but not `checkpoint_lsm`, `flush_lsm`, `compact_lsm`, or
`get_lsm_stats`.
That left the four LSM control methods reachable from `AsyncTable` only.
They are also the four that *only* work against a remote table —
`NativeTable` does not override the `BaseTable` defaults, so on a local
table they return `NotSupported` (`rust/lancedb/src/table.rs:679-701`).
The net effect for sync users:
| | `checkpoint_lsm` / `get_lsm_stats` |
|---|---|
| `LanceTable` (sync, local) | present, but always `NotSupported` |
| `RemoteTable` (sync, remote) | `AttributeError` — method absent |
| `AsyncTable` (remote) | works |
So there was no working sync path at all, despite the Rust `RemoteTable`
implementing every one of these against real endpoints.
## Changes
* Add `checkpoint_lsm`, `flush_lsm`, `compact_lsm`, and `get_lsm_stats`
to `lancedb.remote.table.RemoteTable`, mirroring the delegation style of
their neighbours.
* Correct the docstrings on `set_lsm_write_spec` /
`unset_lsm_write_spec`, which read `"""Not supported on LanceDB
Cloud."""` although `rust/lancedb/src/remote/table.rs:2549-2601`
implements both against `/v1/table/{}/set_lsm_write_spec/` and
`/unset_lsm_write_spec/`. They appear to have been copy-pasted from
`set_unenforced_primary_key` directly above.
No Rust or PyO3 changes — the bindings and the `AsyncTable` methods
already existed. The `Table` ABC is left alone, matching how the
existing `*_lsm_write_spec` methods are declared on the concrete classes
only.
## Tests
Four new tests in `python/python/tests/test_remote_db.py`, against the
existing mock HTTP server:
* `test_get_lsm_stats_sync` — the server payload round-trips into the
dict, and `include_generation_rows` defaults to `False` and is forwarded
when set.
* `test_get_lsm_stats_sync_returns_none_when_lsm_disabled` — a
`{"lsm_stats": null}` envelope yields `None` rather than an error.
* `test_flush_and_compact_lsm_sync` — both are one-shot POSTs answered
`202` with no body.
* `test_checkpoint_lsm_sync` — pins the binding to the endpoints it
drives (`flush_lsm` then `get_lsm_stats`); the convergence loop itself
is already covered in Rust.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Context
Java users opening catalog-backed tables with vended credentials
currently lack a documented workflow. Opening the catalog-returned URI
directly drops the namespace-provided storage options and automatic
credential refresh.
Document the namespace-backed `Dataset.open()` path so temporary object
store credentials are applied and refreshed transparently.
## Summary
`LanceHybridQueryBuilder` (sync hybrid search,
`table.search(query_type="hybrid")`) silently ignored `.offset()`.
`self._offset` was never forwarded to the vector/FTS sub-queries and
never applied when slicing the final combined/reranked result, so
`.offset(N)` behaved identically to `.offset(0)` — no error, just wrong
pagination.
Fixes#3765
## Changes
- `_create_query_builders()`: each sub-query now fetches `limit +
offset` rows so there's enough data to slice the correct window out of
after combining/reranking.
- `_combine_hybrid_results()` / `to_arrow()`: the final table is sliced
with `offset=self._offset` instead of always starting at 0.
## Test plan
- [x] New regression test `test_hybrid_query_offset` in
`python/python/tests/test_hybrid_query.py`
- [x] `uv run --extra tests pytest python/tests/test_hybrid_query.py
-vv` — 13 passed
- [x] `uv run --extra dev ruff format` / `ruff check` — clean
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Will Jones <willjones127@gmail.com>
## Summary
Fixes naive `lit(datetime)` equality filters against table timestamp
columns on non-UTC hosts, and adds the integration matrix from #3262.
## Failure (before)
On a machine in US Eastern (UTC−4 / EDT), with PyPI `lancedb==0.36.0`:
```python
from datetime import datetime
import lancedb
from lancedb.expr import col, lit
db = lancedb.connect("memory://")
ts = datetime(2024, 7, 1, 10, 0, 0) # naive
table = db.create_table("t", [{"id": 1, "ts": ts}])
rows = table.search().where(col("ts") == lit(ts)).to_list()
# actual: [] (0 rows)
# expected: 1 row
```
### Root cause
In `python/src/expr.rs`, `expr_lit` converted every `datetime` via
Python's `.timestamp()`:
- **naive** `.timestamp()` = local wall → UTC epoch (shifted by host
offset)
- **PyArrow naive** storage = UTC wall-clock microseconds (no local
shift)
So `lit(naive)` became `CAST('2024-07-01 14:00:00' AS TIMESTAMP)` on EDT
while the table held `10:00:00`.
## After
Naive datetimes are interpreted as UTC wall clock
(`replace(tzinfo=timezone.utc).timestamp()`), matching Arrow storage.
Aware datetimes still use `.timestamp()` (correct epoch).
Same repro on this branch: **1 matching row**.
## Tests
Added `TestExprDatetimeTimezoneIntegration` covering:
| Case | Result |
|------|--------|
| both naive | match |
| both same TZ (UTC) | match |
| different TZs, same instant | match |
| table TZ + naive lit | match (wall clock) |
| table naive + aware lit | match |
| naive lit SQL is wall clock, not local-shifted | asserts `10:00:00` in
SQL |
### Verification
```bash
cd python
maturin develop
pytest python/tests/test_expr.py -v
```
**102 passed** (full `test_expr.py`, including the 6 new cases).
Closes#3262
---------
Co-authored-by: Will Jones <willjones127@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
LanceDB Cloud and Enterprise support computed columns through the REST
API,
so declaration dispatches per backend: local tables plan the expression
themselves, remote ones send {name, computed} entries for the server to
plan. A remote refresh is the server's backfill job --
refresh_column_async
submits it and returns a handle whose successful wait establishes a
read-freshness baseline on the submitting handle, unless a checkout has
pinned the handle by the time the job completes; the blocking form
refuses
rather than invent a fill count the server does not report.
Declaration entries are built from the namespace client's
AddColumnsEntry
model (lance-namespace 0.11.0, via the lance beta.13 pin), so the
payload
shape is compile-checked against the published contract.
---
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
Updates the Lance Rust workspace dependencies and Java lance-core
dependency to
[v11.0.0-beta.13](https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.13).
Adds the required `ListTablesResponse.context` compatibility field and
validates the workspace with Clippy warnings denied.
Mirrors create_index's dual surface: the blocking refresh_column keeps
returning {rows_filled, version}, and refresh_column_async returns the
same
Job handle create_index uses, running the refresh as an in-process task.
Invalid input is reported by the submitting call rather than by the job.
---
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
table.refresh_column("doubled") fills the rows of a declared column that
hold no value, in two passes per fragment: the first scans only the
unfilled
live rows to count exact gains and decide staging, the second streams
the
fragment's physical rows into a standalone column file published in one
DataReplacement -- committed under the dataset's own session -- so peak
memory is bounded by a scan batch. A row that holds a value keeps it;
deleted and already-filled rows never reach the expression, so a poison
value in them cannot fail the refresh. Refresh refuses under an LSM
write
spec, including the mem-wal catch-up flag that outlives unset and marks
retained SSTable rows.
---
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
add_columns().computed("doubled", "x * 2") stores the expression in
field
metadata and commits the column empty; a later refresh fills it. Type
and
inputs are derived from the expression.
The declaration stays authoritative for its lifetime: writes that would
give
the column a value (append, update, merge, SQL insert), schema changes
that
would break the stored expression or reshape its output, metadata edits,
volatile expressions, declaration metadata arriving through any path but
the
validated declare call, and LSM write specs in either order against
latest
committed state are all refused. The LSM check also refuses on the
mem-wal
catch-up feature flag, which outlives unset and marks retained SSTable
rows.
Simultaneous declare/install interleavings conflict at commit via
lance's
mem-wal rule (lance#8539). Local tables only.
---
<sub>Stack created with <a
href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a
href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
> Stacked on #3780. Blocked only on #3922 (`lance` → `v11.0.0-beta.6`),
so CI
> stays red until that lands.
## Missing coverage must mean "not known to be covered"
#3780 caps the SSTable exclusion watermark at an index's recorded
catch-up when
there is one, and silently ignores the case where there is none. On a
table that
requires catch-up, an absent entry means the index is *not* known to
hold the
compacted rows — and the LSM base arm reads base through the index
(`fast_search`, no brute-force tail), so dropping that SSTable loses
those rows
for that query.
```rust
Some(caught_up) => watermark = watermark.min(caught_up),
None if catchup_required => watermark = 0, // retain everything
None => {}
```
`catchup_required` reads the manifest feature bit directly, and requires
both
words: a half-set manifest is treated as legacy, which is the
conservative side.
Without the bit the field is not maintained at all, so absence carries
no
information and behaviour is unchanged.
## Activation, as a table-level entry point
`Table::require_mem_wal_index_catchup()` performs the one-way switch,
separate
from `set_lsm_write_spec`: a table carrying the bit retains every
generation
until something records catch-up, so it has to follow the deployment of
whatever
repairs coverage, not the creation of the table.
This is a convenience, not the only path — a writer holding the dataset
calls
the equivalent on `DatasetMemWalExt`, which is what the WAL pod does.
Lance
enforces the preconditions either way: the MemWAL index must exist, and
the
table must not already carry `compacted_sstables` from before this
protocol,
since those numbers cannot be validated.
## Still correct after the Lance rework
lance-format/lance#8481 replaced the transmitted `IndexCatchupAdvance`
with a
position derived at commit time from the version a transaction read.
That
changed how a writer earns coverage; it did not change what a reader may
conclude from its absence. The rule here, and the field it reads, are
unchanged.
## Tests
Existing `exclusion_watermarks` unit tests carry the new argument.
Coverage
against a real dataset follows once #3922 lands and this can build.
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v11.0.0-beta.10. No compatibility fixes were required;
workspace clippy with all features and Rust formatting pass.
Lance tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.10
## Summary
- add `drop_table_async` and return a job handle while preserving
`drop_table`
- consume remote 202 responses with cleanup job IDs and retain
older-server compatibility
- expose the API through Python and TypeScript connection wrappers
Say what resolving "latest" actually does: pick the newest release,
preferring stable over pre-release, and skip the run if it is not newer
than the version pinned in Cargo.toml.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`exclusion_watermarks` resolved a single index and capped SSTable
exclusion at that index's catch-up watermark. It now takes every index
the query relies on and retains to the **lowest** of them, and the
resolver collects arms together rather than returning at the first
match.
This is groundwork, not a fix for a reachable bug: `reject_unsupported`
refuses hybrid search, so the vector and full-text arms are mutually
exclusive and the list never holds more than one entry today. The
generalisation is what the remaining work below plugs into.
Unchanged: a plain scan uses the compaction watermark alone, an index
with no catch-up entry contributes no cap, and a caught-up index falls
back to the compaction watermark. Taking a minimum over more indexes can
only lower a watermark, so the failure direction is "read an SSTable
unnecessarily", never "miss rows".
## Tests
Three in `lsm`: the existing lagging-index test updated for the new
signature;
`exclusion_watermark_takes_the_minimum_across_every_index_used` (two
indexes at 7 and 4 against compaction at 9 — each alone stops at its own
watermark, together the lower governs, order-independent); and
`an_untracked_index_does_not_widen_a_lagging_sibling`.
`cargo test -p lancedb --lib` — 45 lsm tests, 484 in the crate. `cargo
fmt --check` clean.
## Follow-ups
This crate pins lance to a released tag, so anything needing unreleased
Lance symbols waits for a bump.
1. **Select legacy versus strict semantics from the feature bit.** On a
table with `FLAG_MEM_WAL_INDEX_CATCHUP` set, a *missing* entry must mean
"not caught up" and retain the SSTables, instead of leaving the
compaction watermark unchanged. Needs the bit from
lance-format/lance#8263. **This must land before any table is
activated** — otherwise the bit is set while queries still read
permissively.
2. **Collect scalar and bitmap-family prefilter indexes.** The genuinely
multi-index query is a vector search with a scalar prefilter, and it is
gated on the vector index alone today. Identifying the others needs the
planner's chosen indexes, not the columns the filter names, so it needs
a Lance-side helper.
3. **Verify a retained SSTable can actually answer.** Both base and
SSTable arms use `fast_search`; a source without a compatible index
contributes nothing, so retention alone does not guarantee its rows are
returned. Needs a flat-search fallback or an explicit error in Lance's
`LsmScanner`.
4. **Planner-level integration tests.** Current tests exercise the
watermark arithmetic directly. End-to-end coverage over real queries —
prefilter forms, legacy versus activated, missing index and missing
shard entries — depends on 1–3.
## What is the bug?
#3731 tries to distinguish a missing table from a corrupt table after
Lance returns `DatasetNotFound`. It does that by listing the database
parent and treating a physical `<name>.lance` entry as evidence that the
table exists.
That premise is not sound for a listing database. Table creation writes
data before atomically committing the first manifest, so the same
physical prefix can represent a live concurrent create, abandoned
uncommitted data, or an old empty directory. It is not evidence of a
committed table. The parent listing also makes every missing-table open,
including the create-on-miss path, perform work proportional to the
number of sibling tables. Cloud `list_with_delimiter` exhausts all pages
before returning.
## How does this PR fix the problem?
This PR makes the committed Lance manifest the sole table-existence
authority for listing-database opens:
- `DatasetNotFound` maps directly to `TableNotFound`; no parent or
target storage probe runs.
- Other Lance load errors continue to propagate unchanged.
- A physical directory, object prefix, or uncommitted data file alone
does not block `Create`.
- Concurrent `Create` requests are arbitrated by the conditional
version-1 manifest commit: one succeeds and the loser receives
`TableAlreadyExists`.
- `table_names` is documented as physical discovery, not an atomic
table-existence check. Its snapshot can contain an entry that is still
being created, has only uncommitted storage, or is concurrently dropped.
This removes the need for a new Lance object-store capability. LanceDB
remains on the official Lance `v11.0.0-beta.6` dependency from `main`;
the merge commit for lance-format/lance#7722 is an ancestor of that tag,
so the ambiguous-GCS-500 corruption-prevention fix is retained.
## Performance evidence
Lower is better. The benchmark uses real `.lance` directories with
marker objects on the local filesystem; fixture creation and teardown
are outside the timed region. Baseline is `origin/main` at `6fb976cf`,
candidate is `e1240751`. Both were built from the same lockfile on the
same macOS arm64 machine with the repository's `release` profile (fat
LTO), then executed in alternating baseline/candidate order for three
pairs. Each run used 10 warmups and 100 distinct missing-table opens per
scale. The table reports the median of the three run-level percentiles.
| Scenario / metric | Baseline | This PR | Benefit |
| --- | ---: | ---: | ---: |
| 1,000 real sibling directories, p50 | 11.905 ms | 21.042 us | 566x
speedup |
| 10,000 real sibling directories, p50 | 143.630 ms | 18.375 us | 7,817x
speedup |
| 100,000 real sibling directories, p50 | 1.991 s | 19.917 us | 99,984x
speedup |
| 100,000 real sibling directories, p95 | 2.346 s | 25.792 us | 90,965x
speedup |
These results validate removal of the sibling-cardinality dependency in
this local-filesystem workload; they are not an extrapolation to
production GCS latency. A structural object-store regression test
separately asserts that opening one missing table performs zero
parent-scoped `list`, `list_with_offset`, or `list_with_delimiter`
calls.
Run with:
```bash
BENCH_SIBLINGS=1000,10000,100000 BENCH_WARMUPS=10 BENCH_TRIALS=100 \
cargo run --locked --release --quiet -p lancedb --example bench_open_missing_table
```
## Correctness and compatibility boundaries
- An empty `.lance` directory or orphan data without a committed
manifest now opens as `TableNotFound` and may be replaced by a
successful `Create`.
- Two synchronized creators sharing one object store deterministically
produce one success and one conditional-manifest conflict mapped to
`TableAlreadyExists`.
- A readable manifest remains authoritative; non-`DatasetNotFound`
corruption, external-manifest, authorization, and object-store errors
are not folded into `TableNotFound`.
- `TableCorrupted` remains in the public error enum for compatibility,
but this listing-database fallback no longer synthesizes it from an
ambiguous physical footprint.
- Reliably distinguishing `Missing`, `Creating`, and `Corrupt` would
require explicit authoritative lifecycle/catalog metadata (for example a
leased creation record). It cannot be inferred from a directory or
prefix, and is outside this incident fix.
## Validation
- `cargo fmt --all -- --check`
- `cargo check --quiet --locked -p lancedb --features remote --tests
--examples`
- `cargo clippy --quiet --locked -p lancedb --features remote --tests
--examples -- -D warnings`
- `cargo test --quiet --locked -p lancedb --features remote --tests`
- library: 843 passed, 1 ignored
- integration groups: 39 passed, 6 passed, 5 passed
- focused coverage for empty directories, orphan data, physical listing
snapshots, zero parent listings, and concurrent manifest arbitration
Closes#3704
## Problem
Transforms can fail on bad data (e.g. nulls/NaNs from incomplete user
surveys). Today any transform exception aborts iteration, and there is
no way to skip invalid rows during loading.
## Solution
New `on_transform_error` parameter on `StreamingDataset`:
- `"raise"` (default, matches current behavior and the convention in
tf.data / WebDataset / Ray Data)
- `"skip"` — drop the failing rows and continue
- `"warn"` — like skip, plus a logged warning per failing batch
- a WebDataset-style callable `handler(exc) -> bool`, so users can skip
only expected error types
Key design points:
- **Row-granular skipping**: when a batch fails, the transform is re-run
on single-row slices so only the rows that actually fail are dropped
(avoids Ray-style whole-block loss). Skips are counted in a new
`rows_skipped` property.
- **No crash on uneven skips**: the round-robin loop now ends the epoch
at the last cycle where every split still has a row, instead of hitting
`IndexError` when a split runs dry early.
- **Exact resumability under skips**: checkpoints are now
position-based. `state_dict` gains `positions_consumed_per_split` (exact
for owned splits), and a new `merge_state_dicts` static method combines
per-rank states via elementwise max for elastic resume across topology
changes. Old checkpoints without the new key still load. Positions equal
sample counts when nothing is skipped, so existing behavior is
unchanged.
- **Guardrail**: transforms returning the wrong number of rows now raise
a clear `ValueError` instead of silently corrupting split accounting.
### Answers to the issue's open questions
- *Can we do this?* Yes — all transforms funnel through one guarded call
in the Stage 2 pipeline.
- *What do other libraries do?* tf.data `ignore_errors()`, WebDataset
`handler=`, Ray `max_errored_blocks`; MosaicML StreamingDataset offers
nothing (skipping conflicts with its determinism model). This design
follows the common conventions: raise by default, opt-in skipping,
count/log drops.
- *Error handling or pre-filtering?* Both: the existing `filter=`
remains the recommended tool for predictable bad data (splits are built
post-filter, so all guarantees hold — now documented);
`on_transform_error` covers failures not expressible as a predicate.
- *Impact on splits / elastic determinism?* Per-split sample sequences
stay deterministic (skips are data-dependent, not topology-dependent).
With unequal bad-row counts across splits the last few global steps of
an epoch can differ across topologies (bounded by the skew), which is
documented on the parameter. With equal counts per split, full
determinism is preserved — covered by a test.
## Testing
15 new tests in `test_elastic_dataloader.py` covering: default raise,
invalid values, uniform and uneven skips (including epoch-end
truncation), warn logging, selective callable handlers, wrong-row-count
guardrail, determinism across runs and across world sizes (1/2/3/4) with
skips, exact mid-epoch resume with skips on the same topology, elastic
resume via `merge_state_dicts` (ws=2 → ws=1), merge validation, and
backward-compat loading of old checkpoints.
Note: relying on CI for the test run — my local machine OOMs during the
final link of the native extension. The change itself is pure Python.
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
PyO3 defaults native extension classes to `builtins`, so
mkdocstrings/Griffe could not resolve the newly documented
`lancedb.Session` alias and `Deploy docs to Pages` failed on `main`.
Declare the extension module for the public native types referenced by
the Python API docs so Griffe resolves them through `lancedb._lancedb`
and Pages can build again.
Validated with the docs toolchain used by CI (`griffe==0.49.0`,
`mkdocstrings==0.25.2`, and `mkdocs==1.6.1`); `PYTHONPATH=. mkdocs
build` succeeds.
## Summary
`opendal 0.58.1` (the version pulled in transitively via Lance) already
ships
`goosefs-sdk 0.1.9`, which includes the upstream fix for the 0.1.6
compile
break. The explicit version pin that lancedb has been carrying since the
GooseFS feature was introduced is therefore no longer necessary and is
now
redundant work to maintain.
## Changes
- Remove the direct `goosefs-sdk` dependency from
`rust/lancedb/Cargo.toml`
(it was pinned to `=0.1.9` with a comment referencing the 0.1.6 compile
break).
- Remove the `dep:goosefs-sdk` entry from the `goosefs` cargo feature,
since
no source file in lancedb imports the crate directly.
- Refresh `Cargo.lock`; `goosefs-sdk 0.1.9` now resolves transitively
through
`lance` → `opendal 0.58.1`.
## Verification
- `cargo fmt --all` — clean
- `cargo check --features remote,goosefs --tests --examples` — passes
- `Cargo.lock` confirms `goosefs-sdk 0.1.9` is still resolved (now
transitively), so the `goosefs` feature continues to enable the same set
of
Lance/IOPaths as before.
## Backwards compatibility
No public API changes. The `goosefs` cargo feature still activates
`lance/goosefs`, `lance-io/goosefs`, and
`lance-namespace-impls/dir-goosefs`,
and the same `goosefs-sdk 0.1.9` version is selected by the resolver.
<!-- lance-gatekeeper-fix:v1 agent=613a074d606e626c5169d601373a32d8
generation=1 -->
## Root cause
When LanceDB accepted an Arrow table created by a different installed
Arrow package, its compatibility sanitizer rebuilt each Data node
without converting the foreign type or preserving nested children. It
also dropped the separate dictionary vector payload and did not preserve
identity shared by dictionary schema types, vector wrappers, or growing
dictionary chunks.
## Fix
Recursively sanitize nested Arrow data types and child data. Use one
table-scoped sanitization context to rebuild and memoize source type
objects, dictionary vectors, and Data nodes in the local Arrow realm,
preserving all identities required by Arrow IPC.
Add Arrow 15 through 18 regressions for list serialization, ordinary
dictionaries, dictionaries shared across fields and batches, growing
dictionaries, and IPC round trips.
## Validation
- pnpm test __test__/arrow.test.ts --runInBand (188 passed)
- pnpm lint
- pnpm build
- pnpm test --runInBand (706 passed, 5 skipped)
- pnpm run docs
Fixes#2256
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Some issues:
- file_size_bytes is optional in the manifest, so if it's not there (old
writer I guess) it'll under-report the table size.
- it changes results a little bit from the old way by including per-file
footers and metadata (probably not a big difference at real scale)
---------
Co-authored-by: Will Jones <willjones127@gmail.com>
## What
`JinaEmbeddings._generate_image_input_dict()` crashes with
`AttributeError: 'function' object has no attribute 'urlparse'` on any
image given as a URL string, local path string, or `pathlib.Path` — i.e.
every documented `jina-clip-v1` image-embedding use case except raw
`bytes`.
## Why
```python
from urllib.parse import urlparse
...
parsed = urlparse.urlparse(image)
```
`urlparse` is imported as a function, then called as if it were the
`urllib.parse` module (`urlparse.urlparse(...)`). The module-level
`is_valid_url()` a few lines above does it correctly (`urlparse(text)`),
which is why this reads as a typo rather than intentional. Fixed to
`urlparse(str(image))` — `str()` is needed because `urlparse()` only
accepts `str`/`bytes` and raises a different `AttributeError` on a raw
`Path`.
## Testing
Added `test_jina_generate_image_input_dict_local_path`, which fails with
the original `AttributeError` before the fix and passes after, covering
both a `str` path and a `pathlib.Path`. Verified locally (built the Rust
extension, ran red→green, then the full `test_embeddings.py` file: 15
passed / 8 skipped, no regressions) and with `ruff check`/`ruff format`.
---
Disclosure: this PR was drafted with AI assistance (Claude); I reviewed,
tested, and take responsibility for the change.
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## What
`LsmWriteSpec::maintained_indexes` becomes `Option<Vec<String>>`:
| value | meaning |
|---|---|
| `None` (new default) | every index the MemWAL supports, resolved when
the spec is installed |
| `Some([])` | maintain nothing — a scan/filter-only WAL table |
| `Some([..])` | exactly these, taken verbatim |
`with_maintained_indexes` keeps its signature;
`with_no_maintained_indexes()` is new. Surfaced through the remote path
(null on the wire), Python, and Node.
## Why
Callers had to state the maintained set by hand every time, which is
both tedious and easy to get wrong — the common case is "maintain what I
already built."
Resolution filters on `IndexConfig::is_memwal_maintainable`, delegating
to lance's `is_maintainable_index_type`. This is load-bearing rather
than cosmetic: lance does **not** skip an index type its memtable cannot
build, it errors when the shard writer opens, so sweeping up a bitmap
index would fail every memtable claim and leave the table unwritable.
The inferred set excludes those, and an explicit list naming one is now
rejected at spec time instead of at claim time.
## Behavior change
A freshly constructed spec used to maintain **nothing**; it now
maintains **everything supported**. This flipped because napi collapses
`undefined` and `null` to `None`, so TypeScript cannot express "absent
means nothing, null means all" — any other choice makes the bindings
disagree with the wire. The error direction also favors it: an unwanted
maintained index costs memory, while a silently unmaintained one
degrades FTS to an unscored scan.
Three existing tests encoded the old default and are updated rather than
worked around.
## Caveat
The resolved set is a snapshot, not a subscription. An index created
after the spec is installed is not maintained until the spec is unset
and set again. `get_lsm_write_spec` therefore always reports a concrete
list — `None` never round-trips.
## Dependency
Needs a lance release carrying `is_maintainable_index_type`
(lance-format/lance#8095) before this builds against the pinned tag.
Draft until then.
## Testing
38 Rust LSM tests and 10 Python tests pass against a local lance build,
including new coverage that a bitmap index is excluded from inference
and rejected when named, and that `[]` stays distinguishable from null
on the wire.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v11.0.0-beta.3. No compatibility fixes were required;
all-features clippy and Rust formatting pass. Triggering tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.3
Converge a table's LSM write path into its base table, and inspect it.
`checkpoint_lsm` is `flush` then `compact`, repeated until the fresh
tier is empty — and the loop runs **client-side**. Putting it on the
server would mean a background task, which means a single-flight intent,
an intent that leaks on panic, a bounded-iteration policy, an "is it
done" observable, and a story for every way a client can vanish
mid-operation. None of that exists in this shape: each request does a
bounded unit of work and reports what is left, so completion is *carried
in the responses* rather than inferred from a shared counter that cannot
distinguish "converged" from "hasn't started yet".
Best-effort by construction. Nothing is frozen, so `converged` means L0
was empty as of the last pass. It is idempotent, abandonable at any
point with zero consequence, and safe to run on a cadence — an
already-converged table costs one round trip and zero compaction passes,
because `flush` reports `generations_remaining` and the loop is never
entered.
## The failure taxonomy is the load-bearing part
Five distinct conditions used to arrive at a client as one 503.
`Error::LsmRoute` carries a classification read from the response body's
namespace error code **at the point of receipt** — before any generic
helper folds the body into a string and keeps only the status.
| condition | wire | client action |
|---|---|---|
| contention (latch held / pool saturated) | 429, code 21 | retry with
backoff |
| owning node draining | 503, code 19 `InvalidTableState` | **stop** |
| fenced / no slot / transport | 503, code 17 | retry with backoff |
| registry entry vanished | 404 | re-issue from `flush` (capped) |
| table being dropped / not WAL-backed | 409 / 400 | stop |
Draining is terminal because the drain gate is a one-way latch —
retrying spins until the deadline to report a failure that was knowable
on the first response. Transport retry is disabled on these routes for
the same reason: it treats every 503 alike and would burn its budget
before the classifier ever saw the body.
`get_lsm_stats` returns `Option<LsmStats>`, matching
`get_lsm_write_spec` — `None` only when the table has no LSM write path,
since a struct of zeros would read as measurements.
Python bindings mirror all four, preserving per-bucket detail rather
than flattening to a table-level summary.
## Testing
Six new unit tests against the mocked endpoint, plus the taxonomy
round-trip:
- flush into an empty L0 issues **zero** compact calls (asserts the call
count — `generations_consumed: 0` is also true of a loop that ran a
pointless pass)
- the loop drives compact until the server reports zero remaining
- **contention is not draining**: a 429 retries and converges; asserts
the retry count
- a draining node stops after **exactly one** request, no retries
- stats round-trips fully populated; `include_generation_rows` off by
default
- every `(status, code)` pair classifies correctly, including
unparseable 503 bodies falling back to *retryable* rather than terminal
`cargo test -p lancedb --features remote --lib`: 723 passed.
## Notes for review
- Depends on the sibling lance change returning `SealedGeneration` from
`force_seal_active` only at the *server* level — no lance API is used
here.
- The branch is based on `codex/update-lance-10-0-0-beta-5`, so it
carries one extra commit (`chore: update lance dependency to
v10.0.0-beta.5`) that is not part of this change.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: lancedb automation <robot@lancedb.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## 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>
## Summary
- add Node regression coverage for vector-search offset pagination
- add equivalent coverage for full-text search
- compare later pages with the corresponding complete-result slice and
assert page sizes
## Root cause
The historical query path requested only the user limit from
nearest-neighbor or full-text search before applying the offset, so a
page became empty when its offset reached that limit. The production
query path on current main already incorporates the later fix from
#2592; this change adds the missing Node binding coverage for the
still-open report and protects both affected APIs from regression.
## Validation
- corepack pnpm build
- corepack pnpm test -- query.test.ts --runInBand
--testNamePattern="Search pagination"
- corepack pnpm lint-ci
- corepack pnpm tsc
- corepack pnpm run docs
Fixes#2229
<!-- lance-gatekeeper-fix:v1 agent=8ba8b18a18260a68a3e605d1bbfa518e
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
## Summary
- Add an issue-specific regression for appending generated embeddings to
an empty table with a non-nullable vector field.
- Verify the custom embedding function produces the declared Float64
vectors and both appended rows are readable.
## Root cause
In v0.4.19, records without a vector value were materialized against the
explicit schema before embeddings were inserted. Apache Arrow inferred
the generated batch vector field as nullable while the table retained
the user-provided non-nullable field, then rejected the mismatched
schemas.
The current conversion path excludes the generated field from the
initial record conversion and realigns the completed batch to the stored
schema after embedding, but the reported empty-table append sequence
lacked permanent regression coverage.
## Validation
- `pnpm exec biome format --write __test__/embedding.test.ts`
- `pnpm lint-ci`
- `pnpm test -- --runInBand __test__/embedding.test.ts` (12 passed, 1
skipped integration test)
- `pnpm build`
- `pnpm run docs`
Fixes#1281
<!-- lance-gatekeeper-fix:v1 agent=6b7270aeb92e6b6c6f5b45022fa83f6a
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add an end-to-end regression for indexed vector search after merging a
pandas column
- verify unmatched rows retain a null merged value instead of failing
Arrow batch assembly
## Root cause
Historical Lance readers could assemble schema-evolved columns in
physical data-file order. Indexed row-ID reads after a merge could
therefore omit or misorder the newly merged column for unmatched rows.
The currently pinned Lance release contains the reader correction, but
LanceDB did not cover the reported merge-then-search path.
## Validation
- uv run --extra tests pytest python/tests/test_table.py::test_merge
python/tests/test_table.py::test_search_after_merge -q
- uv run --project python --extra dev ruff check .
- uv run --project python --extra dev ruff format --check
python/python/tests/test_table.py
Fixes#599
<!-- lance-gatekeeper-fix:v1 agent=4e17331e0542c132eae31e86da508629
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Root cause
`Table.to_polars()` disabled PyArrow predicate pushdown by selecting the
non-PyArrow Polars scan callback. Polars 1.32.3 invokes that callback
with `batch_size` both positionally and through its partial, so
collecting the returned lazy frame raises `TypeError:
_scan_pyarrow_dataset_impl() got multiple values for argument
batch_size`.
## Fix
- Keep the compatible PyArrow callback path.
- Add an identity `map_batches` barrier so predicates stay in Polars
instead of reaching the LanceDB adapter as unsupported PyArrow
expressions.
- Extend the tested Polars range through 1.32.3 and retain lazy-frame
regression coverage.
## Validation
- `python/tests/test_table.py::test_polars` with Polars 1.32.3
- `python/tests/test_table.py::test_polars` with the locked Polars 1.3.0
baseline
- `ruff format --check` on the changed Python files
- `ruff check .`
- `uv lock --check`
Fixes#2619
<!-- lance-gatekeeper-fix:v1 agent=0d42bcda944ac42765b25f2c19ff729f
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- exercise float16 sanitization through the reported direct Arrow-data
table creation path
- assert that the inferred fixed-size vector schema remains float16
- retain end-to-end index creation and vector search coverage
## Root cause and fix
PyArrow 16 does not provide an is_nan kernel for half-float arrays, so
passing float16 vector values directly to that kernel raises
ArrowNotImplementedError. LanceDB's sanitizer already carries the
compatibility fix from #837: it casts float16 values to float32 only for
NaN detection while preserving the stored vector type.
The existing end-to-end regression created an empty schema-defined table
and added data afterward. This change aligns that regression with the
issue reproduction by creating a table directly from a
FixedSizeList<float16> Arrow table and verifying the persisted schema.
## Validation
- uv run --extra tests pytest
python/tests/test_table.py::test_create_f16_table_from_arrow_data -q
- direct 1,000-row by 128-dimension float16 Arrow-table reproduction
- PyArrow 16.1 half-float is_nan kernel reproduction
- uvx ruff@0.15.20 format --check python/python/tests/test_table.py
- uvx ruff@0.15.20 check .
Fixes#835
<!-- lance-gatekeeper-fix:v1 agent=dd0a32a959f691f49de958d4333fb29d
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- require Node.js 18-compatible type declarations when TypeScript
consumers install them
- keep the type peer optional for JavaScript-only consumers
- add a regression test tying the Node type peer range to the supported
runtime
## Root cause
LanceDB requires Node.js 18 or newer, and its public types expose Apache
Arrow declarations that import built-ins through the node: scheme. The
package did not declare a matching @types/node peer requirement, so npm
accepted projects pinned to Node 12 declarations and TypeScript then
reported that node:stream and node:fs/promises did not exist.
## Validation
- pnpm lint
- pnpm build
- pnpm run docs
- pnpm test --runInBand (678 passed, 5 skipped)
- packed-package consumer probe rejects @types/node 12.20.55 and
installs with @types/node 18.19.130
Fixes#1713
<!-- lance-gatekeeper-fix:v1 agent=7a2b68f3daad20bed9e46cb8892d6e6c
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- publish the PEP 561 `py.typed` marker so downstream type checkers
consume the inline public annotations
- add a Pyright contract test that distinguishes synchronous `connect`
from awaited `connect_async`
- verify the marker is present in the installed package
## Root cause
The public Python module already annotated `lancedb.connect` as
synchronous and `lancedb.connect_async` as asynchronous. The private
native `_lancedb.connect` stub is intentionally awaitable because it
backs `connect_async`. However, the distribution did not include a PEP
561 marker, so downstream tools such as mypy could ignore the public
inline annotations and expose misleading or incomplete type information.
## Validation
- `python/.venv/bin/ruff format --check python/python/tests/test_db.py
python/python/type_tests/connect.py`
- `python/.venv/bin/ruff check .`
- `cd python && .venv/bin/pytest
python/tests/test_db.py::test_package_includes_pep_561_marker -q`
- `cd python && .venv/bin/pyright --pythonpath .venv/bin/python`
- downstream mypy contract check for both public connection functions
Fixes#2159
<!-- lance-gatekeeper-fix:v1 agent=b07901451487187fc03f61890d3aa6bb
generation=1 -->
Co-authored-by: lancedb-gatefixer[bot] <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- raise a clear `TypeError` when `Vector` is used without a dimension
- preserve normal `Vector(dim)` behavior across Pydantic v1 and v2
- add a regression test that defines a model without importing PyArrow
## Root cause
Pydantic interpreted the bare `Vector` factory as a callable field type
and inspected its postponed annotations in the user model's namespace.
Because that namespace did not define LanceDB's internal `pa` alias,
model construction failed with the misleading `NameError: name 'pa' is
not defined` instead of explaining that `Vector` must be parameterized.
The factory now exposes Pydantic's v1 and v2 schema hooks and rejects
bare use before signature introspection with guidance to use
`Vector(dim)`.
## Validation
- `uvx --from 'ruff==0.15.20' ruff check .`
- `uvx --from 'ruff==0.15.20' ruff format --check
python/python/lancedb/pydantic.py python/python/tests/test_pydantic.py`
- `cd python && uv run --extra tests pytest
python/tests/test_pydantic.py::test_bare_vector_raises_clear_error -q`
- `cd python && uv run --extra tests pytest
python/tests/test_pydantic.py -q`
- compatibility checks with Pydantic 1.10.22, 2.11.4, and 2.13.4
Fixes#2384
<!-- lance-gatekeeper-fix:v1 agent=71e7473e18c91db5137a3c0d3bb73640
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add regression coverage for adding dictionary rows with a nullable
fixed-size-list column
- verify ordinary list columns remain aligned alongside the null
fixed-size-list value
## Root cause
PyArrow infers an all-`None` dictionary column as the generic `null`
type. The original schema-alignment path treated the target
fixed-size-list type as proof that the inferred source was also
list-like and unconditionally accessed `value_field`, which raised
`AttributeError`. Current alignment logic correctly falls back to the
target type when the source is not list-like; this test locks in that
repair for the reported ingestion path.
## Validation
- `uv run --extra tests pytest
python/tests/test_table.py::test_add_with_empty_fixed_size_list_drops_bad_rows
python/tests/test_table.py::test_add_nullable_fixed_size_list_with_none
python/tests/test_table.py::test_add_nullable_struct_with_none -q`
- `uv run --with pyarrow==19.0.1 --extra tests pytest
python/tests/test_table.py::test_add_nullable_fixed_size_list_with_none
-q`
- `uv run --project python --extra dev ruff format --check
python/python/tests/test_table.py`
- `uv run --project python --extra dev ruff check .`
Fixes#2340
<!-- lance-gatekeeper-fix:v1 agent=cb0475e85e764f79bd03b35eb8955ec4
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
## Summary
- add regression coverage for multiple query vectors in the local
synchronous Python API
- verify that each query vector receives its own limited
nearest-neighbor result and `query_index`
## Root cause
In LanceDB v0.16, the local synchronous scanner passed a nested vector
array as one query, unlike the async and remote implementations. The
subsequent sync-to-async table migration supplied the correct shared
runtime path, but this local sync behavior was never regression-tested
and issue #1857 remained open.
## Validation
- `uv run --extra tests pytest
python/tests/test_query.py::test_query_multiple_vectors -q`
- `uv run --project python --extra tests --extra dev ruff format --check
python/python/tests/test_query.py`
- `uv run --project python --extra tests --extra dev ruff check .`
Fixes#1857
<!-- lance-gatekeeper-fix:v1 agent=6b25bc529d76813c3db7627c8be947ef
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Lancedb does not work with any other version of `futures`.
With futures 0.1 it fails like this:
```console
error[E0432]: unresolved imports `futures::StreamExt`, `futures::TryStreamExt`
--> rust/lancedb/src/arrow.rs:21:23
|
21 | use futures::{Stream, StreamExt, TryStreamExt};
| ^^^^^^^^^ ^^^^^^^^^^^^ no `TryStreamExt` in the root
| |
| no `StreamExt` in the root
|
error[E0432]: unresolved import `futures::StreamExt`
--> rust/lancedb/src/data/scannable.rs:24:5
|
24 | use futures::StreamExt;
| ^^^^^^^^^^^^^^^^^^ no `StreamExt` in the root
|
error[E0432]: unresolved import `futures::TryStreamExt`
--> rust/lancedb/src/dataloader/permutation/builder.rs:9:5
|
9 | use futures::TryStreamExt;
| ^^^^^^^^^^^^^^^^^^^^^ no `TryStreamExt` in the root
error[E0432]: unresolved imports `futures::StreamExt`, `futures::TryStreamExt`
--> rust/lancedb/src/dataloader/permutation/reader.rs:25:15
|
25 | use futures::{StreamExt, TryStreamExt};
| ^^^^^^^^^ ^^^^^^^^^^^^ no `TryStreamExt` in the root
| |
| no `StreamExt` in the root
|
error[E0432]: unresolved imports `futures::StreamExt`, `futures::TryStreamExt`
--> rust/lancedb/src/dataloader/permutation/shuffle.rs:8:15
|
8 | use futures::{StreamExt, TryStreamExt};
| ^^^^^^^^^ ^^^^^^^^^^^^ no `TryStreamExt` in the root
| |
| no `StreamExt` in the root
|
error[E0432]: unresolved imports `futures::StreamExt`, `futures::TryStreamExt`
--> rust/lancedb/src/dataloader/permutation/split.rs:12:15
|
12 | use futures::{StreamExt, TryStreamExt};
| ^^^^^^^^^ ^^^^^^^^^^^^ no `TryStreamExt` in the root
| |
| no `StreamExt` in the root
|
error[E0432]: unresolved import `futures::TryStreamExt`
--> rust/lancedb/src/dataloader/permutation/util.rs:9:5
|
9 | use futures::TryStreamExt;
| ^^^^^^^^^^^^^^^^^^^^^ no `TryStreamExt` in the root
error[E0432]: unresolved imports `futures::StreamExt`, `futures::TryFutureExt`
--> rust/lancedb/src/io/object_store.rs:8:15
|
8 | use futures::{StreamExt, TryFutureExt, stream::BoxStream};
| ^^^^^^^^^ ^^^^^^^^^^^^ no `TryFutureExt` in the root
| |
| no `StreamExt` in the root
|
error[E0432]: unresolved imports `futures::FutureExt`, `futures::TryFutureExt`, `futures::TryStreamExt`, `futures::try_join`
--> rust/lancedb/src/query.rs:12:15
|
12 | use futures::{FutureExt, TryFutureExt, TryStreamExt, stream, try_join};
| ^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^ no `try_join` in the root
| | | |
| | | no `TryStreamExt` in the root
| | no `TryFutureExt` in the root
| no `FutureExt` in the root
|
error[E0432]: unresolved imports `futures::StreamExt`, `futures::TryStreamExt`
--> rust/lancedb/src/remote/table/blobs.rs:13:15
|
13 | use futures::{StreamExt, TryStreamExt};
| ^^^^^^^^^ ^^^^^^^^^^^^ no `TryStreamExt` in the root
| |
| no `StreamExt` in the root
|
error[E0432]: unresolved imports `futures::SinkExt`, `futures::StreamExt`
--> rust/lancedb/src/remote/table/insert.rs:20:15
|
20 | use futures::{SinkExt, StreamExt};
| ^^^^^^^ ^^^^^^^^^ no `StreamExt` in the root
| |
| no `SinkExt` in the root
|
error[E0432]: unresolved imports `futures::StreamExt`, `futures::TryStreamExt`
--> rust/lancedb/src/remote/table.rs:58:15
|
58 | use futures::{StreamExt, TryStreamExt};
| ^^^^^^^^^ ^^^^^^^^^^^^ no `TryStreamExt` in the root
| |
| no `StreamExt` in the root
|
error[E0432]: unresolved import `futures::StreamExt`
--> rust/lancedb/src/remote/util.rs:5:23
|
5 | use futures::{Stream, StreamExt};
| ^^^^^^^^^ no `StreamExt` in the root
|
error[E0432]: unresolved import `futures::StreamExt`
--> rust/lancedb/src/table.rs:14:5
|
14 | use futures::StreamExt;
| ^^^^^^^^^^^^^^^^^^ no `StreamExt` in the root
|
error[E0432]: unresolved import `futures::TryStreamExt`
--> rust/lancedb/src/table/datafusion/insert.rs:20:5
|
20 | use futures::TryStreamExt;
| ^^^^^^^^^^^^^^^^^^^^^ no `TryStreamExt` in the root
error[E0432]: unresolved import `futures::TryStreamExt`
--> rust/lancedb/src/table/datafusion/scannable_exec.rs:14:5
|
14 | use futures::TryStreamExt;
| ^^^^^^^^^^^^^^^^^^^^^ no `TryStreamExt` in the root
error[E0432]: unresolved imports `futures::TryFutureExt`, `futures::TryStreamExt`
--> rust/lancedb/src/table/datafusion.rs:25:15
|
25 | use futures::{TryFutureExt, TryStreamExt};
| ^^^^^^^^^^^^ ^^^^^^^^^^^^ no `TryStreamExt` in the root
| |
| no `TryFutureExt` in the root
error[E0432]: unresolved import `futures::FutureExt`
--> rust/lancedb/src/table/delete.rs:3:5
|
3 | use futures::FutureExt;
| ^^^^^^^^^^^^^^^^^^ no `FutureExt` in the root
|
error[E0432]: unresolved imports `futures::FutureExt`, `futures::TryFutureExt`
--> rust/lancedb/src/table/merge.rs:9:15
|
9 | use futures::{FutureExt, TryFutureExt};
| ^^^^^^^^^ ^^^^^^^^^^^^ no `TryFutureExt` in the root
| |
| no `FutureExt` in the root
|
error[E0432]: unresolved import `futures::future::try_join_all`
--> rust/lancedb/src/table/query.rs:24:5
|
24 | use futures::future::try_join_all;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ no `try_join_all` in `future`
|
error[E0432]: unresolved import `futures::FutureExt`
--> rust/lancedb/src/utils/background_cache.rs:12:5
|
12 | use futures::FutureExt;
| ^^^^^^^^^^^^^^^^^^ no `FutureExt` in the root
|
error[E0432]: unresolved import `futures::FutureExt`
--> rust/lancedb/src/utils/mod.rs:12:15
|
12 | use futures::{FutureExt, Stream};
| ^^^^^^^^^ no `FutureExt` in the root
|
error[E0433]: cannot find `join` in `futures`
--> rust/lancedb/src/remote/table/insert.rs:504:55
|
504 | let (producer_result, send_result) = futures::join!(producer, send);
| ^^^^ could not find `join` in `futures`
error[E0407]: method `poll_next` is not a member of trait `Stream`
--> rust/lancedb/src/arrow.rs:108:5
|
108 | / fn poll_next(
109 | | self: Pin<&mut Self>,
110 | | cx: &mut std::task::Context<'_>,
111 | | ) -> std::task::Poll<Option<Self::Item>> {
112 | | let this = self.project();
113 | | this.stream.poll_next(cx)
114 | | }
| |_____^ not a member of trait `Stream`
error[E0407]: method `poll_next` is not a member of trait `Stream`
--> rust/lancedb/src/utils/mod.rs:362:5
|
362 | / fn poll_next(
363 | | mut self: std::pin::Pin<&mut Self>,
364 | | cx: &mut std::task::Context<'_>,
365 | | ) -> std::task::Poll<Option<Self::Item>> {
... |
391 | | }
| |_____^ not a member of trait `Stream`
error[E0407]: method `poll_next` is not a member of trait `Stream`
--> rust/lancedb/src/utils/mod.rs:433:5
|
433 | / fn poll_next(
434 | | mut self: Pin<&mut Self>,
435 | | cx: &mut std::task::Context<'_>,
436 | | ) -> std::task::Poll<Option<Self::Item>> {
... |
470 | | }
| |_____^ not a member of trait `Stream`
error[E0425]: cannot find function `try_unfold` in module `futures::stream`
--> rust/lancedb/src/remote/table/insert.rs:230:39
|
230 | let stream = futures::stream::try_unfold(
| ^^^^^^^^^^ not found in `futures::stream`
error[E0433]: cannot find `channel` in `futures`
--> rust/lancedb/src/remote/table/insert.rs:418:22
|
418 | futures::channel::mpsc::channel::<Result<Vec<u8>, std::io::Error>>(2);
| ^^^^^^^ could not find `channel` in `futures`
|
error[E0425]: cannot find function `try_join_all` in module `futures::future`
--> rust/lancedb/src/remote/table.rs:1062:40
|
1062 | let streams = futures::future::try_join_all(futures);
| ^^^^^^^^^^^^
|
::: $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/join_all.rs:76:1
|
76 | / pub fn join_all<I>(i: I) -> JoinAll<I>
77 | | where I: IntoIterator,
78 | | I::Item: IntoFuture,
| |______________________________- similarly named function `join_all` defined here
|
error[E0425]: cannot find function `try_join_all` in module `futures::future`
--> rust/lancedb/src/remote/table.rs:1660:40
|
1660 | let results = futures::future::try_join_all(futures).await?;
| ^^^^^^^^^^^^
|
::: $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/join_all.rs:76:1
|
76 | / pub fn join_all<I>(i: I) -> JoinAll<I>
77 | | where I: IntoIterator,
78 | | I::Item: IntoFuture,
| |______________________________- similarly named function `join_all` defined here
|
error[E0425]: cannot find function `try_join_all` in module `futures::future`
--> rust/lancedb/src/remote/table.rs:2243:43
|
2243 | let plan_texts = futures::future::try_join_all(futures).await?;
| ^^^^^^^^^^^^
|
::: $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/join_all.rs:76:1
|
76 | / pub fn join_all<I>(i: I) -> JoinAll<I>
77 | | where I: IntoIterator,
78 | | I::Item: IntoFuture,
| |______________________________- similarly named function `join_all` defined here
|
error[E0425]: cannot find function `try_join_all` in module `futures::future`
--> rust/lancedb/src/remote/table.rs:2290:53
|
2290 | let analyze_result_texts = futures::future::try_join_all(futures).await?;
| ^^^^^^^^^^^^
|
::: $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/join_all.rs:76:1
|
76 | / pub fn join_all<I>(i: I) -> JoinAll<I>
77 | | where I: IntoIterator,
78 | | I::Item: IntoFuture,
| |______________________________- similarly named function `join_all` defined here
|
error[E0425]: cannot find function `try_unfold` in module `futures::stream`
--> rust/lancedb/src/remote/util.rs:21:35
|
21 | let stream = futures::stream::try_unfold(
| ^^^^^^^^^^ not found in `futures::stream`
error[E0191]: the value of the associated type `Error` in `futures::Stream` must be specified
--> rust/lancedb/src/arrow.rs:70:50
|
70 | pub type SendableRecordBatchStream = Pin<Box<dyn RecordBatchStream + Send>>;
| ^^^^^^^^^^^^^^^^^
|
help: specify the associated type
|
70 | pub type SendableRecordBatchStream = Pin<Box<dyn RecordBatchStream<Error = /* Type */> + Send>>;
| ++++++++++++++++++++
error[E0107]: type alias takes 0 lifetime arguments but 1 lifetime argument was supplied
--> rust/lancedb/src/utils/background_cache.rs:15:31
|
15 | type SharedFut<V, E> = Shared<BoxFuture<'static, Result<V, Arc<E>>>>;
| ^^^^^^^^^ ------- help: remove the lifetime argument
| |
| expected 0 lifetime arguments
|
note: type alias defined here, with 0 lifetime parameters
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/mod.rs:106:14
|
106 | pub type BoxFuture<T, E> = ::std::boxed::Box<Future<Item = T, Error = E> + Send>;
| ^^^^^^^^^
error[E0107]: type alias takes 2 generic arguments but 1 generic argument was supplied
--> rust/lancedb/src/utils/background_cache.rs:15:31
|
15 | type SharedFut<V, E> = Shared<BoxFuture<'static, Result<V, Arc<E>>>>;
| ^^^^^^^^^ ----------------- supplied 1 generic argument
| |
| expected 2 generic arguments
|
note: type alias defined here, with 2 generic parameters: `T`, `E`
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/mod.rs:106:14
|
106 | pub type BoxFuture<T, E> = ::std::boxed::Box<Future<Item = T, Error = E> + Send>;
| ^^^^^^^^^ - -
help: add missing generic argument
|
15 | type SharedFut<V, E> = Shared<BoxFuture<'static, Result<V, Arc<E>>, E>>;
| +++
error[E0046]: not all trait items implemented, missing: `Error`, `poll`
--> rust/lancedb/src/arrow.rs:105:1
|
105 | impl<S: Stream<Item = Result<arrow_array::RecordBatch>>> Stream for SimpleRecordBatchStream<S> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Error`, `poll` in implementation
|
= help: implement the missing item: `type Error = /* Type */;`
= help: implement the missing item: `fn poll(&mut self) -> std::result::Result<Async<std::option::Option<<Self as futures::Stream>::Item>>, <Self as futures::Stream>::Error> { todo!() }`
error[E0107]: type alias takes 0 lifetime arguments but 1 lifetime argument was supplied
--> rust/lancedb/src/io/object_store.rs:97:46
|
97 | fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
| ^^^^^^^^^ ------- help: remove the lifetime argument
| |
| expected 0 lifetime arguments
|
note: type alias defined here, with 0 lifetime parameters
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/mod.rs:132:14
|
132 | pub type BoxStream<T, E> = ::std::boxed::Box<Stream<Item = T, Error = E> + Send>;
| ^^^^^^^^^
error[E0107]: type alias takes 2 generic arguments but 1 generic argument was supplied
--> rust/lancedb/src/io/object_store.rs:97:46
|
97 | fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>> {
| ^^^^^^^^^ ------------------ supplied 1 generic argument
| |
| expected 2 generic arguments
|
note: type alias defined here, with 2 generic parameters: `T`, `E`
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/mod.rs:132:14
|
132 | pub type BoxStream<T, E> = ::std::boxed::Box<Stream<Item = T, Error = E> + Send>;
| ^^^^^^^^^ - -
help: add missing generic argument
|
97 | fn list(&self, prefix: Option<&Path>) -> BoxStream<'static, Result<ObjectMeta>, E> {
| +++
error[E0107]: type alias takes 0 lifetime arguments but 1 lifetime argument was supplied
--> rust/lancedb/src/io/object_store.rs:107:20
|
107 | locations: BoxStream<'static, Result<Path>>,
| ^^^^^^^^^ ------- help: remove the lifetime argument
| |
| expected 0 lifetime arguments
|
note: type alias defined here, with 0 lifetime parameters
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/mod.rs:132:14
|
132 | pub type BoxStream<T, E> = ::std::boxed::Box<Stream<Item = T, Error = E> + Send>;
| ^^^^^^^^^
error[E0107]: type alias takes 2 generic arguments but 1 generic argument was supplied
--> rust/lancedb/src/io/object_store.rs:107:20
|
107 | locations: BoxStream<'static, Result<Path>>,
| ^^^^^^^^^ ------------ supplied 1 generic argument
| |
| expected 2 generic arguments
|
note: type alias defined here, with 2 generic parameters: `T`, `E`
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/mod.rs:132:14
|
132 | pub type BoxStream<T, E> = ::std::boxed::Box<Stream<Item = T, Error = E> + Send>;
| ^^^^^^^^^ - -
help: add missing generic argument
|
107 | locations: BoxStream<'static, Result<Path>, E>,
| +++
error[E0107]: type alias takes 0 lifetime arguments but 1 lifetime argument was supplied
--> rust/lancedb/src/io/object_store.rs:108:10
|
108 | ) -> BoxStream<'static, Result<Path>> {
| ^^^^^^^^^ ------- help: remove the lifetime argument
| |
| expected 0 lifetime arguments
|
note: type alias defined here, with 0 lifetime parameters
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/mod.rs:132:14
|
132 | pub type BoxStream<T, E> = ::std::boxed::Box<Stream<Item = T, Error = E> + Send>;
| ^^^^^^^^^
error[E0107]: type alias takes 2 generic arguments but 1 generic argument was supplied
--> rust/lancedb/src/io/object_store.rs:108:10
|
108 | ) -> BoxStream<'static, Result<Path>> {
| ^^^^^^^^^ ------------ supplied 1 generic argument
| |
| expected 2 generic arguments
|
note: type alias defined here, with 2 generic parameters: `T`, `E`
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/mod.rs:132:14
|
132 | pub type BoxStream<T, E> = ::std::boxed::Box<Stream<Item = T, Error = E> + Send>;
| ^^^^^^^^^ - -
help: add missing generic argument
|
108 | ) -> BoxStream<'static, Result<Path>, E> {
| +++
error[E0599]: no method named `map_err` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/dataloader/permutation/builder.rs:208:32
|
208 | let stream = df_stream.map_err(|e| Error::Other {
| ----------^^^^^^^ method not found in `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>`
|
::: $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.32/src/stream/try_stream/mod.rs:248:8
|
248 | fn map_err<E, F>(self, f: F) -> MapErr<Self, F>
| ------- the method is available for `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` here
|
error[E0599]: no method named `try_collect` found for struct `DatasetRecordBatchStream` in the current scope
--> rust/lancedb/src/dataloader/permutation/reader.rs:220:28
|
220 | let batches = data.try_collect::<Vec<_>>().await?;
| ^^^^^^^^^^^
|
error[E0599]: no method named `map_err` found for struct `DatasetRecordBatchStream` in the current scope
--> rust/lancedb/src/dataloader/permutation/reader.rs:287:14
|
286 | let mut stream = row_ids
| __________________________-
287 | | .map_err(Error::from)
| | -^^^^^^^ method not found in `DatasetRecordBatchStream`
| |_____________|
|
error[E0599]: the method `chain` exists for struct `futures::stream::Once<_, _>`, but its trait bounds were not satisfied
--> rust/lancedb/src/dataloader/permutation/reader.rs:307:81
|
307 | let stream = futures::stream::once(std::future::ready(Ok(first_batch))).chain(stream);
| ^^^^^ method cannot be called on `futures::stream::Once<_, _>` due to unsatisfied trait bounds
error[E0308]: mismatched types
--> rust/lancedb/src/dataloader/permutation/shuffle.rs:120:35
|
120 | futures::stream::once(async move { Ok(shuffled) }),
| --------------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, _>`, found `async` block
| |
| arguments to this function are incorrect
|
= note: expected enum `std::result::Result<_, _>`
found `async` block `{async block@rust/lancedb/src/dataloader/permutation/shuffle.rs:120:35: 120:45}`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
120 | futures::stream::once(Ok(async move { Ok(shuffled) })),
| +++ +
120 | futures::stream::once(Err(async move { Ok(shuffled) })),
| ++++ +
error[E0271]: type mismatch resolving `<Range<u64> as IntoIterator>::Item == Result<_, _>`
--> rust/lancedb/src/dataloader/permutation/shuffle.rs:228:44
|
228 | let stream = futures::stream::iter(0..num_files)
| --------------------- ^^^^^^^^^^^^ expected `Result<_, _>`, found `u64`
| |
| required by a bound introduced by this call
|
= note: expected enum `std::result::Result<_, _>`
found type `u64`
note: required by a bound in `futures::stream::iter`
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/iter.rs:31:27
|
30 | pub fn iter<J, T, E>(i: J) -> Iter<J::IntoIter>
| ---- required by a bound in this function
31 | where J: IntoIterator<Item=Result<T, E>>,
| ^^^^^^^^^^^^^^^^^ required by this bound in `iter`
error[E0599]: no method named `then` found for struct `IterStream<I>` in the current scope
--> rust/lancedb/src/dataloader/permutation/shuffle.rs:229:14
|
228 | let stream = futures::stream::iter(0..num_files)
| ______________________-
229 | | .then(move |file_index| {
| | -^^^^ method not found in `IterStream<std::ops::Range<u64>>`
| |_____________|
|
error[E0599]: no method named `try_collect` found for struct `Pin<Box<dyn lance::io::RecordBatchStream>>` in the current scope
--> rust/lancedb/src/dataloader/permutation/shuffle.rs:258:26
|
250 | let batches = reader
| ___________________________________-
251 | | .read_stream(
252 | | ReadBatchParams::RangeFull,
253 | | reader.num_rows() as u32,
... |
257 | | .await?
258 | | .try_collect::<Vec<_>>()
| |_________________________-^^^^^^^^^^^
error[E0599]: no method named `and_then` found for associated type `impl Future<Output = Result<Arc<...>, ...>> + Send` in the current scope
--> rust/lancedb/src/query.rs:766:14
|
765 | / self.create_plan(QueryExecutionOptions::default())
766 | | .and_then(|plan| std::future::ready(Ok(plan.schema())))
| |_____________-^^^^^^^^
|
::: $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-util-0.3.32/src/future/try_future/mod.rs:395:8
|
395 | fn and_then<Fut, F>(self, f: F) -> AndThen<Self, Fut, F>
| -------- the method is available for `impl std::future::Future<Output = std::result::Result<Arc<(dyn ExecutionPlan + 'static)>, error::Error>> + std::marker::Send` here
error[E0599]: no method named `boxed` found for `async` block `{async block@rust/lancedb/src/query.rs:1492:33: 1492:43}` in the current scope
--> rust/lancedb/src/query.rs:1493:18
|
1492 | let hybrid_result = async move { self.execute_hybrid(options).await }
| _________________________________-
1493 | | .boxed()
| | -^^^^^ method not found in `{async block@rust/lancedb/src/query.rs:1492:33: 1492:43}`
| |_________________|
error[E0271]: expected `{closure@blobs.rs:181:58}` to return `Result<_, _>`, but it returns `impl Future<Output = Result<Bytes, Error>>`
--> rust/lancedb/src/remote/table/blobs.rs:181:66
|
181 | futures::stream::iter(ranges.iter().cloned().map(|range| self.read_range(range)))
| --------------------- ------- ^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, _>`, found future
| | |
| | this closure
| required by a bound introduced by this call
error[E0599]: no method named `buffered` found for struct `IterStream<I>` in the current scope
--> rust/lancedb/src/remote/table/blobs.rs:182:14
|
181 | / futures::stream::iter(ranges.iter().cloned().map(|range| self.read_range(range)))
182 | | .buffered(BLOB_REQUEST_CONCURRENCY)
| | -^^^^^^^^ method not found in `Iter<Map<Cloned<Iter<'_, Range<u64>>>, {closure@...}>>`
| |_____________|
error[E0599]: no method named `try_next` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/remote/table/blobs.rs:379:40
|
379 | while let Some(batch) = stream.try_next().await? {
| ^^^^^^^^ method not found in `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>`
error[E0271]: type mismatch resolving `<Vec<...> as IntoIterator>::Item == Result<_, _>`
--> rust/lancedb/src/remote/table/blobs.rs:481:27
|
481 | futures::stream::iter(probe_futures)
| --------------------- ^^^^^^^^^^^^^ expected `Result<_, _>`, found future
| |
| required by a bound introduced by this call
error[E0599]: no method named `buffered` found for struct `IterStream<I>` in the current scope
--> rust/lancedb/src/remote/table/blobs.rs:482:10
|
481 | / futures::stream::iter(probe_futures)
482 | | .buffered(BLOB_REQUEST_CONCURRENCY)
| | -^^^^^^^^ method not found in `Iter<IntoIter<impl Future<Output = Result<..., ...>>>>`
| |_________|
error[E0599]: no method named `next` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/remote/table/insert.rs:324:37
|
324 | let mut first = match input.next().await {
| ^^^^ method not found in `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>`
error[E0599]: no method named `next` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/remote/table/insert.rs:345:33
|
345 | first = match input.next().await {
| ^^^^ method not found in `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>`
error[E0599]: the method `next` exists for mutable reference `&mut Pin<Box<dyn RecordBatchStream + Send>>`, but its trait bounds were not satisfied
--> rust/lancedb/src/remote/table/insert.rs:446:41
|
446 | None => match input.next().await {
| ^^^^ method cannot be called on `&mut Pin<Box<dyn RecordBatchStream + Send>>` due to unsatisfied trait bounds
|
= note: the following trait bounds were not satisfied:
`Pin<Box<(dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send + 'static)>>: Iterator`
which is required by `&mut Pin<Box<(dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send + 'static)>>: Iterator`
error[E0599]: no method named `map_err` found for struct `IterStream<I>` in the current scope
--> rust/lancedb/src/remote/table.rs:688:53
|
688 | let stream = futures::stream::iter(batches).map_err(DataFusionError::from);
| ^^^^^^^ method not found in `Iter<Box<dyn Iterator<Item = Result<..., ...>> + Send>>`
error[E0599]: no method named `try_collect` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/remote/table.rs:1378:49
|
1378 | let result: Result<Vec<_>> = stream.try_collect().await.map_err(Error::from);
| ^^^^^^^^^^^
error[E0599]: no method named `next` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/remote/table.rs:1509:48
|
1509 | while let Some(batch) = stream.next().await {
| ^^^^ method not found in `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>`
error[E0599]: no method named `boxed` found for opaque type `impl Future<Output = Result<DeleteResult, Error>>` in the current scope
--> rust/lancedb/src/table/delete.rs:35:51
|
35 | let delete_result = dataset.delete(s).boxed().await?;
| ^^^^^ method not found in `impl Future<Output = Result<DeleteResult, Error>>`
error[E0599]: no variant, associated function, or constant named `Left` found for enum `Either<A, B>` in the current scope
--> rust/lancedb/src/table/merge.rs:292:17
|
292 | Either::Left(tokio::time::timeout(timeout, future).map(|res| match res {
| ^^^^ variant, associated function, or constant not found in `Either<_, _>`
error[E0599]: `Timeout<impl Future<Output = Result<(Arc<...>, ...), ...>>>` is not an iterator
--> rust/lancedb/src/table/merge.rs:292:60
|
292 | Either::Left(tokio::time::timeout(timeout, future).map(|res| match res {
| --------------------------------------^^^ `Timeout<impl Future<Output = Result<(Arc<...>, ...), ...>>>` is not an iterator
|
::: $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/pin-project-lite-0.2.17/src/lib.rs:745:9
|
745 | / $vis struct $ident $($def_generics)*
746 | | $(where
747 | | $($where_clause)*)?
... |
751 | | ),+
752 | | }
| |_________- doesn't satisfy `_: Iterator`
|
= note: the following trait bounds were not satisfied:
`tokio::time::Timeout<impl std::future::Future<Output = std::result::Result<(Arc<lance::Dataset>, MergeStats), lance::Error>>>: Iterator`
which is required by `&mut tokio::time::Timeout<impl std::future::Future<Output = std::result::Result<(Arc<lance::Dataset>, MergeStats), lance::Error>>>: Iterator`
error[E0599]: no variant, associated function, or constant named `Right` found for enum `Either<A, B>` in the current scope
--> rust/lancedb/src/table/merge.rs:301:17
|
301 | Either::Right(job.execute_reader(new_data).map_err(|e| e.into()))
| ^^^^^ variant, associated function, or constant not found in `Either<_, _>`
error[E0599]: no method named `map_err` found for opaque type `impl Future<Output = Result<(Arc<Dataset>, ...), ...>>` in the current scope
--> rust/lancedb/src/table/merge.rs:301:52
|
301 | Either::Right(job.execute_reader(new_data).map_err(|e| e.into()))
| ^^^^^^^ method not found in `impl Future<Output = Result<(Arc<Dataset>, ...), ...>>`
error[E0277]: the trait bound `Iter<Map<IntoIter<RecordBatch>, ...>>: Stream` is not satisfied
--> rust/lancedb/src/table/query.rs:681:38
|
681 | Ok(DatasetRecordBatchStream::new(record_batch_stream))
| ^^^^^^^^^^^^^^^^^^^ the trait `futures_core::stream::Stream` is not implemented for `Iter<Map<IntoIter<RecordBatch>, ...>>`
error[E0277]: the trait bound `TimeoutStream: futures_core::stream::Stream` is not satisfied
--> rust/lancedb/src/utils/mod.rs:353:28
|
353 | impl RecordBatchStream for TimeoutStream {
| ^^^^^^^^^^^^^ unsatisfied trait bound
error[E0046]: not all trait items implemented, missing: `Error`, `poll`
--> rust/lancedb/src/utils/mod.rs:359:1
|
359 | impl Stream for TimeoutStream {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Error`, `poll` in implementation
|
= help: implement the missing item: `type Error = /* Type */;`
= help: implement the missing item: `fn poll(&mut self) -> std::result::Result<Async<std::option::Option<<Self as futures::Stream>::Item>>, <Self as futures::Stream>::Error> { todo!() }`
error[E0277]: the trait bound `MaxBatchLengthStream: futures_core::stream::Stream` is not satisfied
--> rust/lancedb/src/utils/mod.rs:424:28
|
424 | impl RecordBatchStream for MaxBatchLengthStream {
| ^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
error[E0046]: not all trait items implemented, missing: `Error`, `poll`
--> rust/lancedb/src/utils/mod.rs:430:1
|
430 | impl Stream for MaxBatchLengthStream {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Error`, `poll` in implementation
|
= help: implement the missing item: `type Error = /* Type */;`
= help: implement the missing item: `fn poll(&mut self) -> std::result::Result<Async<std::option::Option<<Self as futures::Stream>::Item>>, <Self as futures::Stream>::Error> { todo!() }`
error[E0599]: no method named `map` found for type parameter `I` in the current scope
--> rust/lancedb/src/arrow.rs:75:45
|
72 | impl<I: lance::io::RecordBatchStream + 'static> From<I> for SendableRecordBatchStream {
| - method `map` not found for this type parameter
...
75 | let mapped_stream = Box::pin(stream.map(|r| r.map_err(Into::into)));
| ^^^
error[E0599]: no method named `poll_next` found for struct `Pin<&mut S>` in the current scope
--> rust/lancedb/src/arrow.rs:113:21
|
113 | this.stream.poll_next(cx)
| ^^^^^^^^^
|
= help: items from traits can only be used if the trait is implemented and in scope
= note: the following traits define an item `poll_next`, perhaps you need to implement one of them:
candidate #1: `futures_core::stream::Stream`
candidate #2: `sorts::stream::PartitionedStream`
help: there is a method `collect` with a similar name, but with different arguments
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/mod.rs:563:5
|
563 | / fn collect(self) -> Collect<Self>
564 | | where Self: Sized
| |_________________________^
error[E0599]: the method `map_err` exists for struct `Pin<Box<dyn Stream<Item = Result<..., ...>> + Send>>`, but its trait bounds were not satisfied
--> rust/lancedb/src/arrow.rs:150:29
|
150 | let stream = stream.map_err(|err| Error::Arrow { source: err });
| ^^^^^^^ method cannot be called due to unsatisfied trait bounds
error[E0308]: mismatched types
--> rust/lancedb/src/data/scannable.rs:80:26
|
80 | stream: once(async move { Ok(batch) }),
| ---- ^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, _>`, found `async` block
| |
| arguments to this function are incorrect
|
= note: expected enum `std::result::Result<_, _>`
found `async` block `{async block@rust/lancedb/src/data/scannable.rs:80:26: 80:36}`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
80 | stream: once(Ok(async move { Ok(batch) })),
| +++ +
80 | stream: once(Err(async move { Ok(batch) })),
| ++++ +
error[E0308]: mismatched types
--> rust/lancedb/src/data/scannable.rs:107:30
|
107 | stream: once(async {
| _________________________----_^
| | |
| | arguments to this function are incorrect
108 | | Err(Error::InvalidInput {
109 | | message: "Cannot scan an empty Vec<RecordBatch>".to_string(),
110 | | })
111 | | }),
| |_________________^ expected `Result<_, _>`, found `async` block
|
= note: expected enum `std::result::Result<_, _>`
found `async` block `{async block@rust/lancedb/src/data/scannable.rs:107:30: 107:35}`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
107 ~ stream: once(Ok(async {
108 | Err(Error::InvalidInput {
109 | message: "Cannot scan an empty Vec<RecordBatch>".to_string(),
110 | })
111 ~ })),
|
107 ~ stream: once(Err(async {
108 | Err(Error::InvalidInput {
109 | message: "Cannot scan an empty Vec<RecordBatch>".to_string(),
110 | })
111 ~ })),
|
error[E0271]: expected `Ok` to return `Result<Result<RecordBatch, Error>, _>`, but it returns `Result<RecordBatch, _>`
--> rust/lancedb/src/data/scannable.rs:117:52
|
117 | Box::pin(SimpleRecordBatchStream { schema, stream })
| ^^^^^^ expected `Result<Result<RecordBatch, Error>, _>`, found `Result<RecordBatch, _>`
error[E0308]: mismatched types
--> rust/lancedb/src/data/scannable.rs:158:59
|
158 | let stream = futures::stream::unfold(rx, |mut rx| async move {
| ___________________________________________________________^
159 | | rx.recv().await.map(|batch| (batch, rx))
160 | | })
| |_________^ expected `Option<_>`, found `async` block
|
= note: expected enum `std::option::Option<_>`
found `async` block `{async block@rust/lancedb/src/data/scannable.rs:158:59: 158:69}`
help: try wrapping the expression in `Some`
|
158 ~ let stream = futures::stream::unfold(rx, |mut rx| Some(async move {
159 | rx.recv().await.map(|batch| (batch, rx))
160 ~ }))
|
error[E0599]: the method `fuse` exists for struct `Unfold<Receiver<Result<RecordBatch, Error>>, ..., _>`, but its trait bounds were not satisfied
--> rust/lancedb/src/data/scannable.rs:161:10
|
158 | let stream = futures::stream::unfold(rx, |mut rx| async move {
| ______________________-
159 | | rx.recv().await.map(|batch| (batch, rx))
160 | | })
161 | | .fuse();
| | -^^^^ method cannot be called due to unsatisfied trait bounds
| |_________|
error[E0308]: mismatched types
--> rust/lancedb/src/data/scannable.rs:178:26
|
178 | stream: once(async {
| _____________________----_^
| | |
| | arguments to this function are incorrect
179 | | Err(Error::InvalidInput {
180 | | message: "Stream has already been consumed".to_string(),
181 | | })
182 | | }),
| |_____________^ expected `Result<_, _>`, found `async` block
|
= note: expected enum `std::result::Result<_, _>`
found `async` block `{async block@rust/lancedb/src/data/scannable.rs:178:26: 178:31}`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
178 ~ stream: once(Ok(async {
179 | Err(Error::InvalidInput {
180 | message: "Stream has already been consumed".to_string(),
181 | })
182 ~ })),
|
178 ~ stream: once(Err(async {
179 | Err(Error::InvalidInput {
180 | message: "Stream has already been consumed".to_string(),
181 | })
182 ~ })),
|
error[E0308]: mismatched types
--> rust/lancedb/src/data/scannable.rs:474:53
|
474 | let prepend = futures::stream::once(std::future::ready(Ok(batch)));
| --------------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, _>`, found `Ready<Result<RecordBatch, _>>`
| |
| arguments to this function are incorrect
|
= note: expected enum `std::result::Result<_, _>`
found struct `std::future::Ready<std::result::Result<arrow_array::RecordBatch, _>>`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
474 | let prepend = futures::stream::once(Ok(std::future::ready(Ok(batch))));
| +++ +
474 | let prepend = futures::stream::once(Err(std::future::ready(Ok(batch))));
| ++++ +
error[E0599]: the method `chain` exists for struct `futures::stream::Once<_, _>`, but its trait bounds were not satisfied
--> rust/lancedb/src/data/scannable.rs:477:37
|
477 | stream: prepend.chain(rest),
| ^^^^^ method cannot be called on `futures::stream::Once<_, _>` due to unsatisfied trait bounds
error[E0308]: mismatched types
--> rust/lancedb/src/data/scannable.rs:482:47
|
482 | stream: futures::stream::once(std::future::ready(Ok(batch))),
| --------------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, _>`, found `Ready<Result<RecordBatch, _>>`
| |
| arguments to this function are incorrect
|
= note: expected enum `std::result::Result<_, _>`
found struct `std::future::Ready<std::result::Result<arrow_array::RecordBatch, _>>`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
482 | stream: futures::stream::once(Ok(std::future::ready(Ok(batch)))),
| +++ +
482 | stream: futures::stream::once(Err(std::future::ready(Ok(batch)))),
| ++++ +
error[E0308]: mismatched types
--> rust/lancedb/src/data/scannable.rs:486:56
|
486 | let stream = futures::stream::once(std::future::ready(err));
| --------------------- ^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<_, _>`, found `Ready<Result<_, Error>>`
| |
| arguments to this function are incorrect
|
= note: expected enum `std::result::Result<_, _>`
found struct `std::future::Ready<std::result::Result<_, error::Error>>`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
486 | let stream = futures::stream::once(Ok(std::future::ready(err)));
| +++ +
486 | let stream = futures::stream::once(Err(std::future::ready(err)));
| ++++ +
error[E0599]: no method named `and_then` found for struct `Pin<Box<dyn Future<Output = Result<(), Error>> + Send>>` in the current scope
--> rust/lancedb/src/io/object_store.rs:153:32
|
153 | Box::pin(put_secondary.and_then(|_| put_primary))
| ^^^^^^^^
error[E0271]: expected `IntoIter<Result<RecordBatch, _>, 1>` to be an iterator that yields `Result<Result<RecordBatch, Error>, _>`, but it yields `Result<RecordBatch, _>`
--> rust/lancedb/src/query.rs:1465:25
|
1465 | return Box::pin(SimpleRecordBatchStream::new(
| ^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<Result<RecordBatch, Error>, _>`, found `Result<RecordBatch, _>`
error[E0271]: expected `IntoIter<Result<RecordBatch, _>>` to be an iterator that yields `Result<Result<RecordBatch, Error>, _>`, but it yields `Result<RecordBatch, _>`
--> rust/lancedb/src/query.rs:1478:14
|
1478 | Box::pin(SimpleRecordBatchStream::new(stream::iter(batches), schema))
| ^^^^^^^^^^^^^^^^^^^^^^^ expected `Result<Result<RecordBatch, Error>, _>`, found `Result<RecordBatch, _>`
error[E0308]: mismatched types
--> rust/lancedb/src/remote/table/insert.rs:626:44
|
626 | let stream = futures::stream::once(async move {
| ______________________---------------------_^
| | |
| | arguments to this function are incorrect
... |
791 | | Ok::<_, DataFusionError>(batch)
792 | | });
| |_________^ expected `Result<_, _>`, found `async` block
|
= note: expected enum `std::result::Result<_, _>`
found `async` block `{async block@rust/lancedb/src/remote/table/insert.rs:626:44: 626:54}`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
626 ~ let stream = futures::stream::once(Ok(async move {
627 | // Multipart writes with a byte budget split the partition into
...
791 | Ok::<_, DataFusionError>(batch)
792 ~ }));
|
626 ~ let stream = futures::stream::once(Err(async move {
627 | // Multipart writes with a byte budget split the partition into
...
791 | Ok::<_, DataFusionError>(batch)
792 ~ }));
|
error[E0277]: the trait bound `futures::stream::Once<_, _>: futures_core::stream::Stream` is not satisfied
--> rust/lancedb/src/remote/table/insert.rs:794:12
|
794 | Ok(Box::pin(RecordBatchStreamAdapter::new(
| ____________^
795 | | COUNT_SCHEMA.clone(),
796 | | stream,
797 | | )))
| |__________^ the trait `futures_core::stream::Stream` is not implemented for `futures::stream::Once<_, _>`
error[E0599]: no method named `try_collect` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/remote/table.rs:2442:49
|
2442 | let result: Result<Vec<_>> = stream.try_collect().await.map_err(Error::from);
| ^^^^^^^^^^^
error[E0277]: the trait bound `impl Stream<Item = Result<Bytes, Error>>: TryStream` is not satisfied
--> rust/lancedb/src/remote/util.rs:47:35
|
47 | Ok(reqwest::Body::wrap_stream(stream))
| -------------------------- ^^^^^^ unsatisfied trait bound
| |
| required by a bound introduced by this call
error[E0599]: no method named `map_ok` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/table/datafusion/insert.rs:200:30
|
200 | input_stream.map_ok(move |batch| {
| -------------^^^^^^ method not found in `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>`
error[E0308]: mismatched types
--> rust/lancedb/src/table/datafusion/insert.rs:208:44
|
208 | let stream = futures::stream::once(async move {
| ______________________---------------------_^
| | |
| | arguments to this function are incorrect
209 | | if let Some(tracker) = tracker
210 | | && write_params.write_progress.is_none()
... |
255 | | )?)
256 | | });
| |_________^ expected `Result<_, _>`, found `async` block
|
= note: expected enum `std::result::Result<_, _>`
found `async` block `{async block@rust/lancedb/src/table/datafusion/insert.rs:208:44: 208:54}`
note: function defined here
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/once.rs:20:8
|
20 | pub fn once<T, E>(item: Result<T, E>) -> Once<T, E> {
| ^^^^
help: try wrapping the expression in a variant of `std::result::Result`
|
208 ~ let stream = futures::stream::once(Ok(async move {
209 | if let Some(tracker) = tracker
...
255 | )?)
256 ~ }));
|
208 ~ let stream = futures::stream::once(Err(async move {
209 | if let Some(tracker) = tracker
...
255 | )?)
256 ~ }));
|
error[E0277]: the trait bound `futures::stream::Once<_, _>: futures_core::stream::Stream` is not satisfied
--> rust/lancedb/src/table/datafusion/insert.rs:258:12
|
258 | Ok(Box::pin(RecordBatchStreamAdapter::new(
| ____________^
259 | | COUNT_SCHEMA.clone(),
260 | | stream,
261 | | )))
| |__________^ the trait `futures_core::stream::Stream` is not implemented for `futures::stream::Once<_, _>`
error[E0599]: no method named `map_ok` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/table/datafusion.rs:128:29
|
128 | let stream = stream.map_ok(move |batch| {
| -------^^^^^^ method not found in `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>`
error[E0599]: no method named `map_err` found for struct `Pin<Box<dyn Future<Output = Result<Arc<...>, ...>> + Send>>` in the current scope
--> rust/lancedb/src/table/datafusion.rs:245:14
|
242 | let plan = self
| ____________________-
243 | | .table
244 | | .create_plan(&AnyQuery::Query(query), options)
245 | | .map_err(|err| DataFusionError::External(err.into()))
| | -^^^^^^^ method not found in `Pin<Box<dyn Future<Output = Result<Arc<...>, ...>> + Send>>`
| |_____________|
error[E0599]: no method named `next` found for struct `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>` in the current scope
--> rust/lancedb/src/table.rs:3048:48
|
3048 | while let Some(batch) = stream.next().await {
| ^^^^ method not found in `Pin<Box<dyn datafusion_physical_plan::RecordBatchStream + std::marker::Send>>`
error[E0277]: the trait bound `JoinHandle<Result<(), Error>>: Future` is not satisfied
--> rust/lancedb/src/table.rs:3038:23
|
3038 | let handles = FuturesUnordered::new();
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `futures::Future` is not implemented for `tokio::task::JoinHandle<std::result::Result<(), error::Error>>`
error[E0277]: `FuturesUnordered<JoinHandle<Result<(), Error>>>` is not an iterator
--> rust/lancedb/src/table.rs:3054:23
|
3054 | for handle in handles {
| ^^^^^^^ `FuturesUnordered<JoinHandle<Result<(), Error>>>` is not an iterator
error[E0277]: the trait bound `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}: futures::IntoFuture` is not satisfied
--> rust/lancedb/src/table.rs:3450:13
|
3449 | let mut sorted_sizes = join_all(
| -------- required by a bound introduced by this call
3450 | / frags
3451 | | .iter()
3452 | | .map(|frag| async move { frag.physical_rows().await.unwrap_or(0) }),
| |___________________________________________________________________________________^ the trait `futures::Future` is not implemented for `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}`
|
= note: `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}` implements similarly named trait `std::future::Future`, but not `futures::Future`
= help: the following other types implement trait `futures::Future`:
&'a mut F
AssertUnwindSafe<F>
BiLockAcquire<T>
Box<F>
Concat2<S>
Either<A, B>
Finished<T, E>
Fold<S, F, Fut, T>
and 43 others
= note: required for `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}` to implement `futures::IntoFuture`
note: required by a bound in `join_all`
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/join_all.rs:78:20
|
76 | pub fn join_all<I>(i: I) -> JoinAll<I>
| -------- required by a bound in this function
77 | where I: IntoIterator,
78 | I::Item: IntoFuture,
| ^^^^^^^^^^ required by this bound in `join_all`
error[E0277]: the trait bound `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}: futures::Future` is not satisfied
--> rust/lancedb/src/table.rs:3449:32
|
3449 | let mut sorted_sizes = join_all(
| ________________________________^
3450 | | frags
3451 | | .iter()
3452 | | .map(|frag| async move { frag.physical_rows().await.unwrap_or(0) }),
3453 | | )
| |_________^ the trait `futures::Future` is not implemented for `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}`
|
= note: `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}` implements similarly named trait `std::future::Future`, but not `futures::Future`
= help: the following other types implement trait `futures::Future`:
&'a mut F
AssertUnwindSafe<F>
BiLockAcquire<T>
Box<F>
Concat2<S>
Either<A, B>
Finished<T, E>
Fold<S, F, Fut, T>
and 43 others
= note: required for `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}` to implement `futures::IntoFuture`
note: required by a bound in `JoinAll`
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/join_all.rs:24:20
|
22 | pub struct JoinAll<I>
| ------- required by a bound in this struct
23 | where I: IntoIterator,
24 | I::Item: IntoFuture,
| ^^^^^^^^^^ required by this bound in `JoinAll`
error[E0277]: `JoinAll<Map<Iter<'_, FileFragment>, {closure@...}>>` is not a future
--> rust/lancedb/src/table.rs:3454:10
|
3449 | let mut sorted_sizes = join_all(
| ________________________________-
3450 | | frags
3451 | | .iter()
3452 | | .map(|frag| async move { frag.physical_rows().await.unwrap_or(0) }),
3453 | | )
| |_________- this call returns `JoinAll<std::iter::Map<std::slice::Iter<'_, FileFragment>, {closure@rust/lancedb/src/table.rs:3452:22: 3452:28}>>`
3454 | .await;
| ^^^^^ `JoinAll<Map<Iter<'_, FileFragment>, {closure@...}>>` is not a future
error[E0277]: the trait bound `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}: futures::Future` is not satisfied
--> rust/lancedb/src/table.rs:3454:10
|
3454 | .await;
| ^^^^^ the trait `futures::Future` is not implemented for `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}`
|
= note: `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}` implements similarly named trait `std::future::Future`, but not `futures::Future`
= help: the following other types implement trait `futures::Future`:
&'a mut F
AssertUnwindSafe<F>
BiLockAcquire<T>
Box<F>
Concat2<S>
Either<A, B>
Finished<T, E>
Fold<S, F, Fut, T>
and 43 others
= note: required for `{async block@rust/lancedb/src/table.rs:3452:29: 3452:39}` to implement `futures::IntoFuture`
note: required by a bound in `JoinAll`
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/future/join_all.rs:24:20
|
22 | pub struct JoinAll<I>
| ------- required by a bound in this struct
23 | where I: IntoIterator,
24 | I::Item: IntoFuture,
| ^^^^^^^^^^ required by this bound in `JoinAll`
error[E0282]: type annotations needed
--> rust/lancedb/src/utils/background_cache.rs:119:40
|
119 | inner: Arc::new(Mutex::new(CacheInner {
| ________________________________________^
120 | | state: State::Empty,
121 | | generation: 0,
122 | | })),
| |_____________^ cannot infer type of the type parameter `E` declared on the struct `CacheInner`
|
help: consider specifying the generic arguments
|
119 | inner: Arc::new(Mutex::new(CacheInner::<V, E> {
| ++++++++
error[E0282]: type annotations needed
--> rust/lancedb/src/utils/background_cache.rs:134:9
|
134 | cache.state.fresh_value(self.ttl, self.refresh_window)
| ^^^^^^^^^^^ cannot infer type for type parameter `E`
error[E0282]: type annotations needed
--> rust/lancedb/src/utils/background_cache.rs:173:23
|
173 | cache.state = State::Current(value, clock::now());
| ^^^^^^^^^^^^^^ cannot infer type of the type parameter `E` declared on the enum `State`
|
help: consider specifying the generic arguments
|
173 | cache.state = State::<V, E>::Current(value, clock::now());
| ++++++++
error[E0282]: type annotations needed
--> rust/lancedb/src/utils/background_cache.rs:182:23
|
182 | cache.state = State::Empty;
| ^^^^^^^^^^^^ cannot infer type of the type parameter `E` declared on the enum `State`
|
help: consider specifying the generic arguments
|
182 | cache.state = State::<V, E>::Empty;
| ++++++++
error[E0599]: no method named `boxed` found for `async` block `{async block@rust/lancedb/src/utils/background_cache.rs:269:22: 269:32}` in the current scope
--> rust/lancedb/src/utils/background_cache.rs:270:14
|
269 | let shared = async move { (fetch)().await.map_err(Arc::new) }
| ______________________-
270 | | .boxed()
| | -^^^^^ method not found in `{async block@rust/lancedb/src/utils/background_cache.rs:269:22: 269:32}`
| |_____________|
error[E0277]: the trait bound `TimeoutStream: futures_core::stream::Stream` is not satisfied
--> rust/lancedb/src/utils/mod.rs:345:9
|
345 | Box::pin(Self::new(inner, timeout))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
error[E0599]: no method named `poll_next` found for struct `Pin<&mut TimeoutStream>` in the current scope
--> rust/lancedb/src/utils/mod.rs:376:22
|
376 | self.poll_next(cx)
| ^^^^^^^^^
|
= help: items from traits can only be used if the trait is implemented and in scope
= note: the following traits define an item `poll_next`, perhaps you need to implement one of them:
candidate #1: `futures_core::stream::Stream`
candidate #2: `sorts::stream::PartitionedStream`
help: there is a method `collect` with a similar name, but with different arguments
--> $CARGO_HOME/registry/src/index.crates.io-1949cf8c6b5b557f/futures-0.1.31/src/stream/mod.rs:563:5
|
563 | / fn collect(self) -> Collect<Self>
564 | | where Self: Sized
| |_________________________^
error[E0599]: no method named `poll_unpin` found for mutable reference `&mut Pin<Box<Sleep>>` in the current scope
--> rust/lancedb/src/utils/mod.rs:378:75
|
378 | TimeoutState::Started { deadline, timeout } => match deadline.poll_unpin(cx) {
| ^^^^^^^^^^ method not found in `&mut Pin<Box<Sleep>>`
error[E0599]: no method named `poll_next` found for struct `Pin<&mut Pin<Box<dyn RecordBatchStream + Send>>>` in the current scope
--> rust/lancedb/src/utils/mod.rs:386:27
|
386 | inner.poll_next(cx)
| ^^^^^^^^^
error[E0277]: the trait bound `MaxBatchLengthStream: futures_core::stream::Stream` is not satisfied
--> rust/lancedb/src/utils/mod.rs:419:13
|
419 | Box::pin(Self::new(inner, max_batch_length))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound
error[E0599]: no method named `poll_next` found for struct `Pin<&mut Pin<Box<dyn RecordBatchStream + Send>>>` in the current scope
--> rust/lancedb/src/utils/mod.rs:439:50
|
439 | return Pin::new(&mut self.inner).poll_next(cx);
| ^^^^^^^^^
error[E0599]: no method named `poll_next` found for struct `Pin<&mut Pin<Box<dyn RecordBatchStream + Send>>>` in the current scope
--> rust/lancedb/src/utils/mod.rs:459:45
|
459 | match Pin::new(&mut self.inner).poll_next(cx) {
| ^^^^^^^^^
Some errors have detailed explanations: E0046, E0107, E0191, E0271, E0277, E0282, E0308, E0407, E0425...
For more information about an error, try `rustc --explain E0046`.
error: could not compile `lancedb` (lib) due to 118 previous errors
```
The docs have no link checking at all, so external links rot silently: a
trial run already found `docs/src/python/python.md` pointing at
`lancedb.github.io/lance-namespace`, which returns 404 since the
repository moved to the lance-format org.
Checking external links on the blocking path would be the wrong trade:
third-party hosts rate-limit automated clients, reject non-browser user
agents, and go down temporarily, so any of them having a bad minute
would turn unrelated PRs red. Following lance-format/lance#8315, this
adds a daily `lychee` run that reports broken links into a single
tracking issue, rewritten in place on each run and closed automatically
once every link resolves. The scan job runs the downloaded lychee binary
with a read-only token; everything that writes lives in a separate
report job, and a non-verdict lychee exit fails the run instead of
publishing a bogus report.
The check is restricted to http(s) links because much of `docs/src` is
generated API reference (the `js/` tree comes from `npm run docs`) and
the hand-written pages use mkdocstrings cross-references and
nav-relative paths that only resolve in the site mkdocs builds, so
relative links would be reported as broken on every run.
The one broken link the trial run surfaced is fixed here; after the fix,
a local run over all 154 files reports 0 errors across 216 unique links.
## 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>
## Summary
- convert PyArrow scalar values through their Python representation
before SQL literal rendering
- add an end-to-end regression for updating a fixed-size-list vector
from a queried FixedSizeListScalar
## Root cause
Python update literal conversion used single dispatch for native Python
and NumPy values but had no PyArrow Scalar registration. A
FixedSizeListScalar returned by a query therefore reached the
unsupported generic conversion instead of the existing recursive list
converter.
## Validation
- uv run --extra tests pytest python/tests/test_table.py::test_update
python/tests/test_table.py::test_update_with_arrow_scalar
python/tests/test_table.py::test_update_types -q
- uv run --extra tests pytest python/tests/test_util.py -q
- uv run --project python --extra tests --extra dev ruff format --check
python/python/lancedb/util.py python/python/tests/test_table.py
- uv run --project python --extra tests --extra dev ruff check .
Fixes#1228
<!-- lance-gatekeeper-fix:v1 agent=950dd892194e53b61c203d5e3715cac7
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
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.
## Summary
- add a public Node API regression test for JSON server errors from
remote table operations
- verify countRows reports the server message instead of an ArrayBuffer
decoding TypeError
## Root cause and fix
The former TypeScript remote HTTP client passed an Axios-decoded JSON
error object to TextDecoder, which masked the server response with an
ArrayBuffer TypeError. The current Rust-backed remote client consumes
non-success response bodies as text and propagates them through the Node
error chain. This test exercises that corrected path through countRows
and prevents the original failure from regressing.
## Validation
- pnpm build
- pnpm lint-ci
- pnpm test --runInBand __test__/remote.test.ts
- pnpm run docs
Fixes#825
<!-- lance-gatekeeper-fix:v1 agent=91591c3d6b065796e6166664ef638aa7
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
## Summary
- add an end-to-end regression for schemas created by a different Apache
Arrow package instance
- cover seeded table creation, filtered scanning, and Float64 vector
search across Arrow 15–18
## Root cause
Apache Arrow's runtime identity checks historically rejected schemas
created by another installed Arrow instance, producing the constructor
failures reported in the issue. LanceDB's peer dependency and
foreign-schema sanitization now handle that boundary, but the complete
reported workflow was only covered by separate unit tests. This
regression keeps the repaired behavior protected end to end.
## Validation
- `pnpm exec jest --runInBand __test__/table.test.ts` (281 passed)
- `pnpm lint-ci`
- `pnpm build`
- `pnpm run docs`
Fixes#882
<!-- lance-gatekeeper-fix:v1 agent=43b19dea581cfbc83ee1e9ed21a335a6
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
## 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>
## Summary
- add a Python regression test for two partial-schema merge inserts
against the same BTree-indexed rows
- verify repeated updates retain one copy of every row and the final
update values
## Root cause
Lance 4.0, used by LanceDB 0.30.2, removed a rewritten fragment from the
index bitmap while stale BTree entries for that fragment remained
searchable. The next merge found each target through both the stale
index and the unindexed-fragment scan, producing the ambiguous-match
error. Lance fixed the root cause in lance-format/lance#6563 by applying
the fragment-bitmap allow-list to index results, and the Lance release
pinned by current LanceDB includes that fix. This test preserves the
corrected behavior through the Python API.
## Validation
- `cd python && uv run --extra tests pytest python/tests/test_table.py
-k merge_insert -q` (9 passed)
- `cd python && uv run --extra tests --extra dev ruff format --check
python/tests/test_table.py`
- `cd python && uv run --extra tests --extra dev ruff check
python/tests/test_table.py`
Repository-wide Ruff also reports 20 pre-existing violations in
untouched CI and plugin scripts.
Fixes#3280
<!-- lance-gatekeeper-fix:v1 agent=ee6b9565f9780712026076930566f116
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add a minimized regression for mostly-null `list<float32>` data at the
v2.2 structural page boundary
- verify scans preserve all 64,885 rows, including 64,668 null list
values
## Root cause
Lance 3.0.0 sliced repetition/definition state using top-level row
offsets in the complex all-null decoder. At this page boundary, the list
and validity children were materialized at different lengths. The
current Lance dependency contains the upstream decoder repair; this test
locks that behavior into the LanceDB Python suite without duplicating
decoder logic.
## Validation
- reproduced the attached 1,892,466-row case on `lancedb==0.30.0` with
`expected 1024 got 285`
- verified the full attachment reads on the current branch
- `python/.venv/bin/ruff format --check
python/python/tests/test_table.py`
- `python/.venv/bin/ruff check .`
- `cd python && uv run --extra tests pytest
python/tests/test_table.py::test_read_mostly_null_list_v2_2_page_boundary
-q`
- `cd python && uv run --extra tests pytest python/tests/test_table.py
-q` (137 passed)
Fixes#3194
<!-- lance-gatekeeper-fix:v1 agent=0445adc5303a3302152cea3d2110bed1
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
## Summary
- add deterministic regression coverage that `Table.add()` releases
backing Arrow buffers without cyclic garbage collection
- track the foreign buffer owner rather than RSS, separating live input
retention from allocator high-water behavior
- preserve the bounded-lifetime behavior of the Scannable writer that
superseded the historical preprocessing path
## Root cause
The historical Python preprocessing/write path produced a high allocator
RSS while ingesting very wide IPC batches. The current Scannable writer
releases each input buffer when `Table.add()` completes; remaining RSS
is allocator high-water rather than a live Arrow reference. The resolved
behavior had no regression coverage, so a future native lifetime
regression could silently reintroduce the original failure mode.
## Validation
- `uv run --extra tests --extra dev maturin develop`
- `uv run --project python --extra tests pytest
python/python/tests/test_table.py::test_add
python/python/tests/test_table.py::test_add_releases_arrow_buffers_without_gc
-q`
- `uv run --project python --extra dev ruff format --check
python/python/tests/test_table.py`
- `uv run --project python --extra dev ruff check .`
Fixes#2512
<!-- lance-gatekeeper-fix:v1 agent=29226408a8d07da592daf341d5384e37
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add an end-to-end Python regression for pandas DataFrame inputs merged
into a table created from a Pydantic model
- verify reordered, nullable Arrow source fields can update and insert
into a non-nullable target schema when the values contain no nulls
## Root cause
Lance merge_insert previously compared source schema nullability with
the target, unlike add. The upstream fix now pinned by LanceDB ignores
declared nullability during schema compatibility and validates actual
null values at write time. LanceDB lacked regression coverage for the
full pandas-to-Pydantic path, so this test locks in the correct behavior
without falsifying the input schema nullability.
## Validation
- 5 focused merge-insert tests passed
- Ruff lint passed for the repository
- Ruff format check passed for the changed file
- git diff --check passed
Fixes#2366
<!-- lance-gatekeeper-fix:v1 agent=f897fccfa206620c8a2acdc3bcd1c21f
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- capture the stale-index state behind the reported fixed-size-binary
panic: the vector and FTS indices cover newer fragments while the BTree
prefilter does not
- verify vector, FTS, and hybrid searches return matches from both
scalar-indexed and unindexed fragments without panicking
- preserve binding-level coverage for the Lance fix in
https://github.com/lance-format/lance/pull/3768, which restricts
incomplete scalar prefilters when search indices are further ahead
The production root cause is in Lance and the current LanceDB dependency
already contains that fix, so this change adds the missing LanceDB
Python regression coverage.
## Validation
- `cd python && uv run --no-sync pytest
python/tests/test_hybrid_query.py::test_hybrid_query_with_stale_fixed_size_binary_prefilter
-q`
- `cd python && uv run --no-sync pytest
python/tests/test_hybrid_query.py -q`
- `python/.venv/bin/ruff check .`
- `python/.venv/bin/ruff format --check
python/python/tests/test_hybrid_query.py`
Fixes#2370
<!-- lance-gatekeeper-fix:v1 agent=5d16e59b9e513fd9247e0698732fa283
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- cover explicit FixedSizeList schemas populated from Float32Array
values
- verify the original vector.0 failure stays fixed across Arrow 15, 16,
17, and 18
## Root cause and fix
In v0.16, schema subset inference treated typed-array vectors as nested
objects and looked up numeric paths such as vector.0, which do not exist
in a FixedSizeList schema. Current typed-array handling correctly
recognizes ArrayBuffer views as vector values instead of traversing
their elements. This change adds the missing regression coverage for the
reported explicit-schema path so that behavior cannot regress unnoticed.
## Validation
- pnpm test __test__/arrow.test.ts --runInBand
- pnpm lint
- pnpm build
- pnpm run docs
- pnpm test --runInBand (681 passed, 5 skipped)
Fixes#2134
<!-- lance-gatekeeper-fix:v1 agent=1d548cb70f6df110ce0a5b119395b52a
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add fast regression coverage for VoyageAI `voyage-3` source embeddings
- verify table text uses `client.embed` and never
`client.multimodal_embed`
## Root cause
The original VoyageAI source-embedding path treated table source values
as images and always invoked the multimodal API. Production routing was
corrected by later merged changes, but the table regression was covered
only by API-gated slow tests. This test locks the corrected text routing
into the regular unit suite.
## Validation
- `cd python && uv run --extra tests pytest
python/tests/test_voyageai_embeddings.py -q`
- `uv run --project python --extra tests --extra dev ruff format --check
python/python/tests/test_voyageai_embeddings.py`
- `uv run --project python --extra tests --extra dev ruff check .`
Fixes#2059
<!-- lance-gatekeeper-fix:v1 agent=49b9e2daeed95a78ce827e2bf90abda0
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- pass an Instructor-compatible `[instruction, text]` pair when
detecting embedding dimensions
- add a regression test that verifies the dimension probe uses the
configured source instruction
## Root cause
`InstructorEmbeddingFunction.ndims()` encoded a bare string even though
Instructor models require instruction/text pairs. With affected
`sentence-transformers` versions, the bare input omitted
`instruction_mask` and raised `KeyError` while defining the LanceDB
schema.
## Validation
- `uv run --extra tests pytest python/tests/test_embeddings.py -q` (`14
passed, 9 skipped`)
- `uv run --project python --extra tests --extra dev ruff format --check
python/python/lancedb/embeddings/instructor.py
python/python/tests/test_embeddings.py`
- `uv run --project python --extra tests --extra dev ruff check .`
Fixes#2041
<!-- lance-gatekeeper-fix:v1 agent=4b05e0d9f3eef17bccfb446e788294f4
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add a Python regression for vector search over a sliced Arrow table
with nullable scalar columns
- verify the nearest row retains its non-null score values after the
table is written
## Root cause
Lance 0.19.2 deep-copied a validity bitmap without preserving its
non-zero bit offset. For a sliced nullable table, scalar values and
vectors began at the slice while the copied validity bitmap began at the
parent table's first row. That made valid score values appear null even
though the corresponding vector stayed intact. The upstream Lance repair
is already present in the current dependency; this adds a LanceDB-level
guard for the reported create/search path.
## Validation
- reproduced on Python 3.12 with LanceDB 0.16.0, pylance 0.19.2, PyArrow
18.0.0, and Polars 1.14.0
- `uv run --project python --extra dev ruff format --check
python/python/tests/test_table.py`
- `uv run --project python --extra dev ruff check .`
- `cd python && uv run --extra tests pytest
python/tests/test_table.py::test_search_preserves_nulls_from_sliced_arrow_table
-q`
Fixes#1879
<!-- lance-gatekeeper-fix:v1 agent=bfa0551793f8e3cf3980cf64ad89908a
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
## Summary
- make the existing #1968 regression explicitly assert that schema-only
table creation succeeds
- verify the new table has zero rows and preserves the requested
fixed-size vector schema before accepting subsequent data
## Root cause
In v0.16.0, schema-only table creation sent an empty table through
vector sanitization, which calculated a remainder using `len(data)` and
raised `ZeroDivisionError`. Later refactors removed that runtime path,
but the issue-specific regression only asserted the final row count
after a subsequent add. This change makes the reported operation and its
expected empty-table state explicit so the original defect remains
directly covered.
## Validation
- `uv run --extra tests pytest
python/tests/test_table.py::test_create_table_without_data_with_vector_schema
-q`
- `uv --project python run --extra tests --extra dev ruff format --check
python/python/tests/test_table.py`
- `uv --project python run --extra tests --extra dev ruff check .`
- `git diff --check`
Fixes#1968
<!-- lance-gatekeeper-fix:v1 agent=b8ec6f40f4bba2f9beeaaae12233e5c4
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
## Summary
- align the PyO3 runtime and build ABI floor with the declared Python
3.10 minimum
- add a regression test that keeps both ABI features synchronized with
`requires-python`
## Root cause
The Python 3.10 support-floor update originally changed PyO3 to
`abi3-py310`, but a later dependency update reverted both PyO3 features
to `abi3-py39`. Published Windows wheels were consequently tagged
`cp39-abi3` while importing `PyCMethod_New`, a stable-ABI procedure
absent from CPython 3.9.0 and 3.9.1. Windows reports that mismatch as
“The specified procedure could not be found” while loading `_lancedb`.
Restoring `abi3-py310` makes the wheel tag and native imports agree with
the package metadata and prevents future wheels from advertising
unsupported Python 3.9 compatibility.
## Validation
- `uv run --extra tests pytest python/tests/test_package_metadata.py -q`
- `uv run --extra tests --extra dev ruff format --check .`
- `uv run --extra tests --extra dev ruff check .`
- `cargo fmt --all`
- `cargo check --quiet -p lancedb-python`
- `uvx --from maturin==1.12.4 maturin build --profile ci` (built
`lancedb-0.37.1b0-cp310-abi3-manylinux_2_34_x86_64.whl`)
Fixes#2051
<!-- lance-gatekeeper-fix:v1 agent=d66c984498190d2207d1c5126cba5047
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- expand the synchronous debugger regression to enumerate every exposed
connection attribute while the Python background loop is unavailable
- retain direct representation checks for connections and tables
## Root cause
VS Code debugpy suspends Python threads at a breakpoint and inspects
local variables. Connection representation and property access
previously dispatched asynchronous work to LanceDBBackgroundEventLoop
and waited for the suspended loop thread, deadlocking the debugger. The
production safeguards landed in #3620 and #3788; this regression
exercises debugger-style whole-object expansion so a newly exposed
property cannot reintroduce the original failure.
## Validation
- uv run --no-sync pytest
python/tests/test_db.py::test_sync_debugger_inspection_does_not_use_background_loop
python/tests/test_db.py::test_read_consistency_interval_does_not_use_background_loop
-q (2 passed)
- uv run --no-sync pytest python/tests/test_db.py -q (48 passed)
- python/.venv/bin/ruff format --check python/python/tests/test_db.py
- python/.venv/bin/ruff check .
- git diff --check
Fixes#3611
<!-- lance-gatekeeper-fix:v1 agent=cdf4b39b2ce2ccb3eb5fe501acae77bb
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- replace the synthetic registry-variable metadata test with the OpenAI
embedding function reported in #2387
- verify the resolved API key survives table metadata reconstruction
- assert the OpenAI client receives the resolved key while serialized
metadata retains the variable reference
## Root cause
LanceDB 0.22.0 reconstructed embedding functions from table metadata
with the model constructor, bypassing EmbeddingFunction.create and
leaving the literal $var:api_key placeholder in OpenAI configuration.
The production path was corrected for duplicate #2181 by #2640; this
change gives that fix direct, network-free OpenAI regression coverage
for #2387.
## Validation
- uv run --extra tests pytest python/tests/test_embeddings.py -q (13
passed, 9 skipped)
- uv run --project python --extra dev ruff check .
- uv run --project python --extra dev ruff format --check
python/python/tests/test_embeddings.py
- git diff --check
Fixes#2387
<!-- lance-gatekeeper-fix:v1 agent=d453b1b9b2a298a776f2e4ea1b1449b5
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- add Python regression coverage for an IVF build that cannot form all
requested non-empty partitions
- verify hierarchical k-means returns an actionable RuntimeError instead
of panicking or silently creating a degenerate index
- exercise the current Lance v10.1.0-beta.1 dependency, which contains
the upstream error-return fix
## Root cause
Hierarchical k-means previously guarded a shortfall in generated
clusters with only a debug assertion. Debug builds panicked, while
release builds could silently publish an index with many empty
partitions. The upstream Lance fix now returns a descriptive error and
is already included in the dependency pinned on main; this test locks in
propagation through the LanceDB Python API.
## Validation
- uv run --extra tests pytest python/tests/test_index.py -q (24 passed)
- uv run --extra tests pytest
python/tests/test_index.py::test_create_ivf_index_reports_unsplittable_partitions
-q (1 passed)
- python/.venv/bin/ruff format python/python/tests/test_index.py
- python/.venv/bin/ruff check .
- git diff --check
Fixes#3649
<!-- lance-gatekeeper-fix:v1 agent=a4d34448a9d350a3e2e659f33f5db6f2
generation=1 -->
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
<!-- lance-gatekeeper-fix:v1 agent=5c80c44c083b3b8ad0da595419d468fc
generation=1 -->
## Root cause
The legacy synchronous Python table called `delete` on a shared, mutable
`lance.Dataset`. Concurrent table operations could hold a PyO3 borrow
while delete requested an exclusive borrow, producing `RuntimeError:
Already borrowed`. The current async-backed binding fixes this by
cloning its thread-safe Rust table handle before awaiting, but that
concurrency contract had no regression coverage.
## Fix
- Document why delete must clone the Rust table handle before entering
its async future.
- Add a barrier-synchronized regression test that deletes distinct rows
through one shared table from eight Python threads.
- Verify every delete commits exactly one row, every commit gets a
distinct version, and no rows remain.
## Validation
- `cargo check --quiet --features remote --tests --examples`
- `cargo fmt --all -- --check`
- `uv run --extra tests --extra dev ruff format --check
python/tests/test_table.py`
- `uv run --extra tests --extra dev ruff check
python/tests/test_table.py`
- `uv run --extra tests --extra dev pytest
python/tests/test_table.py::test_concurrent_deletes_are_thread_safe
python/tests/test_table.py::test_delete
python/tests/test_table.py::test_delete_expr
python/tests/test_table.py::test_delete_expr_async -q` (4 passed)
- Manual stress reproduction: 100 concurrent deletes on one table
completed at versions 2–101 with zero rows remaining.
Fixes#530
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## Summary
- cache the immutable read consistency interval on synchronous
connection wrappers
- keep debugger property expansion from dispatching to the background
event loop
- cover direct connections and wrappers reconstructed from native
connections
## Root cause
The debugger expands connection variables by evaluating properties after
suspending all Python threads.
`LanceDBConnection.read_consistency_interval` dispatched a coroutine to
`LanceDBBackgroundEventLoop` and synchronously waited for it, but that
loop thread was also suspended, causing a deadlock.
## Validation
- `uv run --no-sync pytest python/tests/test_db.py -q` (48 passed)
- `ruff format --check python/python/lancedb/db.py
python/python/tests/test_db.py`
- `ruff check .`
- `git diff --check`
Fixes#3773
<!-- lance-gatekeeper-fix:v1 agent=e2e612236d722d926f64245d3f682bbc
generation=1 -->
---------
Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
## 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>
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.
## Summary
`LanceMergeInsertBuilder.when_not_matched_by_source_delete()` didn't
clear a previously-set condition when called again with no argument (or
a different condition type). Per the docstring, `condition=None` means
"delete all unmatched rows," but if the builder had already been
configured with a string/Expr condition, a later no-arg call left the
stale condition in place instead of widening the delete to
unconditional.
Fixes#3767
## Change
Each call now unconditionally sets both
`_when_not_matched_by_source_condition` and
`_when_not_matched_by_source_condition_expr` (one to the new value, the
other to `None`), so the latest call always wins — consistent with every
other setter on this builder (e.g.
`when_matched_update_all(where=...)`).
## Test plan
- [x] New regression test
`test_merge_insert_by_source_delete_reconfigure` in
`python/python/tests/test_table.py`
- [x] `uv run --extra tests pytest
python/tests/test_table.py::test_merge_insert_by_source_delete_reconfigure
python/tests/test_table.py::test_merge_insert_by_source_delete_expr
python/tests/test_table.py::test_merge_insert_by_source_delete_expr_async
-vv` — 3 passed
- [x] `uv run --extra dev ruff format` / `ruff check` — clean
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
## Summary
`LanceHybridQueryBuilder._create_query_builders()` checked
`self._minimum_nprobes` for truthiness instead of `is not None` — the
very next line correctly checks `is not None` for
`self._maximum_nprobes`. Since `0` is falsy in Python,
`.minimum_nprobes(0)` on a hybrid query silently dropped the value
instead of forwarding it to the vector sub-query, where it would raise
the same `ValueError` a plain vector query raises for the same input
(`minimum_nprobes must be greater than 0`, validated in
`rust/lancedb/src/query.rs` and covered for the plain-query path by
`test_invalid_nprobes_sync`).
Fixes#3766
## Change
One-line fix: `if self._minimum_nprobes:` → `if self._minimum_nprobes is
not None:`, matching the existing `maximum_nprobes` check right below
it.
## Test plan
- [x] New regression test
`test_hybrid_query_minimum_nprobes_zero_raises` in
`python/python/tests/test_hybrid_query.py`
- [x] `uv run --extra tests pytest python/tests/test_hybrid_query.py
-vv` — 13 passed
- [x] `uv run --extra dev ruff format` / `ruff check` — clean
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
## 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>
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>
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
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>
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>
`RemoteDBConnection.open_table` accepts `storage_options` and never uses
it:
```python
def open_table(
self,
name: str,
*,
namespace_path: Optional[List[str]] = None,
storage_options: Optional[Dict[str, str]] = None,
index_cache_size: Optional[int] = None,
...
) -> Table:
...
if index_cache_size is not None:
logging.info("index_cache_size is ignored in LanceDb Cloud ...")
table = LOOP.run(self._conn.open_table(name, namespace_path=namespace_path))
```
The value is never passed down and never mentioned. `index_cache_size`
is ignored on Cloud in the
same way, but it says so.
I checked this at runtime on 0.34.0, not just by reading it: swapping
the inner connection for a
recorder, `open_table("t", storage_options={...})` hands the layer below
`['namespace_path']` and
nothing else, no log record is emitted, and the same probe shows
`index_cache_size` producing its
message as expected.
This adds the matching log line, so the two ignored parameters behave
the same way. `ruff check` and
`ruff format --check` are clean on the file.
A note on severity. This is not a security hole and nothing is exposed.
Someone passing credentials
there gets silence instead of an error, and finds out later.
One thing I am unsure about, and it changes the fix. I have assumed
per-table storage options are
meaningless on Cloud, which is what the `index_cache_size` line next to
it implies about managed
storage. If they are supposed to work, then the right change is to pass
them through to
`self._conn.open_table` instead and this patch is the wrong one. Happy
to redo it that way.
I did not check whether `create_table` or the async connection have the
same gap.
`docs/src/python/python.md` is the whole Python API reference, but it is
maintained by hand and had drifted from the public API. Anything not
listed there simply doesn't get rendered, so a number of public,
documented, tested APIs were invisible to users — most notably branch
management, where `diff` and `merge` live.
I audited every public symbol reachable from `lancedb` and its
subpackages against the `:::` directives on the page. This adds the
missing ones:
- **Branching** — `Branches`, `AsyncBranches` (`list` / `create` /
`checkout` / `delete` / `diff` / `merge`)
- **Tables** — `TableStatistics` (returned by `Table.stats()`; the
fragment-level stats classes were already listed)
- **Full text queries** — `FullTextQuery`, `MatchQuery`, `PhraseQuery`,
`BoostQuery`, `MultiMatchQuery`, `BooleanQuery`, `FullTextOperator`,
`Occur`
- **Querying** — `LanceEmptyQueryBuilder`, `LanceTakeQueryBuilder`,
`AsyncTakeQuery`
- **Indices** — `Fm` (the FM-index for substring search), `IndexConfig`
- **Blobs** — `blob`, `BlobType`, `BlobFile`
- **Namespaces** — `connect_namespace`, `connect_namespace_async`, and
both namespace connection classes
- **Remote config** — `TlsConfig`, `HeaderProvider`, `OAuthConfig`,
`OAuthFlowType`
- **Rerankers** — the `Reranker` base class plus `JinaReranker`,
`RRFReranker`, `MRRReranker`, `AnswerdotaiRerankers`,
`VoyageAIReranker`, `WatsonxReranker` (5 of 12 were listed)
- **Embeddings** — `get_registry`, `register`, and the 14 embedding
functions that were missing (3 of 17 were listed)
- **PyTorch** — `StreamingDataset` and the permutation API it is built
on
- **Misc** — `Session`, `tokenize`, `FtsToken`, `pydantic.Vector`,
`pydantic.MultiVector`, `instrument_lancedb_metrics`, and the two
exception types
It also repairs cross-references in docstrings that no longer resolve:
links into guide pages that have since moved to lancedb.com
(`querying-an-ann-index`, `experimental-full-text-search`),
`lance.dataset` references with no inventory behind them, and the
relative targets `[Table](Table)` and `[PyArrow Table](pyarrow.Table)`.
Deliberately left out: concrete implementation classes reached through
their abstract base (`LanceTable`, `LanceDBConnection`,
`RemoteDBConnection`), query base classes already covered by
`inherited_members: true`, and internal plumbing such as
`FullTextSearchQuery` and `ColumnOrdering`.
## Testing
The docs job only runs on pushes to `main`, so I built the site locally
and compared against a build of `upstream/main`: every added entry
resolves, and no symbol that was rendered before stopped being rendered
when the four packages moved to automodule. `mkdocs build --strict`
exits 0 on this branch, against 61 warnings on `main`.
## Also in this PR
`lancedb.index`, `lancedb.embeddings`, `lancedb.remote` and
`lancedb.rerankers` are now rendered by a single mkdocstrings directive
each, driven by the module's `__all__`, rather than a hand-maintained
list. These four are where most of the drift was, and `__all__` is
harder to forget than a docs page. `lancedb.embeddings` had no
`__all__`; without one mkdocstrings renders no members at all for a
re-export package, so one is added. AGENTS.md gains a section on how the
page is wired up and how to build the docs locally.
Rendering all that code for the first time surfaced ~100 more build
warnings, which would have made #3707 (turning on `mkdocs build
--strict`) harder to land, so the warning backlog is cleared here too.
97 of the 158 warnings were one systematic false positive — griffe
cannot see the generated `__init__` of a pydantic dataclass, so every
documented parameter looks unknown — switched off via
`warn_unknown_params`. The remaining 61 came from 15 docstrings with
real bugs: prose trailing a `Parameters` section (we were rendering
parameters called `The`, `you` and `To`), types dropped because numpydoc
needs spaces around the colon, `num_partitions, default sqrt(num_rows)`
parsing as a list of names and inventing a `default` parameter, and one
parameter indented five spaces. `mkdocs build --strict` now exits 0.
---
#3747 (the coverage test that keeps this from happening again) is
stacked on this branch, so review it after this one.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
In the current LanceDB usage implementation, there is no way to check
whether a table or namespace already exists. This PR introduces the
namespace_exists and table_exists methods to determine the existence of
tables and namespaces.
useage like this:
```
# check table exists
db.table_exists(table_id=['xxx'])
# check namespace exists
db.namespace_exists(namespace_id=['xxx'])
```
fixes: #3419
---------
Signed-off-by: farmer <farmerchillax@outlook.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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>
`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
## 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.
## Summary
- keep the existing synchronous `connect()` path unchanged
- make `LanceDBConnection.__repr__` and `LanceTable.__repr__`
side-effect-free
- add a regression test that verifies sync reprs do not call the Python
background loop
## Root cause
The freeze is caused by debugger rendering, not by `connect()` itself:
1. debugpy stops at a breakpoint and suspends all Python threads.
2. The debugger renders the new `db_connection` local by calling
`repr()`.
3. `LanceDBConnection.__repr__` reads `read_consistency_interval`.
4. That property calls `LOOP.run(...).result()`.
5. The `LanceDBBackgroundEventLoop` thread is suspended by the debugger,
so `repr()` waits for a thread that cannot run.
This explains why the symptom appears immediately after `connect()`: it
is the first point where a connection object exists in locals and is
automatically rendered. `LanceTable.__repr__` had the same problem
because it also read the connection's consistency interval.
This follows the same principle as #3411: `__repr__` must not trigger
async work or I/O that a debugger assumes is lightweight.
## Evidence
I reproduced the behavior with the real LanceDB classes and debugpy
1.8.21 using a DAP client:
- latest `main` (`ff6ff099`): the debugger reported `allThreadsStopped:
true`, and evaluating `repr(db_connection)` timed out
- this branch (`5755a5ba`): the same evaluation returned
`LanceDBConnection(uri='/tmp/lancedb-debug-repro')` immediately
- setting `PYDEVD_UNBLOCK_THREADS_TIMEOUT=0` also allowed the original
repr path to complete, independently confirming that it was waiting on a
suspended thread
The regression test creates a connection and table, replaces `LOOP.run`
with a function that fails, and verifies that both reprs still work.
## Validation
- `maturin develop --manifest-path python/Cargo.toml`
- `python -m pytest
python/python/tests/test_db.py::test_sync_repr_does_not_use_background_loop
python/python/tests/test_table.py::test_consistency -q` (`4 passed`)
- `ruff check .`
- `ruff format --check python/python/lancedb/db.py
python/python/lancedb/table.py python/python/tests/test_db.py
python/python/tests/test_table.py`
- `git diff --check`
Refs #3611.
`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
## 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>
## Summary
- add a keyword-only `transform_parallelism` option to
`StreamingDataset`
- preserve CPU auto-detection by default and fall back to one worker
when unavailable
- apply the configured limit to both the transform executor and
concurrency semaphore
- document and test explicit, default, fallback, and invalid values
## Testing
- `uv run --extra tests --with torch pytest
python/tests/test_elastic_dataloader.py -q` (`136 passed`)
- `uvx ruff check python/lancedb/streaming.py
python/tests/test_elastic_dataloader.py`
- `uvx ruff format --check python/lancedb/streaming.py
python/tests/test_elastic_dataloader.py`
- `git diff --check origin/main...HEAD`
Closes#3695
Co-authored-by: buduoqiu <yaodong-shen@users.noreply.github.com>
Standard GitHub-hosted runners are free on public repos, so all Actions
spend here is on the `*-8x-*` / `4x` larger runners. Measured over 30
days at current (post-Jan-2026) larger-runner rates, that is ~$1,400/mo,
and `npm-publish` is ~70% of it.
## Changes
**Fat LTO was forcing builds onto large runners.** `[profile.release]`
in `.cargo/config.toml` sets `lto = "fat"` with `codegen-units = 1`,
which is single-threaded and the peak-memory step. The macOS
`npm-publish` build was 111 of its 113 minutes in one `napi build` step,
making it the critical path of the whole publish pipeline. The ThinLTO
override already applied to Windows now covers macOS too, and both
Windows builds move from `windows-2025-8x-x64` to the free standard
`windows-2025`.
**The npm-publish cargo cache never existed.** There are zero caches
with its key prefix. The key was static, so `actions/cache` (which only
writes on a miss) could never refresh it, and a multi-GB release
`target/` per target could never fit the repo's 10 GB budget anyway. Now
caches only the crate registry, keyed on `Cargo.lock`. The docker builds
also mounted `.cargo/registry/*` while the cache saved `.cargo-cache`,
so containers re-downloaded the registry every run.
**Cache eviction thrash.** Repo cache usage is 10.4 GB against GitHub's
10 GB cap, so every PR run evicted main's warm entries. `rust.yml` and
`nodejs.yml` now restore everywhere but only save from `main`.
**npm-publish moves to nightly + tags** instead of every push to main
(~90/month). The cross-compiled targets do need watching, so
`report-failure` now fires on scheduled runs, and dedupes onto an
existing open issue rather than filing one per night.
**rust.yml aarch64-pc-windows-msvc** cross-compiled its tests and then
skipped them, paying full codegen and link cost for a compile check.
`windows-11-arm` is now GA and free on public repos, so it builds and
tests natively. Its test step also passes `--target` — without it cargo
used `target/ci/` rather than `target/<triple>/ci/` and rebuilt the
entire dependency graph a second time.
**pypi-publish.yml had no concurrency group**, so force-pushes left a
~74 minute Windows job running.
## What is cost vs. wall-clock
| Change | Cost | Wall-clock |
|---|---|---|
| Windows npm-publish → free runners | **−$570/mo** | slower per job
(8→4 cores) |
| npm-publish nightly | **−$125/mo** | — |
| pypi-publish concurrency | small | — |
| macOS ThinLTO | $0 (already free) | **−~50 min** per release |
| rust aarch64 Windows native | $0 (already free) | **−~25 min** |
| rust `--target` on test step | $0 | large, avoids a second full build
|
| rust-cache `save-if` | small | faster via real cache hits |
## Risks
- The two Windows builds now have 4 cores instead of 8 and ~14 GB of
free disk. If they fail, it is most likely disk rather than memory;
fallback is `windows-2025-4x-x64`, which still halves that line.
- `windows-11-arm` has a thinner toolset (choco/vcpkg/protoc under
emulation) and this enables a test step that has never run, so it may
surface real aarch64 failures. That is the point, but it is the change
most likely to need iteration.
- ThinLTO applies to published macOS and Windows binaries, typically
within a few percent of fat LTO. Linux release builds are untouched.
## Follow-ups
- `python.yml` `pydantic1x` (37 min) and `Doctest` (33 min) each rebuild
the extension from source via `pip install -e .` with no Rust cache;
they should consume the wheel the `linux` job already builds. Worth
~$235/mo and ~70 min of compute per run. Separate PR.
- The three `ubuntu-2404-8x-x64` npm-publish builds (~$420/mo at the old
cadence) are the remaining large-runner spend;
`aarch64-unknown-linux-gnu` could run natively on free
`ubuntu-24.04-arm`. Worth doing after this lands so the ThinLTO change
can be validated first.
- The wheel composite actions declare `python-minor-version` as required
but never use it, and every caller omits it (actionlint warns).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`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.)
## 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.
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.)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v10.0.0-beta.5. No compatibility fixes were required;
full-workspace Clippy passes with warnings denied. Lance tag:
https://github.com/lance-format/lance/releases/tag/v10.0.0-beta.5
## 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`.
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
## 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>
## What
`AnswerdotaiRerankers(return_score="all").rerank_hybrid(...)` (and
`ColbertReranker`, which subclasses it without overriding
`rerank_hybrid`) raises:
```
pyarrow.lib.ArrowInvalid: Invalid sort key column: No match for FieldRef.Name(_relevance_score) in _rowid: int64 ...
```
## Why
```python
combined_results = self.merge_results(vector_results, fts_results)
combined_results = self._rerank(combined_results, query)
if self.score == "relevance":
combined_results = self._keep_relevance_score(combined_results)
elif self.score == "all":
combined_results = self._merge_and_keep_scores(vector_results, fts_results)
```
When `score == "all"`, `combined_results` is unconditionally overwritten
by `_merge_and_keep_scores(vector_results, fts_results)` **after**
`_rerank()` already computed and appended `_relevance_score` —
discarding it. The following `sort_by("_relevance_score", ...)` then has
nothing to sort on.
Every sibling reranker that supports `return_score="all"`
(`cross_encoder`, `openai`, `cohere`, `jinaai`, `voyageai`, `watsonx`)
instead calls `_merge_and_keep_scores()` **before** `_rerank()`. This
file is the one place the ordering got inverted when `"all"` support was
added (#2509) — a copy/paste inconsistency across the six files that PR
touched. Fix mirrors the pattern already used (and tested) by the other
five rerankers.
Also drops the now-stale `"Only 'relevance' is supported for now"`
docstring line on both classes, left over from before `"all"` support
existed.
## Testing
Added `test_answerdotai_reranker_return_all`, mirroring the existing
`test_cross_encoder_reranker_return_all`. Verified locally with the real
built Rust extension: red (reproduces the exact `ArrowInvalid` above) →
green, using the actual `rerank_hybrid`/`_rerank`/`base.py` code path
with the model call mocked out — my local environment's
`rerankers==0.10.0` fails to load the real ColBERT model against the
available `transformers` version (`AttributeError: 'ColBERTModel' object
has no attribute 'all_tied_weights_keys'`), which I confirmed also
breaks the **pre-existing**, unmodified
`test_colbert_reranker`/`test_answerdotai_reranker` baseline tests
identically — an unrelated local dependency-version issue, not a
regression from this change. `ruff check`/`ruff format` clean; full
`test_rerankers.py` run: 9 passed / 8 skipped / 3 failed (the 3 failures
are exactly those two pre-existing tests plus my new one, all failing at
model-loading time for the same unrelated reason before reaching the
changed code).
---
Disclosure: this PR was drafted with AI assistance (Claude); I reviewed,
tested, and take responsibility for the change.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
## Summary
This updates the Java API reference to close the documentation gaps that
can be fixed from the current Java source and generated namespace API.
The patch adds an empty table example, shows how to wrap returned Arrow
IPC query bytes in a reusable `ArrowFileReader` helper, and documents
the Java index operations that are currently exposed by the namespace
client: vector indexes, scalar indexes, full text search indexes, and
listing indexes.
## Issue Links
Fixes https://github.com/lancedb/docs/issues/157
Fixes https://github.com/lancedb/docs/issues/160
Partially addresses https://github.com/lancedb/docs/issues/159 by
documenting the index parameters currently exposed by Java. The
requested `num_partitions` example is still blocked because
`CreateTableIndexRequest` does not expose IVF training parameters yet.
Not included: https://github.com/lancedb/docs/issues/158. The current
Java docs and source remain remote namespace oriented, so local DB
connection documentation should wait until the Java local DB API is
available and can be verified.
## Validation
- Built the Java core module with OpenJDK 17:
`./mvnw -pl lancedb-core -am -DskipTests compile`
- Checked the Markdown diff:
`git diff --check -- docs/src/java/java.md`
The Java build succeeds. It still reports pre-existing checkstyle
warnings in the namespace client builder, but the Maven build is green.
## Summary
- reconstruct foreign Arrow Map schemas from their single sanitized
entries field
- reject malformed Map types with anything other than one child
- preserve the complete Map schema and `keysSorted` value through
empty-table creation and IPC round trips across Arrow 15–18
## Testing
- `./node_modules/.bin/jest --runInBand __test__/arrow.test.ts
__test__/sanitize.test.ts`
- `pnpm lint`
- `pnpm build`
- `pnpm run docs`
Fixes#2337
## What
- Replace legacy model names in `WatsonxEmbeddings` with the current
supported set:
- `ibm/granite-embedding-278m-multilingual` (new default, 768-dim)
- `ibm/slate-125m-english-rtrvr-v2` (768-dim)
- `ibm/slate-30m-english-rtrvr-v2` (384-dim)
- `intfloat/multilingual-e5-large` (1024-dim)
- `sentence-transformers/all-minilm-l6-v2` (384-dim)
- Add `space_id` field — mutually exclusive with `project_id`, mirrors
the
existing pattern in `WatsonxReranker`
- `project_id` / `space_id` resolution now falls back to
`WATSONX_PROJECT_ID` /
`WATSONX_SPACE_ID` env vars; exactly one must be supplied
## Why
The previously hardcoded models (`ibm/slate-125m-english-rtrvr`,
`sentence-transformers/all-minilm-l12-v2`) are legacy and no longer
listed as
supported by the watsonx.ai platform. `space_id` scoping was already
supported
by `WatsonxReranker` but was missing from the embeddings counterpart.
---------
Co-authored-by: Will Jones <willjones127@gmail.com>
Updates the Rust workspace Lance dependencies and Java lance-core from
v9.1.0-beta.5 to v9.1.0-beta.7, including the generated Cargo lockfile.
No LanceDB compatibility changes were required for this release. See the
[Lance v9.1.0-beta.7
release](https://github.com/lance-format/lance/releases/tag/v9.1.0-beta.7).
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>
## 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>
## Summary
Fixes#1653.
`infer_vector_column_name` in `util.py` could silently return `None`
when `query is None` and `query_type` is not `"fts"` or `"hybrid"`. This
`None` then propagated into downstream code, causing a cryptic
`TypeError: expected bytes, NoneType found` rather than a clear error
message.
## Changes
- **Removes the no-op `try/except Exception as e: raise e`** around
`inf_vector_column_query` (it was catching and immediately re-raising
without adding any value)
- - **Adds a `None` guard** after the inference block: if
`vector_column_name` is still `None` at this point, raise a clear
`ValueError` pointing the user to pass `vector_column_name` explicitly
## Before / After
**Before:** cryptic `TypeError: expected bytes, NoneType found` deep in
schema lookup code
**After:**
```
ValueError: No vector column found in the schema. Please specify the vector column name explicitly via the `vector_column_name` parameter.
```
---------
Co-authored-by: Will Jones <willjones127@gmail.com>
Some additions to our lancedb skill to enable agents to use the jobs
methods that we recently added. Eval tests (below, with and without
these additions to the skill) suggest that they're helping, mostly to
find the right method calls. These are a little unusual because they
require REST server connection, they're not yet implemented in the SDKs.
```
┌─────────────────────┬───────────┬────────────┬─────────────┬──────────┬───────────┬──────────┬───────────┐
│ eval │ grade w/o │ grade with │ improvement │ time w/o │ time with │ cost w/o │ cost with │
├─────────────────────┼───────────┼────────────┼─────────────┼──────────┼───────────┼──────────┼───────────┤
│ 8-list-running-jobs │ 2.5/3 │ 3/3 │ +0.5 │ 123s │ 29s │ $0.58 │ $0.18 │
├─────────────────────┼───────────┼────────────┼─────────────┼──────────┼───────────┼──────────┼───────────┤
│ 9-describe-job │ 1/5 │ 5/5 │ +4.0 │ 159s │ 52s │ $0.62 │ $0.25 │
├─────────────────────┼───────────┼────────────┼─────────────┼──────────┼───────────┼──────────┼───────────┤
│ 10-cancel-job │ 3/3 │ 3/3 │ +0.0 │ 99s │ 35s │ $0.55 │ $0.21 │
├─────────────────────┼───────────┼────────────┼─────────────┼──────────┼───────────┼──────────┼───────────┤
│ TOTAL │ 6.5/11 │ 11/11 │ +4.5 │ 381s │ 116s │ $1.75 │ $0.65 │
└─────────────────────┴───────────┴────────────┴─────────────┴──────────┴───────────┴──────────┴───────────┘
```
Failure reasons are because the agent didn't know the right method to
call, spent all its turns guessing REST calls, tried to inspect lancedb
code, but didn't find the answer in here.
## Problem
`table.add(dataset)` with a `pyarrow.dataset.Dataset` OOMs the client
during bulk ingestion of wide rows (e.g. embedding columns), even
against a remote table where the upload itself is streaming.
The cause is in `to_scannable`: a `Dataset` is scanned with pyarrow's
default scanner settings (`batch_size=131072` rows,
`batch_readahead=16`, `fragment_readahead=4`). pyarrow's internal
threads prefetch that read-ahead window independently of LanceDB's
backpressure, so for wide rows a large fraction of the dataset is held
in memory. On the remote path this is then multiplied across the
multipart write partitions (one in-flight batch per partition, up to
CPU-core count).
Reproduced on a 10 GB / 1.55M-row dataset with two 768-dim float32
embeddings: peak client RSS ~11.7 GB for the scan alone (6.8 GB after
consuming a *single* batch), ~15.4 GB for the full remote `add()`.
## Fix
`to_scannable` now sizes the scanner from an estimate of bytes-per-row
derived from the schema:
- **Narrow datasets keep pyarrow's defaults** (empty scanner kwargs) —
no throughput regression. The bound only engages above ~410 bytes/row.
- **Wide rows** get a smaller `batch_size` (~16 MiB/batch) and reduced
read-ahead (`batch_readahead=2`, `fragment_readahead=1`) so peak
in-flight memory stays near a ~1 GiB budget. Read-ahead (not just batch
size) has to drop, because pyarrow pins whole row-group buffers.
On the 10 GB dataset this drops peak client RSS to ~1.4 GB, and it stays
flat as the dataset grows. The `Dataset`/`LanceDataset` scannables
remain rescannable (retry-safe).
## Also: expose `write_parallelism` on `add()`
`AddDataBuilder::write_parallelism` already existed in Rust but was not
exposed in Python. This PR forwards it through the async, sync, and
remote `add()` methods, so users can cap the number of parallel write
partitions (each buffers data in flight) to trade throughput for memory
on large uploads.
## Tests
- `test_scannable.py`: bytes-per-row estimation; narrow → defaults; wide
→ bounded; `Dataset` reader streams bounded batches and stays
rescannable.
- `test_table.py`: `write_parallelism` on sync and async `add()`, and
that `write_parallelism=0` is rejected.
Fixes ENT-1883
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moves the skill from .agents/skills/lancedb to
plugins/lancedb/skills/lancedb, as recommended by codex and claude.
Install path now is:
### Codex/ChatGPT app
Codex: Plugins -> "Create" arrow -> Add plugin marketplace
search for lancedb plugin and install
### Codex CLI
```
codex plugin marketplace add lancedb/lancedb
codex plugin install lancedb@lancedb # name@marketplace
```
### Claude app
Settings -> Plugins -> Add -> Add marketplace
search for lancedb plugin and install
### Claude CLI
```
claude plugin marketplace add lancedb/lancedb
claude plugin install lancedb@lancedb
```
Here's how it looks on ChatGPT/Codex app:
(the main icon has light and dark modes; the smaller one on the skill
doesn't so I made it gray 🤷 )
<img width="764" height="560" alt="Screenshot 2026-07-16 at 2 49 24 PM"
src="https://github.com/user-attachments/assets/b82cda16-3392-4740-ac47-b2f187cb2655"
/>
Hi, and thank you for LanceDB.
Small CI supply-chain hardening. In `make-release-commit.yml`, the
release job checks out with `LANCEDB_RELEASE_TOKEN` (a push-capable PAT)
and its final step pushes the version tag using a third-party action
pinned to a **mutable branch**:
```yaml
- name: Push new version tag
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.LANCEDB_RELEASE_TOKEN }}
```
`@master` can move after review; whatever it points at then runs with
that release token in scope. This PR pins it to the commit behind the
current release (`v1.3.0` → `881a6320…`), keeping the version visible as
a comment. Behavior today is unchanged.
For transparency: I used AI assistance to spot and draft this; I
verified the workflow and resolved the SHA myself.
Tracks #3324. On x86_64 CPUs without AVX2 (Sandy Bridge / Ivy Bridge /
Westmere on Intel; Bulldozer / Piledriver / Steamroller on AMD), `import
lancedb` SIGILLs because the wheel bakes AVX2 + FMA into every compiled
function. Per [westonpace's
review](https://github.com/lancedb/lancedb/issues/3324#issuecomment-4328944354),
the default `lancedb` wheel stays fast; pre-Haswell users get a
separately-published `lancedb-compat` wheel.
## Summary
- Adds a `lancedb-compat` matrix entry to `pypi-publish.yml` that builds
with `RUSTFLAGS="-C target-cpu=x86-64-v2"` (Nehalem-class baseline).
Same Python API (`import lancedb` works) — files install to the same
namespace, so the two wheels conflict at install time and users pick
one. Same pattern as `psycopg2` / `psycopg2-binary` and `tensorflow` /
`tensorflow-cpu`.
- Generalizes `build_linux_wheel` and `upload_wheel` composites with
optional `package-name` and `rustflags` inputs (defaults preserve the
existing 4 `lancedb` matrix entries verbatim).
- Documents the choice in `python/README.md`: `pip install
lancedb-compat` for pre-Haswell hosts.
The default `.cargo/config.toml` baseline is unchanged.
## Sequencing
1. ~~lance-format/lance#6630 merges → runtime SIMD dispatch lands in
lance.~~ **Done — merged.**
2. lancedb's lance dep is bumped to a release that includes it (separate
PR / normal cadence).
3. This PR's `lancedb-compat` wheel build path starts producing a wheel
that runs on pre-Haswell hardware. **Maintainer setup**: register
`lancedb-compat` on PyPI and configure trusted publishing.
## Verified end-to-end on Sandy Bridge Xeon E5-2609
Verification was done locally against a fork-pinned lance dep that
includes the runtime dispatch implementation, using the same
`RUSTFLAGS="-C target-cpu=x86-64-v2"` flags this PR uses in CI:
```
$ RUSTFLAGS="-C target-cpu=x86-64-v2" maturin build --release
$ pip install ./target/wheels/lancedb-*.whl
$ python verify.py
PASS: import + simd dispatch + table create + vector search all work.
```
Pre-fix on the same CPU (default `pip install lancedb`): `Illegal
instruction (core dumped)`. Full reproducer (deps + clone + build +
verification):
https://gist.github.com/tobocop2/2e341358b55c143527416edfdb1e37df.
Fork-internal verification PR with the dep bump and full logs:
[`tobocop2/lancedb#2`](https://github.com/tobocop2/lancedb/pull/2).
## Benchmarks — no regressions on modern CPUs from the lance-side change
These are the numbers I ran for the lance PR, confirming the runtime
dispatch doesn't slow down the default (`target-cpu=haswell`) wheel that
existing users install. Criterion, one machine, one session, base → PR,
no `RUSTFLAGS` override. Full methodology, null experiments, and logs:
[lance-format/lance#6630 benchmark
comment](https://github.com/lance-format/lance/pull/6630#issuecomment-4933063394)
and the [logs
gist](https://gist.github.com/tobocop2/3c6d0f449cbd736aa2501f89a7fe56a2).
| benchmark | EPYC 7B13 (`avx2`, `fma`, no `avx512f`) | Xeon Cascade
Lake (`avx512f`) |
|---|---|---|
| `Cosine(f32, scalar)` *(control)* | +0.04% | +0.09% |
| `Cosine(f64, scalar)` | −0.34% | −1.94% |
| `Cosine(u8, SIMD)` | +2.30% | +3.63% |
| `Dot(f16, SIMD)` | −0.58% | +0.61% |
| `Dot(f32, SIMD)` | +0.34% | **−6.08%** |
| `Dot(f32, arrow_arity)` | +0.02% | −0.00% |
| `L2(f32, scalar)` | −0.10% | −0.02% |
| `L2(f32, simd)` (dim 1024) | +2.63% | −0.53% |
| **`L2(simd,f32x8)` (dim 8)** | **−45.9%** | **−25.1%** |
| `L2(u8, SIMD)` | +0.42% | −3.11% |
| `NormL2(f32, SIMD)` | −1.02% | −4.17% |
| `NormL2(f64, SIMD)` | +3.51% | −0.58% |
Nothing regresses beyond the noise floor. Dim 8 — the PQ sub-vector
width — improves 25–46%.
---
To be transparent: this isn't my domain of expertise and the lance-side
implementation is AI-generated. I verified it works end-to-end on the
failing hardware. Happy to roll in feedback.
Routes local sync child-namespace operations through the Rust-backed
connection instead of the Python namespace-client fallback.
Also keeps lazy namespace-client construction for table-to-Lance
conversion and preserves public namespace error mappings.
Validated locally with ruff format/check and targeted namespace pytest.
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>
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.
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`
## Summary
Fix `on_bad_vectors="fill"` so it replaces only invalid or missing
vector values instead of replacing the entire vector row.
Fixes#3026.
## Reasoning
The old Python sanitizer detected whether a vector row was bad at row
granularity. For `fill`, it then used that row-level flag to replace the
whole vector with `[fill_value] * dim`. That meant an input like `[1.0,
NaN, 3.0]` became `[0.0, 0.0, 0.0]`, even though the documented and more
useful behavior is to preserve valid values and fill only the bad
element.
I checked whether this should be a Rust-side fix so TypeScript users
would benefit too. Today, Rust core exposes `NaNVectorBehavior::{Error,
Keep}` for rejecting or keeping NaN vectors, while the Python
`on_bad_vectors` API (`error`, `drop`, `fill`, `null`) is implemented in
the Python ingestion sanitizer before data reaches Rust. TypeScript does
not expose the Python `on_bad_vectors="fill"` behavior today. Moving
this exact behavior to Rust would be a broader cross-language API
change, so this PR keeps the fix scoped to the currently affected Python
API.
## What changed
- Added a small helper that fills bad vector rows by preserving valid
elements, replacing NaN elements with `fill_value`, truncating vectors
longer than the expected dimension, and padding short vectors with
`fill_value`.
- Kept the existing fast path unchanged: the helper only runs after bad
vectors are detected and `on_bad_vectors="fill"` is selected.
- Updated sanitizer and table tests to assert element-wise NaN
replacement and short-vector padding for both `create_table` and `add`.
## Validation
- `uv run ruff format .`
- `uv run ruff check .`
- `cd python && uv run --no-sync pytest
python/tests/test_util.py::test_handle_bad_vectors_jagged
python/tests/test_util.py::test_handle_bad_vectors_nan
python/tests/test_table.py::test_create_with_nans
python/tests/test_table.py::test_add_with_nans -vv`
Targeted pytest result: `10 passed`.
## Why this fix is Python-side (and not Rust)
The problematic behavior lives in Python’s `on_bad_vectors` sanitizer,
before data is handed off to Rust. Rust currently only exposes
`NaNVectorBehavior::{Error, Keep}` for add operations, while Python has
the richer `on_bad_vectors={"error","drop","fill","null"}` API.
TypeScript does not currently expose the Python-style fill behavior, so
moving this exact fix into Rust would require designing a broader
cross-language bad-vector handling API.
This PR keeps the change scoped to the existing affected surface:
Python’s `on_bad_vectors="fill"` path. This way, Python users
immediately benefit.
## What the new agent skill covers
We want to help users _easily_ write LanceDB pipelines to bring their
data in from other places, no matter whether they use LanceDB OSS or
Enterprise.
The `lancedb` set of skills contains guidance for agents on the
following:
- Distinguishes local and remote table capabilities.
- Promotes bounded reads using `select()` and `limit()`.
- Prevents accidental full-table materialization.
- Documents correct Python sync/async scan APIs.
- Recommends validated Python schemas and batched ingestion.
- Provides indexing, query-tuning, diagnostics, and maintenance
guidance.
- Documents the Enterprise table-name cache issue: avoid immediately
reusing a dropped or overwritten table name; write to a fresh name and
rename after propagation.
- Adds Python and TypeScript API, pattern, and performance references.
- Adds a heuristic scanner for potentially unsafe Python and TypeScript
materialization patterns.
This change only adds agent documentation and tooling: no LanceDB
runtime code, Rust code, SDK APIs, dependencies, or CI configuration are
modified.
## Context
The LanceDB agent skill was accidentally pushed directly to `main` in
`8ea78e3fbcb26718112ab4ddec55a91804b869d3`, bypassing the normal review
workflow. That commit was reverted on `main` by `c12a6dce` so the
protected branch is back to its prior content.
## 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
`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>
## Summary
- serialize sync phrase queries consistently for execution and query
plans
- restore the documented no-argument hybrid `phrase_query()` behavior
- keep reranker input as the original user text without mutating the
builder
Fixes#3653.
## Testing
- `python/.venv/bin/python -m pytest <8 focused test nodes> -q` (`8
passed`)
- `python/.venv/bin/python -m ruff format --check
python/python/lancedb/query.py python/python/tests/test_fts.py
python/python/tests/test_hybrid_query.py`
- `python/.venv/bin/python -m ruff check .`
- `git diff --check origin/main...HEAD`
The complete hybrid module and the real native FTS phrase test were not
completed
in the current PyO3 runtime environment: both stalled in the native
`lancedb.connect()` fixture and were interrupted without an assertion
failure.
The CODEOWNERS file added in #3312 automatically requests reviewers on
every PR — the `*` default owner routes all changes to two reviewers.
This is mostly noise for contributors, and we prefer a single requested
reviewer per PR.
Remove the file.
Reverts #3312.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Carrying over from #2915, this patch introduces:
* Single-API call batching support for Gemini embeddings (up to 100 at a
time, the API limit)
* A versioned user agent header for Gemini API calls
* Support for [variable embedding dimension
size](https://ai.google.dev/gemini-api/docs/embeddings#control-embedding-size)
(Gemini is MRL trained)
## Summary
- preserve explicit `0.0` distance bounds in synchronous hybrid search
- distinguish omitted `None` endpoints from zero-valued endpoints when
configuring the vector child query
- add a public end-to-end regression test for a zero upper bound
## Testing
- `cd python && uv run --extra tests pytest
python/tests/test_hybrid_query.py -q`
- `uv run --project python ruff format --check
python/python/lancedb/query.py python/python/tests/test_hybrid_query.py`
- `uv run --project python ruff check .`
Fixes#3651
description: Branch management for LanceDB tables via the REST API. Use this skill whenever someone wants to create, delete, list, or switch branches on a LanceDB table — or needs to make sure a write (metadata update, index build, etc.) lands on a specific branch instead of main. Invoke it even without the word "branch" if context makes clear they want an experimental copy of a table, want to isolate changes, or want to confirm a mutation didn't touch main. Covers: branches/list, branches/create, branches/delete, and passing "branch" in describe/update_field_metadata/create_index to target a non-main version.
---
## Goal
Manage branches on a LanceDB table: list what exists, create new ones, delete stale ones, and direct read/write operations at a specific branch without touching main.
## Step 0: Establish the connection
Use the `lancedb-connect` skill to resolve the base URL and auth headers (`x-api-key`, `x-lancedb-database`). Skip this only if the connection is already known from the current conversation.
All examples below use `{base_url}` — substitute the resolved endpoint and include the auth headers on every request.
## The branch model (important)
LanceDB branches are named snapshots that diverge from the table's current state at creation time. There is **no checkout command** — you never switch the whole table to a branch. Instead, you **pass `"branch": "<name>"` in the request body** of any operation to target that branch. Omitting the key (or sending an empty body) always targets main.
`branches/list` returns only non-main branches. Main always exists and is not listed.
description: Column metadata authoring for LanceDB tables via the REST API. This skill is required for tasks like writing field descriptions, setting tags on columns (field_type, model, project_id, version), classifying columns as embeddings vs labels vs eval metrics, or grouping versioned columns into logical families — because it has the API integration needed to read the schema and persist metadata back. Invoke whenever someone wants to document, annotate, tag, or classify what their table columns ARE. Trigger even without an explicit "LanceDB" mention, as long as the context is column-level documentation or tagging for an ML or vector database table.
metadata:
short-description: Write column descriptions, tags, and logical groupings to a LanceDB table
---
## Overview
This skill authors column-level metadata for a LanceDB table. It connects to a LanceDB deployment over its REST API, inspects the table schema, generates appropriate metadata, and writes it back.
## Step 0: Establish the connection
Use the `lancedb-connect` skill (invoke it via the Skill tool) to resolve the base URL and auth headers (`x-api-key`, `x-lancedb-database`) for whichever deployment the user is working against — enterprise/self-hosted or a local dev server. Skip it only if the connection details are already established in the conversation.
All examples below use `{base_url}` — substitute the resolved endpoint and include the resolved headers on every request.
## Metadata keys
All metadata uses namespaced keys:
| Key | Purpose | Example value |
|-----|---------|---------------|
| `lancedb:description` | Human-readable explanation of what the column contains | `"CLIP ViT-L/14 image embedding, L2-normalized (768-dim)"` |
| `lancedb:tag:<name>` | Flexible key-value tag; the suffix names the tag category | `lancedb:tag:field_type: "embedding"`, `lancedb:tag:model: "clip"`, `lancedb:tag:project_id: "foo"` |
| `lancedb:logical-column` | Logical group/family this column belongs to | `"clip_features"` |
Tags are open-ended — use whatever key suffix and value make sense given the user's intent. The tag suffix should describe *what is being classified* (e.g., `field_type`, `model`, `project_id`) and the value describes *how*.
## Step 1: Resolve the table identifier
You need:
- **Table name** (required) — e.g., `my_table` or `my_namespace.my_table`
- **Database name** — ask if not provided and not inferable from context; it goes in the `x-lancedb-database` header, never in the URL path
The table identifier in the URL path is typically `table_name` for a top-level table, or `namespace$table_name` if the table lives in a namespace. The API accepts a `delimiter` query parameter to parse compound identifiers (default `$`).
## Step 2: Describe the table
```http
POST{base_url}/v1/table/{table_id}/describe
Content-Type:application/json
{}
```
The response contains `schema.fields` — an array of field objects:
description: Resolve how to connect to a LanceDB deployment over the REST API — figure out the base URL, API key, and database header. Use this before making any REST requests to a LanceDB table, whenever the endpoint or auth setup is not already known. Also useful on its own when someone asks how to connect, authenticate, or curl their LanceDB instance.
metadata:
short-description: Resolve the base URL and auth headers for a LanceDB deployment
---
## Goal
Produce two things every REST request needs:
1.**Base URL** — the endpoint
2.**Headers** — `x-api-key`, and usually `x-lancedb-database`
## Resolution steps
1. If the user already gave a URL and API key (or said which environment they're working against), use that.
2. Otherwise, look for credentials already available in the environment:
- Env vars like `LANCEDB_URI` / `LANCEDB_HOST` / `LANCEDB_API_KEY`
- A LanceDB endpoint already running or port-forwarded locally (the REST default port is 2333, i.e. `http://localhost:2333`)
3. If you didn't find both pieces, ask the user directly: **"What's your LanceDB endpoint's URL, and what's your API key?"** Also ask which database to use if it isn't obvious. Don't guess or probe further — the user knows their deployment.
## Validating the connection
Make a cheap authenticated request and check the status:
"description":"Write, review, debug, and document LanceDB pipelines in Python and TypeScript that work across local LanceDB OSS tables and remote LanceDB Enterprise/Cloud tables.",
description:"Override [project] name in python/pyproject.toml (e.g. 'lancedb-compat'). Default keeps 'lancedb'."
required:false
default:"lancedb"
rustflags:
description:"RUSTFLAGS for the build container, as a single whitespace-free token (e.g. '-Ctarget-cpu=x86-64-v2'). Empty leaves RUSTFLAGS unset, keeping the defaults from .cargo/config.toml."
required:false
default:""
runs:
using:"composite"
steps:
@@ -27,6 +35,18 @@ runs:
ARM_BUILD:${{ inputs.arm-build }}
run:|
echo "ARM BUILD: $ARM_BUILD"
- name:Patch package name for variant build
if:${{ inputs.package-name != 'lancedb' }}
shell:bash
env:
PACKAGE_NAME:${{ inputs.package-name }}
run:|
# Swap the [project] name so this build produces e.g. lancedb-compat
# wheels. The package still installs files under the lancedb/
# namespace -- import lancedb still works after pip install.
sed -i.bak 's/^name = "lancedb"$/name = "'"$PACKAGE_NAME"'"/' python/pyproject.toml
description:"Tag name from Lance. If omitted, the skill will use the latest Lance release that needs an update."
description:"Tag name from Lance (e.g. `v7.2.0-beta.1`). If omitted, the newest release is resolved automatically — stable releases are preferred over pre-releases — and the run is skipped if it is not newer than the version currently pinned in Cargo.toml."
required:false
default:""
type:string
workflow_dispatch:
inputs:
tag:
description:"Tag name from Lance. Leave empty to use the latest Lance release that needs an update."
description:"Tag name from Lance (e.g. `v7.2.0-beta.1`). Leave empty to resolve the newest release automatically — stable releases are preferred over pre-releases — and skip the run if it is not newer than the version currently pinned in Cargo.toml."
echo "Broken documentation links found by [\`$GITHUB_WORKFLOW\`]($run_url)."
echo
echo "This issue is rewritten by every scheduled run and closed automatically once all links resolve."
echo
echo "Entries can be false positives: some sites rate-limit or block automated clients while working fine in a browser. Confirm before editing the docs, and add persistent offenders to \`--exclude\` in \`.github/workflows/docs-link-check.yml\`."
echo
# Timeouts are reported alongside errors: entries land in
# timeout_map with a status text instead of an HTTP code.
jq -r '
"\(.errors) of \(.total) links failed, \(.timeouts) timed out.",
* Use repository-defined Cargo profiles instead of ad hoc LTO overrides.
* Use `release-with-debug` for benchmarks and profiling so optimized builds keep debug symbols without a rebuild.
* Use `release-no-lto` only for local debugging, IO-bound benchmarks, or compile-time-sensitive performance investigation where LTO would not affect the measured bottleneck.
* Format Python: `ruff format .`
* Lint Python: `ruff check .`
* Bootstrap Python dev env: `cd python && uv run --extra tests --extra dev maturin develop --extras tests,dev`
@@ -92,6 +95,8 @@ Python bindings changes:
* Should use `LOOP.run()` to call the corresponding `AsyncTable` method.
6. Add concrete sync method to `RemoteTable` class in `python/python/lancedb/remote/table.py`.
7. Add unit test in `python/tests/test_table.py`.
8. If you added a new public class or module-level function (not just a method on an
existing class), expose it in the API reference. See "Python API reference" below.
TypeScript bindings changes:
@@ -103,6 +108,33 @@ TypeScript bindings changes:
5. Add test in `nodejs/__test__/table.test.ts`.
6. Run `npm run docs` to generate TypeScript documentation.
## Python API reference
`docs/src/python/python.md` is the entire Python API reference. It is maintained by
hand, and anything not listed there is not rendered at all, so new public classes and
module-level functions have to be added explicitly. How depends on the module:
* `lancedb.index`, `lancedb.embeddings`, `lancedb.remote`, and `lancedb.rerankers` are
rendered by a single directive each, driven by the module's `__all__`. Add the new
name to `__all__` and it appears; forget, and it is silently omitted.
* Everything else (`lancedb`, `lancedb.table`, `lancedb.query`, `lancedb.db`, ...) is
listed symbol by symbol. Add a `::: lancedb.<module>.<Name>` line to the matching
section, and remember that the page separates synchronous and asynchronous APIs.
Deliberately undocumented: concrete implementations reached through an abstract base
(`LanceTable`, `LanceDBConnection`, `RemoteDBConnection`), query base classes already
covered by `inherited_members`, and internal helpers.
Cross-references in docstrings use mkdocstrings syntax, `[text][lancedb.table.Table]`.
Plain relative links such as `[Table](Table)` do not resolve. To check your work:
```shell
pip install -r docs/requirements.txt
cd docs && PYTHONPATH=. mkdocs build
```
The docs site only builds on pushes to `main`, so this is not covered by PR CI.
## Review Guidelines
Please consider the following when reviewing code contributions.
This document outlines the process for contributing to LanceDB Typescript.
For general contribution guidelines, see [CONTRIBUTING.md](../CONTRIBUTING.md).
For general contribution guidelines, see [CONTRIBUTING.md](https://github.com/lancedb/lancedb/blob/main/CONTRIBUTING.md).
## Project layout
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.