Compare commits

..

210 Commits

Author SHA1 Message Date
lancedb-gatefixer[bot] 2fbf6d6211 test(python): cover concurrent S3 table opens (#3833)
## Summary

- add regression coverage for the reported synchronous Python workload
with 32 simultaneous `open_table` calls
- verify every independently opened S3-backed table handle can read
through the connection's shared session and object-store client

## Root cause

In Python v0.13.0, each synchronous table handle lazily constructed its
own Lance dataset. Opening many handles in parallel therefore triggered
independent S3 client construction and bucket-region resolution, which
failed under thread pressure. The current Rust-backed connection path
owns a shared Lance session and retains its object-store handle, so
table opens reuse the existing S3 client; these tests lock in that
behavior through the public Python API and a causal Session-registry
invariant.

## Validation

- `uvx --from 'ruff==0.15.20' ruff format --check
python/tests/test_s3.py`
- `uvx --from 'ruff==0.15.20' ruff check .`
- `cargo fmt --all`
- `cargo test --quiet --features remote -p lancedb
test_concurrent_open_table_reuses_connection_object_store`
- `cargo check --quiet --features remote --tests --examples`
- equivalent 32-thread `open_table(...).count_rows()` workload against a
local database
- targeted S3 test collected successfully locally; execution requires
the CI LocalStack service, which is unavailable in this runner

Fixes #1786

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
2026-08-26 14:56:57 +08:00
Jack Ye 391cac9034 fix(remote): centralize timeline consistency (#4053)
Centralizes remote table freshness fencing and response-version tracking
in the default transport path.

Covers schema and blob bypass paths, keeps explicit time-travel and
cross-timeline operations unfenced, and advances freshness after refresh
and index job completion.
2026-08-26 12:54:36 +08:00
LanceDB Robot 21530432a0 chore: update lance dependency to v12.0.0-beta.2 (#4056)
Updates the Rust workspace Lance crates and Java lance-core dependency
to
[v12.0.0-beta.2](https://github.com/lance-format/lance/releases/tag/v12.0.0-beta.2).
No compatibility fixes were required; full-workspace Clippy passes with
all features and warnings denied.
2026-08-25 21:56:26 -05:00
lancedb-gatefixer[bot] 9b825c5f29 fix(node): route auto search using table embeddings (#3832)
## Summary

- Resolve automatic string-search routing from the active table schema
whenever the query executes.
- Defer embedding-provider construction while leaving explicit vector
and FTS routes unchanged.
- Cover unrelated global registrations and metadata transitions across
repeated executions of one query builder.

## Root cause

LocalTable.search used the number of globally registered embedding
providers to choose between vector and full-text search. A provider
registered for any other table therefore sent a plain FTS table down the
vector path. A wrapper-lifetime metadata snapshot avoided that
contamination but became stale after time travel or read-consistency
refreshes. The query now records fluent builder operations and creates
the appropriate native vector or FTS query from the active schema on
each execution.

## Validation

- pnpm build
- pnpm tsc
- pnpm lint
- pnpm run docs
- pnpm test --runInBand (681 passed, 5 skipped)

Fixes #1557

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
2026-08-26 10:08:48 +08:00
LanceDB Robot 8083232dd5 chore: update lance dependency to v12.0.0-beta.1 (#4055)
Updates the Lance Rust workspace dependencies and Java lance-core
dependency to
[v12.0.0-beta.1](https://github.com/lance-format/lance/releases/tag/v12.0.0-beta.1).

Includes compatibility updates for the renamed shard-manifest API and
paginated object-store wrappers.
2026-08-25 16:49:17 -07:00
lancedb-gatefixer[bot] 302b21aa94 test(node): cover nested PDF metadata queries (#3827)
## Summary

- add an end-to-end Node regression matching LangChain PDFLoader
metadata
- verify create/query round trips rich nested `loc` and `pdf.info`
fields against the currently configured Apache Arrow peer

## Root cause

LanceDB v0.14 delegated nested object inference to Apache Arrow. Nested
strings were dictionary-encoded with colliding dictionary IDs, so
serializing query results as an IPC file failed with a
dictionary-replacement error. Current `main` recursively infers nested
fields and avoids those invalid dictionaries, but the reported LangChain
path had no end-to-end regression coverage.

## Validation

- `pnpm build`
- `pnpm lint`
- `pnpm run docs`
- `pnpm test --runInBand` (678 passed, 5 skipped)

Fixes #1963

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
2026-08-26 06:42:29 +08:00
lancedb-gatefixer[bot] 35b5d015ac fix(node): preserve embedding registration in server bundles (#3806)
## Summary

- lazily initialize built-in OpenAI and Hugging Face providers when
consumers call the public embedding registry API
- choose automatic vector versus FTS search from embedding metadata on a
fresh pinned table revision for every execution
- expose automatic string searches as an `AutoQuery` with only
operations common to both native query families
- keep the registry shared and built-in registration safe across
duplicated module graphs

## Root cause

Nitro treats dependency modules as side-effect-free and removes the bare
OpenAI provider import from its generated route. Registration therefore
never runs, so `getRegistry().get("openai")` remains undefined even when
the registry itself is shared globally. Bundlers may also duplicate the
provider and registry module graphs.

The public embedding entry point now initializes built-in providers only
when `getRegistry()` is explicitly called, keeping initialization on a
live path that Nitro retains. Each terminal automatic-search execution
pins the exact table revision visible at dispatch, reads embedding
metadata and computes an embedding from that snapshot, replays the
builder operations, and constructs and executes the selected native
query against the same snapshot. Pinned native snapshots execute locally
when namespace pushdown cannot carry their revision, while remote
snapshots are seeded directly from one version-and-schema response. The
public `AutoQuery` builder exposes only the operations shared by FTS and
vector search, so runtime class narrowing cannot expose invalid
vector-only methods. Repeated built-in registration replaces stale
constructors from duplicated module graphs while public `register()`
retains its duplicate-alias error.

## Validation

- `cargo fmt --all`
- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`
- `pnpm build`
- `pnpm lint`
- `pnpm run docs`
- `pnpm test --runInBand` (783 passed, 5 skipped)
- serial examples suite with a local OpenAI mock (11 passed), including
`sentence-transformers.test.ts`
- packaged Nitro 2.13.4 server route using the reported imports returned
`{"registered":true}`
- fresh-process FTS fixture initialized both public built-ins and
confirmed automatic string search still returned the indexed row
- schema-consistency regressions cover read-consistency refresh,
checkout, checkoutLatest, restore, runtime class narrowing, concurrent
overwrite during embedding computation, and reused automatic-search
builders
- focused regressions confirm pinned native snapshots bypass unversioned
namespace pushdown and remote snapshots use one describe request

Fixes #2429

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
2026-08-26 04:17:32 +08:00
lancedb-gatefixer[bot] a57fb68891 docs(python): fix Azure storage options examples (#3899)
## 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>
2026-08-26 00:31:14 +08:00
Drew a614400755 feat: accept blob URI writes (#3954)
#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>
2026-08-25 22:59:53 +08:00
Xuanwo 1d880f11ff fix(python): use dev profile for editable builds (#4049)
## 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.
2026-08-25 19:51:40 +08:00
Lance Release ec4ad54ba2 Bump version: 0.38.0-beta.9 → 0.38.0-beta.10 2026-08-25 10:37:09 +00:00
Xuanwo d0bcc6c6fe fix: remove unsupported Function secrets contract (#4047)
## 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.
2026-08-25 18:35:27 +08:00
Lance Release 81c3f108ce Bump version: 0.38.0-beta.8 → 0.38.0-beta.9 2026-08-25 10:32:12 +00:00
Xuanwo c988e4848d refactor: simplify Function binding identity (#4046)
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.
2026-08-25 18:30:09 +08:00
Lance Release 2fea7cd48d Bump version: 0.38.0-beta.7 → 0.38.0-beta.8 2026-08-25 06:26:35 +00:00
Wyatt Alt 0e65123bd8 fix: package ordinary @udf bodies and emit only the V1 type grammar (#4044)
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.
2026-08-25 14:15:07 +08:00
Jack Ye 6ed3074d4c feat: pin the base table version for data loader reads (#3982)
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.
2026-08-24 17:16:42 -07:00
lancedb-gatefixer[bot] c1a8c3f089 fix(node): validate inferred types across records (#3786)
## 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>
2026-08-24 16:41:05 -07:00
Will Jones fce45ba9fc feat(nodejs): add listTables, deprecate tableNames (#4041)
`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>
2026-08-24 16:25:14 -07:00
lancedb-gatefixer[bot] 5013c176dd fix: pin remote snapshots during permutation construction (#4022)
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>
2026-08-25 07:14:33 +08:00
Lance Release 71f85a8d9f Bump version: 0.38.0-beta.6 → 0.38.0-beta.7 2026-08-24 21:09:12 +00:00
Wyatt Alt c72f5b2960 feat: bind a materialized view refresh to the view incarnation (#4043)
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.
2026-08-24 14:06:40 -07:00
Will Jones 93f47b8aab fix(remote): stop table_names inventing a page token for a namespace (#4039)
`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>
2026-08-24 13:35:31 -07:00
lancedb-gatefixer[bot] 105fd73bc6 fix(python): commit streaming worker checkpoints on consumption (#4023)
## 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>
2026-08-25 04:22:22 +08:00
Will Jones 94d484f539 fix(listing): don't drop a table at a page boundary (#4040)
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>
2026-08-24 12:00:48 -07:00
Xuanwo b0dae5eb0b feat: return typed refresh job results (#4013)
## 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.
2026-08-25 00:01:32 +08:00
Ayush Chaurasia 242ade8017 feat(python): support sequence packing in streaming dataset (#3920)
## 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>
2026-08-24 15:56:36 +08:00
Lance Release 40d4d012e7 Bump version: 0.38.0-beta.5 → 0.38.0-beta.6 2026-08-23 17:33:19 +00:00
LanceDB Robot 000e3b506b chore: update lance dependency to v11.0.0-beta.22 (#4036)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v11.0.0-beta.22, including the refreshed Cargo lockfile.
No compatibility fixes were required; see the [Lance
tag](https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.22).
2026-08-23 10:31:32 -07:00
Lance Release 1b950188c3 Bump version: 0.38.0-beta.4 → 0.38.0-beta.5 2026-08-23 08:07:48 +00:00
LanceDB Robot 6cc77b573c chore: update lance dependency to v11.0.0-beta.21 (#4029)
Updates the Rust workspace and Java `lance-core` dependency to Lance
v11.0.0-beta.21.

No compatibility fixes were required; workspace Clippy passes with
warnings denied. Triggering tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.21
2026-08-23 00:53:14 -07:00
Weston Pace 1f1d03f306 feat(python): add backpressure to StreamingDataset post-transform queue (#3897)
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>
2026-08-23 15:28:07 +08:00
Lance Release 45cd053478 Bump version: 0.38.0-beta.3 → 0.38.0-beta.4 2026-08-22 16:38:47 +00:00
Wyatt Alt 68749ecfa3 feat(nodejs): materialized view bindings (#3935)
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>
2026-08-21 23:48:43 -07:00
Drew Gallardo e98d8ac685 feat!: rename branch merge to cherry_pick (#3986)
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
2026-08-21 23:37:12 -07:00
Wyatt Alt 851fa16b47 feat(python): materialized view bindings (#3933)
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>
2026-08-21 23:08:41 -07:00
Wyatt Alt d04ac7ed20 test: differential refresh harness for materialized views (#3932)
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>
2026-08-21 23:04:04 -07:00
Wyatt Alt a578e9ff7f feat: refresh materialized views (#4010)
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.
2026-08-21 22:39:47 -07:00
LanceDB Robot 7801e2746a chore: update lance dependency to v11.0.0-beta.19 (#4025)
Updates the Lance dependencies and Java lance-core dependency to
v11.0.0-beta.19. No compatibility fixes were required; workspace clippy
with all features passes. Triggering tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.19
2026-08-21 21:43:45 -07:00
lancedb-gatefixer[bot] 5468f3d490 fix(rust): reject bitmap indexes on JSON fields (#3895)
## 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>
2026-08-21 16:44:21 -07:00
lancedb-gatefixer[bot] c0df2c63b6 test(rust): cover fixed-size-list merge overflow (#3907)
## 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>
2026-08-21 16:39:13 -07:00
lancedb-gatefixer[bot] 9e8f1c1a6d fix(python): expose FTS build memory limits (#3796)
## 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>
2026-08-21 16:31:34 -07:00
Wyatt Alt 01679e37fd feat: materialized view declarations on local tables (#3930)
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>
2026-08-21 16:17:03 -07:00
lancedb-gatefixer[bot] c7cb0b9afa docs(python): clarify threading on two-CPU containers (#3807)
## 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>
2026-08-21 16:13:49 -07:00
lancedb-gatefixer[bot] a35f7044ee test(rust): cover Azure table URI separators on Windows (#3810)
## 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>
2026-08-21 15:43:04 -07:00
Wyatt Alt 29822306d2 fix(python): skip the unrunnable FunctionVersion doctest example (#4014)
The example binds an undefined `function`; only its last line was
skipped, so the doctest suite fails on main and on every PR.
2026-08-21 14:20:03 -07:00
Jack Ye f39a7a4dd9 feat: support remote tables in the data loader (#3981)
`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.
2026-08-21 13:45:39 -07:00
Xuanwo 1baada89ef feat(python): bind function versions to columns (#4012)
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.
2026-08-22 02:01:45 +08:00
Xuanwo ecf4555cfd fix(remote): fence refresh submissions after add_columns (#4007)
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.
2026-08-21 09:53:28 -07:00
Dan Tasse fa3d9b2ce2 refactor: move plugin/skills to lancedb-agent-plugins repo (#4009)
Moving the skills and plugins to
https://github.com/lancedb/lancedb-agent-plugins
2026-08-22 00:33:57 +08:00
Will Jones 217ea1a799 ci: use thin LTO and a larger runner for the Windows wheel build (#3716)
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>
2026-08-21 09:30:20 -07:00
Will Jones bacd0e4c3c ci: group arrow and datafusion dependabot updates into one PR (#3738)
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>
2026-08-21 09:30:12 -07:00
Wyatt Alt 7fd881bbe3 fix(nodejs)!: key parsed embedding configs by vector column (#4003)
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>
2026-08-22 00:23:40 +08:00
Xuanwo 5c3bc7f643 fix: accept non-null Function inputs for nullable parameters (#4006)
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.
2026-08-22 00:22:28 +08:00
Xuanwo fe992bf4ee fix(python): use canonical remote function endpoints (#4008)
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.
2026-08-22 00:17:04 +08:00
Dan Tasse 944398d807 refactor: make branch ops instructions less redundant, point to docs (#3978)
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>
2026-08-21 12:06:20 -04:00
LanceDB Robot fd2a202a46 chore: update lance dependency to v11.0.0-beta.18 (#4000)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v11.0.0-beta.18.

Lance tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.18
2026-08-21 20:30:24 +08:00
Xuanwo 6a0df4de47 fix(python): return None from unit jobs (#3999)
## 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.
2026-08-21 19:42:37 +08:00
XY Zhan fbfb53e30f refactor(lsm): remove the index-catchup activation surface (#3980)
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.)
2026-08-21 19:35:42 +08:00
Lance Release 593ef1c471 Bump version: 0.38.0-beta.2 → 0.38.0-beta.3 2026-08-21 10:25:54 +00:00
LanceDB Robot cf27f6902e chore: update lance dependency to v11.0.0-beta.16 (#3992)
Updates Lance dependencies and Java lance-core to v11.0.0-beta.16. Also
narrows the dependency updater's package matching so the local LanceDB
crate remains a path dependency.

Lance tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.16

---------

Co-authored-by: Yang Cen <bubble-cal@outlook.com>
2026-08-21 18:24:22 +08:00
Xuanwo f76ee304b8 ci: isolate remote Rust tests (#3998)
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
2026-08-21 18:18:55 +08:00
Xuanwo a588208de6 feat: add scalar function authoring and catalog client (#3991)
## 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
2026-08-21 17:19:13 +08:00
Xuanwo 685cb01d6d feat: add grouped function column bindings (#3994)
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.
2026-08-21 17:01:04 +08:00
Xuanwo 4ba2421254 refactor(python): require pydantic v2 (#3990)
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.
2026-08-21 16:50:00 +08:00
Xuanwo 09843410ec build: avoid fat LTO in local Cargo profiles (#3996)
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.
2026-08-21 16:42:59 +08:00
Xuanwo 7adcffc2b4 fix(python): set LsmWriteSpec module metadata (#3995)
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.
2026-08-21 16:37:48 +08:00
Xuanwo c1331e5083 chore: remove repo-scoped lancedb skill reference (#3993)
Remove the `.agents/skills/lancedb` symlink and its README documentation
so the plugin-provided skill is no longer discovered as a repo-scoped
skill.
2026-08-21 16:19:55 +08:00
Xuanwo 426684cf1b feat: add first-class function wire contracts (#3985)
## 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.
2026-08-21 15:48:09 +08:00
Dan Tasse e517ba5205 refactor: remove unnecessary skill references (#3977)
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>
2026-08-20 18:03:24 -04:00
Will Jones 5c1b44020a chore: enforce shared workspace dependencies via cargo-deny (#3975)
`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>
2026-08-20 13:44:51 -07:00
lancedb-gatefixer[bot] 061a3da8b9 fix(python): preserve JSON encoding in merge insert (#3976)
<!-- 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>
2026-08-20 12:05:40 -07:00
dependabot[bot] 4e042af12f chore(deps): bump cmov from 0.5.3 to 0.5.4 (#3974)
Bumps [cmov](https://github.com/RustCrypto/utils) from 0.5.3 to 0.5.4.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/RustCrypto/utils/commit/5c7e4f9bb31af81bf766360e836b6d633b84dbff"><code>5c7e4f9</code></a>
cmov v0.5.4 (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1485">#1485</a>)</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/87cadbce34655ac3c78efa7290f37d942d551b2c"><code>87cadbc</code></a>
cmov: fix clippy (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1484">#1484</a>)</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/85600e91cdf73115c48fafa64650ba9ed9285a12"><code>85600e9</code></a>
rustfmt</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/dba6c355c9f241e3726d5ec2a68f9f3b519f6063"><code>dba6c35</code></a>
Merge commit from fork</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/dad5e3b9e66d929e86144fe7c8f25371892e35f3"><code>dad5e3b</code></a>
block-buffer: pin to <code>zeroize</code> v1.8 (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1483">#1483</a>)</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/66cb272d00988520043aa34299a402abb885f461"><code>66cb272</code></a>
ctutils: bump <code>subtle</code> version requirement to v2.6 (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1482">#1482</a>)</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/34881f258468cc06c037ba53706429ba603ef63e"><code>34881f2</code></a>
build(deps): bump hybrid-array from 0.4.11 to 0.4.12 (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1480">#1480</a>)</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/c211865d9a51f42881d30c5e05070d53cb0373b7"><code>c211865</code></a>
Update crates table (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1479">#1479</a>)</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/9c8674f4fdf00fb524cd06d89da29d125db07582"><code>9c8674f</code></a>
sponge-cursor: initial implementation (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1477">#1477</a>)</li>
<li><a
href="https://github.com/RustCrypto/utils/commit/a00167aa5bc1fcde165b8b02ca2f657e2ca08669"><code>a00167a</code></a>
ctutils: fixup homepage url (<a
href="https://redirect.github.com/RustCrypto/utils/issues/1478">#1478</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/RustCrypto/utils/compare/cmov-v0.5.3...cmov-v0.5.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cmov&package-manager=cargo&previous-version=0.5.3&new-version=0.5.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/lancedb/lancedb/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-20 10:55:16 -07:00
LanceDB Robot 27cea03b7d chore: update lance dependency to v11.0.0-beta.15 (#3968)
Bumps the Rust workspace Lance dependencies and Java lance-core to
v11.0.0-beta.15. Updates the computed-column refresh path for the new
`write_columns` API.

Release:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.15
2026-08-19 15:18:27 -05:00
Dan Rammer f1c4967eeb feat: bring the MemWAL LSM surface to parity across the SDKs (#3962)
## 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>
2026-08-19 11:44:46 -05:00
LanceDB Robot 11c1d81638 chore: update lance dependency to v11.0.0-beta.14 (#3965)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v11.0.0-beta.14. No compatibility fixes were required;
full workspace clippy with all features passes. Trigger:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.14

---------

Co-authored-by: Yang Cen <bubble-cal@outlook.com>
2026-08-19 21:04:45 +08:00
Lance Release f6efdc9e9f Bump version: 0.38.0-beta.1 → 0.38.0-beta.2 2026-08-19 01:59:27 +00:00
Dan Rammer cdebea118d feat(python): expose LSM checkpoint and stats on sync RemoteTable (#3961)
## 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>
2026-08-18 17:25:08 -05:00
Xuanwo 76942306b7 docs(java): add vended credentials example (#3958)
## 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.
2026-08-18 20:03:20 +08:00
Adityaj0 d742b174c4 fix: hybrid search silently ignores .offset() (#3769)
## 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>
2026-08-17 11:38:38 -07:00
Igor Ganapolsky a075aa62f8 fix(python): treat naive lit(datetime) as UTC wall clock (#3262) (#3775)
## 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>
2026-08-17 10:48:02 -07:00
Lance Release 040a4120c8 Bump version: 0.38.0-beta.0 → 0.38.0-beta.1 2026-08-17 16:56:54 +00:00
Wyatt Alt 928c3dde2d feat: computed columns on remote tables (#3941)
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>
2026-08-14 17:21:55 -07:00
LanceDB Robot 980818df26 chore: update lance dependency to v11.0.0-beta.13 (#3947)
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.
2026-08-14 16:20:39 -07:00
Wyatt Alt c429863122 feat: refresh_column_async returns a job handle (#3939)
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>
2026-08-14 16:05:04 -07:00
Wyatt Alt fc0d917d32 feat: refresh computed columns (#3938)
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>
2026-08-14 14:43:41 -07:00
Wyatt Alt def869bb78 feat: declare computed columns by SQL expression (#3937)
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>
2026-08-14 14:17:41 -07:00
LanceDB Robot 9e4d8bd1c7 chore: update lance dependency to v11.0.0-beta.11 (#3946)
Updates the Rust workspace Lance crates and Java lance-core dependency
to v11.0.0-beta.11. No compatibility fixes were required; formatting and
full-workspace clippy validation pass. Lance tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.11
2026-08-14 08:31:58 -07:00
XY Zhan 4148dfef72 feat(lsm): require recorded index catch-up, as an explicit activation (#3911)
> 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.
2026-08-14 09:32:02 -04:00
LanceDB Robot 0ac70a8b9f chore: update lance dependency to v11.0.0-beta.10 (#3944)
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
2026-08-14 18:46:46 +08:00
Lance Release 91c5f344d2 Bump version: 0.37.1-beta.1 → 0.38.0-beta.0 2026-08-14 01:09:50 +00:00
Jack Ye ffd35c1a8f feat: add asynchronous drop table API (#3936)
## 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
2026-08-13 18:05:44 -07:00
Wyatt Alt 790d0c684c docs(ci): clarify tag input on codex-update-lance-dependency (#3924)
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>
2026-08-13 11:26:58 -07:00
XY Zhan 251f194696 refactor(lsm): gate SSTable exclusion on every index a query relies on (#3780)
`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.
2026-08-13 13:23:37 -04:00
LanceDB Robot 4b7325bd74 chore: update lance dependency to v11.0.0-beta.8 (#3928)
Updates the Rust workspace and Java lance-core dependency to Lance
v11.0.0-beta.8, with refreshed Cargo lockfile metadata. No compatibility
fixes were required. Lance tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.8
2026-08-14 00:00:18 +08:00
Yang Cen 1d75638dea fix: make table existence manifest-authoritative (#3919)
## 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
2026-08-13 21:22:42 +08:00
LanceDB Robot 031c3585a8 chore: update lance dependency to v11.0.0-beta.7 (#3925)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v11.0.0-beta.7. No compatibility fixes were required;
full-workspace Clippy passes with warnings denied. Lance tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.7

---------

Co-authored-by: Yang Cen <159225399+BubbleCal@users.noreply.github.com>
2026-08-13 20:37:19 +08:00
LanceDB Robot 6fb976cf89 chore: update lance dependency to v11.0.0-beta.6 (#3922)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v11.0.0-beta.6. Includes compatibility updates for the new
concrete Lance file-version API. Trigger:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.6

---------

Co-authored-by: XYZhan <zhaner08@hotmail.com>
2026-08-12 02:43:44 -04:00
Sravan Avvaru a615306f39 feat(python): add on_transform_error fault tolerance to StreamingDataset (#3763)
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>
2026-08-10 09:22:06 -07:00
Xuanwo 920fc0e455 fix(python): set native module metadata (#3913)
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.
2026-08-10 21:40:31 +08:00
Xuanwo 5acce6782e ci(docs): report link checker failures through issues (#3909) 2026-08-10 15:08:36 +08:00
ForwardXu 12405a4077 chore: drop explicit goosefs-sdk pin in favor of opendal 0.58.1 transitive dep (#3910)
## 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.
2026-08-10 12:16:21 +08:00
lancedb-gatefixer[bot] 36054be576 fix(node): preserve nested Arrow data across versions (#3900)
<!-- 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>
2026-08-09 03:34:39 +08:00
Dan Tasse 77a93fee76 fix: get table size from metadata, not files (#3790)
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>
2026-08-07 17:41:41 -04:00
Lance Release 7bb501839a Bump version: 0.37.1-beta.0 → 0.37.1-beta.1 2026-08-07 21:16:07 +00:00
Andrew Chen 5b347afd99 fix: avoid AttributeError in JinaEmbeddings image input for str/Path (#3670)
## 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>
2026-08-07 14:05:45 -07:00
Dan Rammer 706a9c327f feat: infer maintained indexes when an LsmWriteSpec omits them (#3748)
## 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>
2026-08-07 14:50:22 -05:00
LanceDB Robot be290447d9 chore: update lance dependency to v11.0.0-beta.3 (#3896)
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
2026-08-07 13:54:32 -05:00
Dan Rammer 79ba076429 feat(table): checkpoint_lsm, flush_lsm, compact_lsm, get_lsm_stats (#3736)
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>
2026-08-07 13:44:49 -05:00
lancedb-gatefixer[bot] ec21e37040 test(rust): cover Hugging Face table symlinks (#3887)
## Summary

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

## Root cause

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

## Validation

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

Fixes #3197

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:37:37 +08:00
lancedb-gatefixer[bot] 6ba80a960c fix(node): cover offset pagination in search (#3814)
## 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>
2026-08-07 17:33:13 +08:00
lancedb-gatefixer[bot] 11f24b1df4 fix: explain unsupported object storage mounts (#3823)
## Summary

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

## Root cause

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

## Validation

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

Fixes #2016

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:33:02 +08:00
lancedb-gatefixer[bot] 2ba7407dc3 fix(node): cover non-nullable embedding schema append (#3835)
## 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>
2026-08-07 17:32:39 +08:00
lancedb-gatefixer[bot] 607e556927 test(python): cover search after schema merge (#3784)
## 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>
2026-08-07 17:32:28 +08:00
lancedb-gatefixer[bot] 564e5d0d56 fix(python): support Polars 1.32 table scans (#3801)
## 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>
2026-08-07 17:32:17 +08:00
lancedb-gatefixer[bot] dd5cb4d805 test(python): cover float16 table creation from Arrow data (#3785)
## 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>
2026-08-07 17:32:05 +08:00
lancedb-gatefixer[bot] dbc3687c7b fix(node): require compatible Node.js types (#3829)
## 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>
2026-08-07 17:31:53 +08:00
lancedb-gatefixer[bot] ec80acb668 fix(python): expose inline types to downstream checkers (#3817)
## 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>
2026-08-07 17:31:42 +08:00
lancedb-gatefixer[bot] fc44535cee fix(python): clarify bare Vector annotations (#3809)
## 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>
2026-08-07 17:31:30 +08:00
lancedb-gatefixer[bot] 4048150fdd test(python): cover nullable fixed-size-list ingestion (#3812)
## 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>
2026-08-07 17:31:19 +08:00
lancedb-gatefixer[bot] 2922c171f7 test(rust): cover Azure table URI separators (#3837)
## Root cause

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

## Fix

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

## Validation

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

Fixes #1072

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:31:08 +08:00
lancedb-gatefixer[bot] c5f9efefe9 test(python): cover local sync multiple-vector search (#3830)
## 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>
2026-08-07 17:30:55 +08:00
David Tolnay f4c668e244 chore(deps): declare more specific futures dependency (#3800)
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
```
2026-08-07 17:24:38 +08:00
Xuanwo b1cfe6edb1 ci(docs): add scheduled doc link check (#3888)
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.
2026-08-07 16:31:40 +08:00
LanceDB Robot 001237c7a4 chore: update lance dependency to v11.0.0-beta.2 (#3886)
Updates the Lance dependencies and Java lance-core to v11.0.0-beta.2.
Includes required compatibility fixes for the LanceFileVersion module
move and the updated GooseFS/OpenDAL dependency. Lance tag:
https://github.com/lance-format/lance/releases/tag/v11.0.0-beta.2

---------

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

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

## Root cause

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

## Fix

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

## Validation

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

Fixes #1600

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

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

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

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

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

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

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

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

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

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

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


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


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


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


running 0 tests

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


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


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

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

Encode at the three affected sites, mirroring `fetch_blob_files`. The
shared Rust client covers all bindings.
2026-08-06 16:53:09 +08:00
lancedb-gatefixer[bot] 1493ece3de test(node): cover remote table server errors (#3841)
## 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>
2026-08-06 16:50:05 +08:00
lancedb-gatefixer[bot] e6444ecc05 fix(rust): handle missing mirrored copy sources (#3843)
## Summary

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

## Root cause

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

## Validation

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

Fixes #1176

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:49:49 +08:00
lancedb-gatefixer[bot] cc0139c136 test(node): cover foreign Float64 vector schema workflow (#3844)
## 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>
2026-08-06 16:49:13 +08:00
lancedb-gatefixer[bot] b20696ef9c fix(remote): validate cloud DNS hostnames (#3845)
## Summary

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

## Root cause

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

## Validation

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

Fixes #799

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

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

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

## Root cause

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

## Fix

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

## Validation

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

Fixes #3330

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:48:24 +08:00
lancedb-gatefixer[bot] c1a3fa7f51 fix(python): preserve repeated indexed merge inserts (#3850)
## 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>
2026-08-06 16:48:01 +08:00
lancedb-gatefixer[bot] 0ba82873c5 fix(python): cover nullable list v2.2 decoding (#3853)
## 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>
2026-08-06 16:47:29 +08:00
lancedb-gatefixer[bot] 3af51541a0 test(rust): cover fixed-size binary merge insert regression (#3854)
## Summary

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

## Root cause

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

## Validation

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

Fixes #2869

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:46:49 +08:00
lancedb-gatefixer[bot] 2c06a48bd8 test(python): cover Arrow buffer release after add (#3860)
## 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>
2026-08-06 16:45:10 +08:00
lancedb-gatefixer[bot] ac8b28c010 fix(python): support nullable pandas merge input (#3864)
## 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>
2026-08-06 16:44:38 +08:00
lancedb-gatefixer[bot] 173f889d2a test(python): cover stale scalar prefilters in hybrid search (#3865)
## 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>
2026-08-06 16:44:20 +08:00
lancedb-gatefixer[bot] 03b52e5877 test(node): cover fixed-size list schemas with typed arrays (#3866)
## 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>
2026-08-06 16:43:59 +08:00
lancedb-gatefixer[bot] 798e5364fb test(python): cover VoyageAI text source routing (#3872)
## 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>
2026-08-06 16:42:20 +08:00
lancedb-gatefixer[bot] f1f34dfdd3 fix(python): instruct dimension probe for instructor embeddings (#3874)
## 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>
2026-08-06 16:41:11 +08:00
lancedb-gatefixer[bot] 123c921c4f test(python): cover sliced nullable table search (#3875)
## 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>
2026-08-06 16:40:41 +08:00
lancedb-gatefixer[bot] 99a68db78c test(rust): cover concurrent appends during compaction (#3878)
## Summary

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

## Root cause

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

## Validation

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

Fixes #1498

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:40:01 +08:00
lancedb-gatefixer[bot] 9e73d440a3 test(python): cover schema-only vector table creation (#3882)
## 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>
2026-08-06 16:39:26 +08:00
lancedb-gatefixer[bot] 3956d9dbfa fix(python): prevent OpenSSL linkage in Linux wheels (#3877)
## Summary

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

## Root cause

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

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

## Validation

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

Fixes #1884

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:38:27 +08:00
lancedb-gatefixer[bot] 16e1967efc fix(python): align wheel ABI with supported versions (#3884)
## 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>
2026-08-06 16:36:58 +08:00
lancedb-gatefixer[bot] 27dd92c67e test(python): cover debugger-safe connection inspection (#3880)
## 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>
2026-08-06 16:36:27 +08:00
lancedb-gatefixer[bot] 9e2e711c7a test(python): cover OpenAI registry variable round-trip (#3863)
## 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>
2026-08-06 16:36:05 +08:00
lancedb-gatefixer[bot] c3176a47ce fix(python): report unsplittable IVF partition errors (#3846)
## 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>
2026-08-06 16:13:54 +08:00
lancedb-gatefixer[bot] 7357d63e87 fix(python): guard concurrent table deletes (#3787)
<!-- 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>
2026-08-05 15:17:04 -07:00
lancedb-gatefixer[bot] 624a75edf7 fix(python): avoid debugger deadlock during connection inspection (#3788)
## 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>
2026-08-05 15:15:49 -07:00
Justin Miller c7ea91f3ea test: cover blob null/empty preservation across Table::optimize (#3774)
## Description

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

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

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

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

## Tests

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

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

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

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

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

## Not addressed here

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

Closes #3744

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

---------

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

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

Matches the builders add, update, and merge_insert already use.
2026-08-04 11:18:22 -07:00
Adityaj0 f79dc017c4 fix: when_not_matched_by_source_delete() doesn't reset a previously-set condition (#3771)
## 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>
2026-08-03 15:49:52 -07:00
Adityaj0 e6ae93f52a fix: hybrid search minimum_nprobes(0) silently no-ops instead of raising (#3770)
## 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>
2026-08-03 12:54:41 -07:00
Drew Gallardo 3dd9c598e9 feat(remote): add seekable blob range reads (#3750)
## Summary

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

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

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

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

### Behavior

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

## Testing

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 08:38:08 -07:00
dependabot[bot] 9e26bf3fba chore(deps): bump the rust-minor-patch group with 3 updates (#3758)
Bumps the rust-minor-patch group with 3 updates:
[http](https://github.com/hyperium/http),
[napi-derive](https://github.com/napi-rs/napi-rs) and
[napi-build](https://github.com/napi-rs/napi-rs).

Updates `http` from 1.4.2 to 1.5.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/hyperium/http/releases">http's
releases</a>.</em></p>
<blockquote>
<h2>v1.5.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(method): add QUERY method by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/http/pull/798">hyperium/http#798</a></li>
<li>fix(uri): allow empty paths in uri::Builder by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/http/pull/853">hyperium/http#853</a></li>
<li>perf(header,uri): faster value validation, URI parse/format, map
inserts by <a
href="https://github.com/geeknoid"><code>@​geeknoid</code></a> in <a
href="https://redirect.github.com/hyperium/http/pull/852">hyperium/http#852</a></li>
<li>fix(uri): enforce max length in PathAndQuery by <a
href="https://github.com/seanmonstar"><code>@​seanmonstar</code></a> in
<a
href="https://redirect.github.com/hyperium/http/pull/856">hyperium/http#856</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/geeknoid"><code>@​geeknoid</code></a>
made their first contribution in <a
href="https://redirect.github.com/hyperium/http/pull/852">hyperium/http#852</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/hyperium/http/compare/v1.4.2...v1.5.0">https://github.com/hyperium/http/compare/v1.4.2...v1.5.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/hyperium/http/blob/master/CHANGELOG.md">http's
changelog</a>.</em></p>
<blockquote>
<h1>1.5.0 (July 29, 2026)</h1>
<ul>
<li>Add <code>Method::QUERY</code> constant for the new QUERY method
defined in RFC 10008.</li>
<li>Fix <code>uri::Builder::path_and_query()</code> to allow empty
strings to mean no path.</li>
<li>Fix <code>uri::PathAndQuery</code> parsing to enforce URI max
length.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/hyperium/http/commit/16fc9a7b840c2181e7f8b37397c107b0ffcd050d"><code>16fc9a7</code></a>
v1.5.0</li>
<li><a
href="https://github.com/hyperium/http/commit/e559023f67e3fad6ecc3ee91307be178e0f13626"><code>e559023</code></a>
fix(uri): enforce max length in PathAndQuery (<a
href="https://redirect.github.com/hyperium/http/issues/856">#856</a>)</li>
<li><a
href="https://github.com/hyperium/http/commit/2178e175c4e247a33ba5f6ca3503afb1afbaabba"><code>2178e17</code></a>
perf(header,uri): faster value validation, URI parse/format, map inserts
(<a
href="https://redirect.github.com/hyperium/http/issues/852">#852</a>)</li>
<li><a
href="https://github.com/hyperium/http/commit/03c8cd7faeddfad00873b4d58a45ecdf74ebebe6"><code>03c8cd7</code></a>
fix(uri): allow empty paths in uri::Builder (<a
href="https://redirect.github.com/hyperium/http/issues/853">#853</a>)</li>
<li><a
href="https://github.com/hyperium/http/commit/bb8705b25cdb6e29081edf9ade2ea124f6783e18"><code>bb8705b</code></a>
feat(method): add QUERY method (<a
href="https://redirect.github.com/hyperium/http/issues/798">#798</a>)</li>
<li>See full diff in <a
href="https://github.com/hyperium/http/compare/v1.4.2...v1.5.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `napi-derive` from 3.6.0 to 3.6.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/napi-rs/napi-rs/releases">napi-derive's
releases</a>.</em></p>
<blockquote>
<h2>napi-derive-v3.6.1</h2>
<h3>Other</h3>
<ul>
<li>updated the following local packages: napi-derive-backend</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/58bd87fa524a837a7c962ab4103e5588557ccd81"><code>58bd87f</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3414">#3414</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/9da87236dbc4fef99f066b7a130f4d0377308d44"><code>9da8723</code></a>
chore(release): publish</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/8d22196aa98a1e6e70584561f5446d117d9c802c"><code>8d22196</code></a>
chore(deps): update dependency oxc-parser to ^0.142.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3422">#3422</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/abc30fbafc2e3967d499cef970c68b3edfefd850"><code>abc30fb</code></a>
build(deps): bump postcss from 8.5.17 to 8.5.23 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3421">#3421</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/55421392cbaa24d4df69419e4c6d4958fbcb6a12"><code>5542139</code></a>
build(deps): bump fast-xml-parser from 5.9.3 to 5.10.1 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3418">#3418</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/dc4ee8c89cc27ce30e239482199b3b3d786bf8b6"><code>dc4ee8c</code></a>
build(deps): bump fast-uri from 3.1.3 to 3.1.4 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3419">#3419</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/050d985196174b4be830cdb813d09e2705258455"><code>050d985</code></a>
feat(async-runtime): drain-linger surface + lock-free scheduler
internals (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3">#3</a>...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/e0b87086eefe0e7efeea6d269e9403c4be4ba9aa"><code>e0b8708</code></a>
chore(deps): update dependency oxc-parser to ^0.141.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3417">#3417</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/fc8494010697d078a93a528c3180271f6f187504"><code>fc84940</code></a>
chore(deps): update actions/setup-node action to v7 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3413">#3413</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/ee598db45985ef11e18c7340801c28bb2452b688"><code>ee598db</code></a>
build(deps): bump protobufjs from 7.6.4 to 7.6.5 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3410">#3410</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-derive-v3.6.0...napi-derive-v3.6.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `napi-build` from 2.3.2 to 2.4.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/napi-rs/napi-rs/releases">napi-build's
releases</a>.</em></p>
<blockquote>
<h2>napi-build-v2.4.0</h2>
<h3>Added</h3>
<ul>
<li><em>(cli)</em> support non-threaded WASI targets (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3353">#3353</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/58bd87fa524a837a7c962ab4103e5588557ccd81"><code>58bd87f</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3414">#3414</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/9da87236dbc4fef99f066b7a130f4d0377308d44"><code>9da8723</code></a>
chore(release): publish</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/8d22196aa98a1e6e70584561f5446d117d9c802c"><code>8d22196</code></a>
chore(deps): update dependency oxc-parser to ^0.142.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3422">#3422</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/abc30fbafc2e3967d499cef970c68b3edfefd850"><code>abc30fb</code></a>
build(deps): bump postcss from 8.5.17 to 8.5.23 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3421">#3421</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/55421392cbaa24d4df69419e4c6d4958fbcb6a12"><code>5542139</code></a>
build(deps): bump fast-xml-parser from 5.9.3 to 5.10.1 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3418">#3418</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/dc4ee8c89cc27ce30e239482199b3b3d786bf8b6"><code>dc4ee8c</code></a>
build(deps): bump fast-uri from 3.1.3 to 3.1.4 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3419">#3419</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/050d985196174b4be830cdb813d09e2705258455"><code>050d985</code></a>
feat(async-runtime): drain-linger surface + lock-free scheduler
internals (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3">#3</a>...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/e0b87086eefe0e7efeea6d269e9403c4be4ba9aa"><code>e0b8708</code></a>
chore(deps): update dependency oxc-parser to ^0.141.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3417">#3417</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/fc8494010697d078a93a528c3180271f6f187504"><code>fc84940</code></a>
chore(deps): update actions/setup-node action to v7 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3413">#3413</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/ee598db45985ef11e18c7340801c28bb2452b688"><code>ee598db</code></a>
build(deps): bump protobufjs from 7.6.4 to 7.6.5 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3410">#3410</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-build-v2.3.2...napi-build-v2.4.0">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-02 09:53:09 -07:00
Will Jones 93354baf34 chore: upgrade rust toolchain to 1.97.0 (#3643)
Bumps the pinned Rust toolchain from 1.95.0 to the latest stable
(1.97.0).

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

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

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

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

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

errors() and progress() are not included.

Tested with mocked endpoints in all three languages.

---------

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 07:32:28 -07:00
Cohen Karnell dd2b11eda2 fix(python): log when storage_options is ignored in RemoteDBConnection.open_table (#3743)
`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.
2026-07-30 19:32:31 -07:00
Will Jones 5a1015ba72 docs(python): fill gaps in the Python API reference (#3746)
`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>
2026-07-30 16:50:05 -07:00
Farmer.Chillax 48945d0658 feat(python): add namespace/table exist support (#3460)
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>
2026-07-30 15:20:47 -07:00
Drew Gallardo 77208fd464 feat(remote): add RemoteTable fetch_blobs HTTP client (#3684)
Remote half of the blob read path. #3578 did local Python. This makes
`RemoteTable` hit the server.

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

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

### Example

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

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

Or pass ids yourself:

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

### Testing

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

---------

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

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

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

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

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

> stream error sent by user: unexpected internal error

## Root cause

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

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

## Fix

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

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

## Testing

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

---------

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

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

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

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

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

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

## Why

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

## How

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

## Validation

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

---------

Co-authored-by: Yang Cen <yangcen@Yangs-Mac-mini.local>
2026-07-29 17:40:12 +08:00
Lance Release e5f489818b Bump version: 0.37.0-beta.0 → 0.37.1-beta.0 2026-07-29 07:12:34 +00:00
buduoqiu 98a52267a2 feat(python): configure streaming transform parallelism (#3699)
## 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>
2026-07-28 15:38:34 -07:00
Will Jones ff50e698cf ci: cut Actions cost by moving builds to free runners and fixing caches (#3735)
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>
2026-07-28 14:38:21 -07:00
kid b799ebaa69 fix(node): reject non-string Arrow metadata (#3728)
## Summary

- validate Arrow metadata keys and values independently at runtime
- reject malformed foreign schemas before constructing a local Arrow
schema
- cover valid and invalid metadata entries across Arrow 15–18

## Testing

- `node_modules/.bin/jest --runInBand __test__/arrow.test.ts -t "schema
metadata"`
- `node_modules/.bin/jest --runInBand __test__/arrow.test.ts`
- `node node_modules/@biomejs/biome/bin/biome format --write
lancedb/sanitize.ts __test__/arrow.test.ts`
- `pnpm lint`
- `pnpm build`
- `pnpm run docs`

Fixes #3729
2026-07-28 13:36:08 -07:00
kid 72fc660f9e feat(python): expose AsyncTable.to_lance (#3730)
## Summary

- expose the existing async Lance dataset conversion as
`AsyncTable.to_lance`
- preserve table version, branch, and refreshed storage options when
opening the dataset
- route internal async pandas/query paths through the public API
- cover normal tables, checked-out versions, branches, and forwarded
dataset options

## Testing

- `cd python && uv run --no-sync pytest python/tests/test_table.py -q`
- `cd python && uv run --no-sync pytest python/tests/test_query.py -q`
- `cd python && uv run --no-sync pytest --doctest-modules
python/lancedb/table.py -q`
- `uv run --project python --no-sync ruff format --check
python/python/lancedb/table.py python/python/lancedb/query.py
python/python/tests/test_table.py`
- `uv run --project python --no-sync ruff check .`

Fixes #1387
2026-07-28 13:31:06 -07:00
dependabot[bot] 1ebde1f06c chore(deps): bump arrow from 58.3.0 to 58.4.0 (#3722)
Bumps [arrow](https://github.com/apache/arrow-rs) from 58.3.0 to 58.4.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/apache/arrow-rs/releases">arrow's
releases</a>.</em></p>
<blockquote>
<h2>arrow 58.4.0</h2>
<!-- raw HTML omitted -->
<h1>Changelog</h1>
<h2><a href="https://github.com/apache/arrow-rs/tree/58.4.0">58.4.0</a>
(2026-07-17)</h2>
<p><a
href="https://github.com/apache/arrow-rs/compare/58.3.0...58.4.0">Full
Changelog</a></p>
<p><strong>Merged pull requests:</strong></p>
<ul>
<li>[58_maintenance] [parquet] Allow more encryption algorithms (<a
href="https://redirect.github.com/apache/arrow-rs/issues/9203">#9203</a>)
<a
href="https://redirect.github.com/apache/arrow-rs/pull/10351">#10351</a>
[<a
href="https://github.com/apache/arrow-rs/labels/parquet">parquet</a>]
(<a href="https://github.com/mbutrovich">mbutrovich</a>)</li>
<li>[58_maintenance] Backport cargo audit fixes <a
href="https://redirect.github.com/apache/arrow-rs/pull/10369">#10369</a>
(<a href="https://github.com/alamb">alamb</a>)</li>
<li>[58_maintenance] chore: Ignore py03 vulnerabilities until upgrade <a
href="https://redirect.github.com/apache/arrow-rs/pull/10370">#10370</a>
(<a href="https://github.com/alamb">alamb</a>)</li>
<li>[58_maintenance] Add test for `parquet-testing/bad_data/ARROW-<a
href="https://redirect.github.com/apache/arrow-rs/issues/47662">GH-47662</a>.parquet`
(<a
href="https://redirect.github.com/apache/arrow-rs/issues/10077">#10077</a>)
<a
href="https://redirect.github.com/apache/arrow-rs/pull/10371">#10371</a>
[<a
href="https://github.com/apache/arrow-rs/labels/parquet">parquet</a>]
(<a href="https://github.com/alamb">alamb</a>)</li>
</ul>
<p>* <em>This Changelog was automatically generated by <a
href="https://github.com/github-changelog-generator/github-changelog-generator">github_changelog_generator</a></em></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/apache/arrow-rs/blob/58.4.0/CHANGELOG.md">arrow's
changelog</a>.</em></p>
<blockquote>
<h2><a href="https://github.com/apache/arrow-rs/tree/58.4.0">58.4.0</a>
(2026-07-17)</h2>
<p><a
href="https://github.com/apache/arrow-rs/compare/58.3.0...58.4.0">Full
Changelog</a></p>
<p><strong>Merged pull requests:</strong></p>
<ul>
<li>[58_maintenance] [parquet] Allow more encryption algorithms (<a
href="https://redirect.github.com/apache/arrow-rs/issues/9203">#9203</a>)
<a
href="https://redirect.github.com/apache/arrow-rs/pull/10351">#10351</a>
[<a
href="https://github.com/apache/arrow-rs/labels/parquet">parquet</a>]
(<a href="https://github.com/mbutrovich">mbutrovich</a>)</li>
<li>[58_maintenance] Backport cargo audit fixes <a
href="https://redirect.github.com/apache/arrow-rs/pull/10369">#10369</a>
(<a href="https://github.com/alamb">alamb</a>)</li>
<li>[58_maintenance] chore: Ignore py03 vulnerabilities until upgrade <a
href="https://redirect.github.com/apache/arrow-rs/pull/10370">#10370</a>
(<a href="https://github.com/alamb">alamb</a>)</li>
<li>[58_maintenance] Add test for `parquet-testing/bad_data/ARROW-<a
href="https://redirect.github.com/apache/arrow-rs/issues/47662">GH-47662</a>.parquet`
(<a
href="https://redirect.github.com/apache/arrow-rs/issues/10077">#10077</a>)
<a
href="https://redirect.github.com/apache/arrow-rs/pull/10371">#10371</a>
[<a
href="https://github.com/apache/arrow-rs/labels/parquet">parquet</a>]
(<a href="https://github.com/alamb">alamb</a>)</li>
</ul>
<p>* <em>This Changelog was automatically generated by <a
href="https://github.com/github-changelog-generator/github-changelog-generator">github_changelog_generator</a></em></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/arrow-rs/commit/0ff81c1215cc026a1de93ce3d2078df1ecba6f09"><code>0ff81c1</code></a>
[58_maintenance] Update changelog for <a
href="https://redirect.github.com/apache/arrow-rs/issues/10371">#10371</a>
(<a
href="https://redirect.github.com/apache/arrow-rs/issues/10372">#10372</a>)</li>
<li><a
href="https://github.com/apache/arrow-rs/commit/95d7231227e1ce7a1ec049ab2d45a6cffd7a50f9"><code>95d7231</code></a>
[58_maintenance] Add test for `parquet-testing/bad_data/ARROW-<a
href="https://redirect.github.com/apache/arrow-rs/issues/47662">GH-47662</a>.parque...</li>
<li><a
href="https://github.com/apache/arrow-rs/commit/4544deaa434bbf8e7fe930bf9497fd36e8e737d1"><code>4544dea</code></a>
Prepare for <code>58.4.0</code> release (<a
href="https://redirect.github.com/apache/arrow-rs/issues/10367">#10367</a>)</li>
<li><a
href="https://github.com/apache/arrow-rs/commit/32e8c1809642647ddf87703c410c4713df166281"><code>32e8c18</code></a>
chore: Ignore py03 vulnerabilities until upgrade (<a
href="https://redirect.github.com/apache/arrow-rs/issues/10370">#10370</a>)</li>
<li><a
href="https://github.com/apache/arrow-rs/commit/c12030f29639f9ac36fdfedd7d00f3b6b6bbd2c1"><code>c12030f</code></a>
[58_maintenance] Backport cargo audit fixes (<a
href="https://redirect.github.com/apache/arrow-rs/issues/10369">#10369</a>)</li>
<li><a
href="https://github.com/apache/arrow-rs/commit/01046eed275d4fabfd922f6a8924410102ab1802"><code>01046ee</code></a>
[58_maintenance] [parquet] Allow more encryption algorithms (<a
href="https://redirect.github.com/apache/arrow-rs/issues/9203">#9203</a>)
(<a
href="https://redirect.github.com/apache/arrow-rs/issues/10351">#10351</a>)</li>
<li><a
href="https://github.com/apache/arrow-rs/commit/adb77a16adff42fface41664dc2a3cb564f45fcf"><code>adb77a1</code></a>
[58_maintenance] Fix MSRV CI check (pin tonic to 0.14.5, install
cargo-msrv -...</li>
<li>See full diff in <a
href="https://github.com/apache/arrow-rs/compare/58.3.0...58.4.0">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-28 13:28:54 -07:00
Dan Tasse 29c030f865 fix: accept either timestamp or timestamp_millis for versions (#3733)
`list_versions()` against a remote table on a server that uses
lance-namespace was failing. The server was returning
`timestamp_millis`, while db-catalog deployments were returning
`timestamp`, and the client was only accepting `timestamp`. So, updated
the client to accept both. (assuming we're migrating over time;
eventually we can turn off the `timestamp` code path I suppose.)
2026-07-28 10:44:49 -04:00
Xuanwo ff6ff09998 feat: support batched blob range reads (#3703)
## Summary

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

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

## Motivating example

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

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

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

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

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

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

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

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

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

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

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

Root cause

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

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

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

(This is specific to the empty-projection case: COUNT(*) with no filter
is answered from statistics and never scans, and COUNT(<col>) projects a
column — both already work.)
2026-07-27 12:25:11 -07:00
LanceDB Robot ba4558a64f chore: update lance dependency to v10.0.0-beta.5 (#3718)
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
2026-07-27 15:27:40 +08:00
Heng Ge f655f62e09 feat(query): add use_lsm to read MemWAL LSM data (#3489)
## What

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

## How

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

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

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

## Version

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

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

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

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

## Mechanism

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

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

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

Both GH release jobs used:

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

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

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

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

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

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

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

## Verification

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

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

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

## Notes for review

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

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

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

## Why

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

## How it works

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

## Validation

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

## Limitations

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

Co-authored-by: Yang Cen <yangcen@Yangs-Mac-mini.local>
2026-07-24 15:02:38 -07:00
Andrew Chen 18760f74cd fix: crash in AnswerdotaiRerankers/ColbertReranker for return_score="all" (#3671)
## 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>
2026-07-24 15:02:23 -07:00
LanceDB Robot c9d07ef6fc chore: update lance dependency to v10.0.0-beta.3 (#3710)
Updates the Rust workspace and Java lance-core dependencies to [Lance
v10.0.0-beta.3](https://github.com/lance-format/lance/releases/tag/v10.0.0-beta.3).

Includes compatibility updates for Lance’s nullable blob payload and
handle APIs.
2026-07-24 15:01:30 -07:00
Eran Dagan 0bc081608a fix(python): allow selection of _rowid in Permutation (#3133)
Closes #3132
2026-07-22 14:11:57 -07:00
Prashanth Rao d6f9f8560e docs(java): fill Java API reference gaps (#3615)
## 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.
2026-07-22 17:05:58 -04:00
Dan Tasse 0bd0944062 feat: branch skill updates for merge (#3685)
Skill updates for branch merging. Terra/Sol can do an end-to-end "create
3 branches, add a column, generate embeddings, merge the best" workflow
now.
2026-07-22 13:29:06 -04:00
dependabot[bot] 91f775c093 chore(deps): bump the rust-minor-patch group across 1 directory with 19 updates (#3700)
Bumps the rust-minor-patch group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [async-trait](https://github.com/dtolnay/async-trait) | `0.1.89` |
`0.1.91` |
| [datafusion](https://github.com/apache/datafusion) | `54.0.0` |
`54.1.0` |
| [regex](https://github.com/rust-lang/regex) | `1.13.0` | `1.13.1` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.52.3` | `1.53.1` |
| [serde](https://github.com/serde-rs/serde) | `1.0.228` | `1.0.229` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.150` | `1.0.151`
|
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.5` | `1.24.0` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.103` | `1.0.104` |
| [napi](https://github.com/napi-rs/napi-rs) | `3.10.5` | `3.11.0` |
| [napi-derive](https://github.com/napi-rs/napi-rs) | `3.5.10` | `3.6.0`
|
| [libc](https://github.com/rust-lang/libc) | `0.2.186` | `0.2.189` |


Updates `async-trait` from 0.1.89 to 0.1.91
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/async-trait/releases">async-trait's
releases</a>.</em></p>
<blockquote>
<h2>0.1.90</h2>
<ul>
<li>Update to syn 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dtolnay/async-trait/commit/d049ee02a2d50b72e03d07f06311e23bf5b512a8"><code>d049ee0</code></a>
Release 0.1.91</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/7a0961f275432c40cc5e7aa011362e4b50d763b1"><code>7a0961f</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/async-trait/issues/301">#301</a>
from dtolnay/mutability</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/740f86f23d176229011f2389c8a206ef5ba547e7"><code>740f86f</code></a>
Ignore mut_mut pedantic clippy lint in test</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/4699cd320a8aaaf06a2a369cb9e1f2964b14b71c"><code>4699cd3</code></a>
Fix mutability for by-reference receivers</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/6dd3573df95878d34fcfc0ab9c242aeab3140f82"><code>6dd3573</code></a>
Add regression test for issue 300</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/2371797a3938808bd7e1f4f9abd0eed51bd99634"><code>2371797</code></a>
Release 0.1.90</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/d03f075ecc2b9fcbf6757f3654a7974a518a144e"><code>d03f075</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/async-trait/issues/299">#299</a>
from dtolnay/syn3</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/6cf42c104d1c02aa97d4fc62ff117f8d6b05eacb"><code>6cf42c1</code></a>
Update to syn 3</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/b9daabad756580d31bd2b9221ea599db51bf6cdd"><code>b9daaba</code></a>
Ignore match_same_arms pedantic clippy lint</li>
<li><a
href="https://github.com/dtolnay/async-trait/commit/aa706d127114e57dc163238af947ba495b0b86d2"><code>aa706d1</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li>Additional commits viewable in <a
href="https://github.com/dtolnay/async-trait/compare/0.1.89...0.1.91">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion-catalog` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion-common` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion-execution` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion-expr` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion-functions` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion-physical-plan` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion-physical-expr` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `datafusion-sql` from 54.0.0 to 54.1.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/apache/datafusion/commit/0d1f2ebe2cc97c91b736bc0a160b5b73cf40437a"><code>0d1f2eb</code></a>
[branch-54] chore: Update version 54.1.0, add changelog (<a
href="https://redirect.github.com/apache/datafusion/issues/23689">#23689</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/fcbc9bda1fd004c349fa70f61f8c8b40448294ca"><code>fcbc9bd</code></a>
[branch-54] Handle nulls in type coercion of higher-order UDFs,
map_extract, ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/61b0f76a32b95560b49d5453f021b3ffdb9bb255"><code>61b0f76</code></a>
[branch-54] Resolve lost wakeup in SpillPoolReader with multiple conc…
(<a
href="https://redirect.github.com/apache/datafusion/issues/23654">#23654</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/2142d5b2a737fa01836906d2526265035964ac99"><code>2142d5b</code></a>
[branch-54] chore: fix cargo audit (<a
href="https://redirect.github.com/apache/datafusion/issues/23607">#23607</a>)</li>
<li><a
href="https://github.com/apache/datafusion/commit/c735a49a3634b01a0a6e80fe13ef1059998b3329"><code>c735a49</code></a>
[branch-54] fix: don't duplicate volatile expressions when pushing
projection...</li>
<li><a
href="https://github.com/apache/datafusion/commit/82acf50e23103aa01d16518765c0b3bdf10c60f2"><code>82acf50</code></a>
[branch-54] perf: avoid intermediate slice allocation in Spark slice
function...</li>
<li><a
href="https://github.com/apache/datafusion/commit/887b065e7393ac8912a5c6f5b2b44799407b8f33"><code>887b065</code></a>
[branch-54] fix: preserve no-filter SMJ matches across pending outer
batches ...</li>
<li><a
href="https://github.com/apache/datafusion/commit/06c4d41c3a894cd4576a764cde1486ca062c4f12"><code>06c4d41</code></a>
[branch-54] fix: handle <code>IS TRUE</code> correctly in
<code>EliminateOuterJoin</code> (backport...</li>
<li><a
href="https://github.com/apache/datafusion/commit/7fd1b0dfa1b96b25d8ac357aefb921fcbdc60ce1"><code>7fd1b0d</code></a>
[branch-54] fix: Correctly compute nullability in recursive CTE schemas
(back...</li>
<li><a
href="https://github.com/apache/datafusion/commit/9d41b02983058d692373c7ce18f6bba3b3afd5cc"><code>9d41b02</code></a>
[branch-54] fix: regex simplification of anchored patterns produces
wrong res...</li>
<li>Additional commits viewable in <a
href="https://github.com/apache/datafusion/compare/54.0.0...54.1.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `regex` from 1.13.0 to 1.13.1
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/regex/blob/master/CHANGELOG.md">regex's
changelog</a>.</em></p>
<blockquote>
<h1>1.13.1 (2026-07-15)</h1>
<p>This is a release that fixes a bug where incorrect regex match
offsets could be
reported. Note that this doesn't impact whether a match occurs or not,
just
where it occurs. The match offsets are still valid for slicing, they
just may
not refer to the correct leftmost-first match. See
<a
href="https://redirect.github.com/rust-lang/regex/pull/1364">#1364</a>
for (many) more details.</p>
<p>Bug fixes:</p>
<ul>
<li><a
href="https://redirect.github.com/rust-lang/regex/issues/1354">#1354</a>:
Fixes previously unsound reverse suffix and inner optimizations.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rust-lang/regex/commit/2b527599eb9eea0dcc288c704584f242f26a5c61"><code>2b52759</code></a>
1.13.1, redux</li>
<li><a
href="https://github.com/rust-lang/regex/commit/40e98238fff903f3e1ec95bbdb487185dd60504a"><code>40e9823</code></a>
1.13.1</li>
<li><a
href="https://github.com/rust-lang/regex/commit/75fcb962d6ea1c456f6f023c9537a66389413a85"><code>75fcb96</code></a>
changelog: 1.13.1</li>
<li><a
href="https://github.com/rust-lang/regex/commit/64ad0b618e043b791ed5385dd5504a436da1ddae"><code>64ad0b6</code></a>
automata: fix bug in reverse suffix/inner optimization</li>
<li><a
href="https://github.com/rust-lang/regex/commit/fa91c31a4291c9dda6afe19829e6fe2e3bbc2da5"><code>fa91c31</code></a>
automata: fix a bug caught by Codex review</li>
<li><a
href="https://github.com/rust-lang/regex/commit/30390ec3e8889aad830337cdf3a7a01ae195ae73"><code>30390ec</code></a>
automata: formatting tweaks</li>
<li><a
href="https://github.com/rust-lang/regex/commit/821a8eb1ad7860ddc788fe36f495036df63cfc35"><code>821a8eb</code></a>
automata: refactor reverse suffix/inner search slightly</li>
<li><a
href="https://github.com/rust-lang/regex/commit/10afd704d88d00ddfcd10218883a81b3ae5e4831"><code>10afd70</code></a>
automata: expose the extracted literals for inner literal
extraction</li>
<li><a
href="https://github.com/rust-lang/regex/commit/8c34f41d3c5a0e16ce17dfb964587cb48625a8d5"><code>8c34f41</code></a>
automata: avoid reverse suffix optimization for non-leftmost-first</li>
<li><a
href="https://github.com/rust-lang/regex/commit/5524f02430d2d118d5c34fde54136d08376de711"><code>5524f02</code></a>
test: add regression tests for failed reverse suffix/inner
optimizations</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/regex/compare/1.13.0...1.13.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `tokio` from 1.52.3 to 1.53.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/tokio/releases">tokio's
releases</a>.</em></p>
<blockquote>
<h2>Tokio v1.53.1</h2>
<h1>1.53.1 (July 20th, 2026)</h1>
<h3>Fixed</h3>
<ul>
<li>signal: restore MSRV by removing <code>OnceLock::wait</code> from
the Windows handler (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8300">#8300</a>)</li>
</ul>
<h3>Fixed (unstable)</h3>
<ul>
<li>time: fix alt timer cancellation and insertion race (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8252">#8252</a>)</li>
</ul>
<h3>Documented</h3>
<ul>
<li>runtime: remove dead link definition in Runtime::block_on (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8301">#8301</a>)</li>
</ul>
<p><a
href="https://redirect.github.com/tokio-rs/tokio/issues/8252">#8252</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/8252">tokio-rs/tokio#8252</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8300">#8300</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/8300">tokio-rs/tokio#8300</a>
<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8301">#8301</a>:
<a
href="https://redirect.github.com/tokio-rs/tokio/pull/8301">tokio-rs/tokio#8301</a></p>
<h2>Tokio v1.53.0</h2>
<h1>1.53.0 (July 17th, 2026)</h1>
<h3>Added</h3>
<ul>
<li>fs: implement <code>From&lt;OwnedFd&gt;</code> and
<code>From&lt;OwnedHandle&gt;</code> for <code>File</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8266">#8266</a>)</li>
<li>metrics: add task schedule latency metric (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/7986">#7986</a>)</li>
<li>net: add <code>SocketAddr</code> methods to Unix sockets (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8144">#8144</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>io: add <code>#[inline]</code> to IO trait impls for in-memory types
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8242">#8242</a>)</li>
<li>net: implement UCred::pid on FreeBSD (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8086">#8086</a>)</li>
<li>net: support Nuttx target os (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8259">#8259</a>)</li>
<li>signal: refactor global variables on Windows (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8231">#8231</a>)</li>
<li>sync: <code>mpsc::{Receiver,UnboundedReceiver}</code> now drops
waker on drop, even if there are still senders (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8095">#8095</a>)</li>
<li>taskdump: support taskdumps on s390x (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8192">#8192</a>)</li>
<li>time: add <code>#[track_caller]</code> to <code>timeout_at()</code>
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8077">#8077</a>)</li>
<li>time: consolidate mutex locks on spurious poll (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8124">#8124</a>)</li>
<li>time: defer waker clone on spurious poll (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8107">#8107</a>)</li>
<li>time: move lazy-registration state into <code>Sleep</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8132">#8132</a>)</li>
<li>tracing: remove unnecessary span clone (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8126">#8126</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>io: do not treat zero-length reads as EOF in <code>Chain</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8251">#8251</a>)</li>
<li>net: use getpeereid for QNX peer credentials (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8270">#8270</a>)</li>
<li>runtime: avoid illegal state in <code>FastRand</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8078">#8078</a>)</li>
<li>sync: wake mpsc receiver when a queued <code>reserve[_many]</code>
returns permits (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8260">#8260</a>)</li>
<li>taskdump: skip double wake on
<code>Trace::capture</code>/<code>Trace::trace_with</code> (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8043">#8043</a>)</li>
<li>time: avoid stack overflow in runtime constructor (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8093">#8093</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tokio-rs/tokio/commit/75fef53d0a8590c2d1dbb63672aa7b7d1ef51155"><code>75fef53</code></a>
chore: prepare Tokio v1.53.1 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8303">#8303</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/ae9d01121377cdbef32b9d5e8559843cce9f927e"><code>ae9d011</code></a>
signal: restore MSRV by removing OnceLock::wait from the Windows handler
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8300">#8300</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/eb4988dc2ecb85d2617971fbbabc84938c141bfd"><code>eb4988d</code></a>
time: fix the loom test of the race between cancellation/insertion (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8302">#8302</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/91d3b4c0bccf2234fc3ed19e605e2cd402f19437"><code>91d3b4c</code></a>
time: fix alt timer cancellation and insertion race (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8252">#8252</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/a46338401b9e0ffc9bd68c31100ee99cee717481"><code>a463384</code></a>
runtime: remove dead link definition in <code>Runtime::block_on</code>
(<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8301">#8301</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/be689a35f5ade5a39e507f79d3ec85cdab27806f"><code>be689a3</code></a>
chore: prepare Tokio v1.53.0 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8294">#8294</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/50f76c71ec7203013f7f0cda59deaa9016e93939"><code>50f76c7</code></a>
chore: prepare tokio-macros v2.7.1 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8295">#8295</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/f61fccad3cd598cce743fc511a983364b77af92a"><code>f61fcca</code></a>
Merge 'tokio-1.52.4' into 'master' (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8290">#8290</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/efdba5fcf02c4b93d379114df136b994c3b21445"><code>efdba5f</code></a>
chore: prepare Tokio v1.52.4 (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8289">#8289</a>)</li>
<li><a
href="https://github.com/tokio-rs/tokio/commit/b0ba02e75507518baed6718b0c37105e430f3a93"><code>b0ba02e</code></a>
Merge 'tokio-1.51.4' into 'tokio-1.52.x' (<a
href="https://redirect.github.com/tokio-rs/tokio/issues/8288">#8288</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/tokio-rs/tokio/compare/tokio-1.52.3...tokio-1.53.1">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde` from 1.0.228 to 1.0.229
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.229</h2>
<ul>
<li>Update to syn 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/serde/commit/7fc3b4c30c94f73a96ebd1553f2b090d928fc3a8"><code>7fc3b4c</code></a>
Release 1.0.229</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6d6e9a11101354ce769a3438a088b6b9305c1863"><code>6d6e9a1</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/3085">#3085</a>
from dtolnay/syn3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6dec3b751126c8338cac0fe8085612d695e4ecf3"><code>6dec3b7</code></a>
Update to syn 3</li>
<li><a
href="https://github.com/serde-rs/serde/commit/cfe669241065984177ff63af8b45058e6e9b499d"><code>cfe6692</code></a>
Resolve mut_mut pedantic clippy lint</li>
<li><a
href="https://github.com/serde-rs/serde/commit/1023d077510b4aef36a41ef56fdb7798568a2654"><code>1023d07</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/dd682c2c86aa7629e77c1ccd93212d3729f4c66d"><code>dd682c2</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/serde/commit/5f0f18b9211732f2d82f73b5a43e4f5ff3701251"><code>5f0f18b</code></a>
Update ui test suite to nightly-2026-06-01</li>
<li><a
href="https://github.com/serde-rs/serde/commit/63a1498f0e7be991ffac5939bdd202ca16e9a23f"><code>63a1498</code></a>
Regenerate stderr with trybuild normalization fixes</li>
<li><a
href="https://github.com/serde-rs/serde/commit/fa7da4a93567ed347ad0735c28e439fca688ef26"><code>fa7da4a</code></a>
Fix unused_features warning</li>
<li><a
href="https://github.com/serde-rs/serde/commit/6b1a17851ea3d86a56aa116ca1cbf428f8d5f22d"><code>6b1a178</code></a>
Unpin CI miri toolchain</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229">compare
view</a></li>
</ul>
</details>
<br />

Updates `serde_json` from 1.0.150 to 1.0.151
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/json/releases">serde_json's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.151</h2>
<ul>
<li>Add RawValue::from_string_unchecked (<a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>,
thanks <a
href="https://github.com/WonderLawrence"><code>@​WonderLawrence</code></a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/serde-rs/json/commit/de8500740cdcabffb9734f503e4889def823cf10"><code>de85007</code></a>
Release 1.0.151</li>
<li><a
href="https://github.com/serde-rs/json/commit/3b2b3c5f28c20ed988bd081a4147c535e7e65c74"><code>3b2b3c5</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/json/issues/1331">#1331</a>
from WonderLawrence/rawvalue-from-string-unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/0406d96860e9d8b9252e2002fa3e626ae48ca1b0"><code>0406d96</code></a>
Debug-assert well-formedness and no-whitespace in
from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/cf16f75d81e28c723323bfc60a68fc02d2994fff"><code>cf16f75</code></a>
Add RawValue::from_string_unchecked</li>
<li><a
href="https://github.com/serde-rs/json/commit/827a315bf2198558f0325b07bcc1e2cd973aba2f"><code>827a315</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/serde-rs/json/commit/cea36a5c017ebffdeb95d0cd0f1aad473bfab758"><code>cea36a5</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li>See full diff in <a
href="https://github.com/serde-rs/json/compare/v1.0.150...v1.0.151">compare
view</a></li>
</ul>
</details>
<br />

Updates `uuid` from 1.23.5 to 1.24.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/uuid-rs/uuid/releases">uuid's
releases</a>.</em></p>
<blockquote>
<h2>v1.24.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat(fmt): support encoding into MaybeUninit buffers by <a
href="https://github.com/weifanglab"><code>@​weifanglab</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/892">uuid-rs/uuid#892</a></li>
<li>Prepare for 1.24.0 release by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/896">uuid-rs/uuid#896</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/weifanglab"><code>@​weifanglab</code></a> made
their first contribution in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/892">uuid-rs/uuid#892</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.5...v1.24.0">https://github.com/uuid-rs/uuid/compare/v1.23.5...v1.24.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/uuid-rs/uuid/commit/6a8aeab3d02838f6fef71e69cdfda963e8c4158b"><code>6a8aeab</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/896">#896</a> from
uuid-rs/cargo/v1.24.0</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/e6db8ec0879fc9e703efc1911512c111f86e540d"><code>e6db8ec</code></a>
prepare for 1.24.0 release</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/606f2365c706ccd0309d3263b381f5378b004e4d"><code>606f236</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/892">#892</a> from
weifanglab/main</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/ab848dbdf652c91af3ed5a413d3edd74bc2ebcfb"><code>ab848db</code></a>
feat(fmt): support encoding into MaybeUninit buffers</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/6fa1a1e38afa7536bad4cd0febf689338f65c220"><code>6fa1a1e</code></a>
feat(fmt): support encoding into MaybeUninit buffers</li>
<li>See full diff in <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.5...v1.24.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `anyhow` from 1.0.103 to 1.0.104
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/anyhow/releases">anyhow's
releases</a>.</em></p>
<blockquote>
<h2>1.0.104</h2>
<ul>
<li>Update <code>syn</code> dev-dependency to version 3</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dtolnay/anyhow/commit/1dbe1862aae650423e3361fbd20b7d17c5109cc3"><code>1dbe186</code></a>
Release 1.0.104</li>
<li><a
href="https://github.com/dtolnay/anyhow/commit/f6479f8e5e10761d7fecde0970cff363dc644d92"><code>f6479f8</code></a>
Update to syn 3</li>
<li>See full diff in <a
href="https://github.com/dtolnay/anyhow/compare/1.0.103...1.0.104">compare
view</a></li>
</ul>
</details>
<br />

Updates `napi` from 3.10.5 to 3.11.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/napi-rs/napi-rs/releases">napi's
releases</a>.</em></p>
<blockquote>
<h2>napi-v3.11.0</h2>
<h3>Added</h3>
<ul>
<li>unforgeable <code>#[napi]</code> class identity via Node object type
tags (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3405">#3405</a>)</li>
<li><em>(napi)</em> add pluggable async runtime backend (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3352">#3352</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(napi)</em> release JsDeferred tsfn on null-env teardown drain
(<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3404">#3404</a>
follow-up) (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3408">#3408</a>)</li>
<li><em>(napi)</em> guard JsDeferred against env teardown (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3404">#3404</a>)</li>
<li><em>(napi)</em> register the async runtime env cleanup hook per
registration (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3400">#3400</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li><em>(napi)</em> share tracing callsite (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3409">#3409</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/679eb79f5cf3c7c6b2850f4ab46092126f23dc5c"><code>679eb79</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3401">#3401</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/762a0e389a0196d7446666ee5ef8468994dcac4f"><code>762a0e3</code></a>
chore(release): publish</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/587ae146a0172f7e8c0d8a22f7126fe51b21b4f2"><code>587ae14</code></a>
perf(napi): share tracing callsite (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3409">#3409</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/02d8ccdbc1eceed9cc3c7e61af61c34b97ff6af2"><code>02d8ccd</code></a>
fix(napi): release JsDeferred tsfn on null-env teardown drain (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3404">#3404</a>
follow-u...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/b63520443469b6a217dbc32a78c5b6524d4b932c"><code>b635204</code></a>
fix(napi): guard JsDeferred against env teardown (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3404">#3404</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/729ebed8f432aadbc1ec400744a8fca01e7cd262"><code>729ebed</code></a>
feat: unforgeable <code>#[napi]</code> class identity via Node object
type tags (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3405">#3405</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/0a4681d3ffa0348ae4524c1c92f4f2fbe631eecd"><code>0a4681d</code></a>
fix(cli): don't force-build crates whose optional napi-derive dependency
is d...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/392ec4026623bca357c5c2131ca12fd1ac5ebed0"><code>392ec40</code></a>
chore(deps): update dependency c8 to v12 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3403">#3403</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/d618d7e8cd74ed1082b270c60caaabda354f6f95"><code>d618d7e</code></a>
feat(napi): add pluggable async runtime backend (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3352">#3352</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/1817ed2371c34efacefdf810b54faf517ebde69b"><code>1817ed2</code></a>
fix(napi): register the async runtime env cleanup hook per registration
(<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3400">#3400</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-v3.10.5...napi-v3.11.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `napi-derive` from 3.5.10 to 3.6.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/napi-rs/napi-rs/releases">napi-derive's
releases</a>.</em></p>
<blockquote>
<h2>napi-derive-v3.6.0</h2>
<h3>Added</h3>
<ul>
<li>unforgeable <code>#[napi]</code> class identity via Node object type
tags (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3405">#3405</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/679eb79f5cf3c7c6b2850f4ab46092126f23dc5c"><code>679eb79</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3401">#3401</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/762a0e389a0196d7446666ee5ef8468994dcac4f"><code>762a0e3</code></a>
chore(release): publish</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/587ae146a0172f7e8c0d8a22f7126fe51b21b4f2"><code>587ae14</code></a>
perf(napi): share tracing callsite (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3409">#3409</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/02d8ccdbc1eceed9cc3c7e61af61c34b97ff6af2"><code>02d8ccd</code></a>
fix(napi): release JsDeferred tsfn on null-env teardown drain (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3404">#3404</a>
follow-u...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/b63520443469b6a217dbc32a78c5b6524d4b932c"><code>b635204</code></a>
fix(napi): guard JsDeferred against env teardown (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3404">#3404</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/729ebed8f432aadbc1ec400744a8fca01e7cd262"><code>729ebed</code></a>
feat: unforgeable <code>#[napi]</code> class identity via Node object
type tags (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3405">#3405</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/0a4681d3ffa0348ae4524c1c92f4f2fbe631eecd"><code>0a4681d</code></a>
fix(cli): don't force-build crates whose optional napi-derive dependency
is d...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/392ec4026623bca357c5c2131ca12fd1ac5ebed0"><code>392ec40</code></a>
chore(deps): update dependency c8 to v12 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3403">#3403</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/d618d7e8cd74ed1082b270c60caaabda354f6f95"><code>d618d7e</code></a>
feat(napi): add pluggable async runtime backend (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3352">#3352</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/1817ed2371c34efacefdf810b54faf517ebde69b"><code>1817ed2</code></a>
fix(napi): register the async runtime env cleanup hook per registration
(<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3400">#3400</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-derive-v3.5.10...napi-derive-v3.6.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `libc` from 0.2.186 to 0.2.189
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/libc/releases">libc's
releases</a>.</em></p>
<blockquote>
<h2>0.2.189</h2>
<h3>Added</h3>
<ul>
<li>Emscripten: Add <code>pthread_sigmask</code>, <code>sigwait</code>,
<code>sigwaitinfo</code>, <code>sigtimedwait</code>,
<code>faccessat</code>, and <code>pthread_kill</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/5270">#5270</a>)</li>
<li>Linux SPARC: Enable the <code>clone3</code> syscall (<a
href="https://redirect.github.com/rust-lang/libc/pull/4980">#4980</a>)</li>
<li>Solarish: Add <code>CLOCK_PROCESS_CPUTIME_ID</code> and
<code>CLOCK_THREAD_CPUTIME_ID</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/5274">#5274</a>)</li>
</ul>
<h3>Deprecated</h3>
<ul>
<li>Deprecate <code>CLONE_INTO_CGROUP</code> and
<code>CLONE_CLEAR_SIGHAND</code>. These overflow their types and will be
changed to a larger size in the future. (<a
href="https://github.com/rust-lang/libc/commit/8c6e6710458db4d6aa0766f6f84bbf13f640237e">8c6e6710458d</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Musl riscv32: Rename padding fields to avoid a conflict and fix the
build (<a
href="https://github.com/rust-lang/libc/commit/2499ff0ad9936a036e78a4e0991445efee383564">2499ff0ad993</a>)</li>
<li>NuttX: Fix <code>wchar_t</code> definition under Arm (<a
href="https://redirect.github.com/rust-lang/libc/pull/5245">#5245</a>)</li>
<li>Windows: Add back link names for <code>time</code>-related symbols
(<a
href="https://redirect.github.com/rust-lang/libc/pull/5300">#5300</a>)</li>
</ul>
<h2>0.2.188</h2>
<h3>Changed</h3>
<ul>
<li>Restore <code>Send</code> and <code>Sync</code> for <code>DIR</code>
(<a
href="https://github.com/rust-lang/libc/commit/35b062263401733cd89065c6a553640f2ba51ff1">35b062263401</a>)</li>
</ul>
<p>These were removed in 0.2.187 because <code>libc</code> does not
actually make <code>Send</code> and <code>Sync</code>
guarantees about <code>DIR</code> (or other extern types), but this
caused some crates to break.
The traits are added back for now to allow time to migrate, but will be
removed again
in the future; please make sure your crates are not relying on
<code>libc::DIR: Send</code> or
<code>libc::DIR: Sync</code>.</p>
<h2>0.2.187</h2>
<p>This release contains a number of improvements related to 64-bit
<code>time_t</code> configuration.
Of note the existing <code>RUST_LIBC_UNSTABLE_*</code> environment
variables have been replaced
with configuration options. The new way to use these is:</p>
<pre lang="sh"><code>RUSTFLAGS='--cfg=libc_unstable_musl_v1_2_3' cargo
...
RUSTFLAGS='--cfg=libc_unstable_gnu_time_bits=&quot;64&quot;' cargo ...
</code></pre>
<p>Being able to set this via <code>RUSTFLAGS</code> makes it easier to
only apply configuration to
specific targets (and notably, not the host if build scripts are
used).</p>
<p>There are two other notable changes:</p>
<ul>
<li>
<p>The 32-bit <code>windows-gnu</code> targets now respect
<code>libc_unstable_gnu_time_bits</code></p>
</li>
<li>
<p>uClibc now supports a similar configuration option:</p>
<pre lang="sh"><code>RUSTFLAGS='--cfg=libc_unstable_uclibc_time64'
</code></pre>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/libc/blob/0.2.189/CHANGELOG.md">libc's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/rust-lang/libc/compare/0.2.188...0.2.189">0.2.189</a>
- 2026-07-21</h2>
<h3>Added</h3>
<ul>
<li>Emscripten: Add <code>pthread_sigmask</code>, <code>sigwait</code>,
<code>sigwaitinfo</code>, <code>sigtimedwait</code>,
<code>faccessat</code>, and <code>pthread_kill</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/5270">#5270</a>)</li>
<li>Linux SPARC: Enable the <code>clone3</code> syscall (<a
href="https://redirect.github.com/rust-lang/libc/pull/4980">#4980</a>)</li>
<li>Solarish: Add <code>CLOCK_PROCESS_CPUTIME_ID</code> and
<code>CLOCK_THREAD_CPUTIME_ID</code> (<a
href="https://redirect.github.com/rust-lang/libc/pull/5274">#5274</a>)</li>
</ul>
<h3>Deprecated</h3>
<ul>
<li>Deprecate <code>CLONE_INTO_CGROUP</code> and
<code>CLONE_CLEAR_SIGHAND</code>. These overflow their types and will be
changed to a larger size in the future. (<a
href="https://github.com/rust-lang/libc/commit/8c6e6710458db4d6aa0766f6f84bbf13f640237e">8c6e6710458d</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Musl riscv32: Rename padding fields to avoid a conflict and fix the
build (<a
href="https://github.com/rust-lang/libc/commit/2499ff0ad9936a036e78a4e0991445efee383564">2499ff0ad993</a>)</li>
<li>NuttX: Fix <code>wchar_t</code> definition under Arm (<a
href="https://redirect.github.com/rust-lang/libc/pull/5245">#5245</a>)</li>
<li>Windows: Add back link names for <code>time</code>-related symbols
(<a
href="https://redirect.github.com/rust-lang/libc/pull/5300">#5300</a>)</li>
</ul>
<h2><a
href="https://github.com/rust-lang/libc/compare/0.2.187...0.2.188">0.2.188</a>
- 2026-07-21</h2>
<h3>Changed</h3>
<ul>
<li>Restore <code>Send</code> and <code>Sync</code> for <code>DIR</code>
(<a
href="https://github.com/rust-lang/libc/commit/35b062263401733cd89065c6a553640f2ba51ff1">35b062263401</a>)</li>
</ul>
<p>These were removed in 0.2.187 because <code>libc</code> does not
actually make <code>Send</code> and <code>Sync</code>
guarantees about <code>DIR</code> (or other extern types), but this
caused some crates to break.
The traits are added back for now to allow time to migrate, but will be
removed again
in the future; please make sure your crates are not relying on
<code>libc::DIR: Send</code> or
<code>libc::DIR: Sync</code>.</p>
<h2><a
href="https://github.com/rust-lang/libc/compare/0.2.186...0.2.187">0.2.187</a>
- 2026-07-20</h2>
<p>This release contains a number of improvements related to 64-bit
<code>time_t</code> configuration.
Of note the existing <code>RUST_LIBC_UNSTABLE_*</code> environment
variables have been replaced
with configuration options. The new way to use these is:</p>
<pre lang="sh"><code>RUSTFLAGS='--cfg=libc_unstable_musl_v1_2_3' cargo
...
RUSTFLAGS='--cfg=libc_unstable_gnu_time_bits=&quot;64&quot;' cargo ...
</code></pre>
<p>Being able to set this via <code>RUSTFLAGS</code> makes it easier to
only apply configuration to
specific targets (and notably, not the host if build scripts are
used).</p>
<p>There are two other notable changes:</p>
<ul>
<li>The 32-bit <code>windows-gnu</code> targets now respect
<code>libc_unstable_gnu_time_bits</code></li>
<li>uClibc now supports a similar configuration option:</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rust-lang/libc/commit/ef0906e20828777175f65caa7e681a0ce33c559a"><code>ef0906e</code></a>
libc: Release 0.2.189</li>
<li><a
href="https://github.com/rust-lang/libc/commit/5a79f7642911e17cf9629857b88503e50d433fc4"><code>5a79f76</code></a>
riscv32-musl: Rename padding fields to avoid a conflict</li>
<li><a
href="https://github.com/rust-lang/libc/commit/3e51062f4249054264ae11363d8efbb652f9ab2e"><code>3e51062</code></a>
psp: Fix <code>overflowing_literals</code> warnings</li>
<li><a
href="https://github.com/rust-lang/libc/commit/e352fdd17b43c5e2a911041512c4d62953121018"><code>e352fdd</code></a>
emscripten: add pthread_sigmask, sigwait, sigwaitinfo, sigtimedwait,
faccessa...</li>
<li><a
href="https://github.com/rust-lang/libc/commit/63221b314d46bccaea33bdba2f3d75f25dc9c739"><code>63221b3</code></a>
macros: Require <code>safe</code> in <code>safe_f!</code>
invocations</li>
<li><a
href="https://github.com/rust-lang/libc/commit/707ab528fc31619d80ca8ee5fd714ff7285e818e"><code>707ab52</code></a>
macros: Require <code>unsafe</code> in <code>f!</code> invocations</li>
<li><a
href="https://github.com/rust-lang/libc/commit/8e40c9404b8127d5dd3d6f015c1da1f12b7dd44b"><code>8e40c94</code></a>
Enable clone3() syscall on sparc-linux and sparc64-linux</li>
<li><a
href="https://github.com/rust-lang/libc/commit/8427909fb3c9890bd89c787e8ab18673032b0360"><code>8427909</code></a>
windows: Add back link names for <code>time</code>-related symbols</li>
<li><a
href="https://github.com/rust-lang/libc/commit/b4863fa4c31a95524339a6ee89aa5df042a33745"><code>b4863fa</code></a>
nuttx: fix wchar_t definition under arm</li>
<li><a
href="https://github.com/rust-lang/libc/commit/41c683da26d2c74a69205ca1e5e87a415aa313c8"><code>41c683d</code></a>
nuttx: mirror type definitions</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-lang/libc/compare/0.2.186...0.2.189">compare
view</a></li>
</ul>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 09:21:16 -07:00
LanceDB Robot 2ce88f8e02 chore: update lance dependency to v9.1.0-beta.8 (#3702)
Updates Rust workspace Lance dependencies and Java lance-core to
v9.1.0-beta.8. Removes MemWAL writer settings that are no longer exposed
by Lance.

Lance tag:
https://github.com/lance-format/lance/releases/tag/v9.1.0-beta.8
2026-07-21 23:37:32 -05:00
kid ac99e4dce5 fix(node): sanitize Map fields across Arrow versions (#3650)
## 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
2026-07-21 09:28:57 -07:00
Expyron 82231bf66d chore: replace lazy_static with LazyLock (#3679) 2026-07-21 09:28:37 -07:00
Mateusz Szewczyk 8d2fea9151 chore(python): refactor legacy code in WatsonxEmbeddings component (#3660)
## 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>
2026-07-21 09:28:06 -07:00
Xuanwo 2f27aa377b chore: update lance dependency to v9.1.0-beta.7 (#3698)
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).
2026-07-21 14:57:07 +08:00
LanceDB Robot 1bf6b3ea7e chore: update lance dependency to v9.1.0-beta.5 (#3696)
Updates the Rust workspace Lance dependencies and Java lance-core from
v9.1.0-beta.4 to v9.1.0-beta.5.

No compatibility fixes were required. Triggering tag:
https://github.com/lance-format/lance/releases/tag/v9.1.0-beta.5

---------

Co-authored-by: Lu Qiu <luqiujob@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 19:40:49 -07:00
Xuanwo 8450683b2a chore: update lance dependency to v9.1.0-beta.4 (#3690) 2026-07-20 22:22:07 +08:00
Drew Gallardo 65cd142c7e feat: add remote branch diff and merge client APIs (#3686)
This PR adds some support for `diff` / `merge` in the remote client as
for local tables we stay `NotSupported` until
https://github.com/lance-format/lance/issues/7263.


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

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

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

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

### Example
```python

table = db.open_table("images")

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

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

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

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

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

---------

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

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

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

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

## Change

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

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

## Config

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

## Tests

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

Related to ENT-1883.

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:37:41 -07:00
Vitaliy 82906ecfee fix(python): raise clear ValueError when vector column cannot be infe… (#3567)
## 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>
2026-07-17 08:58:19 -07:00
Dan Tasse ab3041e01e feat: skill references to work with jobs (incl server connection) (#3683)
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.
2026-07-17 11:03:40 -04:00
Will Jones 7813907eb7 fix(python): bound scanner memory for wide-row bulk ingestion (#3625)
## 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>
2026-07-17 07:41:22 -07:00
Dan Tasse f05140f21c refactor: move lancedb skill to a codex/claude plugin (#3681)
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"
/>
2026-07-16 15:51:15 -04:00
Kobi Hikri dfce767f4c ci: pin ad-m/github-push-action to a full commit SHA in the release job (#3677)
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.
2026-07-16 12:16:10 -07:00
Tobias 7b6ee0d655 feat(wheels): publish lancedb-compat for pre-Haswell x86_64 hosts (#3327)
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.
2026-07-16 10:54:42 -07:00
Jack Ye ca39258342 fix(python): route local sync namespace operations through rust (#3606)
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.
2026-07-16 10:53:49 -07:00
312 changed files with 51762 additions and 5809 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"name": "lancedb",
"interface": {
"displayName": "LanceDB"
},
"plugins": [
{
"name": "lancedb",
"source": {
"source": "local",
"path": "./plugins/lancedb"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Developer Tools"
}
]
}
-81
View File
@@ -1,81 +0,0 @@
---
name: lancedb
description: Use when writing, reviewing, debugging, or documenting LanceDB pipelines in Python or TypeScript, especially code that should work across local LanceDB OSS tables and remote LanceDB Enterprise/Cloud tables. Helps avoid non-portable full-table materialization, choose idiomatic query/search patterns, and apply LanceDB performance defaults for ingestion, indexing, filtering, and diagnostics.
---
# Building LanceDB Pipelines
Use this skill to produce LanceDB pipelines that are portable between local and remote tables (for LanceDB Enterprise/Cloud) and idiomatic for the selected SDK.
## LanceDB Table Modes
LanceDB has two common execution modes:
- **Local table**: embedded, open source, in-process LanceDB. The client opens data from a local path or object storage URI and executes queries in the application process.
- **Remote table**: LanceDB Enterprise/Cloud table opened through a `db://...` URI. The data may be very large, commonly backed by object storage, and queried through a remote service.
Do NOT assume local-only table helpers exist on remote tables. If the user asks for LanceDB Enterprise, Cloud, `db://...`, production remote access, or a remote table, focus on the remote table path: use `search()` / `query()`, keep reads bounded with `select()` and `limit()`, and avoid table-level full materialization APIs.
## Workflow
1. Identify the SDK: Python, TypeScript, or both.
2. Identify the table mode: local/embedded OSS, remote Enterprise/Cloud, or portable across both. If the user says "LanceDB Enterprise", choose the remote table path.
3. Read the matching language branch before writing or changing code:
- Python patterns: `references/python/patterns.md`
- Python API quick reference: `references/python/api_reference.md`
- Python performance guidance: `references/python/performance.md`
- TypeScript patterns: `references/typescript/patterns.md`
- TypeScript API quick reference: `references/typescript/api_reference.md`
- TypeScript performance guidance: `references/typescript/performance.md`
- Column metadata authoring (both SDKs): `references/column_metadata.md`
- Branch operations (both SDKs): `references/branch_ops.md`
4. Start with `patterns.md` for the selected SDK. Read `api_reference.md` when choosing method names or return collectors. Read `performance.md` when the task involves ingestion, indexing, filtering, query tuning, diagnostics, or large datasets. Read `column_metadata.md` when the task is documenting, tagging, classifying, or grouping table columns (field descriptions, `lancedb:tag:*` tags, logical column families). Read `branch_ops.md` when the task involves branch lifecycle (list/create/delete), writing to a non-main branch, or verifying a change stayed off main.
5. For Python schemas, favor Pydantic models and validate records before writing. Use PyArrow schemas when Arrow-native, streaming, or highly dynamic data makes them materially better suited.
6. Prefer `search()` or `query()` builders with explicit `select()` and `limit()` for reads.
7. Avoid table-level full materialization in remote or portable code. This is the main local-vs-remote read pitfall.
8. After a successful embedded OSS ingestion, call `table.optimize()`. Do not call it for Enterprise/Cloud; remote maintenance is automatic.
9. For remote Enterprise/Cloud writes, never drop-then-reuse or `mode="overwrite"` the same table name — see "Enterprise: never drop-then-reuse the same table name" below. This is the main local-vs-remote write pitfall.
10. If reviewing an existing file or repo, run `scripts/check_materialization.py` on the relevant paths and inspect each finding before editing.
11. Cross-check unfamiliar or non-trivial API claims against the source tree instead of relying on memory.
## Core Portability Rule
Do not write code that assumes a local table API will exist on a remote table. Remote tables can be very large, so whole-table materialization helpers are intentionally unavailable or unsafe.
This does **not** mean result conversion is forbidden. Bounded query/search result collection is normal:
- Python: `table.search(...).select([...]).limit(10).to_pandas()`
- TypeScript: `await table.search(...).select([...]).limit(10).toArray()`
The unsafe pattern is table-level or unbounded collection, plus local-only dataset escape hatches in remote code:
- Python: `table.to_pandas()`, `table.to_arrow()`, `table.to_polars()`; `table.to_lance()` is local/OSS-only dataset access, not materialization
- TypeScript: `await table.toArrow()`, `await table.query().toArray()` without `limit()`
## Enterprise: never drop-then-reuse the same table name
LanceDB Enterprise/Cloud splits a **control plane** (DDL: create/drop/rename) from a **data plane** (query nodes that serve reads). Query nodes cache the resolved dataset for a table name for up to `table_cache_ttl`**default 300 seconds (5 minutes)**. After you drop or overwrite a table, the control plane updates immediately but the data plane keeps serving the *old* dataset until that cache entry expires. During the window the two planes disagree.
The failure this causes: you `drop_table("t")` then immediately `create_table("t", ...)` (or `create_table("t", ..., mode="overwrite")`). The DDL returns success, but every query against `t` returns **`500 Internal Server Error`** (the query node resolves the stale/deleted dataset), and a fresh `describe` may still show the *old* schema/version. It looks like your write silently failed; it didn't — the name is cached.
**`mode="overwrite"` has the same problem** — it is a drop+create of the same name under the hood.
Rules for portable Enterprise ingestion:
1. **Never reuse a table name you just dropped/overwrote within the cache TTL.** Do not use `mode="overwrite"` to replace an existing Enterprise table in place.
2. To (re)load data, **write to a fresh table name** (e.g. `<table>_v2`, or a run-stamped suffix). A brand-new name has no cached data-plane entry, so writes and reads work immediately.
3. Before creating, `list_tables()` and **fail loudly if the name already exists** rather than overwriting — prompt for a new name.
4. To land on a specific final name that is currently occupied by an old table: drop the old table, **wait out the TTL (~5 min), then `rename_table(fresh_name, final_name)`**. Renaming onto a name whose old dataset is still cached hits the same race, so the wait is mandatory. `rename_table` is a supported control-plane op.
5. When you hand a table name back to a human, tell them which step still needs the propagation wait (usually: "the old `t` was dropped; run the rename in ~5 minutes").
This is Enterprise/Cloud-specific. Local/OSS tables have no separate data plane, so `mode="overwrite"` and immediate same-name reuse are fine there.
## Script
Run the scanner when reviewing or modifying an existing codebase:
```bash
python skills/lancedb/scripts/check_materialization.py path/to/file_or_dir
```
The script reports likely unsafe full-table materialization in Python and TypeScript. Treat results as review prompts, not automatic proof of a bug.
@@ -1,117 +0,0 @@
# Branch Operations
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. Use for branch lifecycle tasks, experimental/isolated table versions, targeting an operation at a non-main branch, or confirming a mutation did not affect main.
Works on local/OSS and remote Enterprise/Cloud tables.
## The branch model (important)
Branches are isolated, writable lines of history forked from another branch (or a specific version). Writes on a branch never affect `main`.
There is **no global "switch branch" state** — you never repoint the whole table at a branch. Instead, **operations are scoped by which table handle you use**:
- The handle you got from `open_table(name)` / `openTable(name)` targets `main`.
- `branches.create(...)` and `branches.checkout(...)` return a **new table handle scoped to that branch**. Every read/write on that handle (add, update, `update_field_metadata`, `create_index`, search, …) lands on the branch.
- The original main handle is unaffected — keep it around to verify isolation.
`branches.list()` returns only non-main branches. Main always exists and is not listed.
## Python
`table.branches` is a property returning the branch manager; `table.current_branch()` tells you what a handle is scoped to (`None` = main).
```python
table = db.open_table("products") # scoped to main
# list — dict of name -> metadata (parent_branch, parent_version, ...); {} = only main
table.branches.list()
# create: forks from main by default and returns a handle scoped to the new branch
exp = table.branches.create("experiment-reindex")
exp = table.branches.create("exp2", from_ref="main", from_version=None) # optional fork point
# checkout an existing branch -> branch-scoped handle
wip = table.branches.checkout("wip-branch")
# with version= it pins to that version (read-only detached view); omit to track latest, writable
# operate on the branch simply by using its handle
wip.update_field_metadata(
{"path": "category", "metadata": {"lancedb:description": "Product category label."}}
)
wip.create_scalar_index("category")
# delete: removes only the branch pointer; main and row data remain intact
table.branches.delete("stale-2024")
# alternatively, open a branch handle directly from the connection
wip = db.open_table("products", branch="wip-branch")
exp.current_branch() # "experiment-reindex"
table.current_branch() # None (main)
```
Async: same shape — `table.branches` returns `AsyncBranches`; `await table.branches.create(...)` etc.
## TypeScript
`table.branches()` is an **async method** returning the `Branches` manager; `table.currentBranch()` returns the scoped branch or `null` for main.
```typescript
const table = await db.openTable("products"); // scoped to main
const branches = await table.branches();
// list — Record<string, BranchContents>; {} = only main
await branches.list();
// create: forks from main by default, returns a Table scoped to the new branch
const exp = await branches.create("experiment-reindex");
const exp2 = await branches.create("exp2", "main" /* fromRef */, undefined /* fromVersion */);
// checkout an existing branch -> branch-scoped Table
const wip = await branches.checkout("wip-branch");
// with a version arg it pins (read-only detached view); omit to track latest, writable
// operate on the branch simply by using its handle
await wip.updateFieldMetadata([
{ path: "category", metadata: { "lancedb:description": "Product category label." } },
]);
await wip.createIndex("category");
// delete: removes only the branch pointer; main and row data remain intact
await branches.delete("stale-2024");
// alternatively, open a branch handle directly from the connection
const wip2 = await db.openTable("products", { branch: "wip-branch" });
exp.currentBranch(); // "experiment-reindex"
table.currentBranch(); // null (main)
```
## Verifying isolation
After writing to a branch, confirm the change did NOT land on main by reading through both handles:
```python
wip = table.branches.checkout("wip-branch")
wip.update_field_metadata({"path": "category", "metadata": {"lancedb:description": "..."}})
assert b"lancedb:description" in (wip.schema.field("category").metadata or {})
assert b"lancedb:description" not in (table.schema.field("category").metadata or {}) # main untouched
```
Two handles on the same branch see each other's writes (e.g. `table.branches.create("exp")` and `db.open_table(name, branch="exp")`); main stays isolated.
## Quick reference
| Goal | Python | TypeScript |
|------|--------|------------|
| List branches (non-main) | `table.branches.list()` | `await (await table.branches()).list()` |
| Create branch (off main) | `table.branches.create(name)` → branch handle | `await branches.create(name)` → branch `Table` |
| Create from a fork point | `table.branches.create(name, from_ref=..., from_version=...)` | `await branches.create(name, fromRef, fromVersion)` |
| Get a branch handle | `table.branches.checkout(name)` or `db.open_table(t, branch=name)` | `await branches.checkout(name)` or `await db.openTable(t, { branch: name })` |
| Pin to a branch version (read-only) | `table.branches.checkout(name, version=v)` | `await branches.checkout(name, v)` |
| Delete branch | `table.branches.delete(name)` | `await branches.delete(name)` |
| Which branch is this handle on? | `table.current_branch()` (`None` = main) | `table.currentBranch()` (`null` = main) |
| Target main | use the original (non-branch) handle | use the original (non-branch) handle |
Branch names must be non-empty; empty names raise a validation error.
@@ -1,183 +0,0 @@
# Column Metadata Authoring
Write column-level descriptions, tags, and logical groupings onto a LanceDB table's schema. Use this when the user wants to document, annotate, tag, or classify what their table columns ARE (embeddings vs labels vs eval metrics, model provenance, version families, etc.).
Works on local/OSS and remote Enterprise/Cloud tables alike — read the schema through the table handle, write through `update_field_metadata` (Python) / `updateFieldMetadata` (TypeScript).
## Metadata key conventions
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*. Multiple tags on the same column are fine — each is a separate key. All values are strings.
## Step 1: Read the schema and existing metadata
Read existing metadata before writing, to avoid redundant updates.
Python — `table.schema` (sync property; async: `await table.schema()`) returns a `pyarrow.Schema`. **Arrow field metadata is bytes-keyed in Python**:
```python
schema = table.schema
for field in schema:
meta = field.metadata or {} # dict[bytes, bytes], e.g. {b"lancedb:description": b"..."}
print(field.name, field.type, field.nullable, meta)
```
TypeScript — `await table.schema()` returns an Arrow `Schema`; field metadata is a `Map<string, string>`:
```typescript
const schema = await table.schema();
for (const field of schema.fields) {
console.log(field.name, field.type, field.nullable, field.metadata); // Map
// field.metadata.get("lancedb:description")
}
```
For struct/nested fields, recurse into the field's children and address them as dot-paths (e.g., `parent.child`).
If the user hasn't specified which columns to update, work with all columns.
## Step 2: Generate metadata
Decide what to generate based on the user's request.
### Descriptions (`lancedb:description`)
Base descriptions on:
- The column name and Arrow type (e.g., `FixedSizeList` of floats → likely an embedding)
- User-supplied context (upstream pipeline, sample values, domain knowledge)
- Name patterns: `_embedding`/`_vec`/`_embed` → vector; `_label`/`_class` → label; `_score`/`_eval`/`_metric` → evaluation metric
Be specific and concise. Good: `"Sentence-BERT embedding of the query text (768-dim)."` Not: `"An embedding column."`
### Tags (`lancedb:tag:<name>`)
Choose tag key names that match what the user asked to annotate. Common patterns:
- Semantic field type → `lancedb:tag:field_type: "embedding"` / `"text"` / `"image"` / `"label"` / `"eval"` / `"id"` / `"metadata"`
- Model or source → `lancedb:tag:model: "clip"` / `"bert"` / `"vit"`
- Project affiliation → `lancedb:tag:project_id: "<name>"`
- Version → `lancedb:tag:version: "v3"` (and `lancedb:tag:latest: "true"` for the newest)
Use Arrow type as a hint: `FixedSizeList` + float → embedding; `Utf8`/`LargeUtf8` → text; `Binary` → image or blob.
### Logical groupings (`lancedb:logical-column`)
Look for naming patterns across columns:
- `clip_v1`, `clip_v2`, `clip_v3` → logical column `"clip"`, latest is `v3`
- `text_embed_20240101`, `text_embed_20240601` → logical column `"text_embed"`, latest is the most recent date suffix
Write `lancedb:logical-column` on all members of a group. Mark the newest with `lancedb:tag:latest: "true"` (in addition to its version tag).
## Step 3: Write the metadata
Each update names a field by dot-path and carries a metadata map. Semantics (identical in both SDKs):
- **Merge by default** (`replace` omitted/false) — preserves existing metadata the user didn't ask to change
- `replace: true` swaps the field's entire metadata map — only if the user explicitly asks to overwrite
- A value of `None`/`null` deletes that specific key
- Batch all field updates into a single call when possible
- Returns the new table version
Python (sync and async take one dict per field, as varargs):
```python
res = table.update_field_metadata(
{
"path": "clip_v3",
"metadata": {
"lancedb:description": "CLIP ViT-L/14 image embedding, L2-normalized (1024-dim).",
"lancedb:tag:field_type": "embedding",
"lancedb:tag:model": "clip",
"lancedb:tag:version": "v3",
"lancedb:tag:latest": "true",
"lancedb:logical-column": "clip",
},
},
{
"path": "clip_v2",
"metadata": {
"lancedb:description": "CLIP ViT-B/32 image embedding (768-dim), superseded by v3.",
"lancedb:tag:field_type": "embedding",
"lancedb:tag:model": "clip",
"lancedb:tag:version": "v2",
"lancedb:logical-column": "clip",
},
},
)
print(res.version) # new table version
# merge semantics: add a key, delete one via None, keep the rest
table.update_field_metadata(
{"path": "clip_v2", "metadata": {"lancedb:tag:archived": "true", "lancedb:tag:latest": None}}
)
```
(`replace_field_metadata` is deprecated — use `update_field_metadata`.)
TypeScript (takes an array of `FieldMetadataUpdate`):
```typescript
const res = await table.updateFieldMetadata([
{
path: "clip_v3",
metadata: {
"lancedb:description": "CLIP ViT-L/14 image embedding, L2-normalized (1024-dim).",
"lancedb:tag:field_type": "embedding",
"lancedb:tag:model": "clip",
"lancedb:tag:version": "v3",
"lancedb:tag:latest": "true",
"lancedb:logical-column": "clip",
},
},
{
path: "clip_v2",
metadata: {
"lancedb:description": "CLIP ViT-B/32 image embedding (768-dim), superseded by v3.",
"lancedb:tag:field_type": "embedding",
"lancedb:tag:model": "clip",
"lancedb:tag:version": "v2",
"lancedb:logical-column": "clip",
},
},
]);
console.log(res.version); // new table version
// merge semantics: add a key, delete one via null, keep the rest
await table.updateFieldMetadata([
{ path: "clip_v2", metadata: { "lancedb:tag:archived": "true", "lancedb:tag:latest": null } },
]);
```
## Step 4: Confirm
Report back:
- Which columns were updated and what was written
- The new table version number (from the result)
- Any columns skipped (e.g., already had up-to-date metadata)
## Quick examples
**"Write descriptions for all columns in the `product_embeddings` table"**
1. Read `table.schema` → all fields + existing metadata
2. Generate a `lancedb:description` for each column based on name + type
3. One `update_field_metadata` call with all descriptions
4. Report
**"Tag the columns in `model_outputs` with their field type and model"**
1. Read the schema
2. For each field, classify by name + Arrow type → set `lancedb:tag:field_type` and `lancedb:tag:model` where applicable
3. Write in one batched call
4. Report
**"Group the feature columns in `training_features` into logical families and mark the latest version"**
1. Read the schema
2. Find version patterns → assign `lancedb:logical-column` and `lancedb:tag:version`; mark newest with `lancedb:tag:latest: "true"`
3. Write in one batched call
4. Show the grouping
@@ -1,131 +0,0 @@
# Python API Reference
Quick method reference for Python LanceDB code. Cross-check source for non-trivial claims.
## Connect
```python
import lancedb
db = lancedb.connect("./camelot-db") # local/OSS
db = lancedb.connect("db://my-db", api_key=api_key, region=region) # remote
```
**Place the local database directory next to the script/entrypoint that opens it** (i.e. resolve the path relative to the script, `Path(__file__).parent / "camelot-db"`), not buried under a shared `data/` folder. The Lance dataset is the database, not a data file — keeping it beside its code makes ownership obvious and paths stable regardless of the working directory the script is launched from.
**Do not name the directory `lancedb`** (e.g. `./lancedb`, `./data/lancedb`). It collides with the imported `lancedb` package name, which is confusing to read and easy to shadow in scripts. Give it a name derived from the repo or dataset with a clear prefix/suffix — for example `./<dataset>-db`, `./<repo>_lancedb`, or `./vectordb`.
Async:
```python
db = await lancedb.connect_async("./camelot-db")
```
## Table Reads
| Task | Preferred API |
| --- | --- |
| Vector search | `table.search(query_vector).limit(k)` |
| Full scan with filters/projection (sync) | `table.search().where(...).select(...).limit(...)` |
| Full scan with filters/projection (async) | `table.query().where(...).select(...).limit(...)` |
| Filter | `.where("col > 10")` |
| Projection | `.select(["id", "text"])` |
| Bound result count | `.limit(20)` |
| Collect bounded result as Python objects (default, no extra deps) | `.to_list()` on query/search result |
| Collect bounded result as Arrow (default, `pyarrow` always available) | `.to_arrow()` on query/search result |
| Collect bounded result as pandas (only if project uses pandas) | `.to_pandas()` on query/search result |
| Collect bounded result as Polars (only if project uses polars) | `.to_polars()` on query/search result |
## Sync vs Async Scan API
The plain-scan entry point differs between the sync and async clients. **Verified against `lancedb` 0.34.0** — re-check if the pinned version changes:
- **Sync** (`lancedb.connect(...)`): the table has **no `.query()` method**. Use `.search()` with no argument for a plain scan; it returns a query builder that supports `.where()`, `.select()`, `.limit()`, and the `.to_list()` / `.to_arrow()` / `.to_pandas()` / `.to_polars()` collectors.
```python
rows = table.search().where("status = 'ready'").select(["id", "text"]).limit(20).to_list()
```
- **Async** (`lancedb.connect_async(...)`): the table has **both** `.query()` and `.search()`. Use `.query()` for a plain scan.
```python
rows = await async_table.query().where("status = 'ready'").select(["id", "text"]).limit(20).to_list()
```
Do not call `table.query()` on a sync table — it raises `AttributeError`.
## Local vs Remote Table Methods
| API | Local table | Remote table | Agent guidance |
| --- | --- | --- | --- |
| `table.search(...)` | Yes | Yes | Preferred read path (sync + async) |
| `table.query()` | Async only | Async only | Sync scan path is `table.search()`; `.query()` is the async scan builder |
| `table.to_pandas()` | Yes | No / unsafe for portability | Avoid in portable code |
| `table.to_arrow()` | Yes | No / unsafe for portability | Avoid in portable code |
| `table.to_polars()` | Yes | No / unsafe for portability | Avoid in portable code |
| `table.to_lance()` | Yes | No | Local/OSS escape hatch only |
## Indexes
Use `create_index(...)` for vector indexes and modern index configs. Use scalar indexes for filtered or merge keys.
Common calls:
```python
table.create_index("vector")
table.create_scalar_index("status")
table.create_fts_index("text")
```
Check source docs before specifying advanced index config names or parameters.
## Filtering And Recall Knobs
```python
table.search(query_vector).where("status = 'ready'") # pre-filter by default
table.search(query_vector).where("status = 'ready'", prefilter=False)
table.search(query_vector).limit(10).refine_factor(20)
table.search(query_vector).limit(10).nprobes(50)
```
Use post-filtering only when fewer than `limit` results are acceptable.
## Diagnostics
```python
print(table.search(query_vector).where("year > 2000").limit(10).analyze_plan())
print(table.index_stats("vector_idx"))
```
Use these before changing indexes or search tuning.
## Column (Field) Metadata
```python
schema = table.schema # sync property; async: await table.schema()
meta = schema.field("category").metadata # dict[bytes, bytes] — Arrow metadata is bytes-keyed
res = table.update_field_metadata( # varargs: one dict per field; works local + remote
{"path": "category", "metadata": {"lancedb:description": "...", "lancedb:tag:field_type": "label"}}
)
res.version # new table version
```
Merges by default; a `None` value deletes that key; `"replace": True` swaps the whole map. Nested fields use dot-paths (`"a.b.c"`). `replace_field_metadata` is deprecated. See `references/column_metadata.md` for key conventions (`lancedb:description`, `lancedb:tag:<name>`, `lancedb:logical-column`) and the authoring workflow.
## Branches
```python
table.branches.list() # non-main branches; {} = only main
exp = table.branches.create("exp") # fork off main -> handle scoped to the branch
wip = table.branches.checkout("wip") # existing branch -> scoped handle (version= pins read-only)
wip = db.open_table("t", branch="wip") # or open scoped directly
table.branches.delete("stale") # removes only the branch pointer
table.current_branch() # None = main
```
There is no global switch — scoping is per table handle: any read/write on a branch handle lands on that branch; the original handle keeps targeting main. See `references/branch_ops.md` for the model and isolation checks.
## Maintenance
```python
table.optimize()
```
Call this after every successful local/OSS ingestion. It handles compaction, cleanup of old versions according to retention, and index optimization. Do not add this for LanceDB Enterprise/Cloud remote tables; Enterprise handles compaction and cleanup automatically from cluster configuration.
@@ -1,173 +0,0 @@
# Python Patterns
Use these patterns when writing Python code with `lancedb`.
## Before Writing Code
Choose the output type from what the project actually depends on. **Do not assume `pandas` or `polars` is installed** — they are heavy dependencies that many LanceDB projects do not use. `pyarrow`, by contrast, ships as a LanceDB dependency and is always available, so it is a safe default to lean on.
Default output (after applying `select()` and `limit()`):
- **Python objects**: `.to_list()` — a list of dicts, no extra dependencies. Prefer this for scripts, examples, and agent-generated code unless there is a reason to do otherwise.
- **PyArrow**: `.to_arrow()` — a `pyarrow.Table`, when the surrounding code is Arrow-native or you need columnar/zero-copy handoff.
Only reach for a DataFrame when the project *already* declares that dependency:
- Pandas projects (pandas in `pyproject.toml`/requirements): `.to_pandas()`.
- Polars projects (polars declared): `.to_polars()`.
If unsure, check the dependency manifest or the imports in surrounding files. When in doubt, use `.to_list()` or `.to_arrow()`.
## Schema Design and Validation
Favor `LanceModel` and Pydantic validation for Python schemas. They keep field
types readable, validate source records before a write, and map directly to a
LanceDB schema. Use `Vector(dimension)` for fixed-size vectors:
```python
from lancedb.pydantic import LanceModel, Vector
class Document(LanceModel):
id: int
text: str
vector: Vector(384, nullable=False)
rows = [Document.model_validate(row) for row in source_rows]
table = db.create_table("documents", schema=Document)
table.add(rows)
```
Use PyArrow schemas instead when the pipeline is already Arrow-native, needs
record-batch streaming, or has runtime schema requirements that would make a
Pydantic model harder to understand. Declare Pydantic as a direct project
dependency when application code imports it, even if LanceDB also depends on it.
## Recommended Patterns
### Bounded search or query
Use this for application reads, examples, notebooks, and agent-generated scripts:
```python
results = (
table.search(query_vector)
.where("status = 'ready'")
.select(["id", "text"])
.limit(20)
.to_list() # or .to_arrow(); .to_pandas()/.to_polars() only if the project uses them
)
```
Why: `search()` works across local and remote tables and on both the sync and async clients. `select()` avoids fetching unused columns. `limit()` prevents accidental full-table reads. `.to_list()` and `.to_arrow()` avoid assuming pandas/polars is installed (see "Before Writing Code").
For a **plain scan** (no query vector), the entry point differs by client:
```python
# Sync client: no .query() method — use .search() with no argument.
rows = table.search().where("status = 'ready'").select(["id", "text"]).limit(20).to_list()
# Async client: use .query().
rows = await async_table.query().where("status = 'ready'").select(["id", "text"]).limit(20).to_list()
```
`table.query()` on a sync table raises `AttributeError` (verified on `lancedb` 0.34.0). See the "Sync vs Async Scan API" section in `api_reference.md`.
### Bounded query result conversion
It is fine to collect bounded query/search results:
```python
arrow_table = table.search().select(["id"]).limit(100).to_arrow() # sync plain scan
rows = table.search(query_vector).limit(10).to_list()
df = table.search(query_vector).limit(10).to_pandas() # only if pandas is a project dep
```
### Local-only Lance dataset API
`table.to_lance()` does not itself materialize the full dataset. It returns the underlying `lance.LanceDataset`, making the table accessible through the PyLance dataset API. Use it when the task is explicitly local/OSS and needs Lance dataset methods not exposed by LanceDB:
```python
# Local/OSS only: RemoteTable does not expose table.to_lance().
ds = table.to_lance()
for batch in ds.to_batches(columns=["id", "text"], batch_size=10_000):
process(batch)
```
### Async Python
Keep the same shape and bound the result before collecting:
```python
results = await (
async_table.query()
.where("status = 'ready'")
.select(["id", "text"])
.limit(20)
.to_list() # or .to_arrow()
)
```
## Anti-Patterns
**Avoid the following anti-patterns in your code.**
### Table-level full materialization
Avoid whole-table collectors in portable or large-table code:
```python
df = table.to_pandas()
arrow_table = table.to_arrow()
polars_df = table.to_polars()
```
Why: local tables expose these whole-table collectors, but remote tables intentionally do not — a remote production table can be far larger than a local development table, so it is easy to accidentally pull the entire table into memory.
`table.to_lance()` is different: it is not a full materialization call, but it is still local/OSS-only and should not appear in code meant to run against remote Enterprise tables.
### Unbounded result collection
Avoid query/search collection without a meaningful limit:
```python
rows = table.search().to_list() # unbounded plain scan
rows = table.search(query_vector).to_list() # unbounded vector search
```
Prefer `select(...).limit(...)` before collecting; for large reads, stream in batches instead.
### Per-row writes
Avoid loops that write one row per call:
```python
for row in rows:
table.add([row]) # one commit + fragment per row
```
Each `add()` creates a new version and fragment. Pass the whole batch in a single call, or chunk very large inputs:
```python
table.add(rows) # single commit
# for very large inputs, add batches of several thousand rows
```
After the final successful write to an embedded OSS table, call
`table.optimize()`. Skip this for Enterprise/Cloud tables because their
maintenance is automatic.
### Drop-then-reuse the same table name (Enterprise/Cloud)
Avoid dropping or overwriting a remote table and then reusing that name right away:
```python
db.drop_table("my_table")
table = db.create_table("my_table", data=rows) # reads 500 for ~5 min
table = db.create_table("my_table", data=rows, mode="overwrite") # same problem
```
Why: Enterprise/Cloud splits DDL (control plane) from query serving (data plane). The data plane caches the dataset behind a table name for up to `table_cache_ttl` (default 300s / 5 min), so after a drop/overwrite the DDL succeeds but queries against the reused name return `500 Internal Server Error` until the cache expires — and a fresh `describe` may still show the old schema. Instead, write to a **fresh name**, use `list_tables()` and fail if it already exists, then `rename_table(fresh, final)` onto the final name only after the old table's drop has propagated (~5 min). See the "Enterprise: never drop-then-reuse the same table name" section in `SKILL.md`. Local/OSS tables have no separate data plane — overwrite freely there.
### Guessing performance fixes
Avoid changing `nprobes`, `refine_factor`, or index types before checking the query plan and index stats. Diagnose first, then tune one knob at a time.
@@ -1,131 +0,0 @@
# Python Performance Guidance
Use this when writing Python code that ingests data, queries large tables, builds indexes, or investigates latency.
## Ingestion
### Recommended: validate schemas and records with Pydantic
Favor `LanceModel` for readable Python schema definitions and validate source
records before writing. Use PyArrow directly for Arrow-native or streaming
pipelines where it is the clearer representation.
```python
from lancedb.pydantic import LanceModel, Vector
class Document(LanceModel):
id: int
text: str
vector: Vector(384, nullable=False)
rows = [Document.model_validate(row) for row in source_rows]
table = db.create_table("documents", schema=Document)
table.add(rows)
```
### Recommended: bulk ingestion for materialized data
```python
table.add(arrow_table)
table.add(df)
table.add(pa.dataset("data/", format="parquet"))
```
For very large initial loads, create the table empty first, then call `add(...)`. Passing data directly to `create_table(name, data)` can skip the auto-parallel write path.
### Recommended: iterator ingestion for generated or streamed data
```python
def batches():
for raw in source:
vectors = model.encode(raw["text"])
yield pa.RecordBatch.from_pydict({**raw, "vector": vectors})
table.add(batches())
```
Use chunks of several thousand rows or more when practical. Tiny batches and per-row writes create many small fragments.
### Anti-pattern: per-row `add()`
```python
for row in rows:
table.add([row])
```
Each call creates a version and fragment. This slows ingestion and later queries.
## Indexing
- Build a vector index once brute-force vector search becomes too slow. As a rule of thumb, local brute force is fine below roughly 100K vectors; beyond that, build an index.
- Use `IVF_PQ` as the general-purpose default. Enterprise builds this automatically.
- Use scalar indexes for filtered columns and merge/upsert keys.
- Use `BTREE` for mostly distinct numeric/string/temporal columns, `BITMAP` for booleans and low-cardinality columns, and `LABEL_LIST` for list membership queries.
- Keep full-text defaults unless phrase queries require position data.
## Querying
Always be explicit:
```python
table.search(query_vector).select(["id", "title"]).limit(20)
```
- `select()` reduces bytes read and transferred.
- `limit()` prevents accidental full-table materialization.
- Pre-filtering is the default and guarantees returned rows satisfy the predicate.
- Use post-filtering only when fewer than `limit` results are acceptable.
## Recall Tuning
Tune one knob at a time:
- Quantized indexes: raise `refine_factor` to rescore more candidates on full vectors.
- HNSW-backed indexes: raise `ef`; start around `1.5 * k`, increase toward `10 * k` if recall is short.
- IVF candidate breadth: `nprobes` is auto-tuned; override only when a selective pre-filter leaves too few neighbors.
## Maintenance
After every successful embedded OSS/local ingestion, call `table.optimize()`.
Do not add this to LanceDB Enterprise/Cloud remote table code; remote compaction
and cleanup are handled automatically based on the Enterprise cluster
configuration.
Why local maintenance is needed:
- Frequent writes can create many small fragments. Queries then need to scan across more files, which can increase latency.
- Updates, deletes, and appends create new table versions. Old versions are retained for time travel and rollback, which can grow disk usage.
- Indexes may have newly added rows that are not yet fully optimized into the index structure.
For local/OSS tables, run `optimize()` after the final successful ingestion
write. Also run it after later batches of update/delete operations or on a
regular maintenance schedule:
```python
table.optimize()
```
If the user wants more aggressive local disk cleanup, pass a shorter cleanup retention window:
```python
from datetime import timedelta
table.optimize(cleanup_older_than=timedelta(days=1))
```
Do not use very short cleanup windows when the application depends on time travel, rollback, or old versions.
## Diagnostics
Before changing code or indexes, inspect:
```python
print(table.search(query_vector).where("year > 2000").limit(10).analyze_plan())
print(table.index_stats("vector_idx"))
```
Look for high scan bytes, missing indexes, fragmented data, and unindexed rows.
## Python Multiprocessing
When using multiprocessing, use `spawn` rather than `fork`. LanceDB is multi-threaded internally, and `fork` plus a multi-threaded process is unsafe.
@@ -1,105 +0,0 @@
# TypeScript API Reference
Quick method reference for TypeScript LanceDB code. Cross-check source for non-trivial claims.
## Connect
```typescript
import * as lancedb from "@lancedb/lancedb";
const db = await lancedb.connect("./camelot-db");
```
**Place the local database directory next to the script/entrypoint that opens it** (resolve the path relative to the module, e.g. via `import.meta.dirname` / `__dirname`), not buried under a shared `data/` folder. The Lance dataset is the database, not a data file — keeping it beside its code makes ownership obvious and paths stable regardless of the working directory the script is launched from.
**Do not name the directory `lancedb`** (e.g. `./lancedb`, `./data/lancedb`). It collides with the imported `lancedb` package/namespace, which is confusing to read. Give it a name derived from the repo or dataset with a clear prefix/suffix — for example `./<dataset>-db`, `./<repo>_lancedb`, or `./vectordb`.
Remote connections use `db://...` plus Enterprise/Cloud credentials and deployment settings. Check current source/docs for exact connection options.
## Table Reads
| Task | Preferred API |
| --- | --- |
| Vector search | `table.search(queryVector).limit(k)` |
| Full scan with filters/projection | `table.query().where(...).select(...).limit(...)` |
| Filter | `.where("col > 10")` |
| Projection | `.select(["id", "text"])` |
| Bound result count | `.limit(20)` |
| Collect bounded result as objects | `.toArray()` on query/search result |
| Collect bounded result as Arrow | `.toArrow()` on query/search result |
| Stream result batches | `for await (const batch of table.query()...)` |
## Local vs Remote Safety
| API | Agent guidance |
| --- | --- |
| `table.search(...)` | Preferred read path |
| `table.query()` | Preferred scan/filter path |
| `await table.toArrow()` | Avoid in portable or large-table code |
| `await table.query().toArray()` with no `limit()` | Avoid; unbounded collection |
| `await table.query().toArrow()` with no `limit()` | Avoid; unbounded collection |
## Indexes
```typescript
await table.createIndex("vector");
await table.createIndex("status");
```
Use vector indexes for large vector search workloads and scalar indexes for filtered columns or merge/upsert keys. Check source/docs before specifying advanced index options.
## Filtering And Recall Knobs
```typescript
await table.search(queryVector).where("status = 'ready'").limit(10).toArray();
await table.search(queryVector).limit(10).refineFactor(20).toArray();
await table.search(queryVector).limit(10).nprobes(50).toArray();
await table.search(queryVector).limit(10).ef(100).toArray();
await table.search(queryVector).where("status = 'ready'").postfilter().limit(10).toArray();
```
Use `postfilter()` only when fewer than `limit` results are acceptable.
## Diagnostics
```typescript
console.log(await table.search(queryVector).where("year > 2000").limit(10).analyzePlan());
console.log(await table.indexStats("vector_idx"));
```
Use these before changing indexes or search tuning.
## Column (Field) Metadata
```typescript
const schema = await table.schema();
const meta = schema.fields.find((f) => f.name === "category")?.metadata; // Map<string, string>
const res = await table.updateFieldMetadata([
{ path: "category", metadata: { "lancedb:description": "...", "lancedb:tag:field_type": "label" } },
]);
res.version; // new table version
```
Merges by default; a `null` value deletes that key; `replace: true` swaps the whole map. Nested fields use dot-paths (`"a.b.c"`). See `references/column_metadata.md` for key conventions (`lancedb:description`, `lancedb:tag:<name>`, `lancedb:logical-column`) and the authoring workflow.
## Branches
```typescript
const branches = await table.branches(); // async manager
await branches.list(); // non-main branches; {} = only main
const exp = await branches.create("exp"); // fork off main -> Table scoped to the branch
const wip = await branches.checkout("wip"); // existing branch -> scoped Table (version arg pins read-only)
const wip2 = await db.openTable("t", { branch: "wip" }); // or open scoped directly
await branches.delete("stale"); // removes only the branch pointer
table.currentBranch(); // null = main
```
There is no global switch — scoping is per table handle: any read/write on a branch handle lands on that branch; the original handle keeps targeting main. See `references/branch_ops.md` for the model and isolation checks.
## Maintenance
```typescript
await table.optimize();
```
Call this after every successful local/OSS ingestion. It handles compaction, cleanup of old versions according to retention, and index optimization. Do not add this for LanceDB Enterprise/Cloud remote tables; Enterprise handles compaction and cleanup automatically from cluster configuration.
@@ -1,100 +0,0 @@
# TypeScript Patterns
Use these patterns when writing TypeScript code with `@lancedb/lancedb`.
## Recommended Patterns
### Bounded query
Use this for application reads, scripts, and examples:
```typescript
const rows = await table
.query()
.where("status = 'ready'")
.select(["id", "text"])
.limit(20)
.toArray();
```
### Bounded vector search
```typescript
const rows = await table
.search(queryVector)
.select(["id", "text"])
.limit(20)
.toArray();
```
### Batch streaming for larger reads
When the task needs many rows, avoid collecting everything at once:
```typescript
for await (const batch of table
.query()
.where("status = 'ready'")
.select(["id", "text"])
.limit(10_000)) {
process(batch);
}
```
## Anti-Patterns
**Avoid the following anti-patterns in your code.**
### Table-level full materialization
Avoid whole-table collectors in portable or large-table code:
```typescript
const tableArrow = await table.toArrow();
```
Why: local tables expose these whole-table collectors, but remote tables intentionally do not — a remote production table can be far larger than a local development table, so it is easy to accidentally pull the entire table into memory.
### Unbounded result collection
Avoid query/search collection without a meaningful limit:
```typescript
const rows = await table.query().toArray(); // unbounded plain scan
const rows = await table.search(queryVector).toArray(); // unbounded vector search
```
Prefer `select(...).limit(...)` before collecting; for large reads, stream in batches instead.
### Per-row writes
Avoid loops that write one row per call:
```typescript
for (const row of rows) {
await table.add([row]); // one commit + fragment per row
}
```
Each `add()` creates a new version and fragment. Pass the whole batch in a single call, or chunk very large inputs:
```typescript
await table.add(rows); // single commit
// for very large inputs, add in chunks of several thousand rows
```
### Drop-then-reuse the same table name (Enterprise/Cloud)
Avoid dropping or overwriting a remote table and then reusing that name right away:
```typescript
await db.dropTable("my_table");
const table = await db.createTable("my_table", rows); // reads 500 for ~5 min
const table = await db.createTable("my_table", rows, { mode: "overwrite" }); // same problem
```
Why: Enterprise/Cloud splits DDL (control plane) from query serving (data plane). The data plane caches the dataset behind a table name for up to `table_cache_ttl` (default 300s / 5 min), so after a drop/overwrite the DDL succeeds but queries against the reused name return `500 Internal Server Error` until the cache expires — and a fresh `describe` may still show the old schema. Instead, write to a **fresh name**, use `tableNames()` and fail if it already exists, then `renameTable(fresh, final)` onto the final name only after the old table's drop has propagated (~5 min). See the "Enterprise: never drop-then-reuse the same table name" section in `SKILL.md`. Local/OSS tables have no separate data plane — overwrite freely there.
### Guessing performance fixes
Avoid changing `nprobes`, `refineFactor`, `ef`, or index settings before checking `analyzePlan()` and `indexStats(...)`. Diagnose first, then tune one knob at a time.
@@ -1,78 +0,0 @@
# TypeScript Performance Guidance
Use this when writing TypeScript code that ingests data, queries large tables, builds indexes, or investigates latency.
## Ingestion
- Prefer bulk or batched writes.
- Avoid per-row write loops; they create many small commits/fragments.
- For generated data, accumulate reasonable batches before adding.
- For file-backed data, prefer APIs that stream from Arrow/Parquet-style inputs when available.
## Indexing
- Build a vector index once brute-force vector search becomes too slow. As a rule of thumb, local brute force is fine below roughly 100K vectors; beyond that, build an index.
- Use the general-purpose vector index defaults unless the task has explicit recall/latency requirements.
- Build scalar indexes for filtered columns and merge/upsert keys.
- Use full-text index phrase options only when phrase queries require them.
## Querying
Always be explicit:
```typescript
await table.search(queryVector).select(["id", "title"]).limit(20).toArray();
```
- `select()` reduces bytes read and transferred.
- `limit()` prevents accidental full-table collection.
- Pre-filtering is the default behavior. Use `postfilter()` only when fewer than `limit` results are acceptable.
## Recall Tuning
Tune one knob at a time:
- Quantized indexes: raise `refineFactor(...)` to rescore more candidates on full vectors.
- HNSW-backed indexes: raise `ef(...)`; start around `1.5 * k`, increase toward `10 * k` if recall is short.
- IVF candidate breadth: `nprobes(...)` is usually auto-tuned; override only when a selective pre-filter leaves too few neighbors.
## Maintenance
After every successful embedded OSS/local ingestion, call `table.optimize()`.
Do not add this to LanceDB Enterprise/Cloud remote table code; remote compaction
and cleanup are handled automatically based on the Enterprise cluster
configuration.
Why local maintenance is needed:
- Frequent writes can create many small fragments. Queries then need to scan across more files, which can increase latency.
- Updates, deletes, and appends create new table versions. Old versions are retained for time travel and rollback, which can grow disk usage.
- Indexes may have newly added rows that are not yet fully optimized into the index structure.
For local/OSS tables, run `optimize()` after the final successful ingestion
write. Also run it after later batches of update/delete operations or on a
regular maintenance schedule:
```typescript
await table.optimize();
```
If the user wants more aggressive local disk cleanup, pass a shorter cleanup retention window:
```typescript
const olderThan = new Date(Date.now() - 24 * 60 * 60 * 1000);
await table.optimize({ cleanupOlderThan: olderThan });
```
Do not use very short cleanup windows when the application depends on time travel, rollback, or old versions.
## Diagnostics
Before changing code or indexes, inspect:
```typescript
console.log(await table.search(queryVector).where("year > 2000").limit(10).analyzePlan());
console.log(await table.indexStats("vector_idx"));
```
Look for high scan cost, missing indexes, fragmented data, and unindexed rows.
@@ -1,135 +0,0 @@
#!/usr/bin/env python3
"""Scan Python and TypeScript for likely unsafe LanceDB materialization."""
from __future__ import annotations
import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path
PY_FULL_TABLE = re.compile(r"\b\w+\.(to_pandas|to_arrow|to_polars)\s*\(")
TS_TABLE_TO_ARROW = re.compile(r"\b\w+\.toArrow\s*\(")
TS_QUERY_COLLECTOR = re.compile(r"\.query\s*\(\s*\)[\s\S]*?\.to(Array|Arrow)\s*\(")
@dataclass
class Finding:
path: Path
line: int
message: str
text: str
def iter_files(paths: list[Path]) -> list[Path]:
files: list[Path] = []
for path in paths:
if path.is_dir():
files.extend(
p
for p in path.rglob("*")
if p.suffix in {".py", ".ts", ".tsx"} and "node_modules" not in p.parts
)
elif path.suffix in {".py", ".ts", ".tsx"}:
files.append(path)
return sorted(set(files))
def line_number(text: str, offset: int) -> int:
return text.count("\n", 0, offset) + 1
def scan_python(path: Path, text: str) -> list[Finding]:
findings: list[Finding] = []
for match in PY_FULL_TABLE.finditer(text):
line_start = text.rfind("\n", 0, match.start()) + 1
line_end = text.find("\n", match.start())
if line_end == -1:
line_end = len(text)
line = text[line_start:line_end].strip()
if ".search(" in line or ".query(" in line:
continue
findings.append(
Finding(
path,
line_number(text, match.start()),
f"Review Python `{match.group(1)}()` call; table-level materialization is not portable to remote tables.",
line,
)
)
return findings
def statement_around(text: str, start: int, end: int) -> str:
before = max(text.rfind(";", 0, start), text.rfind("\n\n", 0, start))
after_candidates = [pos for pos in (text.find(";", end), text.find("\n\n", end)) if pos != -1]
after = min(after_candidates) if after_candidates else len(text)
return text[before + 1 : after].strip()
def scan_typescript(path: Path, text: str) -> list[Finding]:
findings: list[Finding] = []
for match in TS_TABLE_TO_ARROW.finditer(text):
stmt = statement_around(text, match.start(), match.end())
if ".query(" in stmt or ".search(" in stmt:
continue
findings.append(
Finding(
path,
line_number(text, match.start()),
"Review TypeScript `table.toArrow()`-style call; table-level materialization is not portable for large/remote tables.",
stmt.splitlines()[0].strip(),
)
)
for match in TS_QUERY_COLLECTOR.finditer(text):
stmt = statement_around(text, match.start(), match.end())
if ".limit(" in stmt:
continue
findings.append(
Finding(
path,
line_number(text, match.start()),
"Review unbounded TypeScript query collection; add `limit()` or stream batches.",
stmt.splitlines()[0].strip(),
)
)
return findings
def scan_file(path: Path) -> list[Finding]:
text = path.read_text(encoding="utf-8", errors="replace")
if path.suffix == ".py":
return scan_python(path, text)
if path.suffix in {".ts", ".tsx"}:
return scan_typescript(path, text)
return []
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="+", type=Path)
parser.add_argument(
"--no-fail", action="store_true", help="Always exit 0 after reporting findings."
)
args = parser.parse_args()
findings: list[Finding] = []
for path in iter_files(args.paths):
findings.extend(scan_file(path))
for finding in findings:
print(f"{finding.path}:{finding.line}: {finding.message}")
print(f" {finding.text}")
if findings:
print(
f"\n{len(findings)} finding(s). Review manually; bounded query result conversion may be OK."
)
return 0 if args.no_fail or not findings else 1
if __name__ == "__main__":
sys.exit(main())
+8 -1
View File
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "0.32.0-beta.2"
current_version = "0.38.0-beta.10"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
@@ -75,6 +75,13 @@ filename = "nodejs/Cargo.toml"
replace = "\nversion = \"{new_version}\""
search = "\nversion = \"{current_version}\""
# The Python package takes its version from here (pyproject.toml declares
# `dynamic = ["version"]`, so maturin reads it out of the crate manifest).
[[tool.bumpversion.files]]
filename = "python/Cargo.toml"
replace = "\nversion = \"{new_version}\""
search = "\nversion = \"{current_version}\""
# Java documentation
[[tool.bumpversion.files]]
filename = "docs/src/java/java.md"
+12
View File
@@ -9,6 +9,18 @@ debug = true
codegen-units = 16
lto = "thin"
[profile.release-no-lto]
inherits = "release"
debug = true
lto = false
# Prioritize compile time when LTO is not relevant to the measurement.
codegen-units = 16
[profile.bench]
inherits = "release"
lto = "thin"
codegen-units = 16
[target.'cfg(all())']
rustflags = [
"-Wclippy::all",
+19
View File
@@ -0,0 +1,19 @@
{
"name": "lancedb",
"owner": {
"name": "LanceDB"
},
"description": "LanceDB plugins for Claude Code.",
"plugins": [
{
"name": "lancedb",
"source": "./plugins/lancedb",
"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.",
"version": "0.1.0",
"author": {
"name": "LanceDB"
},
"category": "development"
}
]
}
@@ -27,19 +27,31 @@ runs:
# Extract failed job names
FAILED_JOBS=$(echo "$JOB_RESULTS" | jq -r 'to_entries | map(select(.value.result == "failure")) | map(.key) | join(", ")')
# Create issue with workflow name, failed jobs, and run URL
gh issue create \
--title "$WORKFLOW_NAME Failed ($FAILED_JOBS)" \
--body "The workflow **$WORKFLOW_NAME** failed during execution.
TITLE="$WORKFLOW_NAME Failed ($FAILED_JOBS)"
# This action now also runs on nightly schedules, so a breakage that
# persists for a few days would otherwise file one issue per night.
# Comment on the open report instead when one already exists.
EXISTING=$(gh issue list --state open --label ci --limit 100 --json number,title \
| jq -r --arg title "$TITLE" 'map(select(.title == $title)) | .[0].number // empty')
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --body "Failed again: $RUN_URL"
echo "Commented on existing issue #$EXISTING"
else
gh issue create \
--title "$TITLE" \
--body "The workflow **$WORKFLOW_NAME** failed during execution.
**Failed jobs:** $FAILED_JOBS
**Run URL:** $RUN_URL
Please investigate the failed jobs and address any issues." \
--label "ci"
--label "ci"
echo "Issue created successfully"
echo "Issue created successfully"
fi
else
echo "No job failures detected, skipping issue creation"
fi
+12
View File
@@ -17,6 +17,18 @@ updates:
# newer minimum versions.
versioning-strategy: lockfile-only
groups:
# The arrow-rs and datafusion crates are released in lockstep and have to
# move together, so keep them in one PR instead of one per sub-crate.
# Listed first: a dependency joins the first group it matches.
arrow-datafusion:
patterns:
- arrow
- arrow-*
- parquet
- parquet-*
- datafusion
- datafusion-*
- object_store
rust-minor-patch:
update-types:
- minor
+22 -2
View File
@@ -18,6 +18,14 @@ inputs:
description: "The manylinux version to build for"
required: false
default: "2_17"
package-name:
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
rm -f python/pyproject.toml.bak
grep '^name = ' python/pyproject.toml
- name: Build x86_64 Manylinux wheel
if: ${{ inputs.arm-build == 'false' }}
uses: PyO3/maturin-action@v1
@@ -34,7 +54,7 @@ runs:
maturin-version: "1.12.4"
command: build
working-directory: python
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc"
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc ${{ inputs.rustflags != '' && format('-e RUSTFLAGS={0}', inputs.rustflags) || '' }}"
target: x86_64-unknown-linux-gnu
manylinux: ${{ inputs.manylinux }}
args: ${{ inputs.args }}
@@ -51,7 +71,7 @@ runs:
maturin-version: "1.12.4"
command: build
working-directory: python
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc"
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc ${{ inputs.rustflags != '' && format('-e RUSTFLAGS={0}', inputs.rustflags) || '' }}"
target: aarch64-unknown-linux-gnu
manylinux: ${{ inputs.manylinux }}
args: ${{ inputs.args }}
-1
View File
@@ -6,7 +6,6 @@ on:
# We don't publish pre-releases for Rust. Crates.io is just a source
# distribution, so we don't need to publish pre-releases.
- "v*-beta*"
- "*-v*" # for example, python-vX.Y.Z
env:
# This env var is used by Swatinem/rust-cache@v2 for the cache
+30
View File
@@ -0,0 +1,30 @@
name: CI scripts
on:
push:
branches:
- main
paths:
- ci/set_lance_version.py
- ci/tests/**
- .github/workflows/ci-scripts.yml
pull_request:
paths:
- ci/set_lance_version.py
- ci/tests/**
- .github/workflows/ci-scripts.yml
permissions:
contents: read
jobs:
test:
name: Test CI scripts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Run tests
run: python -m unittest discover -s ci/tests -v
@@ -4,14 +4,14 @@ on:
workflow_call:
inputs:
tag:
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."
required: false
default: ""
type: string
+243
View File
@@ -0,0 +1,243 @@
name: Check doc links
# Checking external links is inherently noisy: third-party sites rate-limit
# automated clients, reject non-browser user agents, and go down temporarily.
# Blocking pull requests on that trades a lot of false failures for very little
# signal, so this runs on a schedule and reports findings in a single tracking
# issue instead of failing anyone's build.
on:
schedule:
- cron: "0 7 * * *"
workflow_dispatch:
# The report lives in one repository-global issue, so runs must not overlap: a
# lookup racing a create produces duplicate issues, and a healthy run closing
# the issue while a failing run only rewrites its body would leave a broken
# report closed. The group is deliberately ref-independent so that a manual
# dispatch serializes against the scheduled run.
concurrency:
group: docs-link-check
cancel-in-progress: false
permissions: {}
env:
REPORT_TITLE: "Docs link checker report"
jobs:
scan:
name: Scan links
runs-on: ubuntu-24.04
# lychee-action is pinned by SHA, but its wrapper downloads the lychee
# release tarball at run time without verifying a digest, and hands the
# resulting binary a GitHub token. Release assets remain replaceable, so
# that binary is confined to a job whose token can only read public
# content; everything that writes runs in the report job below.
permissions:
contents: read
outputs:
checker_outcome: ${{ steps.lychee.outcome }}
exit_code: ${{ steps.lychee.outputs.exit_code }}
status: ${{ steps.validate.outputs.status }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
# workflow_dispatch can run from any ref, but the report is
# repository-global. Always measure the default branch so a manual
# run from a topic branch cannot close a report that main warrants,
# or overwrite it with branch-only findings.
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- name: Check links
id: lychee
continue-on-error: true
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
# Restricted to http(s) on purpose. Much of docs/src is generated
# API reference (the js/ tree comes from `npm run docs` in nodejs)
# and the hand-written pages use mkdocstrings cross-references and
# nav-relative paths that only resolve in the site mkdocs builds,
# not in this checkout, so relative links would be reported as
# broken on every run.
args: >-
--scheme https
--scheme http
--no-progress
--max-retries 3
--timeout 20
'docs/src/**/*.md'
format: json
output: ./lychee/out.json
jobSummary: false
# The report issue, not a red workflow run, is the signal for link
# findings and checker failures alike.
fail: false
- name: Validate report
id: validate
# lychee does not reserve exit code 2 for broken links: its CLI
# parser also exits 2 on an invalid option, before any link was
# checked or any report written. Only a parseable report whose
# counts agree with a completed exit code (0 or 2) counts as a link
# verdict. Everything else becomes a checker-error report instead of
# failing the workflow. Exit 2 covers timeouts as well as errors, and a
# timed-out host is exactly the transient unavailability this report
# exists to surface, so both count as findings. Requiring total > 0
# also catches a glob that silently stopped matching any file.
if: always()
env:
CHECKER_OUTCOME: ${{ steps.lychee.outcome }}
EXIT_CODE: ${{ steps.lychee.outputs.exit_code }}
run: |
status=checker-error
if [[ "$CHECKER_OUTCOME" == success ]] &&
[[ "$EXIT_CODE" == 0 || "$EXIT_CODE" == 2 ]] &&
jq -e --argjson code "$EXIT_CODE" '
(.total > 0) and
(if $code == 0
then .errors == 0 and .timeouts == 0
and (.error_map | length == 0) and (.timeout_map | length == 0)
else (.errors + .timeouts) > 0
and ((.error_map | length) + (.timeout_map | length)) > 0
end)
' ./lychee/out.json
then
if [[ "$EXIT_CODE" == 0 ]]; then
status=healthy
else
status=findings
fi
fi
echo "status=$status" >> "$GITHUB_OUTPUT"
echo "Validated link check as $status"
- name: Upload report
if: steps.validate.outputs.status == 'findings'
uses: actions/upload-artifact@v7
with:
name: link-report
path: ./lychee/out.json
retention-days: 7
report:
name: Update report issue
needs: scan
runs-on: ubuntu-24.04
# Deliberately no checkout: this job needs the report artifact and the
# issues API, not the repository contents.
permissions:
issues: write
env:
CHECKER_OUTCOME: ${{ needs.scan.outputs.checker_outcome }}
EXIT_CODE: ${{ needs.scan.outputs.exit_code }}
STATUS: ${{ needs.scan.outputs.status }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Find existing report issue
id: report
# Matched on title alone, and through search rather than a listing:
# the issue action applies labels in a separate call after creating the
# issue, so a label filter misses a half-created report, and this
# repository has far more open issues than one listing page holds.
# Closed issues are included because a healthy run closes the report:
# an open-only lookup would forget that identity and the next failing
# run would open a duplicate. The oldest match stays the canonical
# report and is reopened below when a problem recurs.
run: |
match=$(gh issue list --repo "$GITHUB_REPOSITORY" --state all \
--search "in:title \"$REPORT_TITLE\" author:app/github-actions" \
--limit 50 --json number,title,state \
--jq "[.[] | select(.title == \"$REPORT_TITLE\")] | sort_by(.number) | first // empty")
echo "number=$(jq -r '.number // empty' <<<"$match")" >> "$GITHUB_OUTPUT"
echo "state=$(jq -r '.state // empty' <<<"$match")" >> "$GITHUB_OUTPUT"
- name: Download report
if: env.STATUS == 'findings'
uses: actions/download-artifact@v8
with:
name: link-report
path: ./lychee
- name: Compose report
if: env.STATUS == 'findings'
run: |
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
{
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.",
"",
([(.error_map | to_entries[]), (.timeout_map | to_entries[])]
| group_by(.key)[] |
"### Errors in \(.[0].key)",
"",
(map(.value[])[] | "* [\(.status.code // .status.text // "ERR")] <\(.url)> — \(.status.details // .status.text // "unknown error")"),
"")
' ./lychee/out.json
} > ./lychee/issue.md
- name: Compose checker error report
if: env.STATUS == 'checker-error'
run: |
mkdir -p ./lychee
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
{
echo "The documentation link check did not complete in [the latest run]($run_url)."
echo
echo "This issue is rewritten by every scheduled run and closed automatically once a trustworthy run finds that all links resolve."
echo
echo "The checker did not produce a trustworthy link verdict. Treat the previous result, if any, as stale until a later run completes."
echo
echo "* Action outcome: \`$CHECKER_OUTCOME\`"
echo "* Exit code: \`${EXIT_CODE:-not reported}\`"
echo "* Verdict validation: \`failed\`"
} > ./lychee/issue.md
- name: Reopen report issue
# A healthy run closes the report, and the issue action below only
# rewrites the body of whatever number it is given. Without an
# explicit reopen, a later finding or checker error would rewrite a
# closed issue. A CLOSED state implies the lookup found a canonical
# issue, so no separate emptiness check.
if: >-
env.STATUS != 'healthy' &&
steps.report.outputs.state == 'CLOSED'
env:
ISSUE_NUMBER: ${{ steps.report.outputs.number }}
run: |
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
gh issue reopen "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" \
--comment "The documentation link checker reported a problem again in [the latest run]($run_url)."
- name: Report link-check problem
if: env.STATUS != 'healthy'
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
with:
# Empty on the first failing run, which creates the issue; afterwards
# the same issue is updated in place.
issue-number: ${{ steps.report.outputs.number }}
title: ${{ env.REPORT_TITLE }}
content-filepath: ./lychee/issue.md
labels: documentation
- name: Close report issue once links are healthy
# An OPEN state implies the lookup found a canonical issue; a report
# that is already closed needs nothing.
if: >-
env.STATUS == 'healthy' &&
steps.report.outputs.state == 'OPEN'
env:
ISSUE_NUMBER: ${{ steps.report.outputs.number }}
run: |
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
gh issue close "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" \
--comment "All documentation links resolved in [the latest run]($run_url)."
+85
View File
@@ -0,0 +1,85 @@
name: GitHub Release
# All SDKs share one version, so a single `vX.Y.Z` tag produces a single GitHub
# release covering all of them. The per-package publish workflows (PyPI, NPM,
# Cargo, Maven) trigger off the same tag independently.
on:
push:
tags:
- "v*"
permissions:
contents: read
jobs:
gh-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
- name: Extract version
id: extract_version
env:
GITHUB_REF: ${{ github.ref }}
run: |
set -e
echo "Extracting tag and version from $GITHUB_REF"
if [[ $GITHUB_REF =~ refs/tags/v(.*) ]]; then
VERSION=${BASH_REMATCH[1]}
TAG=v$VERSION
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
else
echo "Failed to extract version from $GITHUB_REF"
exit 1
fi
echo "Extracted version $VERSION from $GITHUB_REF"
if [[ $VERSION =~ beta ]]; then
echo "This is a beta release"
echo "prerelease=true" >> $GITHUB_OUTPUT
# Get last release (that is not this one)
FROM_TAG=$(git tag --sort='version:refname' \
| grep ^v \
| grep -vF "$TAG" \
| python ci/semver_sort.py v \
| tail -n 1)
else
echo "This is a stable release"
echo "prerelease=false" >> $GITHUB_OUTPUT
# Get last stable tag (ignore betas)
FROM_TAG=$(git tag --sort='version:refname' \
| grep ^v \
| grep -vF "$TAG" \
| grep -v beta \
| python ci/semver_sort.py v \
| tail -n 1)
fi
echo "Found from tag $FROM_TAG"
echo "from_tag=$FROM_TAG" >> $GITHUB_OUTPUT
- name: Create Release Notes
id: release_notes
uses: mikepenz/release-changelog-builder-action@v4
with:
configuration: .github/release_notes.json
toTag: ${{ steps.extract_version.outputs.tag }}
fromTag: ${{ steps.extract_version.outputs.from_tag }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GH release
uses: softprops/action-gh-release@v2
with:
# Marking betas as pre-releases keeps them from taking the "Latest"
# badge on the releases page.
prerelease: ${{ steps.extract_version.outputs.prerelease }}
make_latest: ${{ steps.extract_version.outputs.prerelease == 'false' }}
tag_name: ${{ steps.extract_version.outputs.tag }}
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: false
name: LanceDB v${{ steps.extract_version.outputs.version }}
body: ${{ steps.release_notes.outputs.changelog }}
+8 -30
View File
@@ -1,13 +1,14 @@
name: Create release commit
# This workflow increments versions, tags the version, and pushes it.
# This workflow increments the version, tags it, and pushes it. All SDKs share
# a single version, so one tag releases all of them.
# When a tag is pushed, another workflow is triggered that creates a GH release
# and uploads the binaries. This workflow is only for creating the tag.
# This script will enforce that a minor version is incremented if there are any
# breaking changes since the last minor increment. However, it isn't able to
# differentiate between breaking changes in Node versus Python. If you wish to
# bypass this check, you can manually increment the version and push the tag.
# breaking changes since the last minor increment. A breaking change in any SDK
# bumps the minor version for all of them. If you wish to bypass this check, you
# can manually increment the version and push the tag.
on:
workflow_dispatch:
inputs:
@@ -24,16 +25,6 @@ on:
options:
- preview
- stable
python:
description: 'Make a Python release'
required: true
default: true
type: boolean
other:
description: 'Make a Node/Rust/Java release'
required: true
default: true
type: boolean
bump-minor:
description: 'Bump minor version'
required: true
@@ -65,29 +56,16 @@ jobs:
run: |
git config user.name 'Lance Release'
git config user.email 'lance-dev@lancedb.com'
- name: Bump Python version
if: ${{ inputs.python }}
working-directory: python
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Need to get the commit before bumping the version, so we can
# determine if there are breaking changes in the next step as well.
echo "COMMIT_BEFORE_BUMP=$(git rev-parse HEAD)" >> $GITHUB_ENV
pip install bump-my-version PyGithub packaging
bash ../ci/bump_version.sh ${{ inputs.type }} ${{ inputs.bump-minor }} python-v
- name: Bump Node/Rust version
if: ${{ inputs.other }}
- name: Bump version
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pip install bump-my-version PyGithub packaging
bash ci/bump_version.sh ${{ inputs.type }} ${{ inputs.bump-minor }} v $COMMIT_BEFORE_BUMP
bash ci/bump_version.sh ${{ inputs.type }} ${{ inputs.bump-minor }}
bash ci/update_lockfiles.sh --amend
- name: Push new version tag
if: ${{ !inputs.dry_run }}
uses: ad-m/github-push-action@master
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # v1.3.0
with:
# Need to use PAT here too to trigger next workflow. See comment above.
github_token: ${{ secrets.LANCEDB_RELEASE_TOKEN }}
+15
View File
@@ -61,6 +61,11 @@ jobs:
sudo apt update
sudo apt install -y protobuf-compiler libssl-dev
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Format Rust
run: cargo fmt --all -- --check
- name: Lint Rust
@@ -103,6 +108,11 @@ jobs:
cache: 'pnpm'
cache-dependency-path: nodejs/pnpm-lock.yaml
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install dependencies
run: |
sudo apt update
@@ -182,6 +192,11 @@ jobs:
cache-dependency-path: nodejs/pnpm-lock.yaml
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install dependencies
run: |
brew install protobuf
+95 -89
View File
@@ -10,10 +10,16 @@ permissions:
on:
push:
branches:
- main
tags:
- "v*"
# The cross-compiled targets (musl especially) break from toolchain and
# dependency changes that nothing else in CI catches, and discovering that
# mid-release is expensive. A nightly run keeps that signal while dropping
# the full 8-target release matrix from all ~90 pushes to main each month.
# `report-failure` files an issue when a nightly breaks.
schedule:
- cron: "0 8 * * *"
workflow_dispatch:
pull_request:
# This should trigger a dry run (we skip the final publish step)
paths:
@@ -26,73 +32,6 @@ concurrency:
cancel-in-progress: true
jobs:
gh-release:
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
- name: Extract version
id: extract_version
env:
GITHUB_REF: ${{ github.ref }}
run: |
set -e
echo "Extracting tag and version from $GITHUB_REF"
if [[ $GITHUB_REF =~ refs/tags/v(.*) ]]; then
VERSION=${BASH_REMATCH[1]}
TAG=v$VERSION
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
else
echo "Failed to extract version from $GITHUB_REF"
exit 1
fi
echo "Extracted version $VERSION from $GITHUB_REF"
if [[ $VERSION =~ beta ]]; then
echo "This is a beta release"
# Get last release (that is not this one)
FROM_TAG=$(git tag --sort='version:refname' \
| grep ^v \
| grep -vF "$TAG" \
| python ci/semver_sort.py v \
| tail -n 1)
else
echo "This is a stable release"
# Get last stable tag (ignore betas)
FROM_TAG=$(git tag --sort='version:refname' \
| grep ^v \
| grep -vF "$TAG" \
| grep -v beta \
| python ci/semver_sort.py v \
| tail -n 1)
fi
echo "Found from tag $FROM_TAG"
echo "from_tag=$FROM_TAG" >> $GITHUB_OUTPUT
- name: Create Release Notes
id: release_notes
uses: mikepenz/release-changelog-builder-action@v4
with:
configuration: .github/release_notes.json
toTag: ${{ steps.extract_version.outputs.tag }}
fromTag: ${{ steps.extract_version.outputs.from_tag }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create GH release
uses: softprops/action-gh-release@v2
with:
prerelease: ${{ contains('beta', github.ref) }}
tag_name: ${{ steps.extract_version.outputs.tag }}
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: false
name: Node/Rust LanceDB v${{ steps.extract_version.outputs.version }}
body: ${{ steps.release_notes.outputs.changelog }}
build-lancedb:
strategy:
fail-fast: false
@@ -101,9 +40,18 @@ jobs:
- target: aarch64-apple-darwin
host: macos-latest
features: fp16kernels
pre_build: brew install protobuf
pre_build: |-
brew install protobuf
# Fat LTO (the workspace default in .cargo/config.toml) is
# single-threaded and is the peak-memory step of the build. On
# this runner it accounted for ~111 of the job's ~113 minutes,
# making it the critical path of the entire publish pipeline.
# ThinLTO parallelizes it across the runner's cores, for a few
# percent of runtime performance.
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
- target: x86_64-pc-windows-msvc
host: windows-2025-8x-x64
host: windows-2025
features: ","
pre_build: |-
choco install --no-progress protoc ninja nasm
@@ -111,19 +59,19 @@ jobs:
# There is an issue where choco doesn't add nasm to the path
export PATH="$PATH:/c/Program Files/NASM"
nasm -v
# Fat LTO of the cdylib is single-threaded and the peak-memory
# step of the build, and had started hitting rustc-LLVM OOM on the
# Windows runners. ThinLTO parallelizes it across the runner's
# cores and keeps peak memory well under the limit.
# See the ThinLTO note on aarch64-apple-darwin above. Keeping
# peak memory down is also what lets this run on the standard
# 4-core runner: the 8-core larger runner was only needed to
# stop fat LTO from OOMing rustc-LLVM.
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
- target: aarch64-pc-windows-msvc
host: windows-2025-8x-x64
host: windows-2025
features: ","
pre_build: |-
choco install --no-progress protoc
rustup target add aarch64-pc-windows-msvc
# See ThinLTO note on the x86_64-pc-windows-msvc target above.
# See the ThinLTO note on aarch64-apple-darwin above.
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
- target: x86_64-unknown-linux-gnu
@@ -198,16 +146,49 @@ jobs:
with:
toolchain: stable
targets: ${{ matrix.settings.target }}
- name: Cache cargo
uses: actions/cache@v5
# These builds were entirely uncached: the old key was static, so
# `actions/cache` (which only writes on a miss) could never refresh it,
# and the multi-GB whole-`target/` copy it tried to store never fit the
# repo's cache budget, so no entry was ever saved. rust-cache prunes
# `target/` to dependency artifacts and keys on Cargo.lock plus the rustc
# version, which both fixes the key and keeps entries a sane size.
#
# This caches dependency *compilation* only. The LTO link of the cdylib
# re-runs regardless, since the local crate changes every time, so the
# win is larger on the non-LTO jobs than here.
- name: Cache cargo (native builds)
uses: Swatinem/rust-cache@v2
if: ${{ !matrix.settings.docker }}
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
.cargo-cache
target/
key: nodejs-${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}
# The release profile and per-target dirs differ from what the test
# workflows cache, so these need to be separate entries.
key: release-${{ matrix.settings.target }}
# Only the nightly run on main writes, so tag and PR runs restore a
# warm entry without every dependabot PR writing its own (which would
# be unreadable elsewhere anyway, since GitHub scopes caches to the
# creating ref). The nightly cadence also keeps entries inside
# GitHub's 7-day eviction window, which a tag-only trigger would not.
save-if: ${{ github.ref == 'refs/heads/main' }}
# Docker builds can use rust-cache too. `target/` already lives on the
# host because the whole workspace is bind-mounted into the container, and
# rust-cache's prune and save run host-side, so they can manage it -- which
# is what keeps the entry to dependency artifacts rather than a multi-GB
# copy of everything.
#
# Two differences from the native builds. The container's CARGO_HOME is
# bind-mounted from `.cargo-cache` rather than the host's ~/.cargo, so that
# has to be cached explicitly. And the key is derived from the *host* rustc
# version, which is not the compiler that produced these artifacts; that is
# safe because cargo fingerprints the real compiler and rebuilds on a
# mismatch, it just means a base-image toolchain bump costs one cold build
# instead of invalidating the key.
- name: Cache cargo (docker builds)
uses: Swatinem/rust-cache@v2
if: ${{ matrix.settings.docker }}
with:
key: docker-${{ matrix.settings.target }}
cache-directories: .cargo-cache
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Zig
@@ -225,9 +206,13 @@ jobs:
if: ${{ matrix.settings.docker }}
with:
image: ${{ matrix.settings.docker }}
# All three mounts must live under `.cargo-cache`, which is what the
# cache step above saves. Previously the registry mounts pointed at
# `.cargo/...`, a path nothing cached, so the container re-downloaded
# the whole crate registry on every run.
options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db \
-v ${{ github.workspace }}/.cargo/registry/cache:/usr/local/cargo/registry/cache \
-v ${{ github.workspace }}/.cargo/registry/index:/usr/local/cargo/registry/index \
-v ${{ github.workspace }}/.cargo-cache/registry/cache:/usr/local/cargo/registry/cache \
-v ${{ github.workspace }}/.cargo-cache/registry/index:/usr/local/cargo/registry/index \
-v ${{ github.workspace }}:/build -w /build/nodejs"
run: |
set -e
@@ -239,6 +224,16 @@ jobs:
--js ../lancedb/native.js \
--strip \
--output-dir dist/
# The container runs as root (`--user 0:0`), so everything it wrote to the
# mounted cache dirs is root-owned. rust-cache's post step runs as the
# runner user and has to both read these and delete from them while
# pruning, so hand them back before it runs.
- name: Take ownership of docker build output
if: ${{ matrix.settings.docker }}
run: |
sudo chown -R "$(id -u):$(id -g)" \
"${{ github.workspace }}/.cargo-cache" \
"${{ github.workspace }}/target"
- name: Build
run: |
${{ matrix.settings.pre_build }}
@@ -252,6 +247,15 @@ jobs:
--output-dir dist/
if: ${{ !matrix.settings.docker }}
shell: bash
# The standard Windows runners have ~14 GB free, and a release `target/`
# for this workspace is a large fraction of that. Report the remaining
# headroom so a build that only just fits is visible before a dependency
# bump turns it into a failed release. `always()` so the numbers are
# still there when the build is what ran out of space.
- name: Report disk headroom
if: always()
run: df -h
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
@@ -402,7 +406,9 @@ jobs:
name: Report Workflow Failure
runs-on: ubuntu-latest
needs: [build-lancedb, test-lancedb, publish]
if: always() && failure() && startsWith(github.ref, 'refs/tags/v')
# Nightly runs are the only thing watching the cross-compiled targets now,
# so they have to report failures too or the signal is silently lost.
if: always() && failure() && (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'schedule')
permissions:
contents: read
issues: write
+58 -76
View File
@@ -3,7 +3,7 @@ name: PyPI Publish
on:
push:
tags:
- 'python-v*'
- 'v*'
pull_request:
# This should trigger a dry run (we skip the final publish step)
paths:
@@ -20,9 +20,15 @@ env:
permissions:
contents: read
# Without this, a force-push to a PR leaves the previous run going -- including
# a ~74 minute Windows job and a billed arm64 wheel build.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
linux:
name: Python ${{ matrix.config.platform }} manylinux${{ matrix.config.manylinux }}
name: Python ${{ matrix.config.package_name }} ${{ matrix.config.platform }} manylinux${{ matrix.config.manylinux }}
timeout-minutes: 60
strategy:
matrix:
@@ -31,11 +37,28 @@ jobs:
manylinux: "2_28"
extra_args: "--features fp16kernels"
runner: ubuntu-22.04
package_name: "lancedb"
rustflags: ""
# For successful fat LTO builds, we need a large runner to avoid OOM errors.
- platform: aarch64
manylinux: "2_28"
extra_args: "--features fp16kernels"
runner: ubuntu-2404-8x-arm64
package_name: "lancedb"
rustflags: ""
# `lancedb-compat`: pre-Haswell-friendly variant for x86_64 hosts
# without AVX2 (Sandy Bridge / Ivy Bridge / Westmere on Intel,
# Bulldozer / Piledriver / Steamroller on AMD). Compiled at the
# `x86-64-v2` baseline; runtime SIMD dispatch in lance-linalg
# picks the appropriate tier (scalar / AVX / AVX+FMA / AVX2+FMA
# / AVX-512) at load time. Same import as `lancedb` -- conflicts
# at install time, so users pick one.
- platform: x86_64
manylinux: "2_28"
extra_args: ""
runner: ubuntu-22.04
package_name: "lancedb-compat"
rustflags: "-Ctarget-cpu=x86-64-v2"
runs-on: ${{ matrix.config.runner }}
steps:
- uses: actions/checkout@v6
@@ -46,17 +69,29 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Add swap for Arm fat LTO
if: matrix.config.platform == 'aarch64'
shell: bash
run: |
swap_file="$RUNNER_TEMP/lancedb-swap"
sudo fallocate --length 16G "$swap_file"
sudo chmod 600 "$swap_file"
sudo mkswap "$swap_file"
sudo swapon "$swap_file"
free -h
- uses: ./.github/workflows/build_linux_wheel
with:
python-minor-version: 10
args: "--release --strip ${{ matrix.config.extra_args }}"
arm-build: ${{ matrix.config.platform == 'aarch64' }}
manylinux: ${{ matrix.config.manylinux }}
package-name: ${{ matrix.config.package_name }}
rustflags: ${{ matrix.config.rustflags }}
- uses: actions/upload-artifact@v7
if: startsWith(github.ref, 'refs/tags/python-v')
if: startsWith(github.ref, 'refs/tags/v')
with:
name: wheels-linux-${{ matrix.config.platform }}-${{ matrix.config.manylinux }}
path: target/wheels/lancedb-*.whl
name: wheels-linux-${{ matrix.config.package_name }}-${{ matrix.config.platform }}-${{ matrix.config.manylinux }}
path: target/wheels/*.whl
if-no-files-found: error
mac:
timeout-minutes: 90
@@ -82,7 +117,7 @@ jobs:
python-minor-version: 10
args: "--release --strip --target ${{ matrix.config.target }} --features fp16kernels"
- uses: actions/upload-artifact@v7
if: startsWith(github.ref, 'refs/tags/python-v')
if: startsWith(github.ref, 'refs/tags/v')
with:
name: wheels-mac-${{ matrix.config.target }}
path: target/wheels/lancedb-*.whl
@@ -94,6 +129,12 @@ jobs:
# link.exe is single-threaded and the long pole on Windows builds. Use
# rustc's bundled lld-link instead.
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: rust-lld
# Fat LTO of the cdylib is single-threaded and the peak-memory step of the
# build. ThinLTO parallelizes it across the runner's cores, at some cost
# to runtime performance on our least performance-sensitive platform.
# Matches what the nodejs Windows builds already do in npm-publish.yml.
CARGO_PROFILE_RELEASE_LTO: thin
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 16
steps:
- uses: actions/checkout@v6
with:
@@ -103,19 +144,26 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.13"
# NOTE: caching cargo here would be a no-op. This workflow only runs on
# tags and PRs, and GitHub only lets a run restore caches from its own ref
# or the default branch -- so with no run on main there is nothing that
# can populate an entry the release build would be allowed to read. Fixing
# this needs a main/nightly trigger (which would also catch wheel-build
# breakage before a release); the ~74 minutes here is otherwise dominated
# by the fat-LTO link, which no cache avoids.
- uses: ./.github/workflows/build_windows_wheel
with:
python-minor-version: 10
args: "--release --strip"
- uses: actions/upload-artifact@v7
if: startsWith(github.ref, 'refs/tags/python-v')
if: startsWith(github.ref, 'refs/tags/v')
with:
name: wheels-windows
path: target/wheels/lancedb-*.whl
if-no-files-found: error
publish:
name: Publish wheels
if: startsWith(github.ref, 'refs/tags/python-v')
if: startsWith(github.ref, 'refs/tags/v')
needs: [linux, mac, windows]
runs-on: ubuntu-latest
permissions:
@@ -145,7 +193,7 @@ jobs:
FURY_TOKEN: ${{ secrets.FURY_TOKEN }}
run: |
shopt -s nullglob
WHEELS=(target/wheels/lancedb-*.whl)
WHEELS=(target/wheels/*.whl)
if [[ ${#WHEELS[@]} -eq 0 ]]; then
echo "No wheels found in target/wheels/" >&2
exit 1
@@ -164,72 +212,6 @@ jobs:
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: target/wheels/
gh-release:
if: startsWith(github.ref, 'refs/tags/python-v')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
- name: Extract version
id: extract_version
env:
GITHUB_REF: ${{ github.ref }}
run: |
set -e
echo "Extracting tag and version from $GITHUB_REF"
if [[ $GITHUB_REF =~ refs/tags/python-v(.*) ]]; then
VERSION=${BASH_REMATCH[1]}
TAG=python-v$VERSION
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
else
echo "Failed to extract version from $GITHUB_REF"
exit 1
fi
echo "Extracted version $VERSION from $GITHUB_REF"
if [[ $VERSION =~ beta ]]; then
echo "This is a beta release"
# Get last release (that is not this one)
FROM_TAG=$(git tag --sort='version:refname' \
| grep ^python-v \
| grep -vF "$TAG" \
| python ci/semver_sort.py python-v \
| tail -n 1)
else
echo "This is a stable release"
# Get last stable tag (ignore betas)
FROM_TAG=$(git tag --sort='version:refname' \
| grep ^python-v \
| grep -vF "$TAG" \
| grep -v beta \
| python ci/semver_sort.py python-v \
| tail -n 1)
fi
echo "Found from tag $FROM_TAG"
echo "from_tag=$FROM_TAG" >> $GITHUB_OUTPUT
- name: Create Python Release Notes
id: python_release_notes
uses: mikepenz/release-changelog-builder-action@v4
with:
configuration: .github/release_notes.json
toTag: ${{ steps.extract_version.outputs.tag }}
fromTag: ${{ steps.extract_version.outputs.from_tag }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create Python GH release
uses: softprops/action-gh-release@v2
with:
prerelease: ${{ contains('beta', github.ref) }}
tag_name: ${{ steps.extract_version.outputs.tag }}
token: ${{ secrets.GITHUB_TOKEN }}
generate_release_notes: false
name: Python LanceDB v${{ steps.extract_version.outputs.version }}
body: ${{ steps.python_release_notes.outputs.changelog }}
report-failure:
name: Report Workflow Failure
runs-on: ubuntu-latest
@@ -237,7 +219,7 @@ jobs:
permissions:
contents: read
issues: write
if: always() && failure() && startsWith(github.ref, 'refs/tags/python-v')
if: always() && failure() && startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/create-failure-issue
+36 -3
View File
@@ -108,6 +108,15 @@ jobs:
run: |
sudo apt update
sudo apt install -y protobuf-compiler
# `pip install -e .` builds the extension with maturin, which is most of
# this job's ~33 minutes. It had no Rust cache, so every dependency was
# recompiled from scratch on every run.
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install
run: |
pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .[tests,dev,embeddings]
@@ -168,6 +177,14 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.13"
# maturin runs cargo natively on macOS (docker is Linux-only), so the host
# target dir is cacheable. This job had no Rust cache.
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: ./.github/workflows/build_mac_wheel
with:
args: --profile ci
@@ -197,6 +214,14 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.13"
# maturin runs cargo natively on Windows (docker is Linux-only), so the
# host target dir is cacheable. This job had no Rust cache at all and so
# rebuilt every dependency from scratch on every run.
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. The repo sits at
# GitHub's cache cap, so per-PR saves just evict main's entries.
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: ./.github/workflows/build_windows_wheel
with:
args: --profile ci
@@ -204,7 +229,8 @@ jobs:
# Make sure wheels are not included in the Rust cache
- name: Delete wheels
run: rm -rf target/wheels
pydantic1x:
min-deps:
name: "Minimum dependencies"
timeout-minutes: 60
runs-on: "ubuntu-24.04"
defaults:
@@ -224,10 +250,17 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: "3.10"
# As with Doctest, `pip install -e .` compiles the extension and this job
# had no Rust cache, which is most of its ~37 minutes.
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install lancedb
run: |
pip install "pydantic<2"
pip install pyarrow==16
pip install "pydantic==2.7.4" "pyarrow==16"
pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .[tests]
- name: Run tests
run: pytest -m "not slow and not s3_test" -x -v --durations=30 python/tests
+88 -20
View File
@@ -48,6 +48,11 @@ jobs:
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install dependencies
run: |
sudo apt update
@@ -89,6 +94,11 @@ jobs:
run: rm -f Cargo.lock
- uses: rui314/setup-mold@v1
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install dependencies
run: |
sudo apt update
@@ -98,7 +108,7 @@ jobs:
cargo build --profile ci --benches --all-features --tests
linux:
timeout-minutes: 30
timeout-minutes: 60
# To build all features, we need more disk space than is available
# on the free OSS github runner. This is mostly due to the the
# sentence-transformers feature.
@@ -111,13 +121,17 @@ jobs:
# Need up-to-date compilers for kernels
CC: clang-18
CXX: clang++-18
GH_TOKEN: ${{ secrets.SOPHON_READ_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install dependencies
run: |
sudo apt update
@@ -150,15 +164,44 @@ jobs:
- name: Run feature tests
run: CARGO_ARGS="--profile ci" make -C ./lancedb feature-tests
- name: Run examples
run: cargo run --profile ci --example simple --locked
run: cargo run --profile ci --all-features --example simple --locked
remote:
timeout-minutes: 30
# Running this requires access to secrets, so skip if this is a PR from a
# fork. Keep it separate from the all-features build so Cargo does not
# retain both dependency graphs in one target directory.
if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
runs-on: ubuntu-2404-4x-x64
defaults:
run:
shell: bash
working-directory: rust
env:
CC: clang-18
CXX: clang++-18
GH_TOKEN: ${{ secrets.SOPHON_READ_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
- uses: Swatinem/rust-cache@v2
with:
# Remote tests use a different feature graph from the main Linux
# job. Cache downloads, but build into a fresh target directory.
cache-targets: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y protobuf-compiler libssl-dev
- uses: rui314/setup-mold@v1
- name: Run remote tests
# Running this requires access to secrets, so skip if this is
# a PR from a fork.
if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
run: CARGO_ARGS="--profile ci" make -C ./lancedb remote-tests
macos:
timeout-minutes: 30
timeout-minutes: 60
strategy:
matrix:
mac-runner: ["macos-14", "macos-15"]
@@ -175,6 +218,11 @@ jobs:
- name: CPU features
run: sysctl -a | grep cpu
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install dependencies
run: brew install protobuf
- name: Run tests
@@ -187,12 +235,19 @@ jobs:
cargo test --profile ci --features $ALL_FEATURES --locked
windows:
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
target:
- x86_64-pc-windows-msvc
- aarch64-pc-windows-msvc
include:
- target: x86_64-pc-windows-msvc
runner: windows-2022
# windows-11-arm is a standard runner, so it is free on public repos.
# Running natively lets the aarch64 tests actually execute -- this
# job used to cross-compile them and then skip the test step, paying
# full codegen and link cost for a compile check.
- target: aarch64-pc-windows-msvc
runner: windows-11-arm
runs-on: ${{ matrix.runner }}
defaults:
run:
working-directory: rust/lancedb
@@ -201,6 +256,11 @@ jobs:
- name: Set target
run: rustup target add ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install Protoc v21.12
run: choco install --no-progress protoc
- name: Build
@@ -208,11 +268,12 @@ jobs:
$env:VCPKG_ROOT = $env:VCPKG_INSTALLATION_ROOT
cargo build --profile ci --features aws,remote --tests --locked --target ${{ matrix.target }}
- name: Run tests
# Can only run tests when target matches host
if: ${{ matrix.target == 'x86_64-pc-windows-msvc' }}
run: |
$env:VCPKG_ROOT = $env:VCPKG_INSTALLATION_ROOT
cargo test --profile ci --features aws,remote --locked
# `--target` has to match the build step above. Without it cargo uses
# target/ci/ rather than target/<triple>/ci/ and rebuilds the entire
# dependency graph a second time.
cargo test --profile ci --features aws,remote --locked --target ${{ matrix.target }}
msrv:
# Check the minimum supported Rust version
@@ -238,6 +299,11 @@ jobs:
with:
toolchain: ${{ matrix.msrv }}
- uses: Swatinem/rust-cache@v2
with:
# Restore everywhere, but only save from main. Per-PR saves are
# unreadable outside their own branch anyway, since GitHub scopes
# caches to the creating ref.
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Downgrade dependencies
# These packages have newer requirements for MSRV
run: |
@@ -258,16 +324,18 @@ jobs:
cargo update -p aws-types --precise 1.3.9
cargo update -p aws-sigv4 --precise 1.3.5
cargo update -p aws-credential-types --precise 1.2.8
cargo update -p aws-smithy-checksums --precise 0.63.9
# aws-smithy-checksums must stay at or above 0.63.13: OpenDAL's S3
# service needs crc-fast ~1.9, and older releases pin it to ~1.3.
cargo update -p aws-smithy-checksums --precise 0.63.13
cargo update -p aws-smithy-runtime --precise 1.9.3
cargo update -p aws-smithy-http --precise 0.62.4
cargo update -p aws-smithy-eventstream --precise 0.60.12
cargo update -p aws-smithy-http --precise 0.62.6
cargo update -p aws-smithy-eventstream --precise 0.60.14
cargo update -p aws-smithy-http-client --precise 1.1.3
cargo update -p aws-smithy-observability --precise 0.1.4
cargo update -p aws-smithy-query --precise 0.60.8
cargo update -p aws-smithy-runtime-api --precise 1.9.1
cargo update -p aws-smithy-async --precise 1.2.6
cargo update -p aws-smithy-types --precise 1.3.5
cargo update -p aws-smithy-runtime-api --precise 1.9.3
cargo update -p aws-smithy-async --precise 1.2.7
cargo update -p aws-smithy-types --precise 1.3.6
cargo update -p aws-smithy-xml --precise 0.60.11
cargo update -p home --precise 0.5.9
- name: cargo +${{ matrix.msrv }} check
+32
View File
@@ -18,6 +18,9 @@ Common commands:
* Run specific test: `cargo test --quiet --features remote -p <package_name> --test <test_name>`
* Lint: `cargo clippy --quiet --features remote --tests --examples`
* Format Rust: `cargo fmt --all`
* 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.
Generated
+503 -468
View File
File diff suppressed because it is too large Load Diff
+23 -17
View File
@@ -13,20 +13,21 @@ categories = ["database-implementations"]
rust-version = "1.91.0"
[workspace.dependencies]
lance = { "version" = "=9.1.0-beta.2", default-features = false, "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=9.1.0-beta.2", default-features = false, "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=9.1.0-beta.2", default-features = false, "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance = { "version" = "=12.0.0-beta.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=12.0.0-beta.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=12.0.0-beta.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lancedb = { path = "rust/lancedb", default-features = false }
ahash = "0.8"
# Note that this one does not include pyarrow
arrow = { version = "58.0.0", optional = false }
@@ -39,6 +40,7 @@ arrow-schema = "58.0.0"
arrow-select = "58.0.0"
arrow-cast = "58.0.0"
async-trait = "0"
bytes = "1"
datafusion = { version = "54.0.0", default-features = false }
datafusion-catalog = "54.0.0"
datafusion-common = { version = "54.0.0", default-features = false }
@@ -52,7 +54,7 @@ env_logger = "0.11"
half = { "version" = "2.7.1", default-features = false, features = [
"num-traits",
] }
futures = "0"
futures = "0.3"
log = "0.4"
metrics = "0.24"
metrics-util = "0.19"
@@ -64,9 +66,13 @@ snafu = "0.8"
url = "2"
num-traits = "0.2"
regex = "1.10"
lazy_static = "1"
semver = "1.0.25"
chrono = "0.4"
serde = "1"
serde_json = "1"
tempfile = "3.5.0"
tokio = { version = "1.23", features = ["rt-multi-thread", "sync"] }
uuid = { version = "1.7.0", features = ["v4"] }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
[profile.ci]
debug = "line-tables-only"
+3 -3
View File
@@ -2,9 +2,9 @@ set -e
RELEASE_TYPE=${1:-"stable"}
BUMP_MINOR=${2:-false}
TAG_PREFIX=${3:-"v"} # Such as "python-v"
HEAD_SHA=${4:-$(git rev-parse HEAD)}
HEAD_SHA=$(git rev-parse HEAD)
readonly TAG_PREFIX="v"
readonly SELF_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
PREV_TAG=$(git tag --sort='version:refname' | grep ^$TAG_PREFIX | python $SELF_DIR/semver_sort.py $TAG_PREFIX | tail -n 1)
@@ -12,7 +12,7 @@ echo "Found previous tag $PREV_TAG"
# Initially, we don't want to tag if we are doing stable, because we will bump
# again later. See comment at end for why.
if [[ "$RELEASE_TYPE" == 'stable' ]]; then
if [[ "$RELEASE_TYPE" == 'stable' ]]; then
BUMP_ARGS="--no-tag"
fi
+2 -1
View File
@@ -2,6 +2,7 @@
Check whether there are any breaking changes in the PRs between the base and head commits.
If there are, assert that we have incremented the minor version.
"""
import argparse
import os
from packaging.version import parse
@@ -27,7 +28,7 @@ if __name__ == "__main__":
else:
print("No breaking changes found.")
exit(0)
last_stable_version = parse(args.last_stable_version)
current_version = parse(args.current_version)
if current_version.minor <= last_stable_version.minor:
+14 -3
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
"""Determine whether a newer Lance tag exists and expose results for CI."""
from __future__ import annotations
import argparse
@@ -36,8 +37,16 @@ class SemVer:
prerelease: Tuple[Union[int, str], ...]
def __lt__(self, other: "SemVer") -> bool: # pragma: no cover - simple comparison
if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
if (self.major, self.minor, self.patch) != (
other.major,
other.minor,
other.patch,
):
return (self.major, self.minor, self.patch) < (
other.major,
other.minor,
other.patch,
)
if self.prerelease == other.prerelease:
return False
if not self.prerelease:
@@ -142,7 +151,9 @@ def read_current_version(repo_root: Path) -> str:
deps = data["workspace"]["dependencies"]
entry = deps["lance"]
except KeyError as exc: # pragma: no cover - configuration guard
raise RuntimeError("Failed to locate workspace.dependencies.lance in Cargo.toml") from exc
raise RuntimeError(
"Failed to locate workspace.dependencies.lance in Cargo.toml"
) from exc
if isinstance(entry, str):
raw_version = entry
+9 -6
View File
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""A zero-dependency mock OpenAI embeddings API endpoint for testing purposes."""
import argparse
import json
import http.server
@@ -22,11 +23,13 @@ class MockOpenAIRequestHandler(http.server.BaseHTTPRequestHandler):
data = []
for i in range(num_inputs):
data.append({
"object": "embedding",
"embedding": [0.1] * 1536,
"index": i,
})
data.append(
{
"object": "embedding",
"embedding": [0.1] * 1536,
"index": i,
}
)
response = {
"object": "list",
@@ -35,7 +38,7 @@ class MockOpenAIRequestHandler(http.server.BaseHTTPRequestHandler):
"usage": {
"prompt_tokens": 0,
"total_tokens": 0,
}
},
}
self.send_response(200)
+1
View File
@@ -7,6 +7,7 @@ from packaging.version import parse, InvalidVersion
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("prefix", default="v")
args = parser.parse_args()
+4 -4
View File
@@ -22,7 +22,7 @@ def run_command(command: str) -> str:
def get_latest_stable_version() -> str:
version_line = run_command("cargo info lance | grep '^version:'")
# Example output: "version: 0.35.0 (latest 0.37.0)"
match = re.search(r'\(latest ([0-9.]+)\)', version_line)
match = re.search(r"\(latest ([0-9.]+)\)", version_line)
if match:
return match.group(1)
# Fallback: use the first version after 'version:'
@@ -69,7 +69,7 @@ def extract_default_features(line: str) -> bool:
"""
import re
match = re.search(r'default-features\s*=\s*false', line)
match = re.search(r"default-features\s*=\s*false", line)
return match is not None
@@ -104,7 +104,7 @@ def dict_to_toml_line(package_name: str, config: dict) -> str:
# This shouldn't happen with our current usage
parts.append(f'"{key}" = {json.dumps(value)}')
return f'{package_name} = {{ {", ".join(parts)} }}\n'
return f"{package_name} = {{ {', '.join(parts)} }}\n"
def update_cargo_toml(line_updater):
@@ -119,7 +119,7 @@ def update_cargo_toml(line_updater):
lance_line = ""
is_parsing_lance_line = False
for line in lines:
if line.startswith("lance"):
if re.match(r"^lance(?:\s|[-_])", line):
# Check if this is a single-line or multi-line entry
# Single-line entries either:
# 1. End with } (complete inline table)
+185
View File
@@ -0,0 +1,185 @@
import os
import stat
import subprocess
import sys
import tempfile
import textwrap
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPT = REPO_ROOT / "ci" / "set_lance_version.py"
LANCE_GIT_URL = "https://github.com/lance-format/lance.git"
CARGO_TOML = """\
[workspace.dependencies]
lance = { "version" = "=1.0.0", default-features = false, "features" = ["dynamodb"] }
lance-core = "1.0.0"
lance_datafusion = {
"version" = "=1.0.0",
"features" = ["substrait"]
}
lancedb = { path = "rust/lancedb", default-features = false }
lancedb-common = { path = "rust/lancedb-common" }
lancewood = "1.0.0"
my-lance = "1.0.0"
"""
UNTOUCHED_DEPENDENCIES = """\
lancedb = { path = "rust/lancedb", default-features = false }
lancedb-common = { path = "rust/lancedb-common" }
lancewood = "1.0.0"
my-lance = "1.0.0"
"""
class SetLanceVersionTest(unittest.TestCase):
def test_supported_update_modes_only_rewrite_lance_dependencies(self):
cases = {
"stable": (
"""\
lance = { "version" = "=9.9.9", default-features = false, "features" = ["dynamodb"] }
lance-core = "=9.9.9"
lance_datafusion = { "version" = "=9.9.9", "features" = ["substrait"] }
""",
["cargo info lance", "cargo metadata"],
),
"preview": (
f"""\
lance = {{ "version" = "=10.0.0-beta.3", default-features = false, "features" = ["dynamodb"], "tag" = "v10.0.0-beta.3", "git" = "{LANCE_GIT_URL}" }}
lance-core = {{ "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "{LANCE_GIT_URL}" }}
lance_datafusion = {{ "version" = "=10.0.0-beta.3", "features" = ["substrait"], "tag" = "v10.0.0-beta.3", "git" = "{LANCE_GIT_URL}" }}
""",
["git ls-remote --tags", "cargo metadata"],
),
"local": (
"""\
lance = { "path" = "../lance/rust/lance", default-features = false, "features" = ["dynamodb"] }
lance-core = { "path" = "../lance/rust/lance-core" }
lance_datafusion = { "path" = "../lance/rust/lance_datafusion", "features" = ["substrait"] }
""",
["cargo metadata"],
),
"v8.1.2": (
"""\
lance = { "version" = "=8.1.2", default-features = false, "features" = ["dynamodb"] }
lance-core = "=8.1.2"
lance_datafusion = { "version" = "=8.1.2", "features" = ["substrait"] }
""",
["cargo metadata"],
),
"v8.2.0-beta.4": (
f"""\
lance = {{ "version" = "=8.2.0-beta.4", default-features = false, "features" = ["dynamodb"], "tag" = "v8.2.0-beta.4", "git" = "{LANCE_GIT_URL}" }}
lance-core = {{ "version" = "=8.2.0-beta.4", "tag" = "v8.2.0-beta.4", "git" = "{LANCE_GIT_URL}" }}
lance_datafusion = {{ "version" = "=8.2.0-beta.4", "features" = ["substrait"], "tag" = "v8.2.0-beta.4", "git" = "{LANCE_GIT_URL}" }}
""",
["cargo metadata"],
),
}
for version, (updated_dependencies, expected_commands) in cases.items():
with self.subTest(version=version), tempfile.TemporaryDirectory() as tmp:
workdir = Path(tmp)
(workdir / "Cargo.toml").write_text(CARGO_TOML)
command_log = workdir / "commands.log"
fake_bin = workdir / "bin"
fake_bin.mkdir()
self._write_fake_executables(fake_bin)
self._write_fake_python_dependencies(workdir)
env = os.environ.copy()
env["PATH"] = os.pathsep.join([str(fake_bin), env["PATH"]])
env["FAKE_COMMAND_LOG"] = str(command_log)
env["PYTHONPATH"] = os.pathsep.join(
filter(None, [str(workdir), env.get("PYTHONPATH")])
)
result = subprocess.run(
[sys.executable, str(SCRIPT), version],
cwd=workdir,
env=env,
capture_output=True,
text=True,
timeout=10,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(
(workdir / "Cargo.toml").read_text(),
"[workspace.dependencies]\n"
+ updated_dependencies
+ UNTOUCHED_DEPENDENCIES,
)
commands = command_log.read_text().splitlines()
for command in expected_commands:
self.assertTrue(
any(line.startswith(command) for line in commands),
f"{command!r} not found in {commands!r}",
)
def _write_fake_executables(self, fake_bin: Path) -> None:
cargo = fake_bin / "cargo"
cargo.write_text(
textwrap.dedent(
"""\
#!/bin/sh
printf 'cargo %s\\n' "$*" >> "$FAKE_COMMAND_LOG"
case "$1" in
info)
printf '%s\\n' 'version: 8.8.8 (latest 9.9.9)'
;;
metadata)
;;
*)
exit 2
;;
esac
"""
)
)
cargo.chmod(cargo.stat().st_mode | stat.S_IXUSR)
git = fake_bin / "git"
git.write_text(
textwrap.dedent(
"""\
#!/bin/sh
printf 'git %s\\n' "$*" >> "$FAKE_COMMAND_LOG"
if [ "$1" != "ls-remote" ]; then
exit 2
fi
printf '%s\\n' \\
'111111 refs/tags/v9.9.9' \\
'222222 refs/tags/v10.0.0-beta.1' \\
'333333 refs/tags/v10.0.0-beta.3'
"""
)
)
git.chmod(git.stat().st_mode | stat.S_IXUSR)
def _write_fake_python_dependencies(self, workdir: Path) -> None:
packaging = workdir / "packaging"
packaging.mkdir()
(packaging / "__init__.py").write_text("")
(packaging / "version.py").write_text(
textwrap.dedent(
"""\
class Version:
def __init__(self, value):
release, _, prerelease = value.partition("-beta.")
self._key = (
tuple(int(part) for part in release.split(".")),
not prerelease,
int(prerelease or 0),
)
def __lt__(self, other):
return self._key < other._key
"""
)
)
if __name__ == "__main__":
unittest.main()
+1 -1
View File
@@ -12,7 +12,7 @@ with open("Cargo.toml", "rb") as f:
elif isinstance(dep, dict):
# Version doesn't have the beta tag in it, so we instead look
# at the git tag.
version = dep.get('tag', dep.get('version'))
version = dep.get("tag", dep.get("version"))
else:
raise ValueError("Unexpected type for dependency: " + str(dep))
+18
View File
@@ -101,6 +101,19 @@ ignore = [
# https://rustsec.org/advisories/RUSTSEC-2026-0195
{ id = "RUSTSEC-2026-0194", reason = "transitive via inferno/lance/opendal; XML from trusted cloud endpoints, not attacker-controlled" },
{ id = "RUSTSEC-2026-0195", reason = "transitive via inferno/lance/opendal; XML from trusted cloud endpoints, not attacker-controlled" },
# smartstring: unmaintained — the repository was archived by its author on
# 2026-05-03. Not a vulnerability. Reached only transitively through polars
# (polars-core/-io/-ops/-time/-utils); nothing in LanceDB depends on it directly.
# The advisory states no safe upgrade is available: upstream recommends
# compact_str/smol_str, so clearing this requires polars to migrate.
# https://rustsec.org/advisories/RUSTSEC-2026-0249
{ id = "RUSTSEC-2026-0249", reason = "smartstring unmaintained via polars; no fixed upstream release" },
# h2 0.3: empty DATA frames can be queued without limit. The patched
# h2 0.4 line is locked to 0.4.16, but no patched 0.3 release exists.
# The old copy is pulled in by aws-smithy's legacy hyper 0.14 client.
# https://rustsec.org/advisories/RUSTSEC-2026-0258
{ id = "RUSTSEC-2026-0258", reason = "h2 0.3 via legacy aws-smithy/hyper 0.14; no patched 0.3 release" },
]
# ---------------------------------------------------------------------------
@@ -164,6 +177,11 @@ multiple-versions = "warn"
# Wildcard version requirements (`foo = "*"`) are a footgun — they let any
# future release in without review. Ban them outright.
wildcards = "deny"
# Lint every dependency declared by a workspace member against the shared
# `[workspace.dependencies]` table: any crate used by more than one member must
# go through `workspace = true`, and entries nothing uses are an error. This
# keeps versions from drifting between the core crate and the bindings.
workspace-dependencies = { duplicates = "deny", unused = "deny" }
# Internal workspace crates reference each other via `path = "..."`, which
# cargo-deny sees as a wildcard version. That's fine for private workspace
# members (not published to crates.io), so allow it specifically for paths.
+5
View File
@@ -51,6 +51,11 @@ plugins:
paths: [../python/python]
options:
docstring_style: numpy
docstring_options:
# Attributes documented in a `Parameters` section, and pydantic
# dataclasses whose `__init__` griffe cannot see statically, both
# trip this check. It reports nothing actionable here.
warn_unknown_params: false
heading_level: 3
show_signature_annotations: true
show_root_heading: true
+11 -1
View File
@@ -453,6 +453,16 @@ paths:
The metric type to use for the index. l2, Cosine, Dot are supported.
index_type:
type: string
custom_stop_words:
type: [array, "null"]
items:
type: string
description: |
The custom stop-word list for an FTS index. A non-null
array replaces the language's built-in stop-word list and is only
applied when remove_stop_words is enabled. Null uses the built-in
language list, while an empty array explicitly replaces it with no
stop words.
responses:
"200":
description: Index successfully created
@@ -510,4 +520,4 @@ paths:
"401":
$ref: "#/components/responses/unauthorized"
"404":
$ref: "#/components/responses/not_found"
$ref: "#/components/responses/not_found"
+2 -2
View File
@@ -5,5 +5,5 @@ mkdocs-autorefs>=0.5,<=1.0
mkdocstrings[python]>=0.24,<1.0
griffe>=0.40,<1.0
mkdocs-render-swagger-plugin>=0.1.0
pydantic>=2.0,<3.0
mkdocs-redirects>=1.2.0
pydantic>=2.7.4,<3
mkdocs-redirects>=1.2.0
+197 -30
View File
@@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`:
<dependency>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-core</artifactId>
<version>0.32.0-beta.2</version>
<version>0.38.0-beta.10</version>
</dependency>
```
@@ -55,6 +55,38 @@ LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
| `region(String)` | AWS region (default: "us-east-1") | No |
| `config(String, String)` | Additional configuration parameters | No |
### Opening a Table with Vended Credentials
When the catalog vends temporary object store credentials, open the table through the
namespace client. The Lance dataset builder fetches the table location and storage options
from the catalog and refreshes the credentials when they expire.
```java
import com.lancedb.LanceDbNamespaceClientBuilder;
import org.lance.Dataset;
import org.lance.namespace.LanceNamespace;
import java.util.Arrays;
LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
.apiKey(System.getenv("LANCEDB_API_KEY"))
.database(System.getenv("LANCEDB_DATABASE"))
// Set the endpoint for a LanceDB Enterprise deployment.
// .endpoint("https://your-enterprise-endpoint")
.build();
try (Dataset dataset = Dataset.open()
.namespaceClient(namespaceClient)
.tableId(Arrays.asList("my_namespace", "my_table"))
.build()) {
System.out.println("Rows: " + dataset.countRows());
}
```
Do not call `describeTable()` and then open the returned location with `Dataset.open(uri)`.
Opening through `namespaceClient()` is what applies the vended storage options and enables
automatic credential refresh. No object store credentials need to be passed by the application.
## Metadata Operations
### Creating a Namespace Path
@@ -249,6 +281,57 @@ try (BufferAllocator allocator = new RootAllocator();
}
```
### Creating an Empty Table
To create an empty table, send an Arrow IPC stream that contains the table schema and no record batches.
The schema in the IPC stream becomes the table schema, and rows can be inserted later.
```java
import org.lance.namespace.model.CreateTableRequest;
import org.lance.namespace.model.CreateTableResponse;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.ipc.ArrowStreamWriter;
import org.apache.arrow.vector.types.FloatingPointPrecision;
import org.apache.arrow.vector.types.pojo.ArrowType;
import org.apache.arrow.vector.types.pojo.Field;
import org.apache.arrow.vector.types.pojo.FieldType;
import org.apache.arrow.vector.types.pojo.Schema;
import java.io.ByteArrayOutputStream;
import java.nio.channels.Channels;
import java.util.Arrays;
Schema schema = new Schema(Arrays.asList(
new Field("id", FieldType.nullable(new ArrowType.Int(32, true)), null),
new Field("name", FieldType.nullable(new ArrowType.Utf8()), null),
new Field("embedding",
FieldType.nullable(new ArrowType.FixedSizeList(128)),
Arrays.asList(new Field("item",
FieldType.nullable(new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE)),
null)))
));
byte[] emptyTableData;
try (BufferAllocator allocator = new RootAllocator();
VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
root.setRowCount(0);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ArrowStreamWriter writer = new ArrowStreamWriter(root, null, Channels.newChannel(out))) {
writer.start();
writer.end();
}
emptyTableData = out.toByteArray();
}
CreateTableRequest request = new CreateTableRequest();
request.setId(Arrays.asList("my_namespace", "empty_table"));
CreateTableResponse response = namespaceClient.createTable(request, emptyTableData);
```
### Insert
```java
@@ -431,9 +514,88 @@ query.setVector(vector);
byte[] result = namespaceClient.queryTable(query);
```
### Reading Query Results
## Indexing
Query results are returned in Apache Arrow IPC file format. Here's how to read them:
The Java SDK exposes the REST namespace index operations through the same `LanceNamespace` client.
Index creation runs asynchronously, so use `listTableIndices` or `describeTableIndexStats` to check progress.
### Creating a Vector Index
```java
import org.lance.namespace.model.CreateTableIndexRequest;
import org.lance.namespace.model.CreateTableIndexResponse;
CreateTableIndexRequest request = new CreateTableIndexRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
request.setColumn("embedding");
request.setIndexType("IVF_PQ");
request.setDistanceType("cosine");
request.setName("embedding_idx");
CreateTableIndexResponse response = namespaceClient.createTableIndex(request);
System.out.println("Index transaction: " + response.getTransactionId());
```
### Creating a Scalar Index
```java
import org.lance.namespace.model.CreateTableIndexRequest;
import org.lance.namespace.model.CreateTableScalarIndexResponse;
CreateTableIndexRequest request = new CreateTableIndexRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
request.setColumn("category");
request.setIndexType("BTREE");
request.setName("category_idx");
CreateTableScalarIndexResponse response = namespaceClient.createTableScalarIndex(request);
System.out.println("Index transaction: " + response.getTransactionId());
```
### Creating a Full Text Search Index
```java
import org.lance.namespace.model.CreateTableIndexRequest;
import org.lance.namespace.model.CreateTableScalarIndexResponse;
CreateTableIndexRequest request = new CreateTableIndexRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
request.setColumn("text_column");
request.setIndexType("FTS");
request.setName("text_idx");
request.setBaseTokenizer("simple");
request.setLowerCase(true);
request.setWithPosition(true);
CreateTableScalarIndexResponse response = namespaceClient.createTableScalarIndex(request);
System.out.println("Index transaction: " + response.getTransactionId());
```
### Listing Indexes
```java
import org.lance.namespace.model.IndexContent;
import org.lance.namespace.model.ListTableIndicesRequest;
import org.lance.namespace.model.ListTableIndicesResponse;
ListTableIndicesRequest request = new ListTableIndicesRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
ListTableIndicesResponse response = namespaceClient.listTableIndices(request);
for (IndexContent index : response.getIndexes()) {
System.out.println(index.getIndexName() + ": " + index.getStatus());
}
```
!!! note
The current Java namespace API exposes index type, index name, distance type, and full text search tokenizer options.
IVF training parameters such as `num_partitions` are not exposed by `CreateTableIndexRequest` yet.
To make those configurable from Java, the namespace API must add those fields first.
## Reading Query Results
Query results are returned as bytes in Apache Arrow IPC file format. Put the byte-channel
adapter behind a small helper so query code can work with `ArrowFileReader` directly:
```java
import org.apache.arrow.vector.ipc.ArrowFileReader;
@@ -441,45 +603,50 @@ import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
// Helper class to read Arrow data from byte array
class ByteArraySeekableByteChannel implements SeekableByteChannel {
private final byte[] data;
private long position = 0;
private boolean isOpen = true;
public ByteArraySeekableByteChannel(byte[] data) {
this.data = data;
final class ArrowIpc {
static ArrowFileReader openFileReader(byte[] data, BufferAllocator allocator) throws IOException {
return new ArrowFileReader(new ByteArraySeekableByteChannel(data), allocator);
}
@Override
public int read(ByteBuffer dst) {
int remaining = dst.remaining();
int available = (int) (data.length - position);
if (available <= 0) return -1;
int toRead = Math.min(remaining, available);
dst.put(data, (int) position, toRead);
position += toRead;
return toRead;
}
private static final class ByteArraySeekableByteChannel implements SeekableByteChannel {
private final byte[] data;
private long position = 0;
private boolean isOpen = true;
@Override public long position() { return position; }
@Override public SeekableByteChannel position(long newPosition) { position = newPosition; return this; }
@Override public long size() { return data.length; }
@Override public boolean isOpen() { return isOpen; }
@Override public void close() { isOpen = false; }
@Override public int write(ByteBuffer src) { throw new UnsupportedOperationException(); }
@Override public SeekableByteChannel truncate(long size) { throw new UnsupportedOperationException(); }
private ByteArraySeekableByteChannel(byte[] data) {
this.data = data;
}
@Override
public int read(ByteBuffer dst) {
int remaining = dst.remaining();
int available = (int) (data.length - position);
if (available <= 0) return -1;
int toRead = Math.min(remaining, available);
dst.put(data, (int) position, toRead);
position += toRead;
return toRead;
}
@Override public long position() { return position; }
@Override public SeekableByteChannel position(long newPosition) { position = newPosition; return this; }
@Override public long size() { return data.length; }
@Override public boolean isOpen() { return isOpen; }
@Override public void close() { isOpen = false; }
@Override public int write(ByteBuffer src) { throw new UnsupportedOperationException(); }
@Override public SeekableByteChannel truncate(long size) { throw new UnsupportedOperationException(); }
}
}
// Read query results
byte[] queryResult = namespaceClient.queryTable(query);
try (BufferAllocator allocator = new RootAllocator();
ArrowFileReader reader = new ArrowFileReader(
new ByteArraySeekableByteChannel(queryResult), allocator)) {
ArrowFileReader reader = ArrowIpc.openFileReader(queryResult, allocator)) {
for (int i = 0; i < reader.getRecordBlocks().size(); i++) {
reader.loadRecordBatch(reader.getRecordBlocks().get(i));
+1 -1
View File
@@ -1,7 +1,7 @@
# Contributing to LanceDB Typescript
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
+518
View File
@@ -0,0 +1,518 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / AutoQuery
# Class: AutoQuery
A builder for automatic string searches.
Automatic search determines whether to use full-text or vector search from
the table revision selected for each execution. This builder exposes the
common operations supported by both query families.
## Extends
- `StandardQueryBase`&lt;`NativeQuery` \| `NativeVectorQuery`&gt;
## Properties
### inner
```ts
protected inner: Query | VectorQuery | Promise<Query | VectorQuery>;
```
#### Inherited from
`StandardQueryBase.inner`
## Methods
### analyzePlan()
```ts
analyzePlan(distributedMetrics?): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
This is useful for debugging and performance analysis, as it shows how the query was executed
and includes metrics such as elapsed time, rows processed, and I/O statistics.
#### Parameters
* **distributedMetrics?**: [`AnalyzePlanDistributedMetrics`](../type-aliases/AnalyzePlanDistributedMetrics.md)
How distributed worker metrics are displayed for remote query plans.
Defaults to `"aggregate"`.
#### Returns
`Promise`&lt;`string`&gt;
A query execution plan with runtime metrics for each step.
#### Example
```ts
import * as lancedb from "@lancedb/lancedb"
const db = await lancedb.connect("./.lancedb");
const table = await db.createTable("my_table", [
{ vector: [1.1, 0.9], id: "1" },
]);
const plan = await table.query().nearestTo([0.5, 0.2]).analyzePlan();
Example output (with runtime metrics inlined):
AnalyzeExec verbose=true, metrics=[]
ProjectionExec: expr=[id@3 as id, vector@0 as vector, _distance@2 as _distance], metrics=[output_rows=1, elapsed_compute=3.292µs]
Take: columns="vector, _rowid, _distance, (id)", metrics=[output_rows=1, elapsed_compute=66.001µs, batches_processed=1, bytes_read=8, iops=1, requests=1]
CoalesceBatchesExec: target_batch_size=1024, metrics=[output_rows=1, elapsed_compute=3.333µs]
GlobalLimitExec: skip=0, fetch=10, metrics=[output_rows=1, elapsed_compute=167ns]
FilterExec: _distance@2 IS NOT NULL, metrics=[output_rows=1, elapsed_compute=8.542µs]
SortExec: TopK(fetch=10), expr=[_distance@2 ASC NULLS LAST], metrics=[output_rows=1, elapsed_compute=63.25µs, row_replacements=1]
KNNVectorDistance: metric=l2, metrics=[output_rows=1, elapsed_compute=114.333µs, output_batches=1]
LanceScan: uri=/path/to/data, projection=[vector], row_id=true, row_addr=false, ordered=false, metrics=[output_rows=1, elapsed_compute=103.626µs, bytes_read=549, iops=2, requests=2]
```
#### Inherited from
`StandardQueryBase.analyzePlan`
***
### execute()
```ts
protected execute(options?): AsyncGenerator<RecordBatch<any>, void, unknown>
```
Execute the query and return the results as an
#### Parameters
* **options?**: `Partial`&lt;[`QueryExecutionOptions`](../interfaces/QueryExecutionOptions.md)&gt;
#### Returns
`AsyncGenerator`&lt;`RecordBatch`&lt;`any`&gt;, `void`, `unknown`&gt;
#### See
- AsyncIterator
of
- RecordBatch.
By default, LanceDb will use many threads to calculate results and, when
the result set is large, multiple batches will be processed at one time.
This readahead is limited however and backpressure will be applied if this
stream is consumed slowly (this constrains the maximum memory used by a
single query)
#### Inherited from
`StandardQueryBase.execute`
***
### explainPlan()
```ts
explainPlan(verbose): Promise<string>
```
Generates an explanation of the query execution plan.
#### Parameters
* **verbose**: `boolean` = `false`
If true, provides a more detailed explanation. Defaults to false.
#### Returns
`Promise`&lt;`string`&gt;
A Promise that resolves to a string containing the query execution plan explanation.
#### Example
```ts
import * as lancedb from "@lancedb/lancedb"
const db = await lancedb.connect("./.lancedb");
const table = await db.createTable("my_table", [
{ vector: [1.1, 0.9], id: "1" },
]);
const plan = await table.query().nearestTo([0.5, 0.2]).explainPlan();
```
#### Inherited from
`StandardQueryBase.explainPlan`
***
### fastSearch()
```ts
fastSearch(): this
```
Skip searching un-indexed data. This can make search faster, but will miss
any data that is not yet indexed.
Use [Table#optimize](Table.md#optimize) to index all un-indexed data.
#### Returns
`this`
#### Inherited from
`StandardQueryBase.fastSearch`
***
### ~~filter()~~
```ts
filter(predicate): this
```
A filter statement to be applied to this query.
#### Parameters
* **predicate**: `string`
#### Returns
`this`
#### See
where
#### Deprecated
Use `where` instead
#### Inherited from
`StandardQueryBase.filter`
***
### fullTextSearch()
```ts
fullTextSearch(query, options?): this
```
#### Parameters
* **query**: `string` \| [`FullTextQuery`](../interfaces/FullTextQuery.md)
* **options?**: `Partial`&lt;[`FullTextSearchOptions`](../interfaces/FullTextSearchOptions.md)&gt;
#### Returns
`this`
#### Inherited from
`StandardQueryBase.fullTextSearch`
***
### limit()
```ts
limit(limit): this
```
Set the maximum number of results to return.
By default, a plain search has no limit. If this method is not
called then every valid row from the table will be returned.
#### Parameters
* **limit**: `number`
#### Returns
`this`
#### Inherited from
`StandardQueryBase.limit`
***
### offset()
```ts
offset(offset): this
```
Set the number of rows to skip before returning results.
This is useful for pagination.
#### Parameters
* **offset**: `number`
#### Returns
`this`
#### Inherited from
`StandardQueryBase.offset`
***
### orderBy()
```ts
orderBy(ordering): this
```
Sort the results by the specified column(s).
#### Parameters
* **ordering**: [`ColumnOrdering`](../interfaces/ColumnOrdering.md) \| [`ColumnOrdering`](../interfaces/ColumnOrdering.md)[]
#### Returns
`this`
This query builder.
#### Inherited from
`StandardQueryBase.orderBy`
***
### outputSchema()
```ts
outputSchema(): Promise<Schema<any>>
```
Returns the schema of the output that will be returned by this query.
This can be used to inspect the types and names of the columns that will be
returned by the query before executing it.
#### Returns
`Promise`&lt;`Schema`&lt;`any`&gt;&gt;
An Arrow Schema describing the output columns.
#### Inherited from
`StandardQueryBase.outputSchema`
***
### select()
```ts
select(columns): this
```
Return only the specified columns.
By default a query will return all columns from the table. However, this can have
a very significant impact on latency. LanceDb stores data in a columnar fashion. This
means we can finely tune our I/O to select exactly the columns we need.
As a best practice you should always limit queries to the columns that you need. If you
pass in an array of column names then only those columns will be returned.
You can also use this method to create new "dynamic" columns based on your existing columns.
For example, you may not care about "a" or "b" but instead simply want "a + b". This is often
seen in the SELECT clause of an SQL query (e.g. `SELECT a+b FROM my_table`).
To create dynamic columns you can pass in a Map<string, string>. A column will be returned
for each entry in the map. The key provides the name of the column. The value is
an SQL string used to specify how the column is calculated.
For example, an SQL query might state `SELECT a + b AS combined, c`. The equivalent
input to this method would be:
#### Parameters
* **columns**: `string` \| `string`[] \| `Record`&lt;`string`, `string`&gt; \| `Map`&lt;`string`, `string`&gt;
#### Returns
`this`
#### Example
```ts
new Map([["combined", "a + b"], ["c", "c"]])
Columns will always be returned in the order given, even if that order is different than
the order used when adding the data.
Note that you can pass in a `Record<string, string>` (e.g. an object literal). This method
uses `Object.entries` which should preserve the insertion order of the object. However,
object insertion order is easy to get wrong and `Map` is more foolproof.
```
#### Inherited from
`StandardQueryBase.select`
***
### toArray()
```ts
toArray(options?): Promise<any[]>
```
Collect the results as an array of objects.
#### Parameters
* **options?**: `Partial`&lt;[`QueryExecutionOptions`](../interfaces/QueryExecutionOptions.md)&gt;
#### Returns
`Promise`&lt;`any`[]&gt;
#### Inherited from
`StandardQueryBase.toArray`
***
### toArrow()
```ts
toArrow(options?): Promise<Table<any>>
```
Collect the results as an Arrow
#### Parameters
* **options?**: `Partial`&lt;[`QueryExecutionOptions`](../interfaces/QueryExecutionOptions.md)&gt;
#### Returns
`Promise`&lt;`Table`&lt;`any`&gt;&gt;
#### See
ArrowTable.
#### Inherited from
`StandardQueryBase.toArrow`
***
### useLsm()
```ts
useLsm(enable): this
```
Control MemWAL read routing for this query.
By default (unset), when the table carries a MemWAL write spec (see
[Table#setLsmWriteSpec](Table.md#setlsmwritespec)), reads are routed through the LSM scanner so
they also return data written via the `mergeInsert` LSM path that has not yet
been compacted into the base table (the active/frozen in-memory memtables and
the flushed generations), deduplicated by primary key; a table without a spec
reads the base table.
#### Parameters
* **enable**: `boolean`
`true` forces the LSM scanner and errors if the table has no
MemWAL write spec. `false` bypasses the MemWAL and reads the base table only,
even when a spec is present.
Note: the LSM scanner does not support every query shape (e.g. reranking,
hybrid search, `orderBy`). On a MemWAL table those shapes error unless
`useLsm(false)` is set, because a base-only read would silently exclude
un-compacted MemWAL data.
#### Returns
`this`
#### Inherited from
`StandardQueryBase.useLsm`
***
### where()
```ts
where(predicate): this
```
A filter statement to be applied to this query.
The filter should be supplied as an SQL query string. For example:
#### Parameters
* **predicate**: `string`
#### Returns
`this`
#### Example
```ts
x > 10
y > 0 AND y < 100
x > 5 OR y = 'test'
Filtering performance can often be improved by creating a scalar index
on the filter column(s).
Calling this multiple times combines the filters with a logical AND rather
than replacing the previous filter.
```
#### Inherited from
`StandardQueryBase.where`
***
### withRowId()
```ts
withRowId(): this
```
Whether to return the row id in the results.
This column can be used to match results between different queries. For
example, to match results from a full text search and a vector search in
order to perform hybrid search.
#### Returns
`this`
#### Inherited from
`StandardQueryBase.withRowId`
+43
View File
@@ -37,6 +37,31 @@ latest and stays writable.
***
### cherryPick()
```ts
cherryPick(fromBranch, dryRun): Promise<CherryPickResult>
```
Cherry-pick a branch onto main.
Set `dryRun` to `true` to preview. A failed cherry-pick resolves
with `status: "failed"` instead of throwing.
#### Parameters
* **fromBranch**: `string`
Branch to cherry-pick from.
* **dryRun**: `boolean` = `false`
When true, only preview. Defaults to false.
#### Returns
`Promise`&lt;[`CherryPickResult`](../interfaces/CherryPickResult.md)&gt;
***
### create()
```ts
@@ -83,6 +108,24 @@ Delete a branch.
***
### diff()
```ts
diff(fromBranch): Promise<BranchDiff>
```
Compare a branch against main without modifying either branch.
#### Parameters
* **fromBranch**: `string`
#### Returns
`Promise`&lt;[`BranchDiff`](../interfaces/BranchDiff.md)&gt;
***
### list()
```ts
+268 -6
View File
@@ -25,6 +25,27 @@ the underlying connection has been closed.
## Methods
### cancelJob()
```ts
abstract cancelJob(jobId): Promise<boolean>
```
Request cancellation of a server-side job by id.
Resolves to true if the server accepted the cancellation, false if no
such job exists. Cancelling an already-terminal job is a no-op success.
#### Parameters
* **jobId**: `string`
#### Returns
`Promise`&lt;`boolean`&gt;
***
### cloneTable()
```ts
@@ -148,6 +169,45 @@ Creates a new empty Table
***
### createMaterializedView()
```ts
abstract createMaterializedView(
name,
source,
options?): Promise<MaterializedView>
```
Define a materialized view named `name` over the table `source`.
The view is created empty, with the query recorded in its schema
metadata; `view.refresh()` computes the rows. The view is a normal
table: it can be queried, indexed and searched, and it appears in
`tableNames`. The source table must have stable row ids (create it with
the `newTableEnableStableRowIds` storage option); they keep the view's
provenance valid across source compactions and cannot be enabled after
a table exists. Local databases only.
#### Parameters
* **name**: `string`
* **source**: `string`
* **options?**
* **options.limit?**: `number`
* **options.select?**: [`MaterializedViewSelect`](../type-aliases/MaterializedViewSelect.md)
* **options.where?**: `string`
#### Returns
`Promise`&lt;[`MaterializedView`](MaterializedView.md)&gt;
***
### createNamespace()
```ts
@@ -365,6 +425,49 @@ Drop an existing table.
***
### dropTableAsync()
```ts
abstract dropTableAsync(name, namespacePath?): Promise<Job>
```
Start dropping a table and return its cleanup job.
The table may become unavailable before its data files are removed. Wait
on the returned job to know when cleanup has finished.
#### Parameters
* **name**: `string`
* **namespacePath?**: `string`[]
#### Returns
`Promise`&lt;[`Job`](Job.md)&gt;
***
### getJob()
```ts
abstract getJob(jobId): Promise<null | JobDescription>
```
Describe a single server-side job by id.
Resolves to `null` when the server has no such job.
#### Parameters
* **jobId**: `string`
#### Returns
`Promise`&lt;`null` \| [`JobDescription`](../interfaces/JobDescription.md)&gt;
***
### isOpen()
```ts
@@ -379,6 +482,78 @@ Return true if the connection has not been closed
***
### job()
```ts
abstract job(jobId): Job
```
A [Job](Job.md) handle for a server-side job by id.
The handle is constructed without a server round trip; an unknown id
surfaces when the handle is used. Dropping the handle has no effect on
the job itself.
#### Parameters
* **jobId**: `string`
#### Returns
[`Job`](Job.md)
***
### jobHistory()
```ts
abstract jobHistory(jobId?): Promise<Table<any>>
```
The lifecycle event history of a server-side job, as an Arrow table.
Lists history across all jobs when `jobId` is omitted.
#### Parameters
* **jobId?**: `string`
#### Returns
`Promise`&lt;`Table`&lt;`any`&gt;&gt;
***
### listJobs()
```ts
abstract listJobs(): Promise<JobInfo[]>
```
List server-side jobs across the database's tables.
#### Returns
`Promise`&lt;[`JobInfo`](../interfaces/JobInfo.md)[]&gt;
***
### listMaterializedViews()
```ts
abstract listMaterializedViews(): Promise<string[]>
```
The names of the materialized views in this database.
Found by reading every table's schema, so this costs an open per table.
#### Returns
`Promise`&lt;`string`[]&gt;
***
### listNamespaces()
```ts
@@ -409,6 +584,90 @@ Child namespace names and
***
### listTables()
#### listTables(options)
```ts
abstract listTables(options?): Promise<ListTablesResponse>
```
List a page of the tables in this database.
To retrieve the tables after the page, pass the `pageToken` the response
carries back in. A page can be shorter than `limit` without being the last
one, so walk until a response carries no page token:
```ts
const names = [];
let pageToken = undefined;
do {
const page = await conn.listTables({ pageToken, limit: 100 });
names.push(...page.tables);
pageToken = page.pageToken;
} while (pageToken);
```
##### Parameters
* **options?**: `Partial`&lt;[`ListTablesOptions`](../interfaces/ListTablesOptions.md)&gt;
Pagination options
(`pageToken`, `limit`).
##### Returns
`Promise`&lt;[`ListTablesResponse`](../interfaces/ListTablesResponse.md)&gt;
A page of table names and an
optional token for the tables after it.
#### listTables(namespacePath, options)
```ts
abstract listTables(namespacePath?, options?): Promise<ListTablesResponse>
```
List a page of the tables in this database.
##### Parameters
* **namespacePath?**: `string`[]
The namespace path to list tables from
(defaults to root namespace)
* **options?**: `Partial`&lt;[`ListTablesOptions`](../interfaces/ListTablesOptions.md)&gt;
Pagination options
(`pageToken`, `limit`).
##### Returns
`Promise`&lt;[`ListTablesResponse`](../interfaces/ListTablesResponse.md)&gt;
A page of table names and an
optional token for the tables after it.
***
### openMaterializedView()
```ts
abstract openMaterializedView(name): Promise<MaterializedView>
```
Open the materialized view named `name`.
Rejects a table that exists but is not a materialized view.
#### Parameters
* **name**: `string`
#### Returns
`Promise`&lt;[`MaterializedView`](MaterializedView.md)&gt;
***
### openTable()
```ts
@@ -418,18 +677,13 @@ abstract openTable(
options?): Promise<Table>
```
Open a table in the database.
#### Parameters
* **name**: `string`
The name of the table
* **namespacePath?**: `string`[]
The namespace path of the table (defaults to root namespace)
* **options?**: `Partial`&lt;[`OpenTableOptions`](../interfaces/OpenTableOptions.md)&gt;
Additional options
#### Returns
@@ -470,7 +724,7 @@ a "not supported" error.
***
### tableNames()
### ~~tableNames()~~
#### tableNames(options)
@@ -492,6 +746,10 @@ Tables will be returned in lexicographical order.
`Promise`&lt;`string`[]&gt;
##### Deprecated
Use [Connection.listTables](Connection.md#listtables) instead.
#### tableNames(namespacePath, options)
```ts
@@ -514,3 +772,7 @@ Tables will be returned in lexicographical order.
##### Returns
`Promise`&lt;`string`[]&gt;
##### Deprecated
Use [Connection.listTables](Connection.md#listtables) instead.
+83
View File
@@ -0,0 +1,83 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / Job
# Class: Job
A handle to an operation that may still be running.
## Constructors
### new Job()
```ts
new Job(): Job
```
#### Returns
[`Job`](Job.md)
## Accessors
### id
```ts
get id(): null | string
```
Identifies the operation on the server that is running it. Operations
that run in this process have no server id. The value is opaque.
#### Returns
`null` \| `string`
## Methods
### cancel()
```ts
cancel(): Promise<void>
```
Request cancellation. Cancelling a finished operation is a no-op.
#### Returns
`Promise`&lt;`void`&gt;
***
### status()
```ts
status(): Promise<string>
```
The operation's current lifecycle state: "running", "finished",
"failed", or "cancelled".
A point snapshot; unlike [Job.wait](Job.md#wait) it does not block or reject
on a terminal failure state. States a newer server reports that this
client version does not know pass through as-is.
#### Returns
`Promise`&lt;`string`&gt;
***
### wait()
```ts
wait(): Promise<void>
```
Wait until the operation reaches a terminal state.
#### Returns
`Promise`&lt;`void`&gt;
+101
View File
@@ -0,0 +1,101 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MaterializedView
# Class: MaterializedView
A handle on a materialized view: its table plus its definition.
Obtained from [Connection#createMaterializedView](Connection.md#creatematerializedview) or
[Connection#openMaterializedView](Connection.md#openmaterializedview). The view is a normal table --
queries, indexes and search all apply through [MaterializedView#table](MaterializedView.md#table)
-- whose contents are maintained by [MaterializedView#refresh](MaterializedView.md#refresh).
## Constructors
### new MaterializedView()
```ts
new MaterializedView(table): MaterializedView
```
#### Parameters
* **table**: [`Table`](Table.md)
#### Returns
[`MaterializedView`](MaterializedView.md)
## Accessors
### name
```ts
get name(): string
```
#### Returns
`string`
## Methods
### definition()
```ts
definition(): Promise<MaterializedViewDefinition>
```
The query that defines the view, read from its stored schema.
#### Returns
`Promise`&lt;[`MaterializedViewDefinition`](../interfaces/MaterializedViewDefinition.md)&gt;
***
### refresh()
```ts
refresh(options?): Promise<RefreshMaterializedViewResult>
```
Recompute the view from its source.
The refresh is incremental when the source's changes can be reconciled
into the view -- rows added, changed or removed since the last one --
and otherwise rebuilds. `full` forces a rebuild; `sourceVersion`
refreshes to that source version instead of the latest.
Concurrent refreshes of one view do not duplicate its rows. Two that
plan the same source rows conflict on commit, and the loser throws
rather than writing them a second time.
#### Parameters
* **options?**
* **options.full?**: `boolean`
* **options.sourceVersion?**: `number`
#### Returns
`Promise`&lt;[`RefreshMaterializedViewResult`](../interfaces/RefreshMaterializedViewResult.md)&gt;
***
### table()
```ts
table(): Table
```
The view, as the table it is.
#### Returns
[`Table`](Table.md)
+8 -9
View File
@@ -76,24 +76,23 @@ the query optimizer chooses a suboptimal path.
***
### useLsmWrite()
### useLsm()
```ts
useLsmWrite(useLsmWrite): MergeInsertBuilder
useLsm(enable): MergeInsertBuilder
```
Controls whether the merge uses the MemWAL LSM write path.
Control MemWAL routing for this merge.
By default (unset), a `mergeInsert` on a table with an LSM write spec is
routed through Lance's MemWAL shard writer, and a table without one uses
the standard path. Pass `false` to force the standard path even when a
spec is set. Pass `true` to require a spec — `mergeInsert` rejects if none
is installed.
routed through Lance's MemWAL shard writer, and a table without one uses the
standard path.
#### Parameters
* **useLsmWrite**: `boolean`
Whether to use the LSM write path.
* **enable**: `boolean`
`true` forces MemWAL routing and errors if the table has no
LSM write spec. `false` forces the standard write path even when a spec is set.
#### Returns
+36
View File
@@ -497,6 +497,42 @@ ArrowTable.
***
### useLsm()
```ts
useLsm(enable): this
```
Control MemWAL read routing for this query.
By default (unset), when the table carries a MemWAL write spec (see
[Table#setLsmWriteSpec](Table.md#setlsmwritespec)), reads are routed through the LSM scanner so
they also return data written via the `mergeInsert` LSM path that has not yet
been compacted into the base table (the active/frozen in-memory memtables and
the flushed generations), deduplicated by primary key; a table without a spec
reads the base table.
#### Parameters
* **enable**: `boolean`
`true` forces the LSM scanner and errors if the table has no
MemWAL write spec. `false` bypasses the MemWAL and reads the base table only,
even when a spec is present.
Note: the LSM scanner does not support every query shape (e.g. reranking,
hybrid search, `orderBy`). On a MemWAL table those shapes error unless
`useLsm(false)` is set, because a base-only read would silently exclude
un-compacted MemWAL data.
#### Returns
`this`
#### Inherited from
`StandardQueryBase.useLsm`
***
### where()
```ts
+216 -6
View File
@@ -69,14 +69,34 @@ abstract addColumns(newColumnTransforms): Promise<AddColumnsResult>
Add new columns with defined values.
The `{ computed }` form stores the expression rather than evaluating it
now: the column is committed with no values, and rows get them from
[Table#refreshColumn](Table.md#refreshcolumn). Declaring one therefore costs the same on a
large table as on an empty one.
A refresh does not revisit rows it has already filled, so mutating an
input leaves the value computed at fill time; recomputing means dropping
the column and declaring it again. While a declaration reads a column,
that column cannot be renamed, retyped or dropped.
On LanceDB Cloud and Enterprise the expression is planned by the
server, and the refresh runs as a server job -- see
[Table#refreshColumnAsync](Table.md#refreshcolumnasync).
#### Parameters
* **newColumnTransforms**: `Field`&lt;`any`&gt; \| `Field`&lt;`any`&gt;[] \| `Schema`&lt;`any`&gt; \| [`AddColumnsSql`](../interfaces/AddColumnsSql.md)[]
* **newColumnTransforms**:
\| `Field`&lt;`any`&gt;
\| `Field`&lt;`any`&gt;[]
\| `Schema`&lt;`any`&gt;
\| [`AddColumnsSql`](../interfaces/AddColumnsSql.md)[]
\| `object`
Either:
- An array of objects with column names and SQL expressions to calculate values
- A single Arrow Field defining one column with its data type (column will be initialized with null values)
- An array of Arrow Fields defining columns with their data types (columns will be initialized with null values)
- An Arrow Schema defining columns with their data types (columns will be initialized with null values)
- `{ computed }`, declaring columns defined by a SQL expression whose type and inputs are derived from it
#### Returns
@@ -85,6 +105,13 @@ Add new columns with defined values.
A promise that resolves to an object
containing the new version number of the table after adding the columns.
#### Example
```ts
await table.addColumns({ computed: [{ name: "doubled", valueSql: "x * 2" }] });
const { rowsFilled } = await table.refreshColumn("doubled");
```
***
### alterColumns()
@@ -186,6 +213,39 @@ version of the table.
***
### checkpointLsm()
```ts
abstract checkpointLsm(): Promise<void>
```
Converge this table's LSM write path into its base table.
Seals once, then triggers compaction and polls until the L0 that existed
at the start is gone. The target set is fixed at the start, so
generations created *during* the checkpoint are ignored — that is what
lets it terminate under write load, and what makes it best-effort: it
converges the fresh tier as of some instant. Idempotent, abandonable at
any point, and safe to run on a cadence.
There is no liveness bound — the compactor pool is shared across tables,
so a checkpoint queued behind unrelated work looks exactly like one that
is merging. The caller owns the deadline.
#### Returns
`Promise`&lt;`void`&gt;
#### Example
```ts
const before = await table.getLsmStats();
await table.checkpointLsm();
const after = await table.getLsmStats();
```
***
### close()
```ts
@@ -223,6 +283,24 @@ It is a no-op when no writers are cached.
***
### compactLsm()
```ts
abstract compactLsm(): Promise<void>
```
Trigger a background L0 → base compaction pass per bucket.
Returns once the passes are *dispatched*, not once they finish — watch
[Table#getLsmStats](Table.md#getlsmstats) for progress, or use
[Table#checkpointLsm](Table.md#checkpointlsm) to wait for convergence.
#### Returns
`Promise`&lt;`void`&gt;
***
### countRows()
```ts
@@ -295,6 +373,29 @@ await table.createIndex("my_float_col");
***
### createIndexAsync()
```ts
abstract createIndexAsync(column, options?): Promise<Job>
```
Create an index, returning a handle to the indexing job.
The job may already be complete when returned; callers must not assume
the index exists until [Job.wait](Job.md#wait) resolves.
#### Parameters
* **column**: `string`
* **options?**: `Partial`&lt;[`IndexOptions`](../interfaces/IndexOptions.md)&gt;
#### Returns
`Promise`&lt;[`Job`](Job.md)&gt;
***
### currentBranch()
```ts
@@ -398,6 +499,48 @@ Drop an index from the table.
***
### flushLsm()
```ts
abstract flushLsm(): Promise<void>
```
Seal every bucket's active memtable into a new L0 generation.
Returns once the seal is committed. Sealing an empty memtable is a no-op,
so this is safe to call repeatedly.
#### Returns
`Promise`&lt;`void`&gt;
***
### getLsmStats()
```ts
abstract getLsmStats(includeGenerationRows?): Promise<undefined | LsmStats>
```
Read live per-bucket LSM state.
Answers "how far behind is my fresh tier", "which bucket is hot", and
"why is my fresh-tier vector search brute-force". Mutates no table state.
Resolves to `undefined` only when the LSM write path is not enabled.
#### Parameters
* **includeGenerationRows?**: `boolean`
Also count rows per L0 generation.
Off by default because each count opens an uncached Lance dataset.
#### Returns
`Promise`&lt;`undefined` \| [`LsmStats`](../interfaces/LsmStats.md)&gt;
***
### getLsmWriteSpec()
```ts
@@ -408,9 +551,10 @@ Read the [LsmWriteSpec](../interfaces/LsmWriteSpec.md) currently installed on th
Resolves to `undefined` when the MemWAL LSM write path is not enabled (no
spec has been set, or it was removed with [Table#unsetLsmWriteSpec](Table.md#unsetlsmwritespec)).
The returned spec — including its `maintainedIndexes` and
`writerConfigDefaults` — mirrors what was passed to
[Table#setLsmWriteSpec](Table.md#setlsmwritespec).
The returned spec mirrors what was passed to
[Table#setLsmWriteSpec](Table.md#setlsmwritespec), except that `maintainedIndexes` always
reports the concrete list resolved when the spec was set — `undefined`
never round-trips.
#### Returns
@@ -694,6 +838,67 @@ for await (const batch of table.query()) {
***
### refreshColumn()
```ts
abstract refreshColumn(column): Promise<RefreshColumnResult>
```
Fill the rows of a computed column that hold no value yet.
Rows appended since the last refresh are filled by the next one; rows
already filled are left as they are, so the call is idempotent and does
not observe a mutated input. Local tables only: a remote refresh runs
as a server job, through [Table#refreshColumnAsync](Table.md#refreshcolumnasync).
#### Parameters
* **column**: `string`
The name of the computed column to fill.
#### Returns
`Promise`&lt;[`RefreshColumnResult`](../interfaces/RefreshColumnResult.md)&gt;
A promise that resolves to the
number of rows filled and the new version number of the table.
***
### refreshColumnAsync()
```ts
abstract refreshColumnAsync(column): Promise<Job>
```
Like [Table#refreshColumn](Table.md#refreshcolumn), but returns a handle to the refresh
job instead of blocking until it completes.
The job may already be complete when returned; callers must not assume
the column is filled until [Job.wait](Job.md#wait) resolves. Invalid input --
an unknown column, or one that is not computed -- rejects here rather
than failing the job. On local tables the job runs in-process; on
LanceDB Cloud and Enterprise it is the server's backfill job.
#### Parameters
* **column**: `string`
The name of the computed column to fill.
#### Returns
`Promise`&lt;[`Job`](Job.md)&gt;
#### Example
```ts
const job = await table.refreshColumnAsync("doubled");
await job.wait();
console.log(await job.status()); // "finished"
```
***
### restore()
```ts
@@ -737,7 +942,7 @@ Get the schema of the table.
abstract search(
query,
queryType?,
ftsColumns?): Query | VectorQuery
ftsColumns?): Query | VectorQuery | AutoQuery
```
Create a search query to find the nearest neighbors
@@ -759,7 +964,7 @@ of the given query
#### Returns
[`Query`](Query.md) \| [`VectorQuery`](VectorQuery.md)
[`Query`](Query.md) \| [`VectorQuery`](VectorQuery.md) \| [`AutoQuery`](AutoQuery.md)
***
@@ -783,6 +988,11 @@ All variants require the table to have an unenforced primary key
([Table#setUnenforcedPrimaryKey](Table.md#setunenforcedprimarykey)); bucket sharding additionally
requires it to be the single column being bucketed.
Omitting `maintainedIndexes` maintains every index on the table, resolved
here, failing if one cannot be maintained — name them to install anyway.
Naming them pins an exact set, and a still-building index is rejected
rather than quietly omitted.
#### Parameters
* **spec**: [`LsmWriteSpec`](../interfaces/LsmWriteSpec.md)
+23
View File
@@ -273,6 +273,29 @@ ArrowTable.
***
### useLsm()
```ts
useLsm(enable): this
```
Control MemWAL read routing for this take query.
`false` bypasses the MemWAL and reads the base table only — the escape hatch,
since take-by-row-id/offset is not supported on the LSM scanner and, on a
MemWAL table, auto-routes to it and errors otherwise.
#### Parameters
* **enable**: `boolean`
`false` reads the base table only.
#### Returns
`this`
***
### withRowId()
```ts
+36
View File
@@ -746,6 +746,42 @@ ArrowTable.
***
### useLsm()
```ts
useLsm(enable): this
```
Control MemWAL read routing for this query.
By default (unset), when the table carries a MemWAL write spec (see
[Table#setLsmWriteSpec](Table.md#setlsmwritespec)), reads are routed through the LSM scanner so
they also return data written via the `mergeInsert` LSM path that has not yet
been compacted into the base table (the active/frozen in-memory memtables and
the flushed generations), deduplicated by primary key; a table without a spec
reads the base table.
#### Parameters
* **enable**: `boolean`
`true` forces the LSM scanner and errors if the table has no
MemWAL write spec. `false` bypasses the MemWAL and reads the base table only,
even when a spec is present.
Note: the LSM scanner does not support every query shape (e.g. reranking,
hybrid search, `orderBy`). On a MemWAL table those shapes error unless
`useLsm(false)` is set, because a base-only read would silently exclude
un-compacted MemWAL data.
#### Returns
`this`
#### Inherited from
`StandardQueryBase.useLsm`
***
### where()
```ts
+24
View File
@@ -18,6 +18,7 @@
## Classes
- [AutoQuery](classes/AutoQuery.md)
- [BooleanQuery](classes/BooleanQuery.md)
- [BoostQuery](classes/BoostQuery.md)
- [BranchContents](classes/BranchContents.md)
@@ -25,8 +26,10 @@
- [Connection](classes/Connection.md)
- [HeaderProvider](classes/HeaderProvider.md)
- [Index](classes/Index.md)
- [Job](classes/Job.md)
- [MakeArrowTableOptions](classes/MakeArrowTableOptions.md)
- [MatchQuery](classes/MatchQuery.md)
- [MaterializedView](classes/MaterializedView.md)
- [MergeInsertBuilder](classes/MergeInsertBuilder.md)
- [MultiMatchQuery](classes/MultiMatchQuery.md)
- [NativeJsHeaderProvider](classes/NativeJsHeaderProvider.md)
@@ -52,6 +55,15 @@
- [AddDataOptions](interfaces/AddDataOptions.md)
- [AddResult](interfaces/AddResult.md)
- [AlterColumnsResult](interfaces/AlterColumnsResult.md)
- [BranchColumnChange](interfaces/BranchColumnChange.md)
- [BranchColumnSummary](interfaces/BranchColumnSummary.md)
- [BranchDiff](interfaces/BranchDiff.md)
- [BranchIndexSummary](interfaces/BranchIndexSummary.md)
- [BranchRowCountSummary](interfaces/BranchRowCountSummary.md)
- [BucketStats](interfaces/BucketStats.md)
- [CherryPickError](interfaces/CherryPickError.md)
- [CherryPickPreview](interfaces/CherryPickPreview.md)
- [CherryPickResult](interfaces/CherryPickResult.md)
- [ClientConfig](interfaces/ClientConfig.md)
- [ColumnAlteration](interfaces/ColumnAlteration.md)
- [ColumnOrdering](interfaces/ColumnOrdering.md)
@@ -75,6 +87,7 @@
- [FtsToken](interfaces/FtsToken.md)
- [FullTextQuery](interfaces/FullTextQuery.md)
- [FullTextSearchOptions](interfaces/FullTextSearchOptions.md)
- [GenerationStats](interfaces/GenerationStats.md)
- [HnswPqOptions](interfaces/HnswPqOptions.md)
- [HnswSqOptions](interfaces/HnswSqOptions.md)
- [IndexConfig](interfaces/IndexConfig.md)
@@ -83,9 +96,17 @@
- [IvfFlatOptions](interfaces/IvfFlatOptions.md)
- [IvfPqOptions](interfaces/IvfPqOptions.md)
- [IvfRqOptions](interfaces/IvfRqOptions.md)
- [JobDescription](interfaces/JobDescription.md)
- [JobFailureInfo](interfaces/JobFailureInfo.md)
- [JobInfo](interfaces/JobInfo.md)
- [ListNamespacesOptions](interfaces/ListNamespacesOptions.md)
- [ListNamespacesResponse](interfaces/ListNamespacesResponse.md)
- [ListTablesOptions](interfaces/ListTablesOptions.md)
- [ListTablesResponse](interfaces/ListTablesResponse.md)
- [LsmStats](interfaces/LsmStats.md)
- [LsmWriteSpec](interfaces/LsmWriteSpec.md)
- [MaterializedViewDefinition](interfaces/MaterializedViewDefinition.md)
- [MemtableStats](interfaces/MemtableStats.md)
- [MergeResult](interfaces/MergeResult.md)
- [NativeOAuthConfig](interfaces/NativeOAuthConfig.md)
- [OAuthConfig](interfaces/OAuthConfig.md)
@@ -93,6 +114,8 @@
- [OptimizeOptions](interfaces/OptimizeOptions.md)
- [OptimizeStats](interfaces/OptimizeStats.md)
- [QueryExecutionOptions](interfaces/QueryExecutionOptions.md)
- [RefreshColumnResult](interfaces/RefreshColumnResult.md)
- [RefreshMaterializedViewResult](interfaces/RefreshMaterializedViewResult.md)
- [RemovalStats](interfaces/RemovalStats.md)
- [RenameTableOptions](interfaces/RenameTableOptions.md)
- [RestNamespaceConfig](interfaces/RestNamespaceConfig.md)
@@ -125,6 +148,7 @@
- [FieldLike](type-aliases/FieldLike.md)
- [IntoSql](type-aliases/IntoSql.md)
- [IntoVector](type-aliases/IntoVector.md)
- [MaterializedViewSelect](type-aliases/MaterializedViewSelect.md)
- [MultiVector](type-aliases/MultiVector.md)
- [RecordBatchLike](type-aliases/RecordBatchLike.md)
- [SchemaLike](type-aliases/SchemaLike.md)
@@ -0,0 +1,33 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / BranchColumnChange
# Interface: BranchColumnChange
A column whose definition differs between main and the branch.
## Properties
### branch
```ts
branch: BranchColumnSummary;
```
***
### main
```ts
main: BranchColumnSummary;
```
***
### name
```ts
name: string;
```
@@ -0,0 +1,33 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / BranchColumnSummary
# Interface: BranchColumnSummary
Summary of a column in a branch diff.
## Properties
### dataType
```ts
dataType: string;
```
***
### name
```ts
name: string;
```
***
### nullable
```ts
nullable: boolean;
```
+121
View File
@@ -0,0 +1,121 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / BranchDiff
# Interface: BranchDiff
Read-only comparison of a branch against main.
## Properties
### addedColumns
```ts
addedColumns: BranchColumnSummary[];
```
***
### addedIndexes
```ts
addedIndexes: BranchIndexSummary[];
```
***
### baseMoved
```ts
baseMoved: boolean;
```
***
### branchVersion
```ts
branchVersion: number;
```
***
### changedColumns
```ts
changedColumns: BranchColumnChange[];
```
***
### errors
```ts
errors: CherryPickError[];
```
***
### fromBranch
```ts
fromBranch: string;
```
***
### mainVersion
```ts
mainVersion: number;
```
***
### parentVersion
```ts
parentVersion: number;
```
***
### removedColumns
```ts
removedColumns: BranchColumnSummary[];
```
***
### removedIndexes
```ts
removedIndexes: BranchIndexSummary[];
```
***
### rowCountBranch
```ts
rowCountBranch: number;
```
***
### rowCountMain
```ts
rowCountMain: number;
```
***
### rowSummary
```ts
rowSummary: BranchRowCountSummary;
```
@@ -0,0 +1,41 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / BranchIndexSummary
# Interface: BranchIndexSummary
Summary of an index in a branch diff.
## Properties
### columns
```ts
columns: string[];
```
***
### indexName
```ts
indexName: string;
```
***
### indexType?
```ts
optional indexType: string;
```
***
### status
```ts
status: string;
```
@@ -0,0 +1,57 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / BranchRowCountSummary
# Interface: BranchRowCountSummary
Row-level comparison between main and the branch.
## Properties
### deltaAvailable
```ts
deltaAvailable: boolean;
```
***
### inputsChanged
```ts
inputsChanged: number;
```
***
### newOnBase
```ts
newOnBase: number;
```
***
### newOnBranch
```ts
newOnBranch: number;
```
***
### staleRecompute
```ts
staleRecompute: number;
```
***
### unchanged
```ts
unchanged: number;
```
+116
View File
@@ -0,0 +1,116 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / BucketStats
# Interface: BucketStats
Live state of one bucket. A table is N buckets on one node; flattening to a
single number hides the one hot bucket that is usually why someone opened
this endpoint.
## Properties
### compacting
```ts
compacting: boolean;
```
Whether a pass owns this bucket's compaction latch right now. Says *a*
driver is running, not *whose*, and the latch is held from dispatch —
including while the pass queues for a pod-wide compactor permit. Read it
as "do not pile on", never as "mine is progressing".
***
### currentGeneration
```ts
currentGeneration: number;
```
The generation the active memtable will become.
***
### generations
```ts
generations: GenerationStats[];
```
Flushed L0 generations not yet merged into the base table.
***
### manifestVersion
```ts
manifestVersion: number;
```
Version of the shard manifest these numbers were read from.
***
### memtables?
```ts
optional memtables: MemtableStats[];
```
Oldest first, active last. Absent for a `"Sealed"` bucket, whose
in-memory state is torn down.
***
### replayAfterWalEntryPosition
```ts
replayAfterWalEntryPosition: number;
```
WAL position replay resumes from.
***
### shardId
```ts
shardId: string;
```
The shard this bucket writes.
***
### status
```ts
status: string;
```
`"Active"` or `"Sealed"` (drop-table 2PC in flight).
***
### walEntryPositionLastSeen
```ts
walEntryPositionLastSeen: number;
```
Highest WAL position the writer has seen. The difference against
`replayAfterWalEntryPosition` is the WAL lag.
***
### writerEpoch
```ts
writerEpoch: number;
```
Epoch of the writer that currently owns the shard.
+25
View File
@@ -0,0 +1,25 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / CherryPickError
# Interface: CherryPickError
A reason why a cherry-pick cannot currently land.
## Properties
### code
```ts
code: string;
```
***
### message
```ts
message: string;
```
@@ -0,0 +1,17 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / CherryPickPreview
# Interface: CherryPickPreview
Changes that would be, or were, promoted by a cherry-pick.
## Properties
### promotedColumns
```ts
promotedColumns: string[];
```
@@ -0,0 +1,46 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / CherryPickResult
# Interface: CherryPickResult
Result of previewing or attempting a cherry-pick.
## Properties
### diff
```ts
diff: BranchDiff;
```
***
### mainVersionAfter?
```ts
optional mainVersionAfter: number;
```
***
### preview
```ts
preview: CherryPickPreview;
```
***
### status
```ts
status:
| "failed"
| "unknown"
| "ready"
| "notImplemented"
| "cherryPicked";
```
+28
View File
@@ -43,6 +43,34 @@ The following tokenizers are available:
***
### blockSize?
```ts
optional blockSize: 128 | 256;
```
Number of documents per compressed posting block.
The default is 128. Supported values are 128 and 256. A value of 256 uses
the experimental FTS V3 format and may introduce breaking changes.
***
### customStopWords?
```ts
optional customStopWords: string[];
```
Custom stop words that replace the built-in list for `language`.
This option only affects tokenization when `removeStopWords` is true.
`undefined` keeps the built-in language list. An empty array explicitly
replaces it with no stop words.
***
### language?
```ts
+40
View File
@@ -0,0 +1,40 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / GenerationStats
# Interface: GenerationStats
One flushed L0 generation.
## Properties
### bytes
```ts
bytes: number;
```
On-disk size of the generation.
***
### generation
```ts
generation: number;
```
The generation number. Increases as memtables are sealed into L0.
***
### rows?
```ts
optional rows: number;
```
Present only when `includeGenerationRows` was requested. Off by default
because each count opens an uncached Lance dataset.
+66
View File
@@ -0,0 +1,66 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / JobDescription
# Interface: JobDescription
A described job from `Connection.getJob`.
## Properties
### creationMs
```ts
creationMs: number;
```
When the job was created, in milliseconds since the epoch.
***
### failure?
```ts
optional failure: JobFailureInfo;
```
Why the job failed, when the job is failed and the server reports a
reason.
***
### jobId
```ts
jobId: string;
```
***
### jobType
```ts
jobType: string;
```
***
### specJson?
```ts
optional specJson: string;
```
The job-type-specific specification as a JSON string, when present.
***
### state
```ts
state: string;
```
Lifecycle state: "running", "finished", "failed", or "cancelled".
+33
View File
@@ -0,0 +1,33 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / JobFailureInfo
# Interface: JobFailureInfo
The server's account of why a job failed.
## Properties
### message?
```ts
optional message: string;
```
***
### phase?
```ts
optional phase: string;
```
***
### retryable?
```ts
optional retryable: boolean;
```
+58
View File
@@ -0,0 +1,58 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / JobInfo
# Interface: JobInfo
A row from `Connection.listJobs`: one server-side job.
## Properties
### createdAtMillis
```ts
createdAtMillis: number;
```
When the job was created, in milliseconds since the epoch.
***
### jobId
```ts
jobId: string;
```
The job id -- what `Connection.getJob` and `Connection.cancelJob`
accept.
***
### jobType
```ts
jobType: string;
```
***
### state
```ts
state: string;
```
Lifecycle state: "running", "finished", "failed", or "cancelled".
***
### table
```ts
table: string;
```
The table the job runs against, without URI or namespace.
@@ -0,0 +1,34 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / ListTablesOptions
# Interface: ListTablesOptions
## Properties
### limit?
```ts
optional limit: number;
```
An upper bound on how many tables to return.
A page may hold fewer than this and still not be the last one, so keep
going while the response carries a page token rather than while pages are
full.
***
### pageToken?
```ts
optional pageToken: string;
```
Token from a previous response, to resume listing where it left off.
The token is opaque: it carries whatever the database needs to resume, and
callers should not construct or interpret one.
@@ -0,0 +1,23 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / ListTablesResponse
# Interface: ListTablesResponse
## Properties
### pageToken?
```ts
optional pageToken: string;
```
***
### tables
```ts
tables: string[];
```
+22
View File
@@ -0,0 +1,22 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / LsmStats
# Interface: LsmStats
Live per-bucket LSM state, as returned by `Table#getLsmStats`.
Nothing here is derived: sums and differences (total L0 bytes, WAL lag) are
the caller's to compute.
## Properties
### buckets
```ts
buckets: BucketStats[];
```
One entry per bucket backing this table.
+3 -1
View File
@@ -34,7 +34,9 @@ Bucket and identity variants: the sharding column.
optional maintainedIndexes: string[];
```
Names of indexes the MemWAL should keep up to date during writes.
Indexes the MemWAL keeps up to date. Omit to maintain every supported
index, resolved on install — a snapshot, so indexes created later are not
maintained. Pass `[]` for none.
***
@@ -0,0 +1,59 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MaterializedViewDefinition
# Interface: MaterializedViewDefinition
The query that defines a materialized view.
## Properties
### filter?
```ts
optional filter: string;
```
SQL predicate selecting the source rows the view holds.
***
### inputs
```ts
inputs: string[];
```
Source columns the projections and filter read.
***
### limit?
```ts
optional limit: number;
```
Cap on the number of rows the view holds.
***
### projections
```ts
projections: [string, string][];
```
`[output column, SQL expression]` pairs, in view schema order.
***
### sourceTable
```ts
sourceTable: string;
```
Name of the source table, in the same database as the view.
+60
View File
@@ -0,0 +1,60 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MemtableStats
# Interface: MemtableStats
One in-memory memtable.
## Properties
### batches
```ts
batches: number;
```
Record batches currently buffered.
***
### bytes
```ts
bytes: number;
```
Estimated in-memory size.
***
### generation
```ts
generation: number;
```
The generation this memtable will become once sealed.
***
### indexes
```ts
indexes: string[];
```
Names of the indexes this memtable carries. An absent name is the whole
answer to "why is my fresh-tier search on that column brute-force".
***
### rows
```ts
rows: number;
```
Rows currently buffered.
@@ -0,0 +1,23 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / RefreshColumnResult
# Interface: RefreshColumnResult
## Properties
### rowsFilled
```ts
rowsFilled: number;
```
***
### version
```ts
version: number;
```
@@ -0,0 +1,41 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / RefreshMaterializedViewResult
# Interface: RefreshMaterializedViewResult
## Properties
### mode
```ts
mode: string;
```
How the view was brought up to date: "rebuild", "incremental" or "no_op".
***
### rowsWritten
```ts
rowsWritten: number;
```
***
### sourceVersion
```ts
sourceVersion: number;
```
***
### version
```ts
version: number;
```
+8 -3
View File
@@ -4,11 +4,16 @@
[@lancedb/lancedb](../globals.md) / TableNamesOptions
# Interface: TableNamesOptions
# Interface: ~~TableNamesOptions~~
## Deprecated
Use [ListTablesOptions](ListTablesOptions.md) with [Connection.listTables](../classes/Connection.md#listtables)
instead.
## Properties
### limit?
### ~~limit?~~
```ts
optional limit: number;
@@ -18,7 +23,7 @@ An optional limit to the number of results to return.
***
### startAfter?
### ~~startAfter?~~
```ts
optional startAfter: string;
+4 -1
View File
@@ -44,4 +44,7 @@ The number of rows in the table
totalBytes: number;
```
The total number of bytes in the table
The total size, in bytes, of the table's data files, index files, and
overlay files
Read from the manifest, so this excludes deletion files and manifests.
+15
View File
@@ -30,6 +30,21 @@ The tokenizer to use. The default is "simple".
***
### customStopWords?
```ts
optional customStopWords: string[];
```
Custom stop words that replace the built-in list for `language`.
This option only affects tokenization when `removeStopWords` is true.
`undefined` keeps the built-in language list. An empty array explicitly
replaces it with no stop words.
***
### language?
```ts
@@ -25,9 +25,12 @@
### Type Aliases
- [CreateReturnType](type-aliases/CreateReturnType.md)
- [EmbeddingMetadataEntry](type-aliases/EmbeddingMetadataEntry.md)
- [ResolvedEmbeddingFunctionConfig](type-aliases/ResolvedEmbeddingFunctionConfig.md)
### Functions
- [LanceSchema](functions/LanceSchema.md)
- [getRegistry](functions/getRegistry.md)
- [parseEmbeddingMetadata](functions/parseEmbeddingMetadata.md)
- [register](functions/register.md)
@@ -10,16 +10,12 @@
function getRegistry(): EmbeddingFunctionRegistry
```
Utility function to get the global instance of the registry
Get the global embedding function registry.
LanceDB built-in providers are initialized when this public API is first
used, so importing the root package does not change automatic search
selection for tables without embedding metadata.
## Returns
[`EmbeddingFunctionRegistry`](../classes/EmbeddingFunctionRegistry.md)
`EmbeddingFunctionRegistry` The global instance of the registry
## Example
```ts
const registry = getRegistry();
const openai = registry.get("openai").create();
@@ -0,0 +1,22 @@
[**@lancedb/lancedb**](../../../README.md) • **Docs**
***
[@lancedb/lancedb](../../../globals.md) / [embedding](../README.md) / parseEmbeddingMetadata
# Function: parseEmbeddingMetadata()
```ts
function parseEmbeddingMetadata(json): EmbeddingMetadataEntry[]
```
The single parser for `embedding_functions` schema metadata: every reader
goes through here, so the wire contract cannot fork between them.
## Parameters
* **json**: `string`
## Returns
[`EmbeddingMetadataEntry`](../type-aliases/EmbeddingMetadataEntry.md)[]
@@ -0,0 +1,40 @@
[**@lancedb/lancedb**](../../../README.md) • **Docs**
***
[@lancedb/lancedb](../../../globals.md) / [embedding](../README.md) / EmbeddingMetadataEntry
# Type Alias: EmbeddingMetadataEntry
```ts
type EmbeddingMetadataEntry: object;
```
One entry of the `embedding_functions` schema metadata, with the column
keys normalized across the bindings' spellings.
## Type declaration
### model
```ts
model: EmbeddingFunction["TOptions"];
```
### name
```ts
name: string;
```
### sourceColumn
```ts
sourceColumn: string;
```
### vectorColumn
```ts
vectorColumn: string;
```
@@ -0,0 +1,22 @@
[**@lancedb/lancedb**](../../../README.md) • **Docs**
***
[@lancedb/lancedb](../../../globals.md) / [embedding](../README.md) / ResolvedEmbeddingFunctionConfig
# Type Alias: ResolvedEmbeddingFunctionConfig
```ts
type ResolvedEmbeddingFunctionConfig: EmbeddingFunctionConfig & object;
```
An [EmbeddingFunctionConfig] read back from table metadata, where the
vector column is always recorded.
## Type declaration
### vectorColumn
```ts
vectorColumn: string;
```
@@ -0,0 +1,14 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MaterializedViewSelect
# Type Alias: MaterializedViewSelect
```ts
type MaterializedViewSelect: (string | [string, string])[] | Record<string, string>;
```
The view's columns: column names, `[alias, SQL expression]` pairs, or a
record of the same. A bare name projects itself.
+204 -52
View File
@@ -26,6 +26,18 @@ is also an [asynchronous API client](#connections-asynchronous).
::: lancedb.db.DBConnection
::: lancedb.Session
## Namespaces (Synchronous)
A namespace-backed connection resolves tables through a
[Lance namespace](https://lance-format.github.io/lance-namespace/) service instead of
listing a storage directory.
::: lancedb.connect_namespace
::: lancedb.namespace.LanceNamespaceDBConnection
## Tables (Synchronous)
::: lancedb.table.Table
@@ -34,8 +46,68 @@ is also an [asynchronous API client](#connections-asynchronous).
::: lancedb.table.FragmentSummaryStats
::: lancedb.table.TableStatistics
::: lancedb.table.Tags
::: lancedb.table.Branches
::: lancedb.LsmWriteSpec
## Functions and Jobs
::: lancedb.functions.FunctionArtifact
::: lancedb.functions.FunctionParameter
::: lancedb.functions.FunctionResultField
::: lancedb.functions.FunctionOutput
::: lancedb.functions.FunctionSignature
::: lancedb.functions.PythonEnvironmentSpec
::: lancedb.functions.udf
::: lancedb.functions.UdfDefinition
::: lancedb.functions.FunctionRegistrationRequest
::: lancedb.functions.FunctionArtifactRequest
::: lancedb.functions.FunctionArtifactContent
::: lancedb.functions.PythonAdapterSpec
::: lancedb.functions.FunctionVersion
::: lancedb.functions.PythonRuntimeSpec
::: lancedb.functions.FunctionVersionRef
::: lancedb.functions.ApplicationInput
::: lancedb.functions.FunctionApplication
::: lancedb.functions.InputBinding
::: lancedb.functions.OutputMapping
::: lancedb.functions.FunctionBinding
::: lancedb.functions.RefreshColumnResult
::: lancedb.job.Job
::: lancedb.job.AsyncJob
## Materialized Views (Synchronous)
::: lancedb.materialized_view.MaterializedView
::: lancedb.materialized_view.MaterializedViewDefinition
## Expressions
Type-safe expression builder for filters and projections. Use these instead
@@ -62,29 +134,46 @@ of raw SQL strings with [where][lancedb.query.LanceQueryBuilder.where] and
::: lancedb.query.LanceHybridQueryBuilder
::: lancedb.query.LanceEmptyQueryBuilder
::: lancedb.query.LanceTakeQueryBuilder
## Full text queries
Structured full text queries can be passed to
[Table.search][lancedb.table.Table.search] or
[AsyncTable.search][lancedb.table.AsyncTable.search] in place of a query string,
and combined with [BooleanQuery][lancedb.query.BooleanQuery].
::: lancedb.query.FullTextQuery
::: lancedb.query.MatchQuery
::: lancedb.query.PhraseQuery
::: lancedb.query.BoostQuery
::: lancedb.query.MultiMatchQuery
::: lancedb.query.BooleanQuery
::: lancedb.query.FullTextOperator
::: lancedb.query.Occur
## Embeddings
::: lancedb.embeddings.registry.EmbeddingFunctionRegistry
::: lancedb.embeddings.base.EmbeddingFunctionConfig
::: lancedb.embeddings.base.EmbeddingFunction
::: lancedb.embeddings.base.TextEmbeddingFunction
::: lancedb.embeddings.sentence_transformers.SentenceTransformerEmbeddings
::: lancedb.embeddings.openai.OpenAIEmbeddings
::: lancedb.embeddings.open_clip.OpenClipEmbeddings
::: lancedb.embeddings
options:
show_root_heading: false
show_root_toc_entry: false
## Remote configuration
::: lancedb.remote.ClientConfig
::: lancedb.remote.TimeoutConfig
::: lancedb.remote.RetryConfig
::: lancedb.remote
options:
show_root_heading: false
show_root_toc_entry: false
## Context
@@ -94,11 +183,51 @@ of raw SQL strings with [where][lancedb.query.LanceQueryBuilder.where] and
## Full text search
Use [lancedb.table.Table.create_fts_index][] for the synchronous API or
[lancedb.table.AsyncTable.create_index][] with [lancedb.index.FTS][] for the
asynchronous API.
Pass `custom_stop_words` to [lancedb.index.FTS][]:
::: lancedb.index.FTS
```python
from lancedb.index import FTS
table.create_index(
"text",
config=FTS(remove_stop_words=True, custom_stop_words=["acme", "internal"]),
)
```
The list replaces the built-in stop words and is used only when
`remove_stop_words=True`:
- `custom_stop_words=None` uses the built-in list for `language`.
- `custom_stop_words=[]` removes no words.
- Values are passed through without trimming, lowercasing, or other rewriting.
The same option is available on `lancedb.tokenize(...)` and the deprecated
[lancedb.table.Table.create_fts_index][] compatibility helper:
```python
import lancedb
tokens = list(
lancedb.tokenize("acme makes searchable data", custom_stop_words=["acme"])
)
```
::: lancedb.tokenize
::: lancedb.FtsToken
## Blobs
Blob columns store large binary values out of line so they can be read lazily
instead of being materialized with the rest of the row.
::: lancedb.blob
::: lancedb.BlobType
::: lancedb._blob.BlobFile
options:
show_root_full_path: false
## Utilities
@@ -106,6 +235,14 @@ asynchronous API.
::: lancedb.merge.LanceMergeInsertBuilder
::: lancedb.otel.instrument_lancedb_metrics
## Exceptions
::: lancedb.exceptions.MissingValueError
::: lancedb.exceptions.MissingColumnError
## Integrations
## Pydantic
@@ -114,19 +251,32 @@ asynchronous API.
::: lancedb.pydantic.vector
::: lancedb.pydantic.Vector
::: lancedb.pydantic.MultiVector
::: lancedb.pydantic.LanceModel
## PyTorch
::: lancedb.streaming.StreamingDataset
::: lancedb.streaming.StreamingDataLoader
::: lancedb.permutation.permutation_builder
::: lancedb.permutation.PermutationBuilder
::: lancedb.permutation.Permutation
::: lancedb.permutation.Transforms
## Reranking
::: lancedb.rerankers.linear_combination.LinearCombinationReranker
::: lancedb.rerankers.cohere.CohereReranker
::: lancedb.rerankers.colbert.ColbertReranker
::: lancedb.rerankers.cross_encoder.CrossEncoderReranker
::: lancedb.rerankers.openai.OpenaiReranker
::: lancedb.rerankers
options:
show_root_heading: false
show_root_toc_entry: false
## Connections (Asynchronous)
@@ -137,6 +287,12 @@ can be used to create, list, or open tables.
::: lancedb.db.AsyncConnection
## Namespaces (Asynchronous)
::: lancedb.connect_namespace_async
::: lancedb.namespace.AsyncLanceNamespaceDBConnection
## Tables (Asynchronous)
Table hold your actual data as a collection of records / rows.
@@ -145,32 +301,24 @@ Table hold your actual data as a collection of records / rows.
::: lancedb.table.AsyncTags
::: lancedb.table.AsyncBranches
## Materialized Views (Asynchronous)
::: lancedb.materialized_view.AsyncMaterializedView
## Indices (Asynchronous)
Indices can be created on a table to speed up queries. This section
lists the indices that LanceDb supports.
::: lancedb.index.BTree
::: lancedb.index.Bitmap
::: lancedb.index.LabelList
::: lancedb.index.FTS
::: lancedb.index.IvfPq
::: lancedb.index.HnswPq
::: lancedb.index.HnswSq
::: lancedb.index.IvfFlat
::: lancedb.index.IvfSq
::: lancedb.index.IvfRq
::: lancedb.index.HnswFlat
::: lancedb.index
options:
show_root_heading: false
show_root_toc_entry: false
# `lang_mapping` is defined in the module rather than imported, so it is
# picked up despite not being in `__all__`. It is an internal lookup table.
filters: ["!^_", "!^lang_mapping$"]
::: lancedb.table.IndexStatistics
@@ -198,3 +346,7 @@ rows nearest to a query vector and can be created with the
::: lancedb.query.AsyncHybridQuery
options:
inherited_members: true
::: lancedb.query.AsyncTakeQuery
options:
inherited_members: true
+42
View File
@@ -29,6 +29,48 @@ LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
.build();
```
## MemWAL LSM write path
Most table operations reach LanceDB through the `LanceNamespace` above, which is
generated from the Lance Namespace specification. The MemWAL LSM routes are not part
of that specification, so they are issued through a separate client:
```java
import com.lancedb.LanceDbRestClient;
import com.lancedb.LanceDbTableLsm;
import com.lancedb.LsmWriteSpec;
LanceDbRestClient client = LanceDbNamespaceClientBuilder.newBuilder()
.apiKey("your_lancedb_cloud_api_key")
.database("your_database_name")
.buildRestClient();
LanceDbTableLsm lsm = new LanceDbTableLsm(client, "my_table");
// Route future merge_insert upserts through the MemWAL, hash-bucketed by `id`.
lsm.setLsmWriteSpec(LsmWriteSpec.bucket("id", 16));
// ... merge_insert traffic ...
// Converge the fresh tier into the base table.
lsm.checkpointLsm();
// Inspect live per-bucket state.
lsm.getLsmStats().ifPresent(stats -> stats.buckets().forEach(bucket ->
System.out.println(bucket.shardId() + ": " + bucket.generations().size() + " L0 generations")));
client.close();
```
`maintainedIndexes` is tri-state, and the null default is the opposite of what a Java
reader usually expects:
| Value | Meaning |
| --- | --- |
| unset (null) | Maintain **every** index the MemWAL can, resolved on install |
| `Collections.emptyList()` | Maintain **none** |
| `Arrays.asList("id_idx")` | Maintain exactly those |
## Development
Build:
+15 -1
View File
@@ -8,7 +8,7 @@
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.32.0-beta.2</version>
<version>0.38.0-beta.10</version>
<relativePath>../pom.xml</relativePath>
</parent>
@@ -33,6 +33,20 @@
<artifactId>arrow-memory-netty</artifactId>
</dependency>
<!-- Transport for the LanceDB routes outside the Lance Namespace spec.
Versions match what lance-namespace-apache-client resolves to. -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
@@ -0,0 +1,194 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.OptionalLong;
/**
* Live state of one bucket. A table is N buckets on one node; flattening to a single number hides
* the one hot bucket that is usually why someone opened this endpoint.
*/
public class BucketStats {
private static final String CONTEXT = "bucket stats";
private final String shardId;
private final String status;
private final long writerEpoch;
private final long manifestVersion;
private final long currentGeneration;
private final long replayAfterWalEntryPosition;
private final long walEntryPositionLastSeen;
private final List<GenerationStats> generations;
private final boolean compacting;
private final List<MemtableStats> memtables;
BucketStats(
String shardId,
String status,
long writerEpoch,
long manifestVersion,
long currentGeneration,
long replayAfterWalEntryPosition,
long walEntryPositionLastSeen,
List<GenerationStats> generations,
boolean compacting,
List<MemtableStats> memtables) {
this.shardId = shardId;
this.status = status;
this.writerEpoch = writerEpoch;
this.manifestVersion = manifestVersion;
this.currentGeneration = currentGeneration;
this.replayAfterWalEntryPosition = replayAfterWalEntryPosition;
this.walEntryPositionLastSeen = walEntryPositionLastSeen;
this.generations = Collections.unmodifiableList(generations);
this.compacting = compacting;
this.memtables = memtables == null ? null : Collections.unmodifiableList(memtables);
}
/** The shard this bucket writes. */
public String shardId() {
return shardId;
}
/** {@code "Active"} or {@code "Sealed"} (drop-table 2PC in flight). */
public String status() {
return status;
}
/** Epoch of the writer that currently owns the shard. */
public long writerEpoch() {
return writerEpoch;
}
/** Version of the shard manifest these numbers were read from. */
public long manifestVersion() {
return manifestVersion;
}
/** The generation the active memtable will become. */
public long currentGeneration() {
return currentGeneration;
}
/** WAL position replay resumes from. */
public long replayAfterWalEntryPosition() {
return replayAfterWalEntryPosition;
}
/**
* Highest WAL position the writer has seen. The difference against {@link
* #replayAfterWalEntryPosition()} is the WAL lag.
*/
public long walEntryPositionLastSeen() {
return walEntryPositionLastSeen;
}
/** Flushed L0 generations not yet merged into the base table. */
public List<GenerationStats> generations() {
return generations;
}
/**
* Whether a pass owns this bucket's compaction latch right now. Says <em>a</em> driver is
* running, not <em>whose</em>, and the latch is held from dispatch including while the pass
* queues for a pod-wide compactor permit. Read it as "do not pile on", never as "mine is
* progressing".
*/
public boolean compacting() {
return compacting;
}
/** Oldest first, active last. Empty for a {@code "Sealed"} bucket, whose state is torn down. */
public Optional<List<MemtableStats>> memtables() {
return Optional.ofNullable(memtables);
}
/** The newest flushed generation, or empty when L0 is empty. */
OptionalLong newestGeneration() {
OptionalLong newest = OptionalLong.empty();
for (GenerationStats generation : generations) {
if (!newest.isPresent() || generation.generation() > newest.getAsLong()) {
newest = OptionalLong.of(generation.generation());
}
}
return newest;
}
/**
* How many generations at or below {@code target} are still in L0.
*
* <p>A count, not a boolean: one pass drains a bounded prefix rather than the whole target set,
* so a boolean would read as "no progress" for every pass but the last. Compaction drains
* oldest-first, so this decreases monotonically.
*/
long outstandingGenerations(long target) {
long count = 0;
for (GenerationStats generation : generations) {
if (generation.generation() <= target) {
count++;
}
}
return count;
}
static BucketStats fromJson(JsonNode node) {
JsonFields.requiredObject(node, CONTEXT);
List<GenerationStats> generations = new ArrayList<GenerationStats>();
for (JsonNode generation : JsonFields.requiredArray(node, "generations", CONTEXT)) {
generations.add(GenerationStats.fromJson(generation));
}
JsonNode memtablesNode = JsonFields.optionalArray(node, "memtables", CONTEXT);
List<MemtableStats> memtables = null;
if (memtablesNode != null) {
memtables = new ArrayList<MemtableStats>();
for (JsonNode memtable : memtablesNode) {
memtables.add(MemtableStats.fromJson(memtable));
}
}
return new BucketStats(
JsonFields.requiredText(node, "shard_id", CONTEXT),
JsonFields.requiredText(node, "status", CONTEXT),
JsonFields.requiredLong(node, "writer_epoch", CONTEXT),
JsonFields.requiredLong(node, "manifest_version", CONTEXT),
JsonFields.requiredLong(node, "current_generation", CONTEXT),
JsonFields.requiredLong(node, "replay_after_wal_entry_position", CONTEXT),
JsonFields.requiredLong(node, "wal_entry_position_last_seen", CONTEXT),
generations,
JsonFields.requiredBoolean(node, "compacting", CONTEXT),
memtables);
}
@Override
public String toString() {
return "BucketStats{shardId="
+ shardId
+ ", status="
+ status
+ ", currentGeneration="
+ currentGeneration
+ ", generations="
+ generations
+ ", compacting="
+ compacting
+ "}";
}
}
@@ -0,0 +1,64 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.OptionalLong;
/** One flushed L0 generation. */
public class GenerationStats {
private static final String CONTEXT = "generation stats";
private final long generation;
private final long bytes;
private final Long rows;
GenerationStats(long generation, long bytes, Long rows) {
this.generation = generation;
this.bytes = bytes;
this.rows = rows;
}
/** The generation number. Increases as memtables are sealed into L0. */
public long generation() {
return generation;
}
/** On-disk size of the generation. */
public long bytes() {
return bytes;
}
/**
* Rows in this generation, present only when {@code includeGenerationRows} was requested. Off by
* default because each count opens an uncached Lance dataset.
*/
public OptionalLong rows() {
return rows == null ? OptionalLong.empty() : OptionalLong.of(rows);
}
static GenerationStats fromJson(JsonNode node) {
JsonFields.requiredObject(node, CONTEXT);
return new GenerationStats(
JsonFields.requiredLong(node, "generation", CONTEXT),
JsonFields.requiredLong(node, "bytes", CONTEXT),
JsonFields.optionalLong(node, "rows", CONTEXT));
}
@Override
public String toString() {
return "GenerationStats{generation=" + generation + ", bytes=" + bytes + ", rows=" + rows + "}";
}
}
@@ -0,0 +1,109 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Strict readers for decoding LanceDB JSON responses.
*
* <p>Every reader fails closed: a missing, null, or wrong-typed field throws rather than
* defaulting. That mirrors the serde decoding the Rust client applies to the same payloads in
* {@code rust/lancedb/src/table/lsm_stats.rs}, where a required field has no default and a
* malformed response is an error rather than a zero.
*
* <p>The alternative Jackson's {@code path()}, which yields a missing node that reads as an empty
* array or a zero is unsafe here because {@link LanceDbTableLsm#checkpointLsm()} decides
* convergence from these numbers. A defaulted {@code generations} array is indistinguishable from a
* drained one, so a malformed response would report a checkpoint that never happened.
*/
final class JsonFields {
private JsonFields() {}
/** The node itself, once confirmed to be a JSON object. */
static JsonNode requiredObject(JsonNode node, String context) {
if (node == null || !node.isObject()) {
throw new IllegalStateException(context + " is not a JSON object: " + node);
}
return node;
}
static String requiredText(JsonNode owner, String field, String context) {
JsonNode value = required(owner, field, context);
if (!value.isTextual()) {
throw new IllegalStateException(fieldIs(context, field, "a string", value));
}
return value.asText();
}
static long requiredLong(JsonNode owner, String field, String context) {
JsonNode value = required(owner, field, context);
if (!value.isIntegralNumber()) {
throw new IllegalStateException(fieldIs(context, field, "an integer", value));
}
return value.asLong();
}
static boolean requiredBoolean(JsonNode owner, String field, String context) {
JsonNode value = required(owner, field, context);
if (!value.isBoolean()) {
throw new IllegalStateException(fieldIs(context, field, "a boolean", value));
}
return value.asBoolean();
}
static JsonNode requiredArray(JsonNode owner, String field, String context) {
JsonNode value = required(owner, field, context);
if (!value.isArray()) {
throw new IllegalStateException(fieldIs(context, field, "an array", value));
}
return value;
}
/** Null when the field is absent or JSON null, mirroring a serde {@code Option}. */
static Long optionalLong(JsonNode owner, String field, String context) {
JsonNode value = owner.get(field);
if (value == null || value.isNull()) {
return null;
}
if (!value.isIntegralNumber()) {
throw new IllegalStateException(fieldIs(context, field, "an integer", value));
}
return value.asLong();
}
/** Null when the field is absent or JSON null, mirroring a serde {@code Option}. */
static JsonNode optionalArray(JsonNode owner, String field, String context) {
JsonNode value = owner.get(field);
if (value == null || value.isNull()) {
return null;
}
if (!value.isArray()) {
throw new IllegalStateException(fieldIs(context, field, "an array", value));
}
return value;
}
private static JsonNode required(JsonNode owner, String field, String context) {
JsonNode value = owner.get(field);
if (value == null || value.isNull()) {
throw new IllegalStateException(context + " is missing required field '" + field + "'");
}
return value;
}
private static String fieldIs(String context, String field, String expected, JsonNode value) {
return context + " field '" + field + "' is not " + expected + ": " + value;
}
}
@@ -136,29 +136,48 @@ public class LanceDbNamespaceClientBuilder {
* @throws IllegalStateException if required parameters are missing
*/
public LanceNamespace build() {
// Validate required fields
validate();
// Build configuration map
Map<String, String> config = new HashMap<>(additionalConfig);
config.put("header.x-lancedb-database", database);
config.put("header.x-api-key", apiKey);
config.put("uri", resolveUri());
return LanceNamespace.connect("rest", config, null);
}
/**
* Build a {@link LanceDbRestClient} for the same endpoint.
*
* <p>Needed only for LanceDB routes that the Lance Namespace specification does not cover the
* MemWAL LSM write path, reached through {@link LanceDbTableLsm}. Every other table operation
* belongs on the {@link LanceNamespace} from {@link #build()}.
*
* <p>The returned client owns an HTTP connection pool; close it when you are done with it.
*
* @return A configured LanceDbRestClient
* @throws IllegalStateException if required parameters are missing
*/
public LanceDbRestClient buildRestClient() {
validate();
return new LanceDbRestClient(resolveUri(), apiKey, database);
}
private void validate() {
if (apiKey == null) {
throw new IllegalStateException("API key is required");
}
if (database == null) {
throw new IllegalStateException("Database is required");
}
}
// Build configuration map
Map<String, String> config = new HashMap<>(additionalConfig);
config.put("header.x-lancedb-database", database);
config.put("header.x-api-key", apiKey);
// Determine base URL
String uri;
/** The custom endpoint when set, else the LanceDB Cloud URL for this database and region. */
private String resolveUri() {
if (endpoint.isPresent()) {
uri = endpoint.get();
} else {
String effectiveRegion = region.orElse(DEFAULT_REGION);
uri = String.format(CLOUD_URL_PATTERN, database, effectiveRegion);
return endpoint.get();
}
config.put("uri", uri);
return LanceNamespace.connect("rest", config, null);
return String.format(CLOUD_URL_PATTERN, database, region.orElse(DEFAULT_REGION));
}
}
@@ -0,0 +1,119 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.io.entity.StringEntity;
import java.io.Closeable;
import java.io.IOException;
import java.io.UncheckedIOException;
/**
* Minimal HTTP client for LanceDB Cloud and Enterprise routes that the Lance Namespace
* specification does not cover.
*
* <p>Most table operations reach LanceDB through {@link org.lance.namespace.LanceNamespace}, which
* is generated from the namespace spec. A handful of routes the MemWAL LSM write path in
* particular are served by the same endpoint but are not part of that spec, so they are issued
* directly here. See {@link LanceDbTableLsm}.
*
* <p>Obtain one from {@link LanceDbNamespaceClientBuilder#buildRestClient()}.
*/
public class LanceDbRestClient implements Closeable {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final String baseUri;
private final String apiKey;
private final String database;
private final CloseableHttpClient http;
LanceDbRestClient(String baseUri, String apiKey, String database) {
this.baseUri = baseUri.endsWith("/") ? baseUri.substring(0, baseUri.length() - 1) : baseUri;
this.apiKey = apiKey;
this.database = database;
// Automatic retries off, deliberately. The default strategy retries 429 and 503
// exactly the two statuses LanceDbTableLsm.checkpointLsm() acts on which would
// silently double its explicit retry budget and would also retry compact_lsm in
// place, where the loop is designed to fall through to a fresh stats poll instead.
// The checkpoint loop owns the 421/429/503 transitions; the transport must not.
this.http = HttpClients.custom().disableAutomaticRetries().build();
}
/**
* POST {@code path}, sending {@code body} as JSON when it is non-null.
*
* @param path Absolute request path, beginning with {@code /}.
* @param body Object to serialize as the request body, or null to send no body.
* @return The parsed response body, or null when the response carried no content.
* @throws HttpException if the server returned a non-2xx status.
*/
public JsonNode post(String path, Object body) {
HttpPost request = new HttpPost(baseUri + path);
request.setHeader("x-api-key", apiKey);
request.setHeader("x-lancedb-database", database);
try {
if (body != null) {
request.setEntity(
new StringEntity(MAPPER.writeValueAsString(body), ContentType.APPLICATION_JSON));
}
return http.execute(
request,
response -> {
String text =
response.getEntity() == null ? "" : EntityUtils.toString(response.getEntity());
int status = response.getCode();
if (status < 200 || status >= 300) {
throw new HttpException(status, "LanceDB request to " + path + " failed: " + text);
}
return text.isEmpty() ? null : MAPPER.readTree(text);
});
} catch (IOException e) {
throw new UncheckedIOException("LanceDB request to " + path + " failed", e);
}
}
@Override
public void close() throws IOException {
http.close();
}
/**
* A non-2xx response.
*
* <p>The status is exposed because callers act on it: {@link LanceDbTableLsm#checkpointLsm()}
* treats 429 and 503 as retryable and 421 as a lost node claim.
*/
public static class HttpException extends RuntimeException {
private static final long serialVersionUID = 1L;
private final int statusCode;
public HttpException(int statusCode, String message) {
super(message);
this.statusCode = statusCode;
}
/** The HTTP status the failed response carried. */
public int statusCode() {
return statusCode;
}
}
}
@@ -0,0 +1,394 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.OptionalLong;
/**
* The MemWAL LSM write path for one LanceDB Cloud or Enterprise table.
*
* <p>Installing an {@link LsmWriteSpec} routes {@code mergeInsert} upserts through Lance's MemWAL
* an LSM-style append instead of the standard merge path. Rows land in an in-memory memtable,
* seal into L0 generations, and are merged into the base table by compaction.
*
* <p>These routes are not part of the Lance Namespace specification, so they are issued directly
* rather than through {@link org.lance.namespace.LanceNamespace}.
*
* <pre>{@code
* LanceDbRestClient client = LanceDbNamespaceClientBuilder.newBuilder()
* .apiKey("your_lancedb_cloud_api_key")
* .database("your_database_name")
* .buildRestClient();
*
* LanceDbTableLsm lsm = new LanceDbTableLsm(client, "my_table");
* lsm.setLsmWriteSpec(LsmWriteSpec.bucket("id", 16));
* // ... merge_insert traffic ...
* lsm.checkpointLsm();
* }</pre>
*/
public class LanceDbTableLsm {
/**
* Interval between {@code get_lsm_stats} polls during a checkpoint. One interval is roughly one
* compaction pass, the granularity at which the answer can change.
*/
private static final long POLL_INTERVAL_MS = 5_000L;
/**
* Cap on re-issues from {@code flushLsm} after a 421, so a crash-looping node cannot turn flush
* compact 421 flush into a spin.
*
* <p>Deliberately not shared with {@link #MAX_RETRIES}: a claim that keeps evaporating is a
* broken node, while contention is routine and wants a real budget.
*/
private static final int MAX_REISSUES = 3;
/**
* Retryable faults tolerated on a <em>single</em> request, reset on every success scattered
* contention across a long checkpoint must not accumulate toward a cap.
*/
private static final int MAX_RETRIES = 8;
private static final long RETRY_BACKOFF_BASE_MS = 100L;
private static final long RETRY_BACKOFF_MAX_MS = 5_000L;
private final LanceDbRestClient client;
private final String tableIdentifier;
/**
* Bind the LSM routes for one table.
*
* @param client Transport for the LanceDB endpoint.
* @param tableIdentifier The table's full identifier, {@code $}-delimited when it sits inside a
* namespace, such as {@code analytics$events}.
*/
public LanceDbTableLsm(LanceDbRestClient client, String tableIdentifier) {
if (client == null) {
throw new IllegalArgumentException("Client cannot be null");
}
if (tableIdentifier == null || tableIdentifier.trim().isEmpty()) {
throw new IllegalArgumentException("Table identifier cannot be null or empty");
}
this.client = client;
this.tableIdentifier = tableIdentifier;
}
/**
* Install an {@link LsmWriteSpec} on this table, selecting the MemWAL LSM write path for future
* {@code mergeInsert} calls.
*
* <p>All variants require the table to have an unenforced primary key; bucket sharding
* additionally requires it to be the single column being bucketed.
*/
public void setLsmWriteSpec(LsmWriteSpec spec) {
if (spec == null) {
throw new IllegalArgumentException("Spec cannot be null");
}
client.post(route("set_lsm_write_spec"), spec.toRequestBody());
}
/**
* Remove the {@link LsmWriteSpec} from this table, reverting to the standard {@code mergeInsert}
* write path.
*
* <p>Errors if no spec is currently set.
*/
public void unsetLsmWriteSpec() {
client.post(route("unset_lsm_write_spec"), null);
}
/**
* Read the {@link LsmWriteSpec} currently installed on this table.
*
* <p>Empty when the LSM write path is not enabled. The returned spec mirrors what was installed,
* except that {@link LsmWriteSpec#maintainedIndexes()} always reports the concrete list resolved
* when the spec was set a null selection never round-trips.
*/
public Optional<LsmWriteSpec> getLsmWriteSpec() {
JsonNode response = client.post(route("get_lsm_write_spec"), null);
if (response == null || !response.hasNonNull("lsm_write_spec")) {
return Optional.empty();
}
return Optional.of(LsmWriteSpec.fromJson(response.get("lsm_write_spec")));
}
/**
* Seal every bucket's active memtable into a new L0 generation.
*
* <p>Returns once the seal is committed. Sealing an empty memtable is a no-op, so this is safe to
* call repeatedly.
*/
public void flushLsm() {
client.post(route("flush_lsm"), null);
}
/**
* Trigger a background L0 base compaction pass per bucket.
*
* <p>Returns once the passes are <em>dispatched</em>, not once they finish watch {@link
* #getLsmStats}, or use {@link #checkpointLsm} to wait for convergence.
*/
public void compactLsm() {
client.post(route("compact_lsm"), null);
}
/**
* Read live per-bucket LSM state.
*
* <p>Answers "how far behind is my fresh tier", "which bucket is hot", and "why is my fresh-tier
* vector search brute-force". Mutates no table state.
*
* <p>Empty only when the LSM write path is not enabled that is, when the server sends an absent
* or null {@code lsm_stats}. A stats object that is present is decoded strictly, and a malformed
* one throws rather than decoding to something empty, because {@link #checkpointLsm} reads
* convergence out of these numbers and cannot tell a defaulted array from a drained one.
*
* @param includeGenerationRows Also count rows per L0 generation. Off by default because each
* count opens an uncached Lance dataset.
* @throws IllegalStateException if the response is absent or does not decode.
*/
public Optional<LsmStats> getLsmStats(boolean includeGenerationRows) {
Map<String, Object> body = new LinkedHashMap<String, Object>();
body.put("include_generation_rows", includeGenerationRows);
JsonNode response = client.post(route("get_lsm_stats"), body);
if (response == null) {
throw new IllegalStateException("get_lsm_stats returned an empty response body");
}
JsonNode stats = response.get("lsm_stats");
if (stats == null || stats.isNull()) {
return Optional.empty();
}
return Optional.of(LsmStats.fromJson(stats));
}
/** Equivalent to {@code getLsmStats(false)}. */
public Optional<LsmStats> getLsmStats() {
return getLsmStats(false);
}
/**
* Converge this table's LSM write path into its base table.
*
* <p>Seals once, fixes a target watermark from the resulting L0, then triggers compaction and
* polls until that L0 is gone. The target set is fixed at the start, so generations created
* <em>during</em> the checkpoint are ignored that is what lets it terminate under write load,
* and what makes it best-effort: it converges the fresh tier as of some instant. Idempotent,
* abandonable at any point, safe on a cadence.
*
* <p>The loop runs here, not on the server: {@link #compactLsm} dispatches a pass and returns, so
* nothing holds a socket and a client can vanish mid-operation with nothing to reconcile.
* Completion is read from generation numbers in the shard manifest durable state, unlike a
* count in a compact response, which a concurrent write invalidates.
*
* <p>No liveness bound the caller owns the deadline. The compactor pool is shared across
* tables, so a checkpoint queued behind unrelated work looks exactly like one that is merging.
*/
public void checkpointLsm() {
for (int reissue = 0; reissue <= MAX_REISSUES; reissue++) {
// The seal turns everything written before this call into a generation, so the
// watermark has to be read after it. Idempotent: sealing an empty memtable is a
// no-op, so a re-issue does not churn empty generations.
if (issueVoid(this::flushLsm)) {
backoff(reissue);
continue;
}
Attempt<Optional<LsmStats>> stats = issue(() -> getLsmStats(false));
if (stats.lostClaim) {
backoff(reissue);
continue;
}
if (!stats.value.isPresent()) {
// Not WAL-backed; flushLsm would have errored first but for a race.
return;
}
Map<String, Long> targets = newestGenerations(stats.value.get());
if (targets.isEmpty()) {
return;
}
if (drainToTargets(targets)) {
return;
}
backoff(reissue);
}
throw new IllegalStateException(
"checkpointLsm: the owning node kept losing its claim; re-issued from flush the maximum "
+ "number of times");
}
/**
* Trigger and poll until no bucket holds a generation at or below its target.
*
* @return true when the drain finished, false when the table needs re-claiming from flush.
*/
private boolean drainToTargets(Map<String, Long> targets) {
while (true) {
Attempt<Optional<LsmStats>> stats = issue(() -> getLsmStats(false));
if (stats.lostClaim) {
return false;
}
if (!stats.value.isPresent()) {
return true;
}
// `compacting` is the bucket's compaction latch, held from dispatch until the pass
// ends including while it waits on a pod-wide permit. So it answers one question
// only: do not pile on. Buckets with nothing outstanding are skipped, not counted
// as idle.
long outstanding = 0;
boolean allCompacting = true;
for (BucketStats bucket : stats.value.get().buckets()) {
Long target = targets.get(bucket.shardId());
if (target == null) {
continue;
}
long remaining = bucket.outstandingGenerations(target);
if (remaining > 0) {
outstanding += remaining;
allCompacting &= bucket.compacting();
}
}
if (outstanding == 0) {
return true;
}
if (!allCompacting) {
try {
compactLsm();
} catch (LanceDbRestClient.HttpException e) {
if (isLostClaim(e)) {
return false;
}
if (!isRetryable(e)) {
throw e;
}
// A 429 here means the server could latch no bucket at all, which the poll
// above already handles. Not retried in place: the latch it would contend for
// is the one doing the work, so fall through and re-read POLL_INTERVAL_MS is
// the backoff.
}
}
sleep(POLL_INTERVAL_MS);
}
}
/** The newest generation held by each bucket, skipping buckets holding none. */
private static Map<String, Long> newestGenerations(LsmStats stats) {
Map<String, Long> targets = new HashMap<String, Long>();
for (BucketStats bucket : stats.buckets()) {
OptionalLong newest = bucket.newestGeneration();
if (newest.isPresent()) {
targets.put(bucket.shardId(), newest.getAsLong());
}
}
return targets;
}
/**
* 429 (latch held, pool saturated, or the pod replaying its WAL) and 503 (a draining node, or a
* proxy between here and it).
*/
private static boolean isRetryable(LanceDbRestClient.HttpException e) {
return e.statusCode() == 429 || e.statusCode() == 503;
}
/**
* 421: the owning node holds no claim. Only {@code flush} re-claims and replays, so this cannot
* be retried in place the caller has to start over.
*/
private static boolean isLostClaim(LanceDbRestClient.HttpException e) {
return e.statusCode() == 421;
}
/**
* Issue one LSM request, retrying in place while the fault is retryable.
*
* <p>The two recoverable faults have separate budgets: contention clears on its own and retries
* here against {@link #MAX_RETRIES}, while a 421 needs {@code flush} to re-claim, which only the
* caller can drive.
*
* <p>An exhausted budget propagates the last error as itself rather than a synthesized one "429
* after nine tries" beats "checkpoint failed".
*/
private static <T> Attempt<T> issue(Call<T> call) {
int retries = 0;
while (true) {
try {
return new Attempt<T>(call.run(), false);
} catch (LanceDbRestClient.HttpException e) {
if (isLostClaim(e)) {
return new Attempt<T>(null, true);
}
if (!isRetryable(e) || retries >= MAX_RETRIES) {
throw e;
}
backoff(retries);
retries++;
}
}
}
/** {@link #issue} for a call with no return value. Returns true when the claim was lost. */
private static boolean issueVoid(Runnable call) {
return issue(
() -> {
call.run();
return Boolean.TRUE;
})
.lostClaim;
}
/** Sleep before re-issuing a retryable request. Doubles up to {@link #RETRY_BACKOFF_MAX_MS}. */
private static void backoff(int attempt) {
long delay = RETRY_BACKOFF_BASE_MS << Math.min(attempt, 8);
sleep(Math.min(delay, RETRY_BACKOFF_MAX_MS));
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IllegalStateException("Interrupted while waiting on the LSM checkpoint", e);
}
}
private String route(String operation) {
return "/v1/table/" + tableIdentifier + "/" + operation + "/";
}
/** What one LSM request produced: its value, or word that the owning node holds no claim. */
private static final class Attempt<T> {
private final T value;
private final boolean lostClaim;
private Attempt(T value, boolean lostClaim) {
this.value = value;
this.lostClaim = lostClaim;
}
}
@FunctionalInterface
private interface Call<T> {
T run();
}
}
@@ -0,0 +1,56 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Live per-bucket LSM state, as returned by {@link LanceDbTableLsm#getLsmStats()}.
*
* <p>Nothing here is derived: sums and differences (total L0 bytes, WAL lag) are the caller's to
* compute. There is no "LSM is off" shape that case is an empty {@link java.util.Optional},
* because a stats object of zeros would read as measurements.
*/
public class LsmStats {
private static final String CONTEXT = "lsm stats";
private final List<BucketStats> buckets;
LsmStats(List<BucketStats> buckets) {
this.buckets = Collections.unmodifiableList(buckets);
}
/** One entry per bucket. */
public List<BucketStats> buckets() {
return buckets;
}
static LsmStats fromJson(JsonNode node) {
JsonFields.requiredObject(node, CONTEXT);
List<BucketStats> buckets = new ArrayList<BucketStats>();
for (JsonNode bucket : JsonFields.requiredArray(node, "buckets", CONTEXT)) {
buckets.add(BucketStats.fromJson(bucket));
}
return new LsmStats(buckets);
}
@Override
public String toString() {
return "LsmStats{buckets=" + buckets + "}";
}
}
@@ -0,0 +1,260 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Specification selecting Lance's MemWAL LSM-style write path for {@code mergeInsert}.
*
* <p>Construct via {@link #bucket}, {@link #identity}, or {@link #unsharded}, then optionally chain
* {@link #withMaintainedIndexes} and {@link #withWriterConfigDefaults}. Install it with {@link
* LanceDbTableLsm#setLsmWriteSpec} and remove it with {@link LanceDbTableLsm#unsetLsmWriteSpec}.
*
* <p>This is deliberately not {@code org.lance.memwal.InitializeMemWalParams}. That type is Lance's
* own, and its maintained-index default is the opposite of this one: it defaults to maintaining
* <em>nothing</em>, while a fresh spec here maintains <em>every</em> index. It also cannot express
* the null that asks the server to resolve the set.
*/
public class LsmWriteSpec {
/** How writes are routed to MemWAL shards. */
public enum Sharding {
/** Hash-bucket writes by a scalar column. */
BUCKET("bucket"),
/** Shard by the raw value of a scalar column. */
IDENTITY("identity"),
/** Route every write to a single shard. */
UNSHARDED("unsharded");
private final String wireName;
Sharding(String wireName) {
this.wireName = wireName;
}
String wireName() {
return wireName;
}
static Sharding fromWireName(String name) {
for (Sharding s : values()) {
if (s.wireName.equals(name)) {
return s;
}
}
throw new IllegalArgumentException("Unknown sharding mode: " + name);
}
}
private final Sharding sharding;
private final String column;
private final Integer numBuckets;
private final List<String> maintainedIndexes;
private final Map<String, String> writerConfigDefaults;
private LsmWriteSpec(
Sharding sharding,
String column,
Integer numBuckets,
List<String> maintainedIndexes,
Map<String, String> writerConfigDefaults) {
this.sharding = sharding;
this.column = column;
this.numBuckets = numBuckets;
this.maintainedIndexes = maintainedIndexes;
this.writerConfigDefaults = writerConfigDefaults;
}
/**
* Hash-bucket sharding by a scalar column, maintaining every index on the table.
*
* <p>Iceberg-compatible Murmur3-x86-32 (seed 0) is used, so each row's {@code bucket(column,
* numBuckets)} value is stable across processes.
*
* @param column A non-nested column with a supported scalar type.
* @param numBuckets The number of buckets, in {@code [1, 1024]}.
*/
public static LsmWriteSpec bucket(String column, int numBuckets) {
if (column == null || column.trim().isEmpty()) {
throw new IllegalArgumentException("Column cannot be null or empty");
}
return new LsmWriteSpec(
Sharding.BUCKET, column, numBuckets, null, new HashMap<String, String>());
}
/**
* Identity sharding shard by the raw value of {@code column} maintaining every index on the
* table.
*
* <p>{@code column} must be a deterministic function of the unenforced primary key: every row
* with a given primary key must always produce the same {@code column} value, or upserts of that
* key can land in different shards and a stale version can win.
*/
public static LsmWriteSpec identity(String column) {
if (column == null || column.trim().isEmpty()) {
throw new IllegalArgumentException("Column cannot be null or empty");
}
return new LsmWriteSpec(Sharding.IDENTITY, column, null, null, new HashMap<String, String>());
}
/** No sharding — every write goes to a single MemWAL shard — maintaining every index. */
public static LsmWriteSpec unsharded() {
return new LsmWriteSpec(Sharding.UNSHARDED, null, null, null, new HashMap<String, String>());
}
/**
* Set the indexes the MemWAL keeps up to date as rows are appended.
*
* <p>Pass {@code null} the default for a fresh spec to maintain every index the MemWAL can,
* resolved when the spec is installed. That is a snapshot: indexes created later are not
* maintained until the spec is unset and set again. Pass an empty list to maintain none.
*
* <p>Note that {@code null} and the empty list mean opposite things here.
*/
public LsmWriteSpec withMaintainedIndexes(List<String> maintainedIndexes) {
return new LsmWriteSpec(
sharding,
column,
numBuckets,
maintainedIndexes == null ? null : new ArrayList<String>(maintainedIndexes),
writerConfigDefaults);
}
/**
* Set default {@code ShardWriter} configuration recorded in the MemWAL index.
*
* <p>A sparse override map only the keys you set are recorded. Recognized keys include {@code
* durable_write}, {@code max_wal_buffer_size}, {@code max_memtable_size}, {@code
* max_memtable_rows}, {@code max_memtable_batches}, {@code manifest_scan_batch_size}, {@code
* max_unflushed_memtable_bytes}, and {@code enable_memtable}. Duration knobs carry an {@code _ms}
* suffix, such as {@code max_wal_flush_interval_ms}.
*/
public LsmWriteSpec withWriterConfigDefaults(Map<String, String> writerConfigDefaults) {
if (writerConfigDefaults == null) {
throw new IllegalArgumentException("writerConfigDefaults cannot be null");
}
return new LsmWriteSpec(
sharding,
column,
numBuckets,
maintainedIndexes,
new HashMap<String, String>(writerConfigDefaults));
}
/** How writes are routed to shards. */
public Sharding sharding() {
return sharding;
}
/** The sharding column for {@link Sharding#BUCKET} and {@link Sharding#IDENTITY}, else null. */
public String column() {
return column;
}
/** The bucket count for {@link Sharding#BUCKET}, else null. */
public Integer numBuckets() {
return numBuckets;
}
/**
* The indexes the MemWAL maintains, or null to have the server resolve every maintainable index
* on install. An empty list means none.
*/
public List<String> maintainedIndexes() {
return maintainedIndexes == null ? null : Collections.unmodifiableList(maintainedIndexes);
}
/** Default {@code ShardWriter} configuration recorded in the MemWAL index. */
public Map<String, String> writerConfigDefaults() {
return Collections.unmodifiableMap(writerConfigDefaults);
}
/** Render this spec as the {@code set_lsm_write_spec} request body. */
Map<String, Object> toRequestBody() {
Map<String, Object> shardingBody = new LinkedHashMap<String, Object>();
shardingBody.put("mode", sharding.wireName());
if (column != null) {
shardingBody.put("column", column);
}
if (numBuckets != null) {
shardingBody.put("num_buckets", numBuckets);
}
Map<String, Object> body = new LinkedHashMap<String, Object>();
body.put("sharding", shardingBody);
// Null is meaningful: it asks the server to resolve every maintainable index.
body.put("maintained_indexes", maintainedIndexes);
body.put("writer_config_defaults", writerConfigDefaults);
return body;
}
/**
* Rebuild a spec from a {@code get_lsm_write_spec} response body.
*
* <p>The server always reports a concrete maintained-index list, so a null selection never
* round-trips.
*/
static LsmWriteSpec fromJson(JsonNode node) {
JsonNode shardingNode = node.get("sharding");
if (shardingNode == null || shardingNode.get("mode") == null) {
throw new IllegalStateException("get_lsm_write_spec response has no sharding mode");
}
Sharding sharding = Sharding.fromWireName(shardingNode.get("mode").asText());
String column = shardingNode.hasNonNull("column") ? shardingNode.get("column").asText() : null;
Integer numBuckets =
shardingNode.hasNonNull("num_buckets") ? shardingNode.get("num_buckets").asInt() : null;
List<String> maintainedIndexes = new ArrayList<String>();
JsonNode indexesNode = node.get("maintained_indexes");
if (indexesNode != null && indexesNode.isArray()) {
for (JsonNode index : indexesNode) {
maintainedIndexes.add(index.asText());
}
}
Map<String, String> defaults = new HashMap<String, String>();
JsonNode defaultsNode = node.get("writer_config_defaults");
if (defaultsNode != null && defaultsNode.isObject()) {
defaultsNode
.fieldNames()
.forEachRemaining(name -> defaults.put(name, defaultsNode.get(name).asText()));
}
return new LsmWriteSpec(sharding, column, numBuckets, maintainedIndexes, defaults);
}
@Override
public String toString() {
return "LsmWriteSpec{sharding="
+ sharding
+ ", column="
+ column
+ ", numBuckets="
+ numBuckets
+ ", maintainedIndexes="
+ maintainedIndexes
+ ", writerConfigDefaults="
+ writerConfigDefaults
+ "}";
}
}
@@ -0,0 +1,99 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** One in-memory memtable. */
public class MemtableStats {
private static final String CONTEXT = "memtable stats";
private final long generation;
private final long rows;
private final long bytes;
private final long batches;
private final List<String> indexes;
MemtableStats(long generation, long rows, long bytes, long batches, List<String> indexes) {
this.generation = generation;
this.rows = rows;
this.bytes = bytes;
this.batches = batches;
this.indexes = Collections.unmodifiableList(indexes);
}
/** The generation this memtable will become once sealed. */
public long generation() {
return generation;
}
/** Rows currently buffered. */
public long rows() {
return rows;
}
/** Estimated in-memory size. */
public long bytes() {
return bytes;
}
/** Record batches currently buffered. */
public long batches() {
return batches;
}
/**
* Names of the indexes this memtable carries. An absent name is the whole answer to "why is my
* fresh-tier search on that column brute-force".
*/
public List<String> indexes() {
return indexes;
}
static MemtableStats fromJson(JsonNode node) {
JsonFields.requiredObject(node, CONTEXT);
List<String> indexes = new ArrayList<String>();
for (JsonNode index : JsonFields.requiredArray(node, "indexes", CONTEXT)) {
if (!index.isTextual()) {
throw new IllegalStateException(CONTEXT + " has a non-string index name: " + index);
}
indexes.add(index.asText());
}
return new MemtableStats(
JsonFields.requiredLong(node, "generation", CONTEXT),
JsonFields.requiredLong(node, "rows", CONTEXT),
JsonFields.requiredLong(node, "bytes", CONTEXT),
JsonFields.requiredLong(node, "batches", CONTEXT),
indexes);
}
@Override
public String toString() {
return "MemtableStats{generation="
+ generation
+ ", rows="
+ rows
+ ", bytes="
+ bytes
+ ", batches="
+ batches
+ ", indexes="
+ indexes
+ "}";
}
}
@@ -0,0 +1,570 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sun.net.httpserver.HttpServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import static org.junit.jupiter.api.Assertions.*;
/**
* Unit tests for the MemWAL LSM routes, run against a scripted local HTTP server.
*
* <p>The wire assertions mirror the Rust mocked-endpoint tests in {@code
* rust/lancedb/src/remote/table.rs}, which are the contract these routes have to match.
*/
public class LanceDbTableLsmTest {
private static final ObjectMapper MAPPER = new ObjectMapper();
private HttpServer server;
private LanceDbRestClient client;
private LanceDbTableLsm lsm;
private final List<String> requestPaths = Collections.synchronizedList(new ArrayList<String>());
private final List<String> requestBodies = Collections.synchronizedList(new ArrayList<String>());
private final Map<String, Deque<Reply>> replies = new ConcurrentHashMap<String, Deque<Reply>>();
@BeforeEach
public void setUp() throws IOException {
start();
}
/** Tear down and restart the scripted server, for a test that scripts several exchanges. */
private void setUpFresh() {
try {
client.close();
server.stop(0);
requestPaths.clear();
requestBodies.clear();
replies.clear();
start();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
private void start() throws IOException {
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext(
"/",
exchange -> {
String path = exchange.getRequestURI().getPath();
requestPaths.add(path);
requestBodies.add(readAll(exchange.getRequestBody()));
Reply reply = nextReply(path);
byte[] out = reply.body.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(reply.status, out.length == 0 ? -1 : out.length);
if (out.length > 0) {
exchange.getResponseBody().write(out);
}
exchange.close();
});
server.start();
client =
LanceDbNamespaceClientBuilder.newBuilder()
.apiKey("test-key")
.database("test-db")
.endpoint("http://127.0.0.1:" + server.getAddress().getPort())
.buildRestClient();
lsm = new LanceDbTableLsm(client, "my_table");
}
@AfterEach
public void tearDown() throws IOException {
client.close();
server.stop(0);
}
// ===========================================================================
// set / unset / get spec
// ===========================================================================
@Test
public void testSetLsmWriteSpecUnsharded() throws Exception {
enqueue("set_lsm_write_spec", 200, "");
lsm.setLsmWriteSpec(LsmWriteSpec.unsharded());
assertEquals("/v1/table/my_table/set_lsm_write_spec/", requestPaths.get(0));
JsonNode body = MAPPER.readTree(requestBodies.get(0));
assertEquals("unsharded", body.get("sharding").get("mode").asText());
assertFalse(body.get("sharding").has("column"));
assertFalse(body.get("sharding").has("num_buckets"));
}
@Test
public void testSetLsmWriteSpecBucket() throws Exception {
enqueue("set_lsm_write_spec", 200, "");
lsm.setLsmWriteSpec(
LsmWriteSpec.bucket("id", 16).withMaintainedIndexes(Arrays.asList("id_idx")));
JsonNode body = MAPPER.readTree(requestBodies.get(0));
assertEquals("bucket", body.get("sharding").get("mode").asText());
assertEquals("id", body.get("sharding").get("column").asText());
assertEquals(16, body.get("sharding").get("num_buckets").asInt());
assertEquals(1, body.get("maintained_indexes").size());
assertEquals("id_idx", body.get("maintained_indexes").get(0).asText());
}
@Test
public void testSetLsmWriteSpecIdentity() throws Exception {
enqueue("set_lsm_write_spec", 200, "");
lsm.setLsmWriteSpec(LsmWriteSpec.identity("tenant"));
JsonNode body = MAPPER.readTree(requestBodies.get(0));
assertEquals("identity", body.get("sharding").get("mode").asText());
assertEquals("tenant", body.get("sharding").get("column").asText());
assertFalse(body.get("sharding").has("num_buckets"));
}
/**
* The tri-state that motivated a LanceDB-owned spec type: a null selection asks the server to
* resolve every maintainable index, while an empty list asks for none. They must not collapse.
*/
@Test
public void testMaintainedIndexesNullAndEmptyAreDistinctOnTheWire() throws Exception {
enqueue("set_lsm_write_spec", 200, "");
lsm.setLsmWriteSpec(LsmWriteSpec.unsharded());
JsonNode fresh = MAPPER.readTree(requestBodies.get(0));
assertTrue(fresh.has("maintained_indexes"), "the key must be present");
assertTrue(fresh.get("maintained_indexes").isNull(), "a fresh spec sends null, not []");
lsm.setLsmWriteSpec(
LsmWriteSpec.unsharded().withMaintainedIndexes(Collections.<String>emptyList()));
JsonNode none = MAPPER.readTree(requestBodies.get(1));
assertTrue(none.get("maintained_indexes").isArray());
assertEquals(0, none.get("maintained_indexes").size());
}
@Test
public void testSetLsmWriteSpecWriterConfigDefaults() throws Exception {
enqueue("set_lsm_write_spec", 200, "");
Map<String, String> defaults = new HashMap<String, String>();
defaults.put("max_memtable_rows", "50000");
lsm.setLsmWriteSpec(LsmWriteSpec.unsharded().withWriterConfigDefaults(defaults));
JsonNode body = MAPPER.readTree(requestBodies.get(0));
assertEquals("50000", body.get("writer_config_defaults").get("max_memtable_rows").asText());
}
@Test
public void testUnsetLsmWriteSpec() {
enqueue("unset_lsm_write_spec", 200, "");
lsm.unsetLsmWriteSpec();
assertEquals("/v1/table/my_table/unset_lsm_write_spec/", requestPaths.get(0));
assertEquals("", requestBodies.get(0));
}
@Test
public void testGetLsmWriteSpec() {
enqueue(
"get_lsm_write_spec",
200,
"{\"lsm_write_spec\":{\"sharding\":{\"mode\":\"bucket\",\"column\":\"id\","
+ "\"num_buckets\":16},\"maintained_indexes\":[\"id_idx\"],"
+ "\"writer_config_defaults\":{\"durable_write\":\"true\"}}}");
Optional<LsmWriteSpec> spec = lsm.getLsmWriteSpec();
assertTrue(spec.isPresent());
assertEquals(LsmWriteSpec.Sharding.BUCKET, spec.get().sharding());
assertEquals("id", spec.get().column());
assertEquals(Integer.valueOf(16), spec.get().numBuckets());
assertEquals(Arrays.asList("id_idx"), spec.get().maintainedIndexes());
assertEquals("true", spec.get().writerConfigDefaults().get("durable_write"));
}
@Test
public void testGetLsmWriteSpecAbsent() {
enqueue("get_lsm_write_spec", 200, "{\"lsm_write_spec\":null}");
assertFalse(lsm.getLsmWriteSpec().isPresent());
}
// ===========================================================================
// stats
// ===========================================================================
@Test
public void testGetLsmStats() throws Exception {
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 7L, 8L)));
Optional<LsmStats> got = lsm.getLsmStats(true);
assertEquals("/v1/table/my_table/get_lsm_stats/", requestPaths.get(0));
assertTrue(MAPPER.readTree(requestBodies.get(0)).get("include_generation_rows").asBoolean());
assertTrue(got.isPresent());
BucketStats decoded = got.get().buckets().get(0);
assertEquals("shard-0", decoded.shardId());
assertEquals("Active", decoded.status());
assertEquals(1, decoded.writerEpoch());
assertEquals(2, decoded.manifestVersion());
assertEquals(9, decoded.currentGeneration());
assertFalse(decoded.compacting());
assertEquals(Arrays.asList(7L, 8L), generationNumbers(decoded));
assertEquals(1024, decoded.generations().get(0).bytes());
assertFalse(decoded.generations().get(0).rows().isPresent(), "rows absent unless requested");
assertFalse(decoded.memtables().isPresent(), "absent memtables stay absent");
}
/** The optional fields decode when the server does send them. */
@Test
public void testGetLsmStatsDecodesOptionalFields() {
enqueue(
"get_lsm_stats",
200,
"{\"lsm_stats\":{\"buckets\":[{\"shard_id\":\"shard-0\",\"status\":\"Active\","
+ "\"writer_epoch\":1,\"manifest_version\":2,\"current_generation\":9,"
+ "\"replay_after_wal_entry_position\":3,\"wal_entry_position_last_seen\":11,"
+ "\"generations\":[{\"generation\":7,\"bytes\":1024,\"rows\":42}],"
+ "\"compacting\":true,\"memtables\":[{\"generation\":8,\"rows\":5,"
+ "\"bytes\":64,\"batches\":2,\"indexes\":[\"id_idx\"]}]}]}}");
BucketStats decoded = lsm.getLsmStats(true).get().buckets().get(0);
assertEquals(3, decoded.replayAfterWalEntryPosition());
assertEquals(11, decoded.walEntryPositionLastSeen());
assertTrue(decoded.compacting());
assertEquals(42, decoded.generations().get(0).rows().getAsLong());
assertTrue(decoded.memtables().isPresent());
MemtableStats memtable = decoded.memtables().get().get(0);
assertEquals(8, memtable.generation());
assertEquals(5, memtable.rows());
assertEquals(64, memtable.bytes());
assertEquals(2, memtable.batches());
assertEquals(Arrays.asList("id_idx"), memtable.indexes());
}
@Test
public void testGetLsmStatsAbsentWhenLsmDisabled() {
enqueue("get_lsm_stats", 200, "{\"lsm_stats\":null}");
assertFalse(lsm.getLsmStats().isPresent());
}
@Test
public void testGetLsmStatsDefaultsToExcludingGenerationRows() throws Exception {
enqueue("get_lsm_stats", 200, stats());
lsm.getLsmStats();
assertFalse(MAPPER.readTree(requestBodies.get(0)).get("include_generation_rows").asBoolean());
}
// ===========================================================================
// flush / compact
// ===========================================================================
@Test
public void testFlushAndCompactRoutes() {
enqueue("flush_lsm", 200, "");
enqueue("compact_lsm", 200, "");
lsm.flushLsm();
lsm.compactLsm();
assertEquals("/v1/table/my_table/flush_lsm/", requestPaths.get(0));
assertEquals("/v1/table/my_table/compact_lsm/", requestPaths.get(1));
}
@Test
public void testHttpErrorCarriesStatus() {
enqueue("flush_lsm", 404, "no such table");
LanceDbRestClient.HttpException e =
assertThrows(LanceDbRestClient.HttpException.class, () -> lsm.flushLsm());
assertEquals(404, e.statusCode());
}
// ===========================================================================
// checkpoint
// ===========================================================================
@Test
public void testCheckpointReturnsWhenLsmDisabled() {
enqueue("flush_lsm", 200, "");
enqueue("get_lsm_stats", 200, "{\"lsm_stats\":null}");
lsm.checkpointLsm();
assertEquals(0, countCalls("compact_lsm"), "nothing to compact when the LSM path is off");
}
@Test
public void testCheckpointReturnsWhenNoGenerationsOutstanding() {
enqueue("flush_lsm", 200, "");
// A bucket with no L0 generations yields no target, so the drain never starts.
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false)));
lsm.checkpointLsm();
assertEquals(0, countCalls("compact_lsm"));
}
@Test
public void testCheckpointConvergesOnceTargetGenerationsAreGone() {
enqueue("flush_lsm", 200, "");
// Watermark read: shard-0 holds generations 7 and 8, so target = 8.
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 7L, 8L)));
// First drain poll: both still outstanding, nothing compacting -> dispatch a pass.
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 7L, 8L)));
// Second drain poll: drained past the target -> done.
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 9L)));
enqueue("compact_lsm", 200, "");
lsm.checkpointLsm();
assertEquals(1, countCalls("compact_lsm"), "one pass dispatched");
assertEquals(3, countCalls("get_lsm_stats"), "watermark read plus two drain polls");
}
@Test
public void testCheckpointDoesNotPileOnWhileEveryTargetBucketIsCompacting() {
enqueue("flush_lsm", 200, "");
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", true, 4L)));
// Still compacting on the first poll, so no pass is dispatched; then it drains.
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", true, 4L)));
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 5L)));
lsm.checkpointLsm();
assertEquals(0, countCalls("compact_lsm"), "a latched bucket is left alone");
}
@Test
public void testCheckpointRetriesFromFlushAfterLostClaim() {
// 421 on the watermark read: the node lost its claim, so the whole thing restarts
// from flush rather than retrying the read in place.
enqueue("flush_lsm", 200, "");
enqueue("get_lsm_stats", 421, "no claim");
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false)));
lsm.checkpointLsm();
assertEquals(2, countCalls("flush_lsm"), "re-issued from flush");
}
@Test
public void testCheckpointRetriesRetryableStatusInPlace() {
enqueue("flush_lsm", 429, "latch held");
enqueue("flush_lsm", 200, "");
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false)));
lsm.checkpointLsm();
assertEquals(2, countCalls("flush_lsm"), "429 retried in place, not re-issued");
}
@Test
public void testCheckpointPropagatesTerminalStatus() {
enqueue("flush_lsm", 400, "bad request");
LanceDbRestClient.HttpException e =
assertThrows(LanceDbRestClient.HttpException.class, () -> lsm.checkpointLsm());
assertEquals(400, e.statusCode());
assertEquals(1, countCalls("flush_lsm"), "a terminal status is not retried");
}
@Test
public void testCheckpointGivesUpAfterRepeatedLostClaims() {
enqueue("flush_lsm", 421, "no claim");
IllegalStateException e = assertThrows(IllegalStateException.class, () -> lsm.checkpointLsm());
assertTrue(e.getMessage().contains("kept losing its claim"), e.getMessage());
assertEquals(4, countCalls("flush_lsm"), "the initial attempt plus MAX_REISSUES");
}
// ===========================================================================
// strict decoding
// ===========================================================================
/**
* A stats payload that does not decode must fail closed. Every one of these bodies used to be
* read as "no buckets", which is indistinguishable from a drained table, so {@code checkpointLsm}
* reported convergence for a checkpoint that never ran.
*/
@Test
public void testCheckpointRejectsMalformedStats() {
Map<String, String> malformed = new LinkedHashMap<String, String>();
malformed.put("no response body at all", "");
malformed.put("stats object with no buckets", "{\"lsm_stats\":{}}");
malformed.put("bucket missing its required fields", "{\"lsm_stats\":{\"buckets\":[{}]}}");
malformed.put(
"bucket missing generations",
"{\"lsm_stats\":{\"buckets\":[{\"shard_id\":\"shard-0\",\"status\":\"Active\","
+ "\"writer_epoch\":1,\"manifest_version\":2,\"current_generation\":9,"
+ "\"replay_after_wal_entry_position\":0,\"wal_entry_position_last_seen\":0,"
+ "\"compacting\":false}]}}");
malformed.put(
"generation with a non-numeric generation number",
"{\"lsm_stats\":{\"buckets\":[{\"shard_id\":\"shard-0\",\"status\":\"Active\","
+ "\"writer_epoch\":1,\"manifest_version\":2,\"current_generation\":9,"
+ "\"replay_after_wal_entry_position\":0,\"wal_entry_position_last_seen\":0,"
+ "\"generations\":[{\"generation\":\"7\",\"bytes\":1024}],"
+ "\"compacting\":false}]}}");
for (Map.Entry<String, String> each : malformed.entrySet()) {
setUpFresh();
enqueue("flush_lsm", 200, "");
enqueue("get_lsm_stats", 200, each.getValue());
assertThrows(
IllegalStateException.class,
() -> lsm.checkpointLsm(),
each.getKey() + " must not report convergence");
}
}
/** The one shape that legitimately means "this table has no LSM write path". */
@Test
public void testCheckpointTreatsNullStatsAsNotWalBacked() {
enqueue("flush_lsm", 200, "");
enqueue("get_lsm_stats", 200, "{\"lsm_stats\":null}");
lsm.checkpointLsm();
assertEquals(1, countCalls("get_lsm_stats"));
}
// ===========================================================================
// retry budget
// ===========================================================================
/**
* The transport must not retry on the checkpoint loop's behalf. Apache HttpClient's default
* strategy retries exactly 429 and 503 the two statuses {@code isRetryable} owns which
* doubled every budget here and also retried {@code compact_lsm} in place, where the loop is
* built to fall through to a fresh stats poll instead.
*/
@Test
public void testCheckpointRetryBudgetIsNotDoubledByTheTransport() {
enqueue("flush_lsm", 429, "latch held");
LanceDbRestClient.HttpException e =
assertThrows(LanceDbRestClient.HttpException.class, () -> lsm.checkpointLsm());
assertEquals(429, e.statusCode(), "the exhausted budget propagates the last error as itself");
assertEquals(9, countCalls("flush_lsm"), "the initial request plus MAX_RETRIES, and no more");
}
// ===========================================================================
// harness
// ===========================================================================
private static List<Long> generationNumbers(BucketStats bucket) {
List<Long> numbers = new ArrayList<Long>();
for (GenerationStats generation : bucket.generations()) {
numbers.add(generation.generation());
}
return numbers;
}
/** Build an {@code lsm_stats} response body from bucket fragments. */
private static String stats(String... buckets) {
return "{\"lsm_stats\":{\"buckets\":[" + String.join(",", buckets) + "]}}";
}
private static String bucket(String shardId, boolean compacting, Long... generations) {
StringBuilder gens = new StringBuilder();
for (Long generation : generations) {
if (gens.length() > 0) {
gens.append(",");
}
gens.append("{\"generation\":").append(generation).append(",\"bytes\":1024}");
}
return "{\"shard_id\":\""
+ shardId
+ "\",\"status\":\"Active\",\"writer_epoch\":1,\"manifest_version\":2,"
+ "\"current_generation\":9,\"replay_after_wal_entry_position\":0,"
+ "\"wal_entry_position_last_seen\":0,\"generations\":["
+ gens
+ "],\"compacting\":"
+ compacting
+ "}";
}
/** Queue a reply for an operation. The last queued reply repeats once the queue drains. */
private void enqueue(String operation, int status, String body) {
replies.computeIfAbsent(operation, key -> new ArrayDeque<Reply>()).add(new Reply(status, body));
}
private Reply nextReply(String path) {
String operation = operationOf(path);
Deque<Reply> queued = replies.get(operation);
if (queued == null || queued.isEmpty()) {
return new Reply(200, "");
}
return queued.size() > 1 ? queued.poll() : queued.peek();
}
private long countCalls(String operation) {
return requestPaths.stream().filter(path -> operationOf(path).equals(operation)).count();
}
/** {@code /v1/table/my_table/flush_lsm/} -> {@code flush_lsm}. */
private static String operationOf(String path) {
String[] segments = path.split("/");
return segments.length == 0 ? "" : segments[segments.length - 1];
}
private static String readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buffer = new byte[4096];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
private static final class Reply {
private final int status;
private final String body;
private Reply(int status, String body) {
this.status = status;
this.body = body;
}
}
}

Some files were not shown because too many files have changed in this diff Show More