Compare commits

..

29 Commits

Author SHA1 Message Date
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
LanceDB Robot bc8674ab22 chore!: update lance dependency to v9.0.0-rc.1 (#3673)
BREAKING CHANGE: splits generated by the permutation data loader will
not be the same, due to a change in hash function.

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

---------

Co-authored-by: Will Jones <willjones127@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 10:31:30 -07:00
Jack Ye 37032151d3 feat: support distributed analyze plan metrics in clients (#3675)
Adds client-side support for analyze_plan distributed metrics modes
across Rust, Python, and TypeScript clients. Defaults to aggregate for
backward compatibility and sends the remote distributed_metrics
parameter only when a non-default mode is requested.
2026-07-15 21:21:40 -07:00
Dan Tasse 00c4a7b843 chore: consolidate skills into one (#3672)
Consolidating skills so we have only one `lancedb` skill, making it
easier to install and work with, vs. installing and using different
skills for "lancedb-column-metadata", "lancedb-branch-ops", etc.

Also deleted lancedb-connect, because the new monoskill uses the
python/TS APIs so it doesn't need extra handholding to connect to the
REST API.

## does it work?

Test 1: do some column metadata operations with 1. no skills, 2. our
previous baseline lancedb skill, 3. the baseline lancedb skill with the
lancedb-column-metadata skill folded in:

```
┌───────────────────────────┬──────────────────────┬────────────────────┬───────────────────────┐
│           eval            │       no-skill       │ lancedb (original) │ lancedb2-incl-columns │
├───────────────────────────┼──────────────────────┼────────────────────┼───────────────────────┤
│ 2-add-all-metadata-types  │ 2.5/5 · 116s · $0.44 │ 0/5 · 154s · $0.60 │ 5/5 · 58s · $0.26     │
├───────────────────────────┼──────────────────────┼────────────────────┼───────────────────────┤
│ 3-delete-one-metadata-key │ 4/4 · 67s · $0.23    │ 4/4 · 100s · $0.45 │ 4/4 · 45s · $0.20     │
├───────────────────────────┼──────────────────────┼────────────────────┼───────────────────────┤
│ TOTAL (per rep avg)       │ 6.5/9 · 183s · $0.66 │ 4/9 · 254s · $1.05 │ 9/9 · 103s · $0.46    │
└───────────────────────────┴──────────────────────┴────────────────────┴───────────────────────┘
```
without column-metadata-specific content, it failed because it wrote
keys like `description` instead of `lancedb:description`. That's pretty
undiscoverable without the skill.


Test 2: do some simple branch operations with 1. no skills, 2. our
previous baseline lancedb skill, 3. the combined skill (in this PR):
```
┌───────────────────────────┬────────────────────┬────────────────────┬────────────────────────────────┐
│           eval            │      no-skill      │ lancedb (original) │ lancedb3-incl-columns-branches │
├───────────────────────────┼────────────────────┼────────────────────┼────────────────────────────────┤
│ 5-create-branch           │ 2/2 · 64s · $0.30  │ 2/2 · 57s · $0.34  │ 2/2 · 38s · $0.22              │
├───────────────────────────┼────────────────────┼────────────────────┼────────────────────────────────┤
│ 6-delete-branch           │ 2/2 · 38s · $0.21  │ 2/2 · 44s · $0.27  │ 2/2 · 40s · $0.22              │
├───────────────────────────┼────────────────────┼────────────────────┼────────────────────────────────┤
│ 7-switch-branch-and-write │ 1/2 · 96s · $0.49  │ 1/2 · 106s · $0.58 │ 2/2 · 66s · $0.40              │
├───────────────────────────┼────────────────────┼────────────────────┼────────────────────────────────┤
│ TOTAL (per rep avg)       │ 5/6 · 199s · $1.00 │ 5/6 · 207s · $1.20 │ 6/6 · 143s · $0.83             │
└───────────────────────────┴────────────────────┴────────────────────┴────────────────────────────────┘
```

Test 3: run everything, with the lancedb (original) skill,
lancedb(original) + all the separate skills, and
lancedb3-incl-columns-branches
```
┌────────────────────────────────┬──────────────────────┬────────────────────────────────┬─────────────────────────────────────────────────────────────────┐
│              eval              │  lancedb (original)  │ lancedb3-incl-columns-branches │ all-separate (lancedb + connect + column-metadata + branch-ops) │
├────────────────────────────────┼──────────────────────┼────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 1-pick-column-for-image-search │ 2/2 · 141s · $0.67   │ 2/2 · 128s · $0.43             │ 1/2 · 257s · $0.81                                              │
├────────────────────────────────┼──────────────────────┼────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 2-add-all-metadata-types       │ 5/5 · 93s · $0.52    │ 5/5 · 54s · $0.30              │ 5/5 · 40s · $0.24                                               │
├────────────────────────────────┼──────────────────────┼────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 3-delete-one-metadata-key      │ 4/4 · 84s · $0.44    │ 4/4 · 36s · $0.24              │ 4/4 · 28s · $0.20                                               │
├────────────────────────────────┼──────────────────────┼────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 4-build-index                  │ 3/3 · 275s · $0.52   │ 3/3 · 84s · $0.39              │ 3/3 · 68s · $0.50                                               │
├────────────────────────────────┼──────────────────────┼────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 5-create-branch                │ 2/2 · 39s · $0.31    │ 2/2 · 41s · $0.20              │ 2/2 · 15s · $0.17                                               │
├────────────────────────────────┼──────────────────────┼────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 6-delete-branch                │ 2/2 · 59s · $0.25    │ 2/2 · 69s · $0.29              │ 2/2 · 26s · $0.17                                               │
├────────────────────────────────┼──────────────────────┼────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ 7-switch-branch-and-write      │ 1/2 · 84s · $0.46    │ 2/2 · 62s · $0.40              │ 2/2 · 27s · $0.23                                               │
├────────────────────────────────┼──────────────────────┼────────────────────────────────┼─────────────────────────────────────────────────────────────────┤
│ TOTAL                          │ 19/20 · 774s · $3.17 │ 20/20 · 474s · $2.24           │ 19/20 · 462s · $2.32                                            │
└────────────────────────────────┴──────────────────────┴────────────────────────────────┴─────────────────────────────────────────────────────────────────┘
```

("lancedb3-incl-columns-branches" is the combined skill in this PR,
all-separate is using the four separate skills.)

For overall performance, it helps to have the specialized skills for
metadata and branching; doesn't really matter whether they're separate
skills or all together. Also doesn't matter much whether it's REST or
Python. So let's merge these skills to make it easier for users.
2026-07-15 16:54:23 -04:00
LanceDB Robot 1773fb2239 chore: update lance dependency to v9.0.0-beta.24 (#3667)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v9.0.0-beta.24.

Lance tag:
https://github.com/lance-format/lance/releases/tag/v9.0.0-beta.24
2026-07-15 12:42:29 -05:00
Lance Release 8a4eaaa8b9 Bump version: 0.32.0-beta.1 → 0.32.0-beta.2 2026-07-14 23:28:32 +00:00
128 changed files with 5561 additions and 967 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"
}
]
}
+4
View File
@@ -5,3 +5,7 @@ This directory contains repo-scoped code agent skills for the LanceDB project.
Each skill is a folder that contains a required `SKILL.md` and optional bundled resources.
Codex discovers skills from `.agents/skills` in the current working directory and parent directories.
The `lancedb` skill lives in the `plugins/lancedb` plugin (see `plugins/lancedb/skills/lancedb`)
so it can be installed via the plugin marketplaces (`.claude-plugin/marketplace.json` and
`.agents/plugins/marketplace.json`); the `lancedb` entry here is a symlink into that plugin.
+1
View File
@@ -0,0 +1 @@
../../plugins/lancedb/skills/lancedb
-145
View File
@@ -1,145 +0,0 @@
---
name: lancedb-branch-ops
description: >-
Manage LanceDB table branches through the REST API: list, create, and delete
branches; target schema reads, field-metadata updates, and index creation to a
named branch; and verify that branch changes remain isolated from main. Use
when a task involves branch lifecycle, an experimental or isolated table
version, directing an operation to a non-main branch, or confirming that a
mutation did not affect main. This skill also explains that LanceDB has no
checkout operation; each request selects its target branch in the request
body.
---
## Goal
Manage branches on a LanceDB table: list what exists, create new ones, delete stale ones, and direct read/write operations at a specific branch without touching main.
## Step 0: Establish the connection
Use the `lancedb-connect` skill to resolve the base URL and auth headers (`x-api-key`, `x-lancedb-database`). Skip this only if the connection is already known from the current conversation.
All examples below use `{base_url}` — substitute the resolved endpoint and include the auth headers on every request.
## The branch model (important)
LanceDB branches are named snapshots that diverge from the table's current state at creation time. There is **no checkout command** — you never switch the whole table to a branch. Instead, you **pass `"branch": "<name>"` in the request body** of any operation to target that branch. Omitting the key (or sending an empty body) always targets main.
`branches/list` returns only non-main branches. Main always exists and is not listed.
## List branches
```http
POST {base_url}/v1/table/{table_id}/branches/list
Content-Type: application/json
{}
```
Response:
```json
{
"branches": {
"experiment-reindex": {"parentVersion": 1, "createAt": 1782506085, "manifestSize": 1029}
}
}
```
If `branches` is `{}`, the table has no branches besides main.
## Create a branch
```http
POST {base_url}/v1/table/{table_id}/branches/create
Content-Type: application/json
{"name": "experiment-reindex"}
```
HTTP 200 with `{}` body = success. The branch is created off the table's current state on main.
Verify by calling `branches/list` and confirming the new name appears.
## Delete a branch
```http
POST {base_url}/v1/table/{table_id}/branches/delete
Content-Type: application/json
{"name": "stale-2024"}
```
HTTP 200 with `{}` body = success. Only the branch pointer is removed — main and all row data remain intact.
Verify by calling `branches/list` (name gone) and `describe` with no branch param (main still responds).
## Operate on a specific branch
Pass `"branch": "<name>"` in the body of any operation to scope it to that branch:
**Read schema on a branch:**
```http
POST {base_url}/v1/table/{table_id}/describe
Content-Type: application/json
{"branch": "wip-branch"}
```
**Write metadata to a branch (not main):**
```http
POST {base_url}/v1/table/{table_id}/update_field_metadata
Content-Type: application/json
{
"branch": "wip-branch",
"updates": [
{
"path": "category",
"metadata": {"lancedb:description": "Product category label."},
"replace": false
}
]
}
```
**Build an index on a branch:**
```http
POST {base_url}/v1/table/{table_id}/create_index
Content-Type: application/json
{
"branch": "wip-branch",
"column": "category",
"index_type": "BTREE"
}
```
## Verifying isolation
After writing to a branch, always confirm the change did NOT land on main:
```bash
# Should show the new metadata
curl -s -X POST {base_url}/v1/table/{table_id}/describe \
-H "x-api-key: <key>" -H "x-lancedb-database: <db>" \
-H "content-type: application/json" \
-d '{"branch": "wip-branch"}'
# Should NOT show the new metadata
curl -s -X POST {base_url}/v1/table/{table_id}/describe \
-H "x-api-key: <key>" -H "x-lancedb-database: <db>" \
-H "content-type: application/json" \
-d '{}'
```
## Quick reference
| Goal | Endpoint | Body |
|------|----------|------|
| List all branches | `branches/list` | `{}` |
| Create a branch | `branches/create` | `{"name": "..."}` |
| Delete a branch | `branches/delete` | `{"name": "..."}` |
| Read schema on branch | `describe` | `{"branch": "..."}` |
| Write metadata on branch | `update_field_metadata` | `{"branch": "...", "updates": [...]}` |
| Build index on branch | `create_index` | `{"branch": "...", "column": ..., "index_type": ...}` |
| Target main (default) | any endpoint | omit `"branch"` key |
@@ -1,178 +0,0 @@
---
name: lancedb-column-metadata
description: Column metadata authoring for LanceDB tables via the REST API. This skill is required for tasks like writing field descriptions, setting tags on columns (field_type, model, project_id, version), classifying columns as embeddings vs labels vs eval metrics, or grouping versioned columns into logical families — because it has the API integration needed to read the schema and persist metadata back. Invoke whenever someone wants to document, annotate, tag, or classify what their table columns ARE. Trigger even without an explicit "LanceDB" mention, as long as the context is column-level documentation or tagging for an ML or vector database table.
metadata:
short-description: Write column descriptions, tags, and logical groupings to a LanceDB table
---
## Overview
This skill authors column-level metadata for a LanceDB table. It connects to a LanceDB deployment over its REST API, inspects the table schema, generates appropriate metadata, and writes it back.
## Step 0: Establish the connection
Use the `lancedb-connect` skill (invoke it via the Skill tool) to resolve the base URL and auth headers (`x-api-key`, `x-lancedb-database`) for whichever deployment the user is working against — enterprise/self-hosted or a local dev server. Skip it only if the connection details are already established in the conversation.
All examples below use `{base_url}` — substitute the resolved endpoint and include the resolved headers on every request.
## Metadata keys
All metadata uses namespaced keys:
| Key | Purpose | Example value |
|-----|---------|---------------|
| `lancedb:description` | Human-readable explanation of what the column contains | `"CLIP ViT-L/14 image embedding, L2-normalized (768-dim)"` |
| `lancedb:tag:<name>` | Flexible key-value tag; the suffix names the tag category | `lancedb:tag:field_type: "embedding"`, `lancedb:tag:model: "clip"`, `lancedb:tag:project_id: "foo"` |
| `lancedb:logical-column` | Logical group/family this column belongs to | `"clip_features"` |
Tags are open-ended — use whatever key suffix and value make sense given the user's intent. The tag suffix should describe *what is being classified* (e.g., `field_type`, `model`, `project_id`) and the value describes *how*.
## Step 1: Resolve the table identifier
You need:
- **Table name** (required) — e.g., `my_table` or `my_namespace.my_table`
- **Database name** — ask if not provided and not inferable from context; it goes in the `x-lancedb-database` header, never in the URL path
The table identifier in the URL path is typically `table_name` for a top-level table, or `namespace$table_name` if the table lives in a namespace. The API accepts a `delimiter` query parameter to parse compound identifiers (default `$`).
## Step 2: Describe the table
```http
POST {base_url}/v1/table/{table_id}/describe
Content-Type: application/json
{}
```
The response contains `schema.fields` — an array of field objects:
```json
{
"schema": {
"fields": [
{
"name": "clip_embedding_v3",
"type": { "type": "FixedSizeList", "fields": [...], "listSize": 768 },
"nullable": true,
"metadata": { "lancedb:description": "..." }
}
]
}
}
```
Each field has:
- `name` — field name
- `type` — Arrow data type (check `type.type` for the type string)
- `nullable` — boolean
- `metadata` — existing key-value metadata (read this before writing to avoid redundant updates)
For struct/nested fields, recurse into `type.fields` and represent them as dot-notation paths (e.g., `parent.child`).
If the user hasn't specified which columns to update, work with all columns.
## Step 3: Generate metadata
Decide what to generate based on the user's request.
### Writing 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."`
### Tagging columns (`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.
Multiple tags on the same column are fine — each is a separate key.
### Grouping into logical columns (`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 4: Write the metadata
```http
POST {base_url}/v1/table/{table_id}/update_field_metadata
Content-Type: application/json
{
"updates": [
{
"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"
},
"replace": false
},
{
"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"
},
"replace": false
}
]
}
```
Rules:
- **Use `"replace": false`** (merge) by default — this preserves existing metadata the user didn't ask to change
- Use `"replace": true` only if the user explicitly asks to overwrite all existing metadata on a column
- Set a value to `null` to delete a specific key
- Batch all updates in a single request when possible
The response includes `version` (new table version) and `fields` (the updated metadata per field).
## Step 5: Confirm
Report back:
- Which columns were updated and what was written
- The new table version number
- Any columns skipped (e.g., already had up-to-date metadata)
---
## Quick examples
**"Write descriptions for all columns in the `product_embeddings` table"**
1. POST `/v1/table/product_embeddings/describe` → get all fields
2. Generate a `lancedb:description` for each column based on name + type
3. POST `update_field_metadata` with descriptions
4. Report
**"Tag the columns in `model_outputs` with their field type and model"**
1. Describe `model_outputs`
2. For each field, classify by name + Arrow type → set `lancedb:tag:field_type` and `lancedb:tag:model` where applicable
3. POST `update_field_metadata`
4. Report
**"Group the feature columns in `training_features` into logical families and mark the latest version"**
1. Describe the table
2. Find version patterns → assign `lancedb:logical-column` and `lancedb:tag:version`; mark newest with `lancedb:tag:latest: "true"`
3. POST `update_field_metadata`
4. Show the grouping
-42
View File
@@ -1,42 +0,0 @@
---
name: lancedb-connect
description: Resolve how to connect to a LanceDB deployment over the REST API — figure out the base URL, API key, and database header. Use this before making any REST requests to a LanceDB table, whenever the endpoint or auth setup is not already known. Also useful on its own when someone asks how to connect, authenticate, or curl their LanceDB instance.
metadata:
short-description: Resolve the base URL and auth headers for a LanceDB deployment
---
## Goal
Produce two things every REST request needs:
1. **Base URL** — the endpoint
2. **Headers**`x-api-key`, and usually `x-lancedb-database`
## Resolution steps
1. If the user already gave a URL and API key (or said which environment they're working against), use that.
2. Otherwise, look for credentials already available in the environment:
- Env vars like `LANCEDB_URI` / `LANCEDB_HOST` / `LANCEDB_API_KEY`
- A LanceDB endpoint already running or port-forwarded locally (the REST default port is 2333, i.e. `http://localhost:2333`)
3. If you didn't find both pieces, ask the user directly: **"What's your LanceDB endpoint's URL, and what's your API key?"** Also ask which database to use if it isn't obvious. Don't guess or probe further — the user knows their deployment.
## Validating the connection
Make a cheap authenticated request and check the status:
```bash
curl -s -w "\n%{http_code}" "{base_url}/v1/table/?limit=1" \
-H "x-api-key: <key>" \
-H "x-lancedb-database: <database>"
```
- `200` — connection, key, and database header all good
- `401` — API key missing or wrong
- `400` mentioning a database header — this deployment expects `x-lancedb-database`
## Non-REST equivalents
If the caller would rather use the SDK or CLI than raw REST, the same credentials work:
- Python SDK: `lancedb.connect("db://<database>", api_key="<key>", host_override="<base_url>")`
- `lancedb` CLI: a `[profiles.<name>]` entry in `~/.lancedb/config.toml` with `http_server_url`, `api_key`, `database`
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "0.32.0-beta.1"
current_version = "0.32.0-beta.2"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
+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"
}
]
}
+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 -1
View File
@@ -87,7 +87,7 @@ jobs:
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 }}
+23 -4
View File
@@ -22,7 +22,7 @@ permissions:
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 +31,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
@@ -52,11 +69,13 @@ jobs:
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')
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
@@ -145,7 +164,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
+2 -2
View File
@@ -98,7 +98,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.
@@ -158,7 +158,7 @@ jobs:
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"]
Generated
+204 -170
View File
File diff suppressed because it is too large Load Diff
+23 -24
View File
@@ -13,20 +13,20 @@ categories = ["database-implementations"]
rust-version = "1.91.0"
[workspace.dependencies]
lance = { "version" = "=9.0.0-beta.23", default-features = false, "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=9.0.0-beta.23", default-features = false, "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=9.0.0-beta.23", default-features = false, "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=9.0.0-beta.23", "tag" = "v9.0.0-beta.23", "git" = "https://github.com/lance-format/lance.git" }
lance = { "version" = "=10.0.0-beta.3", default-features = false, "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=10.0.0-beta.3", default-features = false, "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=10.0.0-beta.3", default-features = false, "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
ahash = "0.8"
# Note that this one does not include pyarrow
arrow = { version = "58.0.0", optional = false }
@@ -39,15 +39,15 @@ arrow-schema = "58.0.0"
arrow-select = "58.0.0"
arrow-cast = "58.0.0"
async-trait = "0"
datafusion = { version = "53.0.0", default-features = false }
datafusion-catalog = "53.0.0"
datafusion-common = { version = "53.0.0", default-features = false }
datafusion-execution = "53.0.0"
datafusion-expr = "53.0.0"
datafusion-functions = "53.0.0"
datafusion-physical-plan = "53.0.0"
datafusion-physical-expr = "53.0.0"
datafusion-sql = "53.0.0"
datafusion = { version = "54.0.0", default-features = false }
datafusion-catalog = "54.0.0"
datafusion-common = { version = "54.0.0", default-features = false }
datafusion-execution = "54.0.0"
datafusion-expr = "54.0.0"
datafusion-functions = "54.0.0"
datafusion-physical-plan = "54.0.0"
datafusion-physical-expr = "54.0.0"
datafusion-sql = "54.0.0"
env_logger = "0.11"
half = { "version" = "2.7.1", default-features = false, features = [
"num-traits",
@@ -64,7 +64,6 @@ snafu = "0.8"
url = "2"
num-traits = "0.2"
regex = "1.10"
lazy_static = "1"
semver = "1.0.25"
chrono = "0.4"
+165 -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.1</version>
<version>0.32.0-beta.2</version>
</dependency>
```
@@ -249,6 +249,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 +482,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 +571,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));
+43
View File
@@ -83,6 +83,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
@@ -94,3 +112,28 @@ List all branches, mapping name to branch metadata.
#### Returns
`Promise`&lt;`Record`&lt;`string`, [`BranchContents`](BranchContents.md)&gt;&gt;
***
### merge()
```ts
merge(fromBranch, dryRun): Promise<MergeBranchResult>
```
Merge a branch into main.
Set `dryRun` to `true` to preview the merge. A rejected merge resolves
with `status: "rejected"` instead of throwing.
#### Parameters
* **fromBranch**: `string`
Branch to merge from.
* **dryRun**: `boolean` = `false`
When true, only preview the merge. Defaults to false.
#### Returns
`Promise`&lt;[`MergeBranchResult`](../interfaces/MergeBranchResult.md)&gt;
+7 -1
View File
@@ -33,7 +33,7 @@ protected inner: Query | Promise<Query>;
### analyzePlan()
```ts
analyzePlan(): Promise<string>
analyzePlan(distributedMetrics?): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
@@ -41,6 +41,12 @@ Executes the query and returns the physical query plan annotated with runtime me
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;
+7 -1
View File
@@ -38,7 +38,7 @@ protected inner: NativeQueryType | Promise<NativeQueryType>;
### analyzePlan()
```ts
analyzePlan(): Promise<string>
analyzePlan(distributedMetrics?): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
@@ -46,6 +46,12 @@ Executes the query and returns the physical query plan annotated with runtime me
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;
+7 -1
View File
@@ -29,7 +29,7 @@ protected inner: TakeQuery | Promise<TakeQuery>;
### analyzePlan()
```ts
analyzePlan(): Promise<string>
analyzePlan(distributedMetrics?): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
@@ -37,6 +37,12 @@ Executes the query and returns the physical query plan annotated with runtime me
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;
+7 -1
View File
@@ -51,7 +51,7 @@ addQueryVector(vector): VectorQuery
### analyzePlan()
```ts
analyzePlan(): Promise<string>
analyzePlan(distributedMetrics?): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
@@ -59,6 +59,12 @@ Executes the query and returns the physical query plan annotated with runtime me
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;
+9
View File
@@ -52,6 +52,11 @@
- [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)
- [ClientConfig](interfaces/ClientConfig.md)
- [ColumnAlteration](interfaces/ColumnAlteration.md)
- [ColumnOrdering](interfaces/ColumnOrdering.md)
@@ -86,6 +91,9 @@
- [ListNamespacesOptions](interfaces/ListNamespacesOptions.md)
- [ListNamespacesResponse](interfaces/ListNamespacesResponse.md)
- [LsmWriteSpec](interfaces/LsmWriteSpec.md)
- [MergeBlocker](interfaces/MergeBlocker.md)
- [MergeBranchResult](interfaces/MergeBranchResult.md)
- [MergePreview](interfaces/MergePreview.md)
- [MergeResult](interfaces/MergeResult.md)
- [NativeOAuthConfig](interfaces/NativeOAuthConfig.md)
- [OAuthConfig](interfaces/OAuthConfig.md)
@@ -118,6 +126,7 @@
## Type Aliases
- [AnalyzePlanDistributedMetrics](type-aliases/AnalyzePlanDistributedMetrics.md)
- [BaseTokenizer](type-aliases/BaseTokenizer.md)
- [Data](type-aliases/Data.md)
- [DataLike](type-aliases/DataLike.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;
```
+129
View File
@@ -0,0 +1,129 @@
[**@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[];
```
***
### fromBranch
```ts
fromBranch: string;
```
***
### mainVersion
```ts
mainVersion: number;
```
***
### mergeBlockers
```ts
mergeBlockers: MergeBlocker[];
```
***
### mergeable
```ts
mergeable: boolean;
```
***
### 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;
```
+13
View File
@@ -43,6 +43,19 @@ 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.
***
### language?
```ts
+25
View File
@@ -0,0 +1,25 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MergeBlocker
# Interface: MergeBlocker
A reason why a branch cannot currently be merged.
## Properties
### code
```ts
code: string;
```
***
### message
```ts
message: string;
```
@@ -0,0 +1,46 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MergeBranchResult
# Interface: MergeBranchResult
Result of previewing or attempting a branch merge.
## Properties
### diff
```ts
diff: BranchDiff;
```
***
### mainVersionAfter?
```ts
optional mainVersionAfter: number;
```
***
### preview
```ts
preview: MergePreview;
```
***
### status
```ts
status:
| "unknown"
| "rejected"
| "ready"
| "notImplemented"
| "merged";
```
+17
View File
@@ -0,0 +1,17 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MergePreview
# Interface: MergePreview
Changes that would be, or were, promoted by a branch merge.
## Properties
### promotedColumns
```ts
promotedColumns: string[];
```
@@ -0,0 +1,11 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / AnalyzePlanDistributedMetrics
# Type Alias: AnalyzePlanDistributedMetrics
```ts
type AnalyzePlanDistributedMetrics: "aggregate" | "per_worker" | "full";
```
+1 -1
View File
@@ -8,7 +8,7 @@
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.32.0-beta.1</version>
<version>0.32.0-beta.2</version>
<relativePath>../pom.xml</relativePath>
</parent>
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.32.0-beta.1</version>
<version>0.32.0-beta.2</version>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>LanceDB Java SDK Parent POM</description>
@@ -28,7 +28,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<arrow.version>15.0.0</arrow.version>
<lance-core.version>9.0.0-beta.23</lance-core.version>
<lance-core.version>10.0.0-beta.3</lance-core.version>
<spotless.skip>false</spotless.skip>
<spotless.version>2.30.0</spotless.version>
<spotless.java.googlejavaformat.version>1.7</spotless.java.googlejavaformat.version>
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "lancedb-nodejs"
edition.workspace = true
version = "0.32.0-beta.1"
version = "0.32.0-beta.2"
publish = false
license.workspace = true
description.workspace = true
+53
View File
@@ -52,6 +52,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
Float64,
Struct,
List,
Map_,
Int16,
Int32,
Int64,
@@ -69,6 +70,30 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
type Schema = ApacheArrow["Schema"];
type Table = ApacheArrow["Table"];
function expectValidMapField(
// biome-ignore lint/suspicious/noExplicitAny: Arrow Field types vary across supported versions
field: any,
): void {
expect(DataType.isMap(field.type)).toBe(true);
expect(field.type.keysSorted).toBe(true);
expect(field.type.children).toHaveLength(1);
const entries = field.type.children[0];
expect(entries.name).toBe("entries");
expect(entries.nullable).toBe(false);
expect(DataType.isStruct(entries.type)).toBe(true);
expect(entries.type.children).toHaveLength(2);
const [key, value] = entries.type.children;
expect([key.name, value.name]).toEqual(["key", "value"]);
expect(key.nullable).toBe(false);
expect(DataType.isUtf8(key.type)).toBe(true);
expect(value.nullable).toBe(true);
expect(DataType.isInt(value.type)).toBe(true);
expect(value.type.bitWidth).toBe(32);
expect(value.type.isSigned).toBe(true);
}
// Helper method to verify various ways to create a table
async function checkTableCreation(
tableCreationMethod: (
@@ -938,6 +963,34 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
false,
);
});
it("will make an empty table with a Map field", async function () {
const schema = new Schema([
new Field(
"attributes",
new Map_(
new Field(
"entries",
new Struct([
new Field("key", new Utf8(), false),
new Field("value", new Int32(), true),
]),
false,
),
true,
),
),
]);
const table = makeEmptyTable(schema);
expectValidMapField(table.schema.fields[0]);
const buffer = await fromTableToBuffer(table);
const roundTripped = tableFromIPC(buffer);
expectValidMapField(roundTripped.schema.fields[0]);
});
});
describe("when using two versions of arrow", function () {
+156
View File
@@ -15,6 +15,7 @@ import {
OAuthHeaderProvider,
StaticHeaderProvider,
} from "../lancedb/header";
import { Index } from "../lancedb/indices";
// Test-only header providers
class CustomProvider extends HeaderProvider {
@@ -225,6 +226,161 @@ describe("remote connection", () => {
);
});
it("sends the FTS posting block size to remote tables", async () => {
let createIndexBody: Record<string, unknown> | undefined;
await withMockDatabase(
(req, res) => {
const path = req.url ?? "";
if (path.endsWith("/describe/")) {
res.writeHead(200, { "Content-Type": "application/json" }).end(
JSON.stringify({
name: "t",
version: 1,
schema: {
fields: [
{ name: "text", type: { type: "string" }, nullable: false },
],
},
}),
);
return;
}
if (path.endsWith("/create_index/")) {
let raw = "";
req.on("data", (chunk) => {
raw += chunk;
});
req.on("end", () => {
createIndexBody = JSON.parse(raw);
res.writeHead(200).end();
});
return;
}
res.writeHead(404).end();
},
async (db) => {
const table = await db.openTable("t");
await table.createIndex("text", {
config: Index.fts({ blockSize: 256 }),
});
},
);
expect(createIndexBody?.["column"]).toBe("text");
expect(createIndexBody?.["index_type"]).toBe("FTS");
expect(createIndexBody?.["block_size"]).toBe(256);
});
it("diffs and merges remote branches", async () => {
const sampleDiff = {
fromBranch: "exp",
parentVersion: 1,
mainVersion: 2,
branchVersion: 3,
baseMoved: false,
rowCountMain: 3,
rowCountBranch: 3,
rowSummary: {
unchanged: 3,
newOnBase: 0,
newOnBranch: 0,
staleRecompute: 0,
inputsChanged: 0,
deltaAvailable: false,
},
addedColumns: [{ name: "tag", dataType: "utf8", nullable: true }],
removedColumns: [],
changedColumns: [],
addedIndexes: [],
removedIndexes: [],
mergeable: true,
mergeBlockers: [],
};
const mergeBodies: Record<string, unknown>[] = [];
await withMockDatabase(
(req, res) => {
const path = req.url ?? "";
if (path.endsWith("/describe/")) {
res.writeHead(200, { "Content-Type": "application/json" }).end(
JSON.stringify({
name: "t",
version: 2,
schema: { fields: [] },
}),
);
return;
}
let raw = "";
req.on("data", (chunk) => {
raw += chunk;
});
req.on("end", () => {
const body = raw ? JSON.parse(raw) : {};
if (path.endsWith("/branches/diff/")) {
// biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format
expect(body).toEqual({ from_branch: "exp" });
res
.writeHead(200, { "Content-Type": "application/json" })
.end(JSON.stringify(sampleDiff));
return;
}
if (path.endsWith("/branches/merge/")) {
mergeBodies.push(body);
const dryRun = body["dry_run"] === true;
const response = {
status: dryRun ? "ready" : "rejected",
diff: dryRun
? sampleDiff
: {
...sampleDiff,
mergeable: false,
mergeBlockers: [
{ code: "baseMoved", message: "main has advanced" },
],
},
preview: { promotedColumns: dryRun ? ["tag"] : [] },
};
res
.writeHead(dryRun ? 200 : 409, {
"Content-Type": "application/json",
})
.end(JSON.stringify(response));
return;
}
res.writeHead(404).end();
});
},
async (db) => {
const table = await db.openTable("t");
const branches = await table.branches();
await expect(branches.diff("exp")).resolves.toEqual(sampleDiff);
const rejected = await branches.merge("exp");
expect(rejected.status).toBe("rejected");
expect(rejected.diff.mergeBlockers).toEqual([
{ code: "baseMoved", message: "main has advanced" },
]);
const preview = await branches.merge("exp", true);
expect(preview.status).toBe("ready");
expect(preview.preview.promotedColumns).toEqual(["tag"]);
},
);
expect(mergeBodies).toEqual([
// biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format
{ from_branch: "exp", dry_run: false },
// biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format
{ from_branch: "exp", dry_run: true },
]);
});
describe("TlsConfig", () => {
it("should create TlsConfig with all fields", () => {
const tlsConfig: TlsConfig = {
+12 -1
View File
@@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
import * as arrow from "../lancedb/arrow";
import { sanitizeField, sanitizeType } from "../lancedb/sanitize";
import { sanitizeField, sanitizeMap, sanitizeType } from "../lancedb/sanitize";
describe("sanitize", function () {
describe("sanitizeType function", function () {
@@ -181,4 +181,15 @@ describe("sanitize", function () {
);
});
});
describe("sanitizeMap function", function () {
it.each([
["no children", []],
["two children", [{}, {}]],
])("should reject a Map type with %s", function (_, children) {
expect(() => sanitizeMap({ children, keysSorted: false })).toThrow(
"Expected a Map type to have exactly one child",
);
});
});
});
+35 -1
View File
@@ -2527,6 +2527,35 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
expect(results3.length).toBe(1);
});
test("full text search with custom posting block size", async () => {
const db = await connect(tmpDir.name);
const data = [
{ text: "hello world", vector: [0.1, 0.2, 0.3] },
{ text: "goodbye world", vector: [0.4, 0.5, 0.6] },
];
const table = await db.createTable("test", data);
await table.createIndex("text", {
config: Index.fts({ blockSize: 256 }),
});
const index = (await table.listIndices()).find(
(index) => index.indexType === "FTS",
);
expect(index?.indexVersion).toBe(3);
expect(
(index?.indexDetails as Record<string, unknown>)["block_size"],
).toBe(256);
const results = await table.search("hello").toArray();
expect(results[0].text).toBe(data[0].text);
});
test("rejects invalid full text posting block size", () => {
expect(() => Index.fts({ blockSize: 129 as 128 | 256 })).toThrow(
"128 or 256",
);
});
test("full text search without lowercase", async () => {
const db = await connect(tmpDir.name);
const data = [
@@ -2775,8 +2804,13 @@ describe("when calling analyzePlan", () => {
.fill(1)
.map(() => Math.random());
const plan = await table.query().nearestTo(queryVec).analyzePlan();
console.log("Query Plan:\n", plan); // <--- Print the plan
expect(plan).toMatch("AnalyzeExec");
const fullPlan = await table
.query()
.nearestTo(queryVec)
.analyzePlan("full");
expect(fullPlan).toMatch("AnalyzeExec");
});
});
+9
View File
@@ -93,6 +93,7 @@ export {
QueryBase,
VectorQuery,
TakeQuery,
AnalyzePlanDistributedMetrics,
QueryExecutionOptions,
ColumnOrdering,
FullTextSearchOptions,
@@ -123,6 +124,14 @@ export {
export {
Table,
Branches,
BranchColumnSummary,
BranchColumnChange,
BranchIndexSummary,
BranchRowCountSummary,
MergeBlocker,
BranchDiff,
MergePreview,
MergeBranchResult,
AddDataOptions,
UpdateOptions,
OptimizeOptions,
+9
View File
@@ -572,6 +572,14 @@ export interface FtsOptions {
* whether to only index the prefix of the token for ngram tokenizer
*/
prefixOnly?: boolean;
/**
* 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.
*/
blockSize?: 128 | 256;
}
export class Index {
@@ -751,6 +759,7 @@ export class Index {
options?.ngramMinLength,
options?.ngramMaxLength,
options?.prefixOnly,
options?.blockSize,
),
);
}
+12 -3
View File
@@ -79,6 +79,8 @@ export interface QueryExecutionOptions {
timeoutMs?: number;
}
export type AnalyzePlanDistributedMetrics = "aggregate" | "per_worker" | "full";
export interface ColumnOrdering {
columnName: string;
ascending?: boolean;
@@ -311,13 +313,20 @@ export class QueryBase<
* 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]
*
* @param distributedMetrics - How distributed worker metrics are displayed for remote query plans.
* Defaults to `"aggregate"`.
* @returns A query execution plan with runtime metrics for each step.
*/
async analyzePlan(): Promise<string> {
async analyzePlan(
distributedMetrics?: AnalyzePlanDistributedMetrics,
): Promise<string> {
const distributedMetricsMode = distributedMetrics ?? "aggregate";
if (this.inner instanceof Promise) {
return this.inner.then((inner) => inner.analyzePlan());
return this.inner.then((inner) =>
inner.analyzePlan(distributedMetricsMode),
);
} else {
return this.inner.analyzePlan();
return this.inner.analyzePlan(distributedMetricsMode);
}
}
+4 -5
View File
@@ -288,12 +288,11 @@ export function sanitizeMap(typeLike: object) {
if (!("keysSorted" in typeLike) || typeof typeLike.keysSorted !== "boolean") {
throw Error("Expected a Map type to have a `keysSorted` property");
}
if (typeLike.children.length !== 1) {
throw Error("Expected a Map type to have exactly one child");
}
return new Map_(
// biome-ignore lint/suspicious/noExplicitAny: skip
typeLike.children.map((field) => sanitizeField(field)) as any,
typeLike.keysSorted,
);
return new Map_(sanitizeField(typeLike.children[0]), typeLike.keysSorted);
}
export function sanitizeDuration(typeLike: object) {
+94
View File
@@ -1329,6 +1329,76 @@ export interface FieldMetadataUpdate {
replace?: boolean;
}
/** Summary of a column in a branch diff. */
export interface BranchColumnSummary {
name: string;
dataType: string;
nullable: boolean;
}
/** A column whose definition differs between main and the branch. */
export interface BranchColumnChange {
name: string;
main: BranchColumnSummary;
branch: BranchColumnSummary;
}
/** Summary of an index in a branch diff. */
export interface BranchIndexSummary {
indexName: string;
columns: string[];
indexType?: string;
status: string;
}
/** Row-level comparison between main and the branch. */
export interface BranchRowCountSummary {
unchanged: number;
newOnBase: number;
newOnBranch: number;
staleRecompute: number;
inputsChanged: number;
deltaAvailable: boolean;
}
/** A reason why a branch cannot currently be merged. */
export interface MergeBlocker {
code: string;
message: string;
}
/** Read-only comparison of a branch against main. */
export interface BranchDiff {
fromBranch: string;
parentVersion: number;
mainVersion: number;
branchVersion: number;
baseMoved: boolean;
rowCountMain: number;
rowCountBranch: number;
rowSummary: BranchRowCountSummary;
addedColumns: BranchColumnSummary[];
removedColumns: BranchColumnSummary[];
changedColumns: BranchColumnChange[];
addedIndexes: BranchIndexSummary[];
removedIndexes: BranchIndexSummary[];
mergeable: boolean;
mergeBlockers: MergeBlocker[];
}
/** Changes that would be, or were, promoted by a branch merge. */
export interface MergePreview {
promotedColumns: string[];
}
/** Result of previewing or attempting a branch merge. */
export interface MergeBranchResult {
status: "ready" | "rejected" | "notImplemented" | "merged" | "unknown";
diff: BranchDiff;
preview: MergePreview;
mainVersionAfter?: number;
}
/**
* Branch manager for a {@link Table}.
*
@@ -1381,4 +1451,28 @@ export class Branches {
async delete(name: string): Promise<void> {
return await this.#inner.delete(name);
}
/** Compare a branch against main without modifying either branch. */
async diff(fromBranch: string): Promise<BranchDiff> {
return (await this.#inner.diff(fromBranch)) as unknown as BranchDiff;
}
/**
* Merge a branch into main.
*
* Set `dryRun` to `true` to preview the merge. A rejected merge resolves
* with `status: "rejected"` instead of throwing.
*
* @param fromBranch Branch to merge from.
* @param dryRun When true, only preview the merge. Defaults to false.
*/
async merge(
fromBranch: string,
dryRun: boolean = false,
): Promise<MergeBranchResult> {
return (await this.#inner.merge(
fromBranch,
dryRun,
)) as unknown as MergeBranchResult;
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-darwin-arm64",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"os": ["darwin"],
"cpu": ["arm64"],
"main": "lancedb.darwin-arm64.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-arm64-gnu",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"os": ["linux"],
"cpu": ["arm64"],
"main": "lancedb.linux-arm64-gnu.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-arm64-musl",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"os": ["linux"],
"cpu": ["arm64"],
"main": "lancedb.linux-arm64-musl.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-x64-gnu",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"os": ["linux"],
"cpu": ["x64"],
"main": "lancedb.linux-x64-gnu.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-x64-musl",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"os": ["linux"],
"cpu": ["x64"],
"main": "lancedb.linux-x64-musl.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-win32-arm64-msvc",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"os": [
"win32"
],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-win32-x64-msvc",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"os": ["win32"],
"cpu": ["x64"],
"main": "lancedb.win32-x64-msvc.node",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@lancedb/lancedb",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@lancedb/lancedb",
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"cpu": [
"x64",
"arm64"
+1 -1
View File
@@ -11,7 +11,7 @@
"ann"
],
"private": false,
"version": "0.32.0-beta.1",
"version": "0.32.0-beta.2",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
+10 -4
View File
@@ -226,7 +226,8 @@ impl Index {
ngram_min_length: Option<u32>,
ngram_max_length: Option<u32>,
prefix_only: Option<bool>,
) -> Self {
block_size: Option<u32>,
) -> napi::Result<Self> {
let mut opts = FtsIndexBuilder::default();
if let Some(with_position) = with_position {
opts = opts.with_position(with_position);
@@ -261,10 +262,15 @@ impl Index {
if let Some(prefix_only) = prefix_only {
opts = opts.ngram_prefix_only(prefix_only);
}
Self {
inner: Mutex::new(Some(LanceDbIndex::FTS(opts))),
if let Some(block_size) = block_size {
opts = opts
.block_size(block_size as usize)
.map_err(|err| napi::Error::from_reason(err.to_string()))?;
}
Ok(Self {
inner: Mutex::new(Some(LanceDbIndex::FTS(opts))),
})
}
#[napi(factory)]
+56 -21
View File
@@ -19,6 +19,7 @@ use lancedb::index::scalar::{
BooleanQuery, BoostQuery, FtsQuery, FullTextSearchQuery, MatchQuery, MultiMatchQuery, Occur,
Operator, PhraseQuery,
};
use lancedb::query::AnalyzePlanDistributedMetrics;
use lancedb::query::ExecutableQuery;
use lancedb::query::Query as LanceDbQuery;
use lancedb::query::QueryBase;
@@ -47,6 +48,28 @@ impl From<ColumnOrdering> for LanceDbColumnOrdering {
}
}
fn analyze_plan_options(
distributed_metrics: Option<String>,
) -> napi::Result<QueryExecutionOptions> {
let analyze_plan_distributed_metrics =
match distributed_metrics.as_deref().unwrap_or("aggregate") {
"aggregate" => AnalyzePlanDistributedMetrics::Aggregate,
"per_worker" => AnalyzePlanDistributedMetrics::PerWorker,
"full" => AnalyzePlanDistributedMetrics::Full,
mode => {
return Err(napi::Error::from_reason(format!(
"Invalid distributedMetrics value '{}'. Expected one of: \
'aggregate', 'per_worker', 'full'",
mode
)));
}
};
let mut options = QueryExecutionOptions::default();
options.analyze_plan_distributed_metrics = analyze_plan_distributed_metrics;
Ok(options)
}
fn bytes_to_arrow_array(data: Uint8Array, dtype: String) -> napi::Result<Arc<dyn Array>> {
let buf = arrow_buffer::Buffer::from(data.to_vec());
let num_bytes = buf.len();
@@ -200,13 +223,17 @@ impl Query {
}
#[napi(catch_unwind)]
pub async fn analyze_plan(&self) -> napi::Result<String> {
self.inner.analyze_plan().await.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
pub async fn analyze_plan(&self, distributed_metrics: Option<String>) -> napi::Result<String> {
let options = analyze_plan_options(distributed_metrics)?;
self.inner
.analyze_plan_with_options(options)
.await
.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
}
}
@@ -412,13 +439,17 @@ impl VectorQuery {
}
#[napi(catch_unwind)]
pub async fn analyze_plan(&self) -> napi::Result<String> {
self.inner.analyze_plan().await.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
pub async fn analyze_plan(&self, distributed_metrics: Option<String>) -> napi::Result<String> {
let options = analyze_plan_options(distributed_metrics)?;
self.inner
.analyze_plan_with_options(options)
.await
.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
}
}
@@ -491,13 +522,17 @@ impl TakeQuery {
}
#[napi(catch_unwind)]
pub async fn analyze_plan(&self) -> napi::Result<String> {
self.inner.analyze_plan().await.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
pub async fn analyze_plan(&self, distributed_metrics: Option<String>) -> napi::Result<String> {
let options = analyze_plan_options(distributed_metrics)?;
self.inner
.analyze_plan_with_options(options)
.await
.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
}
}
+4
View File
@@ -232,6 +232,10 @@ impl From<ClientConfig> for lancedb::remote::ClientConfig {
tls_config: config.tls_config.map(Into::into),
header_provider: None, // the header provider is set separately later
user_id: config.user_id,
// Resolved from LANCE_CLIENT_MAX_BYTES_PER_REQUEST or the default.
max_bytes_per_request: None,
// Resolved from LANCE_CLIENT_MAX_REQUEST_DURATION or the read timeout.
max_request_duration: None,
}
}
}
+24
View File
@@ -1355,4 +1355,28 @@ impl Branches {
pub async fn delete(&self, name: String) -> napi::Result<()> {
self.inner.delete_branch(&name).await.default_error()
}
#[napi(ts_return_type = "Promise<Record<string, unknown>>")]
pub async fn diff(&self, from_branch: String) -> napi::Result<serde_json::Value> {
let diff = self.inner.diff_branch(&from_branch).await.default_error()?;
serde_json::to_value(diff).map_err(|err| {
napi::Error::from_reason(format!("failed to serialize branch diff: {err}"))
})
}
#[napi(ts_return_type = "Promise<Record<string, unknown>>")]
pub async fn merge(
&self,
from_branch: String,
dry_run: Option<bool>,
) -> napi::Result<serde_json::Value> {
let result = self
.inner
.merge_branch(&from_branch, dry_run.unwrap_or(false))
.await
.default_error()?;
serde_json::to_value(result).map_err(|err| {
napi::Error::from_reason(format!("failed to serialize branch merge result: {err}"))
})
}
}
@@ -0,0 +1,21 @@
{
"name": "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, with idiomatic query/search patterns and performance defaults for ingestion, indexing, filtering, and diagnostics.",
"version": "0.1.0",
"author": {
"name": "LanceDB"
},
"homepage": "https://www.lancedb.com",
"keywords": [
"lancedb",
"vector-search",
"full-text-search",
"hybrid-search",
"python",
"typescript",
"pipelines",
"ingestion",
"indexing",
"performance"
]
}
+33
View File
@@ -0,0 +1,33 @@
{
"name": "lancedb",
"version": "0.1.0",
"description": "Codex plugin for building LanceDB pipelines in Python and TypeScript.",
"author": {
"name": "LanceDB"
},
"keywords": [
"lancedb",
"vector-search",
"full-text-search",
"hybrid-search",
"python",
"typescript",
"pipelines"
],
"skills": "./skills/",
"interface": {
"displayName": "LanceDB",
"shortDescription": "Build LanceDB pipelines in Python and TypeScript.",
"longDescription": "Write, review, debug, and document LanceDB pipelines in Python and TypeScript that work across local LanceDB OSS tables and remote LanceDB Enterprise/Cloud tables, with idiomatic query/search patterns and performance defaults for ingestion, indexing, filtering, and diagnostics.",
"developerName": "LanceDB",
"websiteURL": "https://www.lancedb.com",
"category": "Developer Tools",
"capabilities": [
"Developer Tools"
],
"defaultPrompt": "Create a LanceDB table, embed sample text, and run a vector search.",
"composerIcon": "./assets/logo.png",
"logo": "./assets/logo.png",
"logoDark": "./assets/logo-dark.png"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

@@ -1,6 +1,6 @@
---
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.
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, apply LanceDB performance defaults for ingestion, indexing, filtering, and diagnostics, and resolve connections to the remote server for Enterprise-only operations such as jobs.
---
# Building LanceDB Pipelines
@@ -19,7 +19,7 @@ Do NOT assume local-only table helpers exist on remote tables. If the user asks
## 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.
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. If the task involves jobs in any way (listing, inspecting, creating, or canceling jobs), it is always the remote path and requires a remote server connection — see "Connecting to the LanceDB remote server" below before doing anything else.
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`
@@ -27,7 +27,11 @@ Do NOT assume local-only table helpers exist on remote tables. If the user asks
- TypeScript patterns: `references/typescript/patterns.md`
- TypeScript API quick reference: `references/typescript/api_reference.md`
- TypeScript performance guidance: `references/typescript/performance.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.
- Column metadata authoring (both SDKs): `references/column_metadata.md`
- Branch operations (both SDKs): `references/branch_ops.md`
- Remote server connection resolution (jobs, raw REST): `references/remote_connect.md`
- Job operations REST API (list/describe/cancel/query_events): `references/remote_jobs.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. Read `remote_connect.md` when the task involves jobs or direct REST access to an Enterprise deployment, and `remote_jobs.md` for the job REST methods themselves (list, describe, cancel, query_events).
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.
@@ -68,6 +72,10 @@ Rules for portable Enterprise ingestion:
This is Enterprise/Cloud-specific. Local/OSS tables have no separate data plane, so `mode="overwrite"` and immediate same-name reuse are fine there.
## Connecting to the LanceDB remote server
LanceDB Enterprise/Cloud deployments are served by a server implementing the lance-namespace OpenAPI spec (<https://github.com/lance-format/lance-namespace/blob/main/docs/src/spec.yaml>). Every remote (`db://...`) connection talks to such a server, and some operations exist only there. In particular, **all operations around jobs (listing, inspecting, creating, or canceling jobs) run server-side** — there is no local/OSS equivalent. Before any job work, or any direct REST call to an Enterprise deployment, read `references/remote_connect.md` to resolve the base URL, credentials, and database header and to validate the connection. Then use the four job REST methods documented in `references/remote_jobs.md` (list, describe, cancel, query_events).
## Script
Run the scanner when reviewing or modifying an existing codebase:
@@ -0,0 +1,6 @@
interface:
display_name: "LanceDB"
short_description: "Build LanceDB pipelines in Python and TypeScript"
default_prompt: "Use $lancedb to create a table, embed sample text, and run a vector search."
icon_small: "./assets/icon.png"
icon_large: "./assets/icon.png"
Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

@@ -0,0 +1,182 @@
# 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, except merging a branch into main, which is Enterprise-only.
## 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.
## Merging a branch into main (Enterprise only)
Merge is available through the SDKs (`table.branches.merge(...)`) on **Enterprise tables only** — it is not supported on Cloud or local/OSS tables, which raise `NotSupported`.
`merge` takes the branch to merge **from** and a `dry_run` flag. Both the SDK method and the underlying REST endpoint **actually merge by default** (`dry_run=False`); pass `dry_run=True` to only preview. A rejected merge is **not an exception** — it returns a result with `status="rejected"` rather than raising, so inspect the return value. Use `branches.diff(from_branch)` to inspect a branch's pending diff without attempting a merge.
```python
exp = "experiment-reindex"
# preview only — returns status="ready" if it would merge cleanly
preview = table.branches.merge(exp, dry_run=True)
# actually merge (default)
result = table.branches.merge(exp)
if result["status"] == "merged":
print("landed at", result["mainVersionAfter"])
elif result["status"] == "rejected":
print(result["diff"]["mergeBlockers"]) # why it was refused
# inspect a branch's pending diff without merging
diff = table.branches.diff(exp)
```
Async: `await table.branches.merge(exp)`, `await table.branches.diff(exp)`.
```typescript
const branches = await table.branches();
const exp = "experiment-reindex";
// preview only (second arg is dryRun)
const preview = await branches.merge(exp, true);
// actually merge (default)
const result = await branches.merge(exp);
if (result.status === "merged") {
console.log("landed at", result.mainVersionAfter);
} else if (result.status === "rejected") {
console.log(result.diff.mergeBlockers);
}
const diff = await branches.diff(exp);
```
The result is the wire JSON, containing `status` (`ready` on a passing dry run, `merged` on success, `rejected` when refused — also `notImplemented`/`unknown`), the branch `diff` (including `mergeBlockers` explaining any rejection), a `preview` of the columns that would be promoted, and — after a real merge — `mainVersionAfter`.
### Merge preconditions
Merge only **promotes newly added columns** onto main; it does not replay arbitrary commits. Practically, a branch is mergeable only if it has **exactly one commit since it was created, and that commit added a column**. The merge is rejected (`status: "rejected"`, with `mergeBlockers` set) if:
- the branch was forked from another branch rather than directly from main
- main has advanced since the branch was forked
- the branch's rows changed since the fork (row counts must match main exactly)
- the branch removed columns or changed a column's type/nullability
- the branch added no columns (index-only changes are not merged)
### Adding a column in a single commit
Because the branch must contain just one column-adding commit, add the column with its values in one operation rather than add-then-backfill:
1. **SQL transformation**`add_columns` with a SQL expression computed from existing columns, so the column lands populated in one commit.
2. **Precompute the values** — compute the column's values externally, then add the fully-populated column in a single operation (e.g. via `merge_insert`/`add_columns` with the data ready).
3. **Lance-format-level data evolution (pylance)** — use Lance's data evolution with backfill, documented at <https://lance.org/guide/data_evolution/#with-data-backfill>.
## 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 |
| Merge branch into main (Enterprise only) | `table.branches.merge(from_branch, dry_run=False)` | `await branches.merge(fromBranch, dryRun)` |
| Preview a branch's pending diff (Enterprise only) | `table.branches.diff(from_branch)` | `await branches.diff(fromBranch)` |
Branch names must be non-empty; empty names raise a validation error.
@@ -0,0 +1,183 @@
# 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
@@ -4,12 +4,19 @@ Quick method reference for Python LanceDB code. Cross-check source for non-trivi
## Connect
If you're connecting to a remote database, use this:
```python
import lancedb
db = lancedb.connect("./camelot-db") # local/OSS
db = lancedb.connect("db://my-db", api_key=api_key, region=region) # remote
db = lancedb.connect("db://my-db", api_key=api_key, host_override=host_override) # remote
```
(values may be found in LANCEDB_API_KEY and LANCEDB_HOST_OVERRIDE, either in env vars or a .env file)
If you're connecting to a local table using OSS LanceDB, use this:
```python
db = lancedb.connect("./camelot-db") # local/OSS
```
If you're not sure which, or if you can't find the api_key or host_override params, ask the user.
**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.
@@ -96,6 +103,32 @@ 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
@@ -0,0 +1,45 @@
# Connecting to a LanceDB remote server
LanceDB Enterprise/Cloud deployments are served by a server implementing the
lance-namespace OpenAPI spec
(<https://github.com/lance-format/lance-namespace/blob/main/docs/src/spec.yaml>).
Every remote (`db://...`) connection talks to such a server, and some operations
exist only there. In particular, all operations around jobs (listing, inspecting,
creating, or canceling jobs) run server-side — there is no local/OSS equivalent, so
resolve a server connection before attempting any job work. The job REST methods
themselves are documented in `references/remote_jobs.md`.
Every request needs two things:
1. **Base URL** — the server endpoint
2. **Credentials** — an API key (`x-api-key` header over REST), and usually a database name (`x-lancedb-database` header)
## Resolution steps
1. If the user already gave a URL and API key (or said which environment they're working against), use that.
2. Otherwise, look for credentials already available in the environment:
- Env vars like `LANCEDB_URI` / `LANCEDB_HOST` / `LANCEDB_API_KEY`
- A server endpoint already running or port-forwarded locally (the REST default port is 2333, i.e. `http://localhost:2333`)
3. If you didn't find both pieces, ask the user directly: **"What's your LanceDB endpoint's URL, and what's your API key?"** Also ask which database to use if it isn't obvious. Don't guess or probe further — the user knows their deployment.
## Validating the connection
Make a cheap authenticated request and check the status before starting real work:
```bash
curl -s -w "\n%{http_code}" "{base_url}/v1/table/?limit=1" \
-H "x-api-key: <key>" \
-H "x-lancedb-database: <database>"
```
- `200` — connection, key, and database header all good
- `401` — API key missing or wrong
- `400` mentioning a database header — this deployment expects `x-lancedb-database`
## Non-REST equivalents
The same credentials work through the SDKs and CLI:
- Python SDK: `lancedb.connect("db://<database>", api_key="<key>", host_override="<base_url>")`
- TypeScript SDK: `await lancedb.connect("db://<database>", { apiKey: "<key>", hostOverride: "<base_url>" })`
- `lancedb` CLI: a `[profiles.<name>]` entry in `~/.lancedb/config.toml` with `http_server_url`, `api_key`, `database`
@@ -0,0 +1,151 @@
# Job operations over the LanceDB remote server REST API
Jobs are server-side background operations on LanceDB Enterprise/Cloud — index builds,
column backfills, materialized view refreshes, and similar async work. Endpoints that
trigger async work (e.g. the column backfill or materialized view refresh endpoints)
return a `job_id`; these four methods are how you track and manage those jobs.
Resolve the connection first — see `references/remote_connect.md`. All four methods
are **POST** requests under `{base_url}/v1/jobs/` with JSON bodies, and take the usual
`x-api-key` / `x-lancedb-database` headers. If every job call returns `501`, job APIs
are disabled on that deployment (the server has no job registry configured) — report
that rather than retrying.
## 1. List jobs — `POST /v1/jobs/list`
The body is optional; an empty body lists everything. All fields are filters:
```json
{
"limit": 100,
"table_name": "my_table",
"job_type": "...",
"job_subtype": "...",
"state": "...",
"page_token": "..."
}
```
```bash
curl -s -X POST "{base_url}/v1/jobs/list" \
-H "x-api-key: <key>" -H "x-lancedb-database: <database>" \
-H "content-type: application/json" \
-d '{"table_name": "my_table"}'
```
Response:
```json
{
"jobs": [
{
"job_id": "...",
"table": "my_table",
"job_type": "...",
"job_subtype": "...",
"state": "done",
"created_at_millis": 1720000000000
}
],
"page_token": "..."
}
```
A `page_token` in the response means there are more results — pass it back in the next
request to continue. Note list rows use a lowercase `state` string, while describe uses
an uppercase `job_state`.
## 2. Describe a job — `POST /v1/jobs/describe`
Body: `{"job_id": "<id>"}`. Returns full detail for one job:
```json
{
"job_id": "...",
"job_type": "...",
"job_subtype": "...",
"job_state": "IN_PROGRESS",
"creation_ms": 1720000000000,
"spec": {},
"status": {}
}
```
`job_state` is one of `IN_PROGRESS`, `CANCELLED`, `FAILED`, `DONE`. `spec` and `status`
are job-type-specific JSON objects (the job's input specification and its current
progress/status). Returns `404` for an unknown job id.
## 3. Cancel a job — `POST /v1/jobs/cancel`
Body: `{"job_id": "<id>"}`; response echoes `{"job_id": "<id>"}`. Cancellation is a
service-level operation requiring the same administrative authorization as the
`/admin` routes — a database-scoped API key that can list and describe jobs may still
get a permission error here. Other errors: `404` unknown job, `409` state conflict
(e.g. already in a terminal state), `429` too much write contention (safe to retry).
## 4. Query job event history — `POST /v1/jobs/query_events`
Returns the event history (state transitions, progress updates) for one or more jobs.
Body: `{"job_id": "<id>"}` for one job, or `{"job_ids": ["<id>", ...]}` for a batch.
Optional fields: `limit` (max event rows), `limit_per_job` (per job in a batch query),
and `filter` — a SQL-like expression over the columns `state`, `updated_by`,
`owner_component`, and `claim_entity`. (`full_text_search` is reserved and currently
rejected as not implemented.)
The response is **not JSON** — it is an Arrow IPC stream
(`content-type: application/vnd.apache.arrow.stream`). Decode it, e.g. in Python:
```python
import pyarrow.ipc
import requests
resp = requests.post(
f"{base_url}/v1/jobs/query_events",
headers={"x-api-key": key, "x-lancedb-database": database},
json={"job_id": job_id},
)
resp.raise_for_status()
events = pyarrow.ipc.open_stream(resp.content).read_all()
```
## Feature engineering (Geneva) jobs
Feature engineering jobs — UDF column backfills and materialized view refreshes run
through Geneva — are tracked **separately** from the `/v1/jobs` registry above. Their
records live in a `geneva_jobs` table inside the database itself (in the `__system`
namespace), and you access them through a Python `geneva` connection rather than the
REST endpoints above:
```python
import geneva
from geneva.jobs import JobStateManager
# Same credentials as lancedb.connect / the REST API
conn = geneva.connect("db://<database>", api_key="<key>", host_override="<base_url>")
jsm = JobStateManager(conn)
# List jobs. NOTE: status defaults to "RUNNING"; pass status=None for all jobs.
# Statuses: PENDING | RUNNING | DONE | FAILED | CANCELLED
jobs = jsm.list_jobs(table_name="my_table", status=None)
# Fetch one job by id (returns a list of JobRecord)
records = jsm.get("<job_id>")
```
Each `JobRecord` has `table_name`, `column_name`, `job_id`, `job_type`, `status`,
`launched_at`, `completed_at`, `config`, `launched_by`, `manifest_id`, `cluster_name`,
`metrics` (progress counters), `events` (human-readable history), and `updated_at`.
For filters `list_jobs` doesn't support (e.g. time ranges), query the underlying table
directly: `jsm.get_table(True).search().where("launched_at >= TIMESTAMP '...'")`
pass `True` to check out the latest version, since other processes update job state.
Stale-status caveat: nothing reaps dead Geneva jobs, so a job can sit in
`RUNNING`/`PENDING` forever if its worker died. Treat a job as effectively `FAILED`
when it has been running longer than ~36 hours, or its `updated_at` is more than ~2
hours old (this matches the heuristic the Geneva console UI applies on read).
## Workflow tips
- To wait for async work (a backfill, an index build), poll `describe` until
`job_state` leaves `IN_PROGRESS`; on `FAILED`, pull `status` and `query_events` for
the failure detail.
@@ -69,6 +69,33 @@ 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
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "0.35.0-beta.2"
current_version = "0.35.0-beta.3"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "lancedb-python"
version = "0.35.0-beta.2"
version = "0.35.0-beta.3"
publish = false
edition.workspace = true
description = "Python bindings for LanceDB"
+21
View File
@@ -8,6 +8,27 @@ A Python library for [LanceDB](https://github.com/lancedb/lancedb).
pip install lancedb
```
### Pre-Haswell x86_64 hosts: `lancedb-compat`
The default `lancedb` wheel targets `x86-64-haswell` (AVX2 + FMA + F16C) for full performance on modern hardware. Pre-Haswell hosts — Intel Sandy Bridge / Ivy Bridge / Westmere; AMD Bulldozer / Piledriver / Steamroller — don't have AVX2 and crash with `Illegal instruction` at `import lancedb`.
For those hosts, install the `lancedb-compat` package instead:
```bash
pip install lancedb-compat
```
Same Python API (`import lancedb` works as usual). The compat wheel is compiled at the `x86-64-v2` baseline (Nehalem-class) and uses runtime SIMD dispatch in the embedded lance crate to pick the right kernel tier (scalar / AVX / AVX+FMA / AVX2+FMA / AVX-512) at load time, so it still goes fast on modern hardware while running cleanly on the pre-Haswell silicon. Use `lance.simd_info()` from Python to verify which tier was selected.
`lancedb` and `lancedb-compat` install to the same `lancedb/` namespace and conflict at install time. Pick one. To switch, `pip uninstall lancedb` first, then `pip install lancedb-compat` (or vice-versa).
If you need a custom baseline (or `lancedb-compat` isn't yet published for your platform), build from source with the override:
```bash
RUSTFLAGS="-C target-cpu=x86-64-v2" maturin build --release
pip install ./target/wheels/lancedb-*.whl
```
### Preview Releases
Stable releases are created about every 2 weeks. For the latest features and bug fixes, you can install the preview release. These releases receive the same level of testing as stable releases, but are not guaranteed to be available for more than 6 months after they are released. Once your application is stable, we recommend switching to stable releases.
+3 -2
View File
@@ -61,10 +61,11 @@ tests = [
"duckdb>=0.9.0",
"pytz>=2023.3",
"polars>=0.19, <=1.3.0",
"pyarrow<25",
"pyarrow-stubs>=16.0",
"pylance>=5.0.0b5",
"pylance==9.0.0rc1",
"requests>=2.31.0",
"datafusion>=52,<53",
"datafusion>=54,<55",
"opentelemetry-sdk>=1.30.0",
]
dev = [
+21 -1
View File
@@ -30,6 +30,7 @@ from .types import BaseTokenizerType
IvfHnswPq: type[HnswPq] = HnswPq
IvfHnswSq: type[HnswSq] = HnswSq
IvfHnswFlat: type[HnswFlat] = HnswFlat
AnalyzePlanDistributedMetrics = Literal["aggregate", "per_worker", "full"]
class MetricPoint:
name: str
@@ -218,6 +219,7 @@ class Table:
data: pa.RecordBatchReader,
mode: Literal["append", "overwrite"],
progress: Optional[Any] = None,
write_parallelism: Optional[int] = None,
) -> AddResult: ...
async def update(
self, updates: Dict[str, str], where: Optional[str]
@@ -317,6 +319,10 @@ class Branches:
) -> Table: ...
async def checkout(self, name: str, version: Optional[int] = None) -> Table: ...
async def delete(self, name: str) -> None: ...
async def diff(self, from_branch: str) -> Dict[str, Any]: ...
async def merge(
self, from_branch: str, dry_run: bool = False
) -> Dict[str, Any]: ...
class IndexConfig:
name: str
@@ -393,7 +399,9 @@ class Query:
self, max_batch_length: Optional[int], timeout: Optional[timedelta]
) -> RecordBatchStream: ...
async def explain_plan(self, verbose: Optional[bool]) -> str: ...
async def analyze_plan(self) -> str: ...
async def analyze_plan(
self, distributed_metrics: Optional[AnalyzePlanDistributedMetrics] = None
) -> str: ...
def to_query_request(self) -> PyQueryRequest: ...
class TakeQuery:
@@ -401,6 +409,10 @@ class TakeQuery:
def with_row_id(self): ...
async def output_schema(self) -> pa.Schema: ...
async def execute(self) -> RecordBatchStream: ...
async def explain_plan(self, verbose: Optional[bool]) -> str: ...
async def analyze_plan(
self, distributed_metrics: Optional[AnalyzePlanDistributedMetrics] = None
) -> str: ...
def to_query_request(self) -> PyQueryRequest: ...
class FTSQuery:
@@ -421,6 +433,10 @@ class FTSQuery:
async def execute(
self, max_batch_length: Optional[int], timeout: Optional[timedelta]
) -> RecordBatchStream: ...
async def explain_plan(self, verbose: Optional[bool]) -> str: ...
async def analyze_plan(
self, distributed_metrics: Optional[AnalyzePlanDistributedMetrics] = None
) -> str: ...
def to_query_request(self) -> PyQueryRequest: ...
class VectorQuery:
@@ -443,6 +459,10 @@ class VectorQuery:
def bypass_vector_index(self): ...
def nearest_to_text(self, query: dict) -> HybridQuery: ...
def order_by(self, ordering: Optional[List[ColumnOrdering]]): ...
async def explain_plan(self, verbose: Optional[bool]) -> str: ...
async def analyze_plan(
self, distributed_metrics: Optional[AnalyzePlanDistributedMetrics] = None
) -> str: ...
def to_query_request(self) -> PyQueryRequest: ...
class HybridQuery:
+38 -62
View File
@@ -41,6 +41,7 @@ from lance_namespace import (
ListTablesResponse,
connect as namespace_connect,
)
from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError
from . import __version__
from ._lancedb import connect as lancedb_connect # type: ignore
@@ -746,10 +747,12 @@ class LanceDBConnection(DBConnection):
"""
if namespace_path is None:
namespace_path = []
return self._namespace_conn().list_namespaces(
namespace_path=namespace_path,
page_token=page_token,
limit=limit,
return LOOP.run(
self._conn.list_namespaces(
namespace_path=namespace_path,
page_token=page_token,
limit=limit,
)
)
@override
@@ -759,10 +762,12 @@ class LanceDBConnection(DBConnection):
mode: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> CreateNamespaceResponse:
return self._namespace_conn().create_namespace(
namespace_path=namespace_path,
mode=mode,
properties=properties,
return LOOP.run(
self._conn.create_namespace(
namespace_path=namespace_path,
mode=mode,
properties=properties,
)
)
@override
@@ -772,19 +777,24 @@ class LanceDBConnection(DBConnection):
mode: Optional[str] = None,
behavior: Optional[str] = None,
) -> DropNamespaceResponse:
return self._namespace_conn().drop_namespace(
namespace_path=namespace_path,
mode=mode,
behavior=behavior,
)
try:
return LOOP.run(
self._conn.drop_namespace(
namespace_path=namespace_path,
mode=mode,
behavior=behavior,
)
)
except RuntimeError as e:
if "Namespace not empty" in str(e):
raise NamespaceNotEmptyError(str(e)) from e
raise
@override
def describe_namespace(
self, namespace_path: List[str]
) -> DescribeNamespaceResponse:
return self._namespace_conn().describe_namespace(
namespace_path=namespace_path,
)
return LOOP.run(self._conn.describe_namespace(namespace_path=namespace_path))
@override
def list_tables(
@@ -813,12 +823,6 @@ class LanceDBConnection(DBConnection):
"""
if namespace_path is None:
namespace_path = []
if namespace_path:
return self._namespace_conn().list_tables(
namespace_path=namespace_path,
page_token=page_token,
limit=limit,
)
return LOOP.run(
self._conn.list_tables(
namespace_path=namespace_path, page_token=page_token, limit=limit
@@ -916,22 +920,6 @@ class LanceDBConnection(DBConnection):
raise ValueError("mode must be either 'create' or 'overwrite'")
validate_table_name(name)
if namespace_path:
return self._namespace_conn().create_table(
name,
data=data,
schema=schema,
mode=mode,
exist_ok=exist_ok,
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
embedding_functions=embedding_functions,
namespace_path=namespace_path,
storage_options=storage_options,
data_storage_version=data_storage_version,
enable_v2_manifest_paths=enable_v2_manifest_paths,
)
tbl = LanceTable.create(
self,
name,
@@ -944,22 +932,11 @@ class LanceDBConnection(DBConnection):
embedding_functions=embedding_functions,
namespace_path=namespace_path,
storage_options=storage_options,
data_storage_version=data_storage_version,
enable_v2_manifest_paths=enable_v2_manifest_paths,
)
return tbl
def _namespace_conn(self) -> DBConnection:
"""Return a LanceNamespaceDBConnection backed by this connection's
directory namespace. Used to delegate child-namespace operations."""
from lancedb.namespace import LanceNamespaceDBConnection
return LanceNamespaceDBConnection(
self.namespace_client(),
read_consistency_interval=self.read_consistency_interval,
storage_options=self.storage_options,
namespace_client_impl=None,
namespace_client_properties=None,
)
@override
def open_table(
self,
@@ -1006,14 +983,7 @@ class LanceDBConnection(DBConnection):
stacklevel=2,
)
if namespace_path:
tbl = self._namespace_conn().open_table(
name,
namespace_path=namespace_path,
storage_options=storage_options,
index_cache_size=index_cache_size,
)
else:
try:
tbl = LanceTable.open(
self,
name,
@@ -1021,6 +991,15 @@ class LanceDBConnection(DBConnection):
storage_options=storage_options,
index_cache_size=index_cache_size,
)
except (RuntimeError, ValueError) as e:
if namespace_path and (
"Table not found" in str(e) or "was not found" in str(e)
):
table_id = namespace_path + [name]
raise TableNotFoundError(
f"Table not found: {'$'.join(table_id)}"
) from e
raise
if branch is not None:
tbl = tbl.branches.checkout(branch, version)
@@ -1104,9 +1083,6 @@ class LanceDBConnection(DBConnection):
"""
if namespace_path is None:
namespace_path = []
if namespace_path:
self._namespace_conn().drop_table(name, namespace_path=namespace_path)
return
LOOP.run(
self._conn.drop_table(
name, namespace_path=namespace_path, ignore_missing=ignore_missing
+103 -34
View File
@@ -14,29 +14,76 @@ import numpy as np
DEFAULT_WATSONX_URL = "https://us-south.ml.cloud.ibm.com"
MODELS_DIMS = {
# Models currently available on the watsonx.ai SaaS platform.
# These are the IDs advertised to new users via model_names() and shown in
# validation error messages. Regional availability and withdrawal dates are
# documented at:
# https://www.ibm.com/docs/en/watsonx/saas?topic=models-supported-encoder
CURRENT_MODELS: dict[str, int] = {
"ibm/granite-embedding-278m-multilingual": 768,
"ibm/slate-125m-english-rtrvr-v2": 768,
"ibm/slate-30m-english-rtrvr-v2": 384,
"intfloat/multilingual-e5-large": 1024,
}
# Full dimension map including legacy model IDs from earlier releases.
# Kept so that existing tables whose stored metadata uses these names can still
# resolve dimensions on load without raising an error. These IDs are NOT
# advertised to new users.
MODELS_DIMS: dict[str, int] = {
**CURRENT_MODELS,
# Deprecated — withdrawal announced but still functional until the dates above.
"sentence-transformers/all-minilm-l6-v2": 384,
# Pre-v2 legacy names retained for metadata compatibility only.
"ibm/slate-125m-english-rtrvr": 768,
"ibm/slate-30m-english-rtrvr": 384,
"sentence-transformers/all-minilm-l12-v2": 384,
"intfloat/multilingual-e5-large": 1024,
}
@register("watsonx")
class WatsonxEmbeddings(TextEmbeddingFunction):
"""
An embedding function that uses the IBM watsonx.ai Embeddings API.
API Docs:
---------
https://cloud.ibm.com/apidocs/watsonx-ai#text-embeddings
https://cloud.ibm.com/apidocs/watsonx-ai#text-embeddings
Supported embedding models:
---------------------------
https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx
https://dataplatform.cloud.ibm.com/docs/content/wsj/analyze-data/fm-models-embed.html?context=wx
Parameters
----------
name : str, default "ibm/slate-125m-english-rtrvr"
The ID of the embedding model to use. For new tables,
``"ibm/granite-embedding-278m-multilingual"`` is recommended.
api_key : str, optional
IBM Cloud API key. Falls back to the ``WATSONX_API_KEY`` environment
variable when not provided.
project_id : str, optional
watsonx.ai project ID. Explicit value takes precedence over the
``WATSONX_PROJECT_ID`` environment variable. Mutually exclusive with
``space_id`` — exactly one must be supplied.
space_id : str, optional
watsonx.ai deployment space ID. Explicit value takes precedence over
the ``WATSONX_SPACE_ID`` environment variable. Mutually exclusive with
``project_id`` — exactly one must be supplied.
url : str, optional
watsonx.ai service URL. Defaults to
``"https://us-south.ml.cloud.ibm.com"``.
params : dict, optional
Extra parameters forwarded verbatim to ``Embeddings`` (e.g.
``{"truncate_input_tokens": 512}``).
"""
# Intentionally kept at the original pre-PR default so that existing tables
# whose stored metadata contains model:{} reload with the same model they
# were created with. New users should pass name= explicitly, e.g.
# name="ibm/granite-embedding-278m-multilingual".
name: str = "ibm/slate-125m-english-rtrvr"
api_key: Optional[str] = None
project_id: Optional[str] = None
space_id: Optional[str] = None
url: Optional[str] = None
params: Optional[Dict] = None
@@ -46,12 +93,13 @@ class WatsonxEmbeddings(TextEmbeddingFunction):
@staticmethod
def model_names():
return [
"ibm/slate-125m-english-rtrvr",
"ibm/slate-30m-english-rtrvr",
"sentence-transformers/all-minilm-l12-v2",
"intfloat/multilingual-e5-large",
]
"""Return the IDs of models currently available for new tables.
Legacy / deprecated IDs are intentionally excluded. They remain
resolvable for dimension lookups on existing tables via ``MODELS_DIMS``,
but should not be used when creating new tables.
"""
return list(CURRENT_MODELS.keys())
def ndims(self):
return self._ndims
@@ -59,7 +107,10 @@ class WatsonxEmbeddings(TextEmbeddingFunction):
@cached_property
def _ndims(self):
if self.name not in MODELS_DIMS:
raise ValueError(f"Unknown model name {self.name}")
raise ValueError(
f"Unknown model '{self.name}'. "
f"Available models: {list(CURRENT_MODELS.keys())}"
)
return MODELS_DIMS[self.name]
def generate_embeddings(
@@ -81,27 +132,45 @@ class WatsonxEmbeddings(TextEmbeddingFunction):
"ibm_watsonx_ai.foundation_models"
)
kwargs = {"model_id": self.name}
# --- credentials ---
# Explicit field takes priority; env var is the fallback.
api_key = self.api_key or os.environ.get("WATSONX_API_KEY")
if not api_key:
raise ValueError(
"WATSONX_API_KEY not set. Either set it in your environment or "
"pass it as `api_key` argument to WatsonxEmbeddings."
)
credentials = ibm_watsonx_ai.Credentials(
api_key=api_key,
url=self.url or DEFAULT_WATSONX_URL,
)
# --- project_id / space_id (exactly one required) ---
# Explicit field always wins; env var is consulted only when the
# corresponding field was not set, so passing project_id= never
# conflicts with a stray WATSONX_SPACE_ID env var and vice-versa.
space_id, project_id = self.space_id, self.project_id
if project_id is None and space_id is None:
# Neither was passed explicitly — fall back to env vars.
project_id = os.environ.get("WATSONX_PROJECT_ID")
space_id = os.environ.get("WATSONX_SPACE_ID")
if project_id and space_id:
raise ValueError("Provide either `project_id` or `space_id`, not both.")
if not project_id and not space_id:
raise ValueError(
"Either WATSONX_PROJECT_ID or WATSONX_SPACE_ID must be set. "
"Pass one as an argument to WatsonxEmbeddings or set the "
"corresponding environment variable."
)
client_kwargs: Dict = dict(model_id=self.name, credentials=credentials)
if self.params:
kwargs["params"] = self.params
if self.project_id:
kwargs["project_id"] = self.project_id
elif "WATSONX_PROJECT_ID" in os.environ:
kwargs["project_id"] = os.environ["WATSONX_PROJECT_ID"]
client_kwargs["params"] = self.params
if project_id:
client_kwargs["project_id"] = project_id
else:
raise ValueError("WATSONX_PROJECT_ID must be set or passed")
client_kwargs["space_id"] = space_id
creds_kwargs = {}
if self.api_key:
creds_kwargs["api_key"] = self.api_key
elif "WATSONX_API_KEY" in os.environ:
creds_kwargs["api_key"] = os.environ["WATSONX_API_KEY"]
else:
raise ValueError("WATSONX_API_KEY must be set or passed")
if self.url:
creds_kwargs["url"] = self.url
else:
creds_kwargs["url"] = DEFAULT_WATSONX_URL
kwargs["credentials"] = ibm_watsonx_ai.Credentials(**creds_kwargs)
return ibm_watsonx_ai_foundation_models.Embeddings(**kwargs)
return ibm_watsonx_ai_foundation_models.Embeddings(**client_kwargs)
+11
View File
@@ -115,6 +115,12 @@ class FTS:
For example, it works with `title`, `description`, `content`, etc.
Examples
--------
Create an index configuration that uses 256-document posting blocks:
>>> config = FTS(block_size=256)
Attributes
----------
with_position : bool, default False
@@ -148,6 +154,10 @@ class FTS:
ascii_folding : bool, default True
Whether to fold ASCII characters. This converts accented characters to
their ASCII equivalent. For example, "café" would be converted to "cafe".
block_size : int, default 128
The number of documents per compressed posting block. Supported values
are 128 and 256. A value of 256 uses the experimental FTS V3 format
and may introduce breaking changes.
Notes
-----
@@ -168,6 +178,7 @@ class FTS:
ngram_min_length: int = 3
ngram_max_length: int = 3
prefix_only: bool = False
block_size: int = 128
@dataclass
+9 -4
View File
@@ -885,7 +885,7 @@ class Permutation:
This method refines the current selection, potentially removing columns. It
will not add back columns that were previously removed.
If any of the columns do not exist then an error will be raised
If any of the columns do not exist then an error will be raised.
This does not introduce a post-processing step. It simply reduces the amount
of data we read.
@@ -898,9 +898,14 @@ class Permutation:
for name in columns:
value = self.selection.get(name, None)
if value is None:
raise ValueError(
f"Cannot select column `{name}` because it does not exist"
)
if name == "_rowid":
# _rowid is a system column not in the default schema
# but can be explicitly selected
value = "_rowid"
else:
raise ValueError(
f"Cannot select column `{name}` because it does not exist"
)
new_selection[name] = value
return self._with_selection(new_selection)
+80 -23
View File
@@ -79,6 +79,7 @@ if TYPE_CHECKING:
from typing_extensions import Self
T = TypeVar("T", bound="LanceModel")
AnalyzePlanDistributedMetrics = Literal["aggregate", "per_worker", "full"]
@runtime_checkable
@@ -1372,7 +1373,9 @@ class LanceQueryBuilder(ABC):
self._order_by = ordering
return self
def analyze_plan(self) -> str:
def analyze_plan(
self, distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate"
) -> str:
"""
Run the query and return its execution plan with runtime metrics.
@@ -1410,12 +1413,22 @@ class LanceQueryBuilder(ABC):
fragments_scanned=..., ranges_scanned=1, rows_scanned=1,
bytes_read=..., iops=..., requests=..., task_wait_time=...]
Parameters
----------
distributed_metrics : Literal["aggregate", "per_worker", "full"]
Defaults to "aggregate".
How distributed worker metrics are displayed for remote query plans.
"aggregate" preserves the legacy summary, "per_worker" shows each
worker separately, and "full" includes both.
Returns
-------
plan : str
The physical query execution plan with runtime metrics.
"""
return self._table._analyze_plan(self.to_query_object())
return self._table._analyze_plan(
self.to_query_object(), distributed_metrics=distributed_metrics
)
def vector(self, vector: Union[np.ndarray, list]) -> Self:
"""Set the vector to search for.
@@ -2581,9 +2594,17 @@ class LanceHybridQueryBuilder(LanceQueryBuilder):
indented_fts = "\n".join(" " + line for line in fts_plan.splitlines())
return f"{reranker_label}\n {indented_vector}\n {indented_fts}"
def analyze_plan(self):
def analyze_plan(
self, distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate"
) -> str:
"""Execute the query and display with runtime metrics.
Parameters
----------
distributed_metrics : Literal["aggregate", "per_worker", "full"]
Defaults to "aggregate".
How distributed worker metrics are displayed for remote query plans.
Returns
-------
plan : str
@@ -2591,9 +2612,19 @@ class LanceHybridQueryBuilder(LanceQueryBuilder):
self._create_query_builders()
results = ["Vector Search Plan:"]
results.append(self._table._analyze_plan(self._vector_query.to_query_object()))
results.append(
self._table._analyze_plan(
self._vector_query.to_query_object(),
distributed_metrics=distributed_metrics,
)
)
results.append("FTS Search Plan:")
results.append(self._table._analyze_plan(self._fts_query.to_query_object()))
results.append(
self._table._analyze_plan(
self._fts_query.to_query_object(),
distributed_metrics=distributed_metrics,
)
)
return "\n".join(results)
def _create_query_builders(self):
@@ -3080,14 +3111,22 @@ class AsyncQueryBase(object):
""" # noqa: E501
return await self._inner.explain_plan(verbose)
async def analyze_plan(self):
async def analyze_plan(
self, distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate"
) -> str:
"""Execute the query and display with runtime metrics.
Parameters
----------
distributed_metrics : Literal["aggregate", "per_worker", "full"]
Defaults to "aggregate".
How distributed worker metrics are displayed for remote query plans.
Returns
-------
plan : str
"""
return await self._inner.analyze_plan()
return await self._inner.analyze_plan(distributed_metrics)
class AsyncStandardQuery(AsyncQueryBase):
@@ -3836,18 +3875,16 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
>>> asyncio.run(doctest_example()) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
RRFReranker(K=60)
ProjectionExec: expr=[vector@0 as vector, text@3 as text, _distance@2 as _distance]
Take: columns="vector, _rowid, _distance, (text)"
CoalesceBatchesExec: target_batch_size=1024
GlobalLimitExec: skip=0, fetch=10
FilterExec: _distance@2 IS NOT NULL
SortExec: TopK(fetch=10), expr=[_distance@2 ASC NULLS LAST, _rowid@1 ASC NULLS LAST], preserve_partitioning=[false]
KNNVectorDistance: metric=l2
LanceRead: uri=..., projection=[vector], ...
LanceRead: uri=..., projection=[text], source=stream(_rowid)
GlobalLimitExec: skip=0, fetch=10
FilterExec: _distance@2 IS NOT NULL
SortExec: TopK(fetch=10), expr=[_distance@2 ASC NULLS LAST, _rowid@1 ASC NULLS LAST], preserve_partitioning=[false]
KNNVectorDistance: metric=l2
LanceRead: uri=..., projection=[vector], ...
ProjectionExec: expr=[vector@2 as vector, text@3 as text, _score@1 as _score]
Take: columns="_rowid, _score, (vector), (text)"
CoalesceBatchesExec: target_batch_size=1024
GlobalLimitExec: skip=0, fetch=10
MatchQuery: column=text, query=hello
LanceRead: uri=..., projection=[vector, text], source=stream(_rowid)
GlobalLimitExec: skip=0, fetch=10
MatchQuery: column=text, query=[hello]
Parameters
----------
@@ -3866,7 +3903,9 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
indented_fts = "\n".join(" " + line for line in fts_plan.splitlines())
return f"{self._reranker}\n {indented_vector}\n {indented_fts}"
async def analyze_plan(self):
async def analyze_plan(
self, distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate"
) -> str:
"""
Execute the query and return the physical execution plan with runtime metrics.
@@ -3875,14 +3914,24 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
elapsed time, I/O stats, and more. Its useful for debugging and
performance analysis.
Parameters
----------
distributed_metrics : Literal["aggregate", "per_worker", "full"]
Defaults to "aggregate".
How distributed worker metrics are displayed for remote query plans.
Returns
-------
plan : str
"""
results = ["Vector Search Query:"]
results.append(await self._inner.to_vector_query().analyze_plan())
results.append(
await self._inner.to_vector_query().analyze_plan(distributed_metrics)
)
results.append("FTS Search Query:")
results.append(await self._inner.to_fts_query().analyze_plan())
results.append(
await self._inner.to_fts_query().analyze_plan(distributed_metrics)
)
return "\n".join(results)
@@ -4166,14 +4215,22 @@ class BaseQueryBuilder(object):
""" # noqa: E501
return LOOP.run(self._inner.explain_plan(verbose))
def analyze_plan(self):
def analyze_plan(
self, distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate"
) -> str:
"""Execute the query and display with runtime metrics.
Parameters
----------
distributed_metrics : Literal["aggregate", "per_worker", "full"]
Defaults to "aggregate".
How distributed worker metrics are displayed for remote query plans.
Returns
-------
plan : str
"""
return LOOP.run(self._inner.analyze_plan())
return LOOP.run(self._inner.analyze_plan(distributed_metrics))
class LanceTakeQueryBuilder(BaseQueryBuilder):
+25 -3
View File
@@ -56,7 +56,12 @@ from lancedb.merge import LanceMergeInsertBuilder
from lancedb.embeddings import EmbeddingFunctionRegistry
from lancedb.table import _normalize_progress
from ..query import LanceVectorQueryBuilder, LanceQueryBuilder, LanceTakeQueryBuilder
from ..query import (
AnalyzePlanDistributedMetrics,
LanceQueryBuilder,
LanceTakeQueryBuilder,
LanceVectorQueryBuilder,
)
from ..table import AsyncTable, BlobMode, Branches, IndexStatistics, Query, Table, Tags
from ..types import BaseTokenizerType
@@ -339,6 +344,7 @@ class RemoteTable(Table):
ngram_min_length: int = 3,
ngram_max_length: int = 3,
prefix_only: bool = False,
block_size: int = 128,
name: Optional[str] = None,
):
"""Create a full-text search index on a column.
@@ -359,6 +365,7 @@ class RemoteTable(Table):
ngram_min_length=ngram_min_length,
ngram_max_length=ngram_max_length,
prefix_only=prefix_only,
block_size=block_size,
)
LOOP.run(
self._table.create_index(
@@ -569,6 +576,7 @@ class RemoteTable(Table):
on_bad_vectors: str = "error",
fill_value: float = 0.0,
progress: Optional[Union[bool, Callable, Any]] = None,
write_parallelism: Optional[int] = None,
) -> AddResult:
"""Add more data to the [Table](Table). It has the same API signature as
the OSS version.
@@ -594,6 +602,12 @@ class RemoteTable(Table):
progress: bool, callable, or tqdm-like, optional
A callback or tqdm-compatible progress bar. See
:meth:`Table.add` for details.
write_parallelism: int, optional
Number of partitions to write in parallel. Higher values increase
throughput but also peak memory use, since each partition buffers
data in flight. Defaults to an estimate based on the data size,
capped at the number of CPU cores. Lower this if bulk ingestion is
using too much memory.
Returns
-------
@@ -609,6 +623,7 @@ class RemoteTable(Table):
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
progress=progress,
write_parallelism=write_parallelism,
)
)
finally:
@@ -718,8 +733,15 @@ class RemoteTable(Table):
def _explain_plan(self, query: Query, verbose: Optional[bool] = False) -> str:
return LOOP.run(self._table._explain_plan(query, verbose))
def _analyze_plan(self, query: Query) -> str:
return LOOP.run(self._table._analyze_plan(query))
def _analyze_plan(
self,
query: Query,
*,
distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate",
) -> str:
return LOOP.run(
self._table._analyze_plan(query, distributed_metrics=distributed_metrics)
)
def _output_schema(self, query: Query) -> pa.Schema:
return LOOP.run(self._table._output_schema(query))
@@ -23,7 +23,7 @@ class AnswerdotaiRerankers(Reranker):
column : str, default "text"
The name of the column to use as input to the cross encoder model.
return_score : str, default "relevance"
options are "relevance" or "all". Only "relevance" is supported for now.
options are "relevance" or "all".
**kwargs
Additional keyword arguments to pass to the model. For example, 'device'.
See AnswerDotAI/rerankers for more information.
@@ -77,12 +77,13 @@ class AnswerdotaiRerankers(Reranker):
vector_results: pa.Table,
fts_results: pa.Table,
):
combined_results = self.merge_results(vector_results, fts_results)
if self.score == "all":
combined_results = self._merge_and_keep_scores(vector_results, fts_results)
else:
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)
combined_results = combined_results.sort_by(
[("_relevance_score", "descending")]
)
+1 -1
View File
@@ -16,7 +16,7 @@ class ColbertReranker(AnswerdotaiRerankers):
column : str, default "text"
The name of the column to use as input to the cross encoder model.
return_score : str, default "relevance"
options are "relevance" or "all". Only "relevance" is supported for now.
options are "relevance" or "all".
**kwargs
Additional keyword arguments to pass to the model, for example, 'device'.
See AnswerDotAI/rerankers for more information.
+14 -6
View File
@@ -40,12 +40,12 @@ class WatsonxReranker(Reranker):
IBM Cloud API key. Falls back to the ``WATSONX_API_KEY`` environment
variable when not provided.
project_id : str, optional
watsonx.ai project ID. Falls back to the ``WATSONX_PROJECT_ID``
environment variable when not provided. Mutually exclusive with
watsonx.ai project ID. Explicit value takes precedence over the
``WATSONX_PROJECT_ID`` environment variable. Mutually exclusive with
``space_id`` exactly one must be supplied.
space_id : str, optional
watsonx.ai deployment space ID. Falls back to the ``WATSONX_SPACE_ID``
environment variable when not provided. Mutually exclusive with
watsonx.ai deployment space ID. Explicit value takes precedence over
the ``WATSONX_SPACE_ID`` environment variable. Mutually exclusive with
``project_id`` exactly one must be supplied.
url : str, optional
watsonx.ai service URL. Defaults to
@@ -100,8 +100,16 @@ class WatsonxReranker(Reranker):
)
# --- project_id / space_id (exactly one required) ---
project_id = self.project_id or os.environ.get("WATSONX_PROJECT_ID")
space_id = self.space_id or os.environ.get("WATSONX_SPACE_ID")
# Explicit field always wins; env vars are consulted only when neither
# was passed explicitly, so a stray WATSONX_SPACE_ID never overrides an
# explicit project_id and vice-versa.
project_id = self.project_id
space_id = self.space_id
if project_id is None and space_id is None:
# Neither was passed explicitly — fall back to env vars.
project_id = os.environ.get("WATSONX_PROJECT_ID")
space_id = os.environ.get("WATSONX_SPACE_ID")
if project_id and space_id:
raise ValueError("Provide either `project_id` or `space_id`, not both.")
+154 -2
View File
@@ -7,10 +7,158 @@ import sys
from typing import Callable, Iterator, Optional
from lancedb.arrow import to_arrow
import pyarrow as pa
import pyarrow.compute as pc
import pyarrow.dataset as ds
from .pydantic import LanceModel
# pyarrow's default scanner settings are tuned for narrow rows. For wide rows
# (e.g. embedding columns) they buffer a huge read-ahead window in host memory
# and can OOM the client during bulk ingestion. We size the scanner so the
# estimated in-flight memory stays within a budget, while leaving narrow
# datasets on pyarrow's defaults (no throughput regression).
_SCAN_MEMORY_BUDGET_BYTES = 1024 * 1024 * 1024 # ~1 GiB in-flight target
_TARGET_BATCH_BYTES = 16 * 1024 * 1024 # ~16 MiB per batch
_MIN_BATCH_ROWS = 512
# pyarrow defaults (see arrow/dataset ScanOptions); we never exceed these.
_PA_DEFAULT_BATCH_ROWS = 131_072
_PA_DEFAULT_BATCH_READAHEAD = 16
_PA_DEFAULT_FRAGMENT_READAHEAD = 4
# Read-ahead used for wide rows. pyarrow reads a whole parquet row group at a
# time and keeps `batch_readahead` of them resident, so read-ahead depth (not
# batch size) dominates peak memory for wide data; keep both small but leave a
# little prefetch for throughput. Tuned empirically against embedding datasets.
_WIDE_BATCH_READAHEAD = 2
_WIDE_FRAGMENT_READAHEAD = 1
# Estimate for variable-width columns (string/binary/list) whose true width is
# unknown from the schema alone. Only needs to be large enough to flag "wide".
_VARIABLE_WIDTH_ESTIMATE = 128
# Rows peeked from a rescannable source to refine the list-length guess for
# variable-length list columns (e.g. embeddings stored as `list<float32>`
# instead of `list<float32, N>`), whose per-row width the schema can't tell us.
_SAMPLE_ROWS = 10
def _observed_list_length(sample: pa.ChunkedArray) -> Optional[int]:
"""Average element count per row observed in a list/large_list sample."""
if len(sample) == 0:
return None
mean = pc.mean(pc.list_value_length(sample)).as_py()
return None if mean is None else max(1, round(mean))
def _estimate_field_width(
dtype: pa.DataType, sample: Optional[pa.ChunkedArray] = None
) -> int:
if pa.types.is_fixed_size_list(dtype):
return dtype.list_size * _estimate_field_width(dtype.value_type)
if pa.types.is_struct(dtype):
return sum(
_estimate_field_width(
dtype.field(i).type,
pc.struct_field(sample, [i]) if sample is not None else None,
)
for i in range(dtype.num_fields)
)
if pa.types.is_dictionary(dtype):
return _estimate_field_width(dtype.value_type)
if pa.types.is_fixed_size_binary(dtype):
return dtype.byte_width
if pa.types.is_boolean(dtype):
return 1
if (pa.types.is_list(dtype) or pa.types.is_large_list(dtype)) and (
sample is not None
):
observed_length = _observed_list_length(sample)
if observed_length is not None:
return observed_length * _estimate_field_width(dtype.value_type)
# Fixed-width scalars (ints, floats, temporal, decimal) expose bit_width;
# variable-width types (string, binary, list, map, ...) raise ValueError.
try:
return max(1, dtype.bit_width // 8)
except (ValueError, AttributeError):
return _VARIABLE_WIDTH_ESTIMATE
def _estimate_bytes_per_row(
schema: pa.Schema, sample: Optional[pa.Table] = None
) -> int:
return max(
1,
sum(
_estimate_field_width(
field.type, sample.column(field.name) if sample is not None else None
)
for field in schema
),
)
def _sample_head(head: Callable[..., pa.Table]) -> Optional[pa.Table]:
"""Best-effort peek at a few rows to refine the bytes-per-row estimate.
Uses a tight batch size and no read-ahead so the peek itself can't trigger
the wide-row memory blowup this module exists to avoid. Returns None (fall
back to the schema-only estimate) if sampling isn't possible for any
reason, e.g. an empty dataset.
"""
try:
sample = head(
_SAMPLE_ROWS,
batch_size=_SAMPLE_ROWS,
batch_readahead=1,
fragment_readahead=1,
)
except Exception:
return None
return sample if sample.num_rows > 0 else None
def _bounded_scanner_kwargs(
schema: pa.Schema, sample: Optional[pa.Table] = None
) -> dict:
"""Scanner kwargs that cap in-flight memory for wide rows.
Narrow datasets keep pyarrow's defaults unchanged (no throughput
regression). For wide rows (e.g. embedding columns) pyarrow's default
read-ahead buffers many large batches/row-groups at once, which can OOM the
client during bulk ingestion, so we shrink the batch size and read-ahead to
keep the estimated in-flight memory near the budget.
Read-ahead (not just batch size) has to drop: pyarrow reads a whole parquet
row group at a time and keeps `batch_readahead`/`fragment_readahead` of them
resident, so a small batch size alone still pins large row-group buffers.
`sample`, if given, is a small (see `_SAMPLE_ROWS`) table of rows from the
source used to refine the estimate for variable-length list columns (e.g.
embeddings stored without a fixed size), whose width the schema alone
can't tell us.
"""
bytes_per_row = _estimate_bytes_per_row(schema, sample)
# If pyarrow's defaults already stay within budget, leave them alone so
# narrow datasets keep their throughput. A "unit" of in-flight memory is one
# default-sized batch, held `batch_readahead + fragment_readahead` deep.
default_in_flight = (
_PA_DEFAULT_BATCH_ROWS
* bytes_per_row
* (_PA_DEFAULT_BATCH_READAHEAD + _PA_DEFAULT_FRAGMENT_READAHEAD)
)
if default_in_flight <= _SCAN_MEMORY_BUDGET_BYTES:
return {}
# Wide rows: cap batch bytes and pull read-ahead down so only a couple of
# large row-group buffers are resident at once.
batch_size = min(
_PA_DEFAULT_BATCH_ROWS,
max(_MIN_BATCH_ROWS, _TARGET_BATCH_BYTES // bytes_per_row),
)
return {
"batch_size": batch_size,
"batch_readahead": _WIDE_BATCH_READAHEAD,
"fragment_readahead": _WIDE_FRAGMENT_READAHEAD,
}
@dataclass
class Scannable:
@@ -56,10 +204,12 @@ def _from_table(data: pa.Table) -> Scannable:
@to_scannable.register(ds.Dataset)
def _from_dataset(data: ds.Dataset) -> Scannable:
sample = _sample_head(data.head)
scanner_kwargs = _bounded_scanner_kwargs(data.schema, sample)
return Scannable(
schema=data.schema,
num_rows=data.count_rows(),
reader=lambda: data.scanner().to_reader(),
reader=lambda: data.scanner(**scanner_kwargs).to_reader(),
)
@@ -206,10 +356,12 @@ def _register_optional_converters():
@to_scannable.register(lance.LanceDataset)
def _from_lance(data: lance.LanceDataset) -> Scannable:
sample = _sample_head(data.head)
scanner_kwargs = _bounded_scanner_kwargs(data.schema, sample)
return Scannable(
schema=data.schema,
num_rows=data.count_rows(),
reader=lambda: data.scanner().to_reader(),
reader=lambda: data.scanner(**scanner_kwargs).to_reader(),
)
+89 -10
View File
@@ -73,6 +73,7 @@ from .expr import Expr
from .merge import LanceMergeInsertBuilder
from .pydantic import LanceModel, model_to_dict
from .query import (
AnalyzePlanDistributedMetrics,
AsyncFTSQuery,
AsyncHybridQuery,
AsyncQuery,
@@ -1105,6 +1106,7 @@ class Table(ABC):
ngram_min_length: int = 3,
ngram_max_length: int = 3,
prefix_only: bool = False,
block_size: int = 128,
wait_timeout: Optional[timedelta] = None,
name: Optional[str] = None,
):
@@ -1176,6 +1178,10 @@ class Table(ABC):
The maximum length of an n-gram.
prefix_only: bool, default False
Whether to only index the prefix of the token for ngram tokenizer.
block_size: int, default 128
The number of documents per compressed posting block. Must be 128
or 256. A value of 256 uses the experimental FTS V3 format and
may introduce breaking changes.
wait_timeout: timedelta, optional
The timeout to wait if indexing is asynchronous.
name: str, optional
@@ -1198,6 +1204,7 @@ class Table(ABC):
on_bad_vectors: OnBadVectorsType = "error",
fill_value: float = 0.0,
progress: Optional[Union[bool, Callable, Any]] = None,
write_parallelism: Optional[int] = None,
) -> AddResult:
"""Add more data to the [Table](Table).
@@ -1243,6 +1250,13 @@ class Table(ABC):
with tqdm() as pbar:
table.add(data, progress=pbar)
write_parallelism: int, optional
Number of partitions to write in parallel. Higher values increase
throughput but also peak memory use, since each partition buffers
data in flight. Defaults to an estimate based on the data size,
capped at the number of CPU cores. Lower this if bulk ingestion is
using too much memory.
Returns
-------
AddResult
@@ -1552,7 +1566,12 @@ class Table(ABC):
def _explain_plan(self, query: Query, verbose: Optional[bool] = False) -> str: ...
@abstractmethod
def _analyze_plan(self, query: Query) -> str: ...
def _analyze_plan(
self,
query: Query,
*,
distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate",
) -> str: ...
@abstractmethod
def _output_schema(self, query: Query) -> pa.Schema: ...
@@ -2189,7 +2208,7 @@ class LanceTable(Table):
namespace_client = self._namespace_client
if namespace_client is None:
conn_uri = getattr(self._conn, "uri", "")
if get_uri_scheme(conn_uri) == "namespace":
if get_uri_scheme(conn_uri) == "namespace" or self._namespace_path:
namespace_client = self._conn.namespace_client()
self._namespace_client = namespace_client
@@ -3012,6 +3031,7 @@ class LanceTable(Table):
ngram_min_length: int = 3,
ngram_max_length: int = 3,
prefix_only: bool = False,
block_size: int = 128,
name: Optional[str] = None,
):
"""Create a full-text search index on a column.
@@ -3061,9 +3081,7 @@ class LanceTable(Table):
else:
tokenizer_configs = self.infer_tokenizer_configs(tokenizer_name)
config = FTS(
**tokenizer_configs,
)
config = FTS(block_size=block_size, **tokenizer_configs)
try:
LOOP.run(
@@ -3152,6 +3170,7 @@ class LanceTable(Table):
on_bad_vectors: OnBadVectorsType = "error",
fill_value: float = 0.0,
progress: Optional[Union[bool, Callable, Any]] = None,
write_parallelism: Optional[int] = None,
) -> AddResult:
"""Add data to the table.
If vector columns are missing and the table
@@ -3173,6 +3192,12 @@ class LanceTable(Table):
progress: bool, callable, or tqdm-like, optional
A callback or tqdm-compatible progress bar. See
:meth:`Table.add` for details.
write_parallelism: int, optional
Number of partitions to write in parallel. Higher values increase
throughput but also peak memory use, since each partition buffers
data in flight. Defaults to an estimate based on the data size,
capped at the number of CPU cores. Lower this if bulk ingestion is
using too much memory.
Returns
-------
@@ -3188,6 +3213,7 @@ class LanceTable(Table):
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
progress=progress,
write_parallelism=write_parallelism,
)
)
finally:
@@ -3630,8 +3656,15 @@ class LanceTable(Table):
def _explain_plan(self, query: Query, verbose: Optional[bool] = False) -> str:
return LOOP.run(self._table._explain_plan(query, verbose))
def _analyze_plan(self, query: Query) -> str:
return LOOP.run(self._table._analyze_plan(query))
def _analyze_plan(
self,
query: Query,
*,
distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate",
) -> str:
return LOOP.run(
self._table._analyze_plan(query, distributed_metrics=distributed_metrics)
)
def _output_schema(self, query: Query) -> pa.Schema:
return LOOP.run(self._table._output_schema(query))
@@ -4923,6 +4956,7 @@ class AsyncTable:
on_bad_vectors: Optional[OnBadVectorsType] = None,
fill_value: Optional[float] = None,
progress: Optional[Union[bool, Callable, Any]] = None,
write_parallelism: Optional[int] = None,
) -> AddResult:
"""Add more data to the [Table](Table).
@@ -4947,6 +4981,12 @@ class AsyncTable:
progress: callable or tqdm-like, optional
A callback or tqdm-compatible progress bar. See
:meth:`Table.add` for details.
write_parallelism: int, optional
Number of partitions to write in parallel. Higher values increase
throughput but also peak memory use, since each partition buffers
data in flight. Defaults to an estimate based on the data size,
capped at the number of CPU cores. Lower this if bulk ingestion is
using too much memory.
"""
schema = await self.schema()
@@ -4978,7 +5018,12 @@ class AsyncTable:
data = to_scannable(data)
progress, owns = _normalize_progress(progress)
try:
return await self._inner.add(data, mode or "append", progress=progress)
return await self._inner.add(
data,
mode or "append",
progress=progress,
write_parallelism=write_parallelism,
)
except RuntimeError as e:
if "Cast error" in str(e):
raise ValueError(e)
@@ -5390,10 +5435,15 @@ class AsyncTable:
async_query = self._sync_query_to_async(query)
return await async_query.explain_plan(verbose)
async def _analyze_plan(self, query: Query) -> str:
async def _analyze_plan(
self,
query: Query,
*,
distributed_metrics: AnalyzePlanDistributedMetrics = "aggregate",
) -> str:
# This method is used by the sync table
async_query = self._sync_query_to_async(query)
return await async_query.analyze_plan()
return await async_query.analyze_plan(distributed_metrics)
async def _output_schema(self, query: Query) -> pa.Schema:
async_query = self._sync_query_to_async(query)
@@ -6221,6 +6271,24 @@ class Branches:
"""Delete a branch."""
LOOP.run(self._table.branches.delete(name))
def diff(self, from_branch: str) -> Dict[str, Any]:
"""Diff a branch against main."""
return LOOP.run(self._table.branches.diff(from_branch))
def merge(self, from_branch: str, dry_run: bool = False) -> Dict[str, Any]:
"""Merge a branch into main, or dry-run.
Parameters
----------
from_branch: str
Branch to merge from.
dry_run: bool, default False
When True, only preview. When False, attempt the merge.
A rejected merge returns ``status="rejected"`` instead of raising.
"""
return LOOP.run(self._table.branches.merge(from_branch, dry_run))
def _wrap(
self, async_table: "AsyncTable", version: Optional[int] = None
) -> "Table":
@@ -6350,3 +6418,14 @@ class AsyncBranches:
async def delete(self, name: str) -> None:
"""Delete a branch."""
await self._table.branches.delete(name)
async def diff(self, from_branch: str) -> Dict[str, Any]:
"""Diff a branch against main."""
return await self._table.branches.diff(from_branch)
async def merge(self, from_branch: str, dry_run: bool = False) -> Dict[str, Any]:
"""Merge a branch into main, or dry-run.
A rejected merge returns ``status="rejected"`` instead of raising.
"""
return await self._table.branches.merge(from_branch, dry_run)
+13 -7
View File
@@ -307,13 +307,19 @@ def infer_vector_column_name(
# FTS queries do not require a vector column
return None
if query is not None or query_type == "hybrid":
try:
vector_column_name = inf_vector_column_query(
schema, dim=_query_vector_dim(query)
)
except Exception as e:
raise e
if query is None and query_type != "hybrid":
# No vector search was requested (e.g. a plain scan), so there's
# nothing to infer.
return None
vector_column_name = inf_vector_column_query(schema, dim=_query_vector_dim(query))
if vector_column_name is None:
raise ValueError(
"No vector column found in the schema. Please specify the "
"vector column name explicitly via the `vector_column_name` "
"parameter."
)
return vector_column_name
+42
View File
@@ -13,6 +13,7 @@ import numpy as np
import pandas as pd
import pyarrow as pa
import pytest
from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError
from lancedb.pydantic import LanceModel, Vector
@@ -955,6 +956,47 @@ def test_local_namespace_operations(tmp_path):
assert db.list_namespaces().namespaces == []
def test_local_sync_namespace_uses_rust_without_python_client(tmp_path, monkeypatch):
"""Sync local namespace operations should avoid the Python namespace client."""
db = lancedb.connect(tmp_path)
def fail_namespace_client():
raise AssertionError("Python namespace client should not be constructed")
monkeypatch.setattr(db, "namespace_client", fail_namespace_client)
db.create_namespace(["child"])
assert "child" in db.list_namespaces().namespaces
schema = pa.schema([pa.field("id", pa.int64())])
table = db.create_table("tbl", schema=schema, namespace_path=["child"])
assert table.namespace == ["child"]
assert "tbl" in db.table_names(namespace_path=["child"])
assert db.list_tables(namespace_path=["child"]).tables == ["tbl"]
opened = db.open_table("tbl", namespace_path=["child"])
assert opened.namespace == ["child"]
db.drop_table("tbl", namespace_path=["child"])
assert db.list_tables(namespace_path=["child"]).tables == []
db.drop_namespace(["child"])
assert db.list_namespaces().namespaces == []
def test_local_sync_namespace_preserves_public_errors(tmp_path):
db = lancedb.connect(tmp_path)
db.create_namespace(["child"])
db.create_table(
"tbl", schema=pa.schema([pa.field("id", pa.int64())]), namespace_path=["child"]
)
with pytest.raises(TableNotFoundError, match="child\\$missing"):
db.open_table("missing", namespace_path=["child"])
with pytest.raises(NamespaceNotEmptyError):
db.drop_namespace(["child"])
def test_create_namespace_invalid_mode_raises(tmp_path):
"""Unrecognized create namespace modes raise a clear error."""
db = lancedb.connect(tmp_path)
+17
View File
@@ -226,6 +226,23 @@ def test_create_inverted_index(table, with_position):
assert any(i.name == "custom_fts_index" for i in fts_indices)
@pytest.mark.parametrize("block_size", [128, 256])
def test_create_inverted_index_block_size(table, block_size):
table.create_index("text", config=FTS(block_size=block_size))
index = next(index for index in table.list_indices() if index.index_type == "FTS")
assert index.index_details["block_size"] == block_size
assert index.index_version == (2 if block_size == 128 else 3)
results = table.search("puppy").limit(5).to_list()
assert len(results) == 5
def test_create_inverted_index_rejects_invalid_block_size(table):
with pytest.raises(ValueError, match="128 or 256"):
table.create_index("text", config=FTS(block_size=129))
def test_search_fts(table):
table.create_fts_index("text")
results = table.search("puppy").select(["id", "text"]).limit(5).to_list()
+10 -2
View File
@@ -196,18 +196,26 @@ async def test_analyze_plan(table: AsyncTable):
def test_hybrid_phrase_query_is_preserved_in_analyze_plan():
table = mock.Mock()
analyzed_queries = []
table._analyze_plan.side_effect = lambda query: analyzed_queries.append(query) or ""
distributed_metric_modes = []
def capture_query(query, *, distributed_metrics="aggregate"):
analyzed_queries.append(query)
distributed_metric_modes.append(distributed_metrics)
return ""
table._analyze_plan.side_effect = capture_query
(
LanceHybridQueryBuilder(table)
.vector([0.1, 0.2])
.text("puppy runs")
.phrase_query()
.analyze_plan()
.analyze_plan(distributed_metrics="full")
)
assert len(analyzed_queries) == 2
assert analyzed_queries[1].full_text_query.query == '"puppy runs"'
assert distributed_metric_modes == ["full", "full"]
@pytest.fixture
+62 -1
View File
@@ -134,8 +134,11 @@ def test_split_hash_with_discard(mem_db):
)
permutation_tbl = (
# Hash a high-cardinality column: "category" has only two distinct
# values, so whether anything is discarded would hinge on where those
# two hashes land rather than on the discard weight.
permutation_builder(tbl)
.split_hash(["category"], [1, 1], discard_weight=2) # Should discard ~50%
.split_hash(["id"], [1, 1], discard_weight=2) # Should discard ~50%
.execute()
)
@@ -1133,3 +1136,61 @@ def test_take_offsets_empty_permutation(some_permutation: Permutation):
result = some_permutation.take_offsets([])
assert result == []
def test_select_rowid(some_permutation: Permutation):
"""Test that _rowid can be selected alongside regular columns."""
perm_with_rowid = some_permutation.select_columns(["_rowid", "id"])
assert "_rowid" in perm_with_rowid.column_names
batches = list(perm_with_rowid.iter(100, skip_last_batch=False))
for batch in batches:
assert "_rowid" in batch[0]
def test_select_rowid_only(some_permutation: Permutation):
"""Test that _rowid can be selected as the sole column."""
perm_rowid_only = some_permutation.select_columns(["_rowid"])
assert perm_rowid_only.column_names == ["_rowid"]
batches = list(perm_rowid_only.iter(100, skip_last_batch=False))
assert len(batches) > 0
for batch in batches:
assert list(batch[0].keys()) == ["_rowid"]
def test_select_rowid_not_in_default(some_permutation: Permutation):
"""Test that _rowid is NOT in the default column_names or schema."""
assert "_rowid" not in some_permutation.column_names
assert "_rowid" not in some_permutation.schema.names
def test_select_rowid_identity_permutation(mem_db):
"""Test that _rowid works with an identity permutation."""
tbl = mem_db.create_table(
"test_rowid_identity", pa.table({"id": range(10), "value": range(10)})
)
perm = Permutation.identity(tbl)
perm_with_rowid = perm.select_columns(["_rowid", "id"])
batches = list(perm_with_rowid.iter(10, skip_last_batch=False))
assert len(batches) == 1
assert "_rowid" in batches[0][0]
def test_rename_rowid(some_permutation: Permutation):
"""Test that _rowid can be selected and then renamed."""
perm_with_rowid = some_permutation.select_columns(["_rowid", "id"])
renamed = perm_with_rowid.rename_column("_rowid", "my_row_id")
assert "my_row_id" in renamed.column_names
assert "_rowid" not in renamed.column_names
batches = list(renamed.iter(100, skip_last_batch=False))
for batch in batches:
assert "my_row_id" in batch[0]
assert "_rowid" not in batch[0]
def test_remove_rowid_after_select(some_permutation: Permutation):
"""Test that _rowid can be selected and then removed."""
perm_with_rowid = some_permutation.select_columns(["_rowid", "id"])
assert "_rowid" in perm_with_rowid.column_names
perm_without_rowid = perm_with_rowid.remove_columns(["_rowid"])
assert "_rowid" not in perm_without_rowid.column_names
assert perm_without_rowid.column_names == ["id"]
+4 -4
View File
@@ -1273,7 +1273,7 @@ async def test_explain_plan_fts(table_async: AsyncTable):
query = await table_async.search("dog", query_type="fts", fts_columns="text")
plan = await query.explain_plan()
# Should show FTS details (issue #2465 is now fixed)
assert "MatchQuery: column=text, query=dog" in plan
assert "MatchQuery: column=text, query=[dog]" in plan
assert "GlobalLimitExec" in plan # Default limit
# Test FTS query with limit
@@ -1281,7 +1281,7 @@ async def test_explain_plan_fts(table_async: AsyncTable):
"dog", query_type="fts", fts_columns="text"
)
plan_with_limit = await query_with_limit.limit(1).explain_plan()
assert "MatchQuery: column=text, query=dog" in plan_with_limit
assert "MatchQuery: column=text, query=[dog]" in plan_with_limit
assert "GlobalLimitExec: skip=0, fetch=1" in plan_with_limit
# Test FTS query with offset and limit
@@ -1289,7 +1289,7 @@ async def test_explain_plan_fts(table_async: AsyncTable):
"dog", query_type="fts", fts_columns="text"
)
plan_with_offset = await query_with_offset.offset(1).limit(1).explain_plan()
assert "MatchQuery: column=text, query=dog" in plan_with_offset
assert "MatchQuery: column=text, query=[dog]" in plan_with_offset
assert "GlobalLimitExec: skip=1, fetch=1" in plan_with_offset
@@ -1333,7 +1333,7 @@ async def test_explain_plan_with_filters(table_async: AsyncTable):
"dog", query_type="fts", fts_columns="text"
)
plan_fts_filter = await query_fts_filter.where("id = 1").explain_plan()
assert "MatchQuery: column=text, query=dog" in plan_fts_filter
assert "MatchQuery: column=text, query=[dog]" in plan_fts_filter
assert "LanceRead" in plan_fts_filter
assert "full_filter=id = Int64(1)" in plan_fts_filter # Should show filter details
+66 -2
View File
@@ -236,6 +236,65 @@ def test_remote_table_branches_sync():
table.branches.delete("exp")
def test_remote_table_branch_merge_defaults_to_execute():
merge_bodies = []
diff = {
"fromBranch": "exp",
"parentVersion": 1,
"mainVersion": 2,
"branchVersion": 3,
"baseMoved": False,
"rowCountMain": 3,
"rowCountBranch": 3,
"rowSummary": {
"unchanged": 3,
"newOnBase": 0,
"newOnBranch": 0,
"staleRecompute": 0,
"inputsChanged": 0,
"deltaAvailable": False,
},
"addedColumns": [],
"removedColumns": [],
"changedColumns": [],
"addedIndexes": [],
"removedIndexes": [],
"mergeable": True,
"mergeBlockers": [],
}
def handler(request):
if request.path.endswith("/describe/"):
status = 200
body = {"version": 2, "schema": {"fields": []}}
else:
content_len = int(request.headers.get("Content-Length"))
request_body = json.loads(request.rfile.read(content_len))
merge_bodies.append(request_body)
dry_run = request_body["dry_run"]
status = 200 if dry_run else 409
body = {
"status": "ready" if dry_run else "rejected",
"diff": diff,
"preview": {"promotedColumns": []},
}
request.send_response(status)
request.send_header("Content-Type", "application/json")
request.end_headers()
request.wfile.write(json.dumps(body).encode())
with mock_lancedb_connection(handler) as db:
branches = db.open_table("test").branches
assert branches.merge("exp")["status"] == "rejected"
assert branches.merge("exp", dry_run=True)["status"] == "ready"
assert merge_bodies == [
{"from_branch": "exp", "dry_run": False},
{"from_branch": "exp", "dry_run": True},
]
@pytest.mark.asyncio
async def test_async_remote_open_table_branch_and_version():
async with mock_lancedb_connection_async(_branch_open_handler) as db:
@@ -709,7 +768,10 @@ def test_table_create_indices():
# Test create_fts_index with custom name (legacy method)
with pytest.warns(DeprecationWarning, match="create_fts_index"):
table.create_fts_index(
"text", wait_timeout=timedelta(seconds=2), name="custom_fts_idx"
"text",
wait_timeout=timedelta(seconds=2),
block_size=256,
name="custom_fts_idx",
)
# Test create_index with custom name (legacy form: vector_column_name kwarg)
@@ -732,6 +794,7 @@ def test_table_create_indices():
fts_req = received_requests[1]
assert "name" in fts_req
assert fts_req["name"] == "custom_fts_idx"
assert fts_req["block_size"] == 256
# Check vector index request has custom name
vector_req = received_requests[2]
@@ -817,7 +880,7 @@ def test_remote_create_index_new_api():
_warnings.simplefilter("error", DeprecationWarning)
table.create_index("vector", config=IvfPq(distance_type="l2"))
table.create_index("category", config=BTree())
table.create_index("text", config=FTS())
table.create_index("text", config=FTS(block_size=256))
# IvfRq via new API
table.create_index("vector", config=IvfRq(distance_type="l2"))
@@ -837,6 +900,7 @@ def test_remote_create_index_new_api():
"vector",
"vector",
]
assert received_requests[2]["block_size"] == 256
def test_table_wait_for_index_timeout():
+15
View File
@@ -644,6 +644,21 @@ def test_cross_encoder_reranker_return_all(tmp_path):
assert "_distance" in result.column_names
def test_answerdotai_reranker_return_all(tmp_path):
pytest.importorskip("rerankers")
reranker = AnswerdotaiRerankers(return_score="all")
table, schema = get_test_table(tmp_path)
query = "single player experience"
result = (
table.search(query, query_type="hybrid", vector_column_name="vector")
.rerank(reranker=reranker)
.to_arrow()
)
assert "_relevance_score" in result.column_names
assert "_score" in result.column_names
assert "_distance" in result.column_names
# ---------------------------------------------------------------------------
# Regression tests for LinearCombinationReranker scoring bugs (issue #3154)
# ---------------------------------------------------------------------------
+183
View File
@@ -0,0 +1,183 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import numpy as np
import pyarrow as pa
import pyarrow.dataset as ds
import pyarrow.parquet as pq
from lancedb.scannable import (
_PA_DEFAULT_BATCH_ROWS,
_SAMPLE_ROWS,
_VARIABLE_WIDTH_ESTIMATE,
_WIDE_BATCH_READAHEAD,
_WIDE_FRAGMENT_READAHEAD,
_bounded_scanner_kwargs,
_estimate_bytes_per_row,
_sample_head,
to_scannable,
)
def test_estimate_bytes_per_row():
# fixed-width scalars
assert _estimate_bytes_per_row(pa.schema([("a", pa.int64())])) == 8
assert (
_estimate_bytes_per_row(pa.schema([("a", pa.int32()), ("b", pa.float64())]))
== 12
)
assert _estimate_bytes_per_row(pa.schema([("a", pa.bool_())])) == 1
# fixed-size list (embedding) dominates
assert (
_estimate_bytes_per_row(pa.schema([("v", pa.list_(pa.float32(), 768))]))
== 768 * 4
)
# struct sums its children
struct = pa.struct([("x", pa.int32()), ("y", pa.int32())])
assert _estimate_bytes_per_row(pa.schema([("s", struct)])) == 8
# variable-width columns get a flat estimate, not zero
assert _estimate_bytes_per_row(pa.schema([("s", pa.string())])) > 0
def test_estimate_bytes_per_row_uses_sample_for_variable_length_lists():
# A vector column without a fixed size (e.g. `list<float32>` instead of
# `list<float32, 768>`) has no width the schema alone can tell us.
schema = pa.schema([("v", pa.list_(pa.float32()))])
assert _estimate_bytes_per_row(schema) == _VARIABLE_WIDTH_ESTIMATE
sample = pa.table({"v": pa.array([[0.0] * 768], type=pa.list_(pa.float32()))})
assert _estimate_bytes_per_row(schema, sample) == 768 * 4
def test_estimate_bytes_per_row_sample_ignores_missing_or_null_lists():
schema = pa.schema([("v", pa.list_(pa.float32()))])
# an all-null sample column can't tell us anything either
sample = pa.table({"v": pa.array([None], type=pa.list_(pa.float32()))})
assert _estimate_bytes_per_row(schema, sample) == _VARIABLE_WIDTH_ESTIMATE
def test_bounded_scanner_kwargs_narrow_uses_defaults():
# Narrow rows stay on pyarrow defaults (empty kwargs) so throughput is
# unchanged.
for schema in [
pa.schema([("a", pa.int64()), ("b", pa.int32()), ("c", pa.string())]),
pa.schema([("a", pa.int64()), ("t", pa.string()), ("u", pa.string())]),
# a 100-dim float32 vector is still under the per-row budget
pa.schema([("id", pa.int64()), ("v", pa.list_(pa.float32(), 100))]),
]:
assert _bounded_scanner_kwargs(schema) == {}, schema
def test_bounded_scanner_kwargs_wide_is_bounded():
schema = pa.schema(
[
("uid", pa.string()),
("img", pa.list_(pa.float32(), 768)),
("txt", pa.list_(pa.float32(), 768)),
]
)
kwargs = _bounded_scanner_kwargs(schema)
assert kwargs, "wide schema should be throttled"
assert kwargs["batch_readahead"] == _WIDE_BATCH_READAHEAD
assert kwargs["fragment_readahead"] == _WIDE_FRAGMENT_READAHEAD
# batch is capped well below the pyarrow default for wide rows
assert kwargs["batch_size"] < _PA_DEFAULT_BATCH_ROWS
def test_bounded_scanner_kwargs_variable_length_list_needs_sample():
# Without a sample, a variable-length (not fixed-size) vector column looks
# narrow because its true width is unknown from the schema alone.
schema = pa.schema([("uid", pa.string()), ("vec", pa.list_(pa.float32()))])
assert _bounded_scanner_kwargs(schema) == {}
sample = pa.table(
{
"uid": pa.array(["a"]),
"vec": pa.array([[0.0] * 768], type=pa.list_(pa.float32())),
}
)
kwargs = _bounded_scanner_kwargs(schema, sample)
assert kwargs, "sample should reveal the wide vector column"
assert kwargs["batch_size"] < _PA_DEFAULT_BATCH_ROWS
def _write_wide_dataset(
path, *, files=2, rows_per_file=20_000, dim=768, fixed_size=True
):
rng = np.random.default_rng(0)
for i in range(files):
emb = rng.standard_normal((rows_per_file, dim), dtype=np.float32)
vec_type = pa.list_(pa.float32(), dim) if fixed_size else pa.list_(pa.float32())
vec_array = (
pa.FixedSizeListArray.from_arrays(pa.array(emb.reshape(-1)), dim)
if fixed_size
else pa.array(emb.tolist(), type=vec_type)
)
table = pa.table(
{
"uid": pa.array([f"{i}_{j}" for j in range(rows_per_file)]),
"vec": vec_array,
}
)
pq.write_table(table, f"{path}/part-{i}.parquet")
def test_dataset_reader_respects_bounded_batch_size(tmp_path):
# The Dataset path should stream small batches for wide rows, not pyarrow's
# 131072-row default, and still return every row.
_write_wide_dataset(str(tmp_path))
dataset = ds.dataset(str(tmp_path), format="parquet")
expected = _bounded_scanner_kwargs(dataset.schema)["batch_size"]
scannable = to_scannable(dataset)
assert scannable.rescannable
assert scannable.num_rows == 40_000
total = 0
for batch in scannable.reader():
assert batch.num_rows <= expected
total += batch.num_rows
assert total == 40_000
# factory can be called again (rescannable)
assert sum(b.num_rows for b in scannable.reader()) == 40_000
def test_dataset_reader_samples_variable_length_list_width(tmp_path):
# A vector column stored without a fixed size (e.g. produced by tools that
# don't tag list columns with their length) is invisible to the
# schema-only estimate, so `to_scannable` must peek a sample of rows to
# detect that it's wide and bound the scanner accordingly.
_write_wide_dataset(str(tmp_path), fixed_size=False)
dataset = ds.dataset(str(tmp_path), format="parquet")
schema_only_kwargs = _bounded_scanner_kwargs(dataset.schema)
assert schema_only_kwargs == {}, "schema alone can't see the list width"
scannable = to_scannable(dataset)
assert scannable.rescannable
assert scannable.num_rows == 40_000
total = 0
for batch in scannable.reader():
assert batch.num_rows < _PA_DEFAULT_BATCH_ROWS
total += batch.num_rows
assert total == 40_000
def test_sample_head_is_bounded_rows(tmp_path):
# The peek itself must not read the whole dataset.
_write_wide_dataset(str(tmp_path), files=1, rows_per_file=1000, fixed_size=False)
dataset = ds.dataset(str(tmp_path), format="parquet")
sample = _sample_head(dataset.head)
assert sample.num_rows == _SAMPLE_ROWS
def test_sample_head_returns_none_for_empty_dataset(tmp_path):
table = pa.table({"v": pa.array([], type=pa.list_(pa.float32()))})
pq.write_table(table, f"{tmp_path}/empty.parquet")
dataset = ds.dataset(str(tmp_path), format="parquet")
assert _sample_head(dataset.head) is None
+23
View File
@@ -434,6 +434,29 @@ def test_add(mem_db: DBConnection):
_add(table, schema)
def test_add_write_parallelism(mem_db: DBConnection):
schema = pa.schema([pa.field("id", pa.int64())])
table = mem_db.create_table("test", schema=schema)
data = pa.table({"id": list(range(1000))}, schema=schema)
table.add(data, write_parallelism=4)
assert len(table) == 1000
# invalid parallelism is rejected
with pytest.raises(ValueError, match="write_parallelism"):
table.add(data, write_parallelism=0)
@pytest.mark.asyncio
async def test_add_write_parallelism_async(mem_db_async: AsyncConnection):
schema = pa.schema([pa.field("id", pa.int64())])
table = await mem_db_async.create_table("test", schema=schema)
data = pa.table({"id": list(range(1000))}, schema=schema)
await table.add(data, write_parallelism=4)
assert await table.count_rows() == 1000
def test_add_struct(mem_db: DBConnection):
# https://github.com/lancedb/lancedb/issues/2114
schema = pa.schema(

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