Commit Graph

1152 Commits

Author SHA1 Message Date
Xuanwo 2b10f2a7ce feat: bridge Python UDF definitions to Rust 2026-08-12 04:34:58 +08:00
Xuanwo f8bb90405f feat: declare Python function capabilities 2026-08-12 03:57:52 +08:00
Xuanwo 76aac96749 feat: validate Python UDF source packages 2026-08-12 03:47:17 +08:00
Xuanwo 0093bc8179 feat: add Python UDF declarations 2026-08-12 03:28:05 +08:00
Xuanwo ac35a687f1 feat: expose Python function job results 2026-08-12 03:12:36 +08:00
Sravan Avvaru a615306f39 feat(python): add on_transform_error fault tolerance to StreamingDataset (#3763)
Closes #3704

## Problem

Transforms can fail on bad data (e.g. nulls/NaNs from incomplete user
surveys). Today any transform exception aborts iteration, and there is
no way to skip invalid rows during loading.

## Solution

New `on_transform_error` parameter on `StreamingDataset`:

- `"raise"` (default, matches current behavior and the convention in
tf.data / WebDataset / Ray Data)
- `"skip"` — drop the failing rows and continue
- `"warn"` — like skip, plus a logged warning per failing batch
- a WebDataset-style callable `handler(exc) -> bool`, so users can skip
only expected error types

Key design points:

- **Row-granular skipping**: when a batch fails, the transform is re-run
on single-row slices so only the rows that actually fail are dropped
(avoids Ray-style whole-block loss). Skips are counted in a new
`rows_skipped` property.
- **No crash on uneven skips**: the round-robin loop now ends the epoch
at the last cycle where every split still has a row, instead of hitting
`IndexError` when a split runs dry early.
- **Exact resumability under skips**: checkpoints are now
position-based. `state_dict` gains `positions_consumed_per_split` (exact
for owned splits), and a new `merge_state_dicts` static method combines
per-rank states via elementwise max for elastic resume across topology
changes. Old checkpoints without the new key still load. Positions equal
sample counts when nothing is skipped, so existing behavior is
unchanged.
- **Guardrail**: transforms returning the wrong number of rows now raise
a clear `ValueError` instead of silently corrupting split accounting.

### Answers to the issue's open questions

- *Can we do this?* Yes — all transforms funnel through one guarded call
in the Stage 2 pipeline.
- *What do other libraries do?* tf.data `ignore_errors()`, WebDataset
`handler=`, Ray `max_errored_blocks`; MosaicML StreamingDataset offers
nothing (skipping conflicts with its determinism model). This design
follows the common conventions: raise by default, opt-in skipping,
count/log drops.
- *Error handling or pre-filtering?* Both: the existing `filter=`
remains the recommended tool for predictable bad data (splits are built
post-filter, so all guarantees hold — now documented);
`on_transform_error` covers failures not expressible as a predicate.
- *Impact on splits / elastic determinism?* Per-split sample sequences
stay deterministic (skips are data-dependent, not topology-dependent).
With unequal bad-row counts across splits the last few global steps of
an epoch can differ across topologies (bounded by the skew), which is
documented on the parameter. With equal counts per split, full
determinism is preserved — covered by a test.

## Testing

15 new tests in `test_elastic_dataloader.py` covering: default raise,
invalid values, uniform and uneven skips (including epoch-end
truncation), warn logging, selective callable handlers, wrong-row-count
guardrail, determinism across runs and across world sizes (1/2/3/4) with
skips, exact mid-epoch resume with skips on the same topology, elastic
resume via `merge_state_dicts` (ws=2 → ws=1), merge validation, and
backward-compat loading of old checkpoints.

Note: relying on CI for the test run — my local machine OOMs during the
final link of the native extension. The change itself is pure Python.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 09:22:06 -07:00
Xuanwo 920fc0e455 fix(python): set native module metadata (#3913)
PyO3 defaults native extension classes to `builtins`, so
mkdocstrings/Griffe could not resolve the newly documented
`lancedb.Session` alias and `Deploy docs to Pages` failed on `main`.
Declare the extension module for the public native types referenced by
the Python API docs so Griffe resolves them through `lancedb._lancedb`
and Pages can build again.

Validated with the docs toolchain used by CI (`griffe==0.49.0`,
`mkdocstrings==0.25.2`, and `mkdocs==1.6.1`); `PYTHONPATH=. mkdocs
build` succeeds.
2026-08-10 21:40:31 +08:00
Dan Tasse 77a93fee76 fix: get table size from metadata, not files (#3790)
Some issues:
- file_size_bytes is optional in the manifest, so if it's not there (old
writer I guess) it'll under-report the table size.
- it changes results a little bit from the old way by including per-file
footers and metadata (probably not a big difference at real scale)

---------

Co-authored-by: Will Jones <willjones127@gmail.com>
2026-08-07 17:41:41 -04:00
Lance Release 7bb501839a Bump version: 0.37.1-beta.0 → 0.37.1-beta.1 2026-08-07 21:16:07 +00:00
Andrew Chen 5b347afd99 fix: avoid AttributeError in JinaEmbeddings image input for str/Path (#3670)
## What

`JinaEmbeddings._generate_image_input_dict()` crashes with
`AttributeError: 'function' object has no attribute 'urlparse'` on any
image given as a URL string, local path string, or `pathlib.Path` — i.e.
every documented `jina-clip-v1` image-embedding use case except raw
`bytes`.

## Why

```python
from urllib.parse import urlparse
...
parsed = urlparse.urlparse(image)
```

`urlparse` is imported as a function, then called as if it were the
`urllib.parse` module (`urlparse.urlparse(...)`). The module-level
`is_valid_url()` a few lines above does it correctly (`urlparse(text)`),
which is why this reads as a typo rather than intentional. Fixed to
`urlparse(str(image))` — `str()` is needed because `urlparse()` only
accepts `str`/`bytes` and raises a different `AttributeError` on a raw
`Path`.

## Testing

Added `test_jina_generate_image_input_dict_local_path`, which fails with
the original `AttributeError` before the fix and passes after, covering
both a `str` path and a `pathlib.Path`. Verified locally (built the Rust
extension, ran red→green, then the full `test_embeddings.py` file: 15
passed / 8 skipped, no regressions) and with `ruff check`/`ruff format`.

---
Disclosure: this PR was drafted with AI assistance (Claude); I reviewed,
tested, and take responsibility for the change.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-07 14:05:45 -07:00
Dan Rammer 706a9c327f feat: infer maintained indexes when an LsmWriteSpec omits them (#3748)
## What

`LsmWriteSpec::maintained_indexes` becomes `Option<Vec<String>>`:

| value | meaning |
|---|---|
| `None` (new default) | every index the MemWAL supports, resolved when
the spec is installed |
| `Some([])` | maintain nothing — a scan/filter-only WAL table |
| `Some([..])` | exactly these, taken verbatim |

`with_maintained_indexes` keeps its signature;
`with_no_maintained_indexes()` is new. Surfaced through the remote path
(null on the wire), Python, and Node.

## Why

Callers had to state the maintained set by hand every time, which is
both tedious and easy to get wrong — the common case is "maintain what I
already built."

Resolution filters on `IndexConfig::is_memwal_maintainable`, delegating
to lance's `is_maintainable_index_type`. This is load-bearing rather
than cosmetic: lance does **not** skip an index type its memtable cannot
build, it errors when the shard writer opens, so sweeping up a bitmap
index would fail every memtable claim and leave the table unwritable.
The inferred set excludes those, and an explicit list naming one is now
rejected at spec time instead of at claim time.

## Behavior change

A freshly constructed spec used to maintain **nothing**; it now
maintains **everything supported**. This flipped because napi collapses
`undefined` and `null` to `None`, so TypeScript cannot express "absent
means nothing, null means all" — any other choice makes the bindings
disagree with the wire. The error direction also favors it: an unwanted
maintained index costs memory, while a silently unmaintained one
degrades FTS to an unscored scan.

Three existing tests encoded the old default and are updated rather than
worked around.

## Caveat

The resolved set is a snapshot, not a subscription. An index created
after the spec is installed is not maintained until the spec is unset
and set again. `get_lsm_write_spec` therefore always reports a concrete
list — `None` never round-trips.

## Dependency

Needs a lance release carrying `is_maintainable_index_type`
(lance-format/lance#8095) before this builds against the pinned tag.
Draft until then.

## Testing

38 Rust LSM tests and 10 Python tests pass against a local lance build,
including new coverage that a bitmap index is excluded from inference
and rejected when named, and that `[]` stays distinguishable from null
on the wire.

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 14:50:22 -05:00
Dan Rammer 79ba076429 feat(table): checkpoint_lsm, flush_lsm, compact_lsm, get_lsm_stats (#3736)
Converge a table's LSM write path into its base table, and inspect it.

`checkpoint_lsm` is `flush` then `compact`, repeated until the fresh
tier is empty — and the loop runs **client-side**. Putting it on the
server would mean a background task, which means a single-flight intent,
an intent that leaks on panic, a bounded-iteration policy, an "is it
done" observable, and a story for every way a client can vanish
mid-operation. None of that exists in this shape: each request does a
bounded unit of work and reports what is left, so completion is *carried
in the responses* rather than inferred from a shared counter that cannot
distinguish "converged" from "hasn't started yet".

Best-effort by construction. Nothing is frozen, so `converged` means L0
was empty as of the last pass. It is idempotent, abandonable at any
point with zero consequence, and safe to run on a cadence — an
already-converged table costs one round trip and zero compaction passes,
because `flush` reports `generations_remaining` and the loop is never
entered.

## The failure taxonomy is the load-bearing part

Five distinct conditions used to arrive at a client as one 503.
`Error::LsmRoute` carries a classification read from the response body's
namespace error code **at the point of receipt** — before any generic
helper folds the body into a string and keeps only the status.

| condition | wire | client action |
|---|---|---|
| contention (latch held / pool saturated) | 429, code 21 | retry with
backoff |
| owning node draining | 503, code 19 `InvalidTableState` | **stop** |
| fenced / no slot / transport | 503, code 17 | retry with backoff |
| registry entry vanished | 404 | re-issue from `flush` (capped) |
| table being dropped / not WAL-backed | 409 / 400 | stop |

Draining is terminal because the drain gate is a one-way latch —
retrying spins until the deadline to report a failure that was knowable
on the first response. Transport retry is disabled on these routes for
the same reason: it treats every 503 alike and would burn its budget
before the classifier ever saw the body.

`get_lsm_stats` returns `Option<LsmStats>`, matching
`get_lsm_write_spec` — `None` only when the table has no LSM write path,
since a struct of zeros would read as measurements.

Python bindings mirror all four, preserving per-bucket detail rather
than flattening to a table-level summary.

## Testing

Six new unit tests against the mocked endpoint, plus the taxonomy
round-trip:
- flush into an empty L0 issues **zero** compact calls (asserts the call
count — `generations_consumed: 0` is also true of a loop that ran a
pointless pass)
- the loop drives compact until the server reports zero remaining
- **contention is not draining**: a 429 retries and converges; asserts
the retry count
- a draining node stops after **exactly one** request, no retries
- stats round-trips fully populated; `include_generation_rows` off by
default
- every `(status, code)` pair classifies correctly, including
unparseable 503 bodies falling back to *retryable* rather than terminal

`cargo test -p lancedb --features remote --lib`: 723 passed.

## Notes for review

- Depends on the sibling lance change returning `SealedGeneration` from
`force_seal_active` only at the *server* level — no lance API is used
here.
- The branch is based on `codex/update-lance-10-0-0-beta-5`, so it
carries one extra commit (`chore: update lance dependency to
v10.0.0-beta.5`) that is not part of this change.

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

---------

Co-authored-by: lancedb automation <robot@lancedb.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 13:44:49 -05:00
lancedb-gatefixer[bot] 607e556927 test(python): cover search after schema merge (#3784)
## Summary

- add an end-to-end regression for indexed vector search after merging a
pandas column
- verify unmatched rows retain a null merged value instead of failing
Arrow batch assembly

## Root cause

Historical Lance readers could assemble schema-evolved columns in
physical data-file order. Indexed row-ID reads after a merge could
therefore omit or misorder the newly merged column for unmatched rows.
The currently pinned Lance release contains the reader correction, but
LanceDB did not cover the reported merge-then-search path.

## Validation

- uv run --extra tests pytest python/tests/test_table.py::test_merge
python/tests/test_table.py::test_search_after_merge -q
- uv run --project python --extra dev ruff check .
- uv run --project python --extra dev ruff format --check
python/python/tests/test_table.py

Fixes #599

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:32:28 +08:00
lancedb-gatefixer[bot] 564e5d0d56 fix(python): support Polars 1.32 table scans (#3801)
## Root cause

`Table.to_polars()` disabled PyArrow predicate pushdown by selecting the
non-PyArrow Polars scan callback. Polars 1.32.3 invokes that callback
with `batch_size` both positionally and through its partial, so
collecting the returned lazy frame raises `TypeError:
_scan_pyarrow_dataset_impl() got multiple values for argument
batch_size`.

## Fix

- Keep the compatible PyArrow callback path.
- Add an identity `map_batches` barrier so predicates stay in Polars
instead of reaching the LanceDB adapter as unsupported PyArrow
expressions.
- Extend the tested Polars range through 1.32.3 and retain lazy-frame
regression coverage.

## Validation

- `python/tests/test_table.py::test_polars` with Polars 1.32.3
- `python/tests/test_table.py::test_polars` with the locked Polars 1.3.0
baseline
- `ruff format --check` on the changed Python files
- `ruff check .`
- `uv lock --check`

Fixes #2619

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:32:17 +08:00
lancedb-gatefixer[bot] dd5cb4d805 test(python): cover float16 table creation from Arrow data (#3785)
## Summary

- exercise float16 sanitization through the reported direct Arrow-data
table creation path
- assert that the inferred fixed-size vector schema remains float16
- retain end-to-end index creation and vector search coverage

## Root cause and fix

PyArrow 16 does not provide an is_nan kernel for half-float arrays, so
passing float16 vector values directly to that kernel raises
ArrowNotImplementedError. LanceDB's sanitizer already carries the
compatibility fix from #837: it casts float16 values to float32 only for
NaN detection while preserving the stored vector type.

The existing end-to-end regression created an empty schema-defined table
and added data afterward. This change aligns that regression with the
issue reproduction by creating a table directly from a
FixedSizeList<float16> Arrow table and verifying the persisted schema.

## Validation

- uv run --extra tests pytest
python/tests/test_table.py::test_create_f16_table_from_arrow_data -q
- direct 1,000-row by 128-dimension float16 Arrow-table reproduction
- PyArrow 16.1 half-float is_nan kernel reproduction
- uvx ruff@0.15.20 format --check python/python/tests/test_table.py
- uvx ruff@0.15.20 check .

Fixes #835

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:32:05 +08:00
lancedb-gatefixer[bot] ec80acb668 fix(python): expose inline types to downstream checkers (#3817)
## Summary

- publish the PEP 561 `py.typed` marker so downstream type checkers
consume the inline public annotations
- add a Pyright contract test that distinguishes synchronous `connect`
from awaited `connect_async`
- verify the marker is present in the installed package

## Root cause

The public Python module already annotated `lancedb.connect` as
synchronous and `lancedb.connect_async` as asynchronous. The private
native `_lancedb.connect` stub is intentionally awaitable because it
backs `connect_async`. However, the distribution did not include a PEP
561 marker, so downstream tools such as mypy could ignore the public
inline annotations and expose misleading or incomplete type information.

## Validation

- `python/.venv/bin/ruff format --check python/python/tests/test_db.py
python/python/type_tests/connect.py`
- `python/.venv/bin/ruff check .`
- `cd python && .venv/bin/pytest
python/tests/test_db.py::test_package_includes_pep_561_marker -q`
- `cd python && .venv/bin/pyright --pythonpath .venv/bin/python`
- downstream mypy contract check for both public connection functions

Fixes #2159

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

Co-authored-by: lancedb-gatefixer[bot] <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:31:42 +08:00
lancedb-gatefixer[bot] fc44535cee fix(python): clarify bare Vector annotations (#3809)
## Summary

- raise a clear `TypeError` when `Vector` is used without a dimension
- preserve normal `Vector(dim)` behavior across Pydantic v1 and v2
- add a regression test that defines a model without importing PyArrow

## Root cause

Pydantic interpreted the bare `Vector` factory as a callable field type
and inspected its postponed annotations in the user model's namespace.
Because that namespace did not define LanceDB's internal `pa` alias,
model construction failed with the misleading `NameError: name 'pa' is
not defined` instead of explaining that `Vector` must be parameterized.

The factory now exposes Pydantic's v1 and v2 schema hooks and rejects
bare use before signature introspection with guidance to use
`Vector(dim)`.

## Validation

- `uvx --from 'ruff==0.15.20' ruff check .`
- `uvx --from 'ruff==0.15.20' ruff format --check
python/python/lancedb/pydantic.py python/python/tests/test_pydantic.py`
- `cd python && uv run --extra tests pytest
python/tests/test_pydantic.py::test_bare_vector_raises_clear_error -q`
- `cd python && uv run --extra tests pytest
python/tests/test_pydantic.py -q`
- compatibility checks with Pydantic 1.10.22, 2.11.4, and 2.13.4

Fixes #2384

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:31:30 +08:00
lancedb-gatefixer[bot] 4048150fdd test(python): cover nullable fixed-size-list ingestion (#3812)
## Summary

- add regression coverage for adding dictionary rows with a nullable
fixed-size-list column
- verify ordinary list columns remain aligned alongside the null
fixed-size-list value

## Root cause

PyArrow infers an all-`None` dictionary column as the generic `null`
type. The original schema-alignment path treated the target
fixed-size-list type as proof that the inferred source was also
list-like and unconditionally accessed `value_field`, which raised
`AttributeError`. Current alignment logic correctly falls back to the
target type when the source is not list-like; this test locks in that
repair for the reported ingestion path.

## Validation

- `uv run --extra tests pytest
python/tests/test_table.py::test_add_with_empty_fixed_size_list_drops_bad_rows
python/tests/test_table.py::test_add_nullable_fixed_size_list_with_none
python/tests/test_table.py::test_add_nullable_struct_with_none -q`
- `uv run --with pyarrow==19.0.1 --extra tests pytest
python/tests/test_table.py::test_add_nullable_fixed_size_list_with_none
-q`
- `uv run --project python --extra dev ruff format --check
python/python/tests/test_table.py`
- `uv run --project python --extra dev ruff check .`

Fixes #2340

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:31:19 +08:00
lancedb-gatefixer[bot] c5f9efefe9 test(python): cover local sync multiple-vector search (#3830)
## Summary
- add regression coverage for multiple query vectors in the local
synchronous Python API
- verify that each query vector receives its own limited
nearest-neighbor result and `query_index`

## Root cause
In LanceDB v0.16, the local synchronous scanner passed a nested vector
array as one query, unlike the async and remote implementations. The
subsequent sync-to-async table migration supplied the correct shared
runtime path, but this local sync behavior was never regression-tested
and issue #1857 remained open.

## Validation
- `uv run --extra tests pytest
python/tests/test_query.py::test_query_multiple_vectors -q`
- `uv run --project python --extra tests --extra dev ruff format --check
python/python/tests/test_query.py`
- `uv run --project python --extra tests --extra dev ruff check .`

Fixes #1857

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-07 17:30:55 +08:00
lancedb-gatefixer[bot] 1c3cd1d918 fix(python): accept Arrow scalars in table updates (#3838)
## Summary
- convert PyArrow scalar values through their Python representation
before SQL literal rendering
- add an end-to-end regression for updating a fixed-size-list vector
from a queried FixedSizeListScalar

## Root cause
Python update literal conversion used single dispatch for native Python
and NumPy values but had no PyArrow Scalar registration. A
FixedSizeListScalar returned by a query therefore reached the
unsupported generic conversion instead of the existing recursive list
converter.

## Validation
- uv run --extra tests pytest python/tests/test_table.py::test_update
python/tests/test_table.py::test_update_with_arrow_scalar
python/tests/test_table.py::test_update_types -q
- uv run --extra tests pytest python/tests/test_util.py -q
- uv run --project python --extra tests --extra dev ruff format --check
python/python/lancedb/util.py python/python/tests/test_table.py
- uv run --project python --extra tests --extra dev ruff check .

Fixes #1228

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

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

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

## Root cause

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

## Validation

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

Fixes #799

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:48:56 +08:00
lancedb-gatefixer[bot] c1a3fa7f51 fix(python): preserve repeated indexed merge inserts (#3850)
## Summary

- add a Python regression test for two partial-schema merge inserts
against the same BTree-indexed rows
- verify repeated updates retain one copy of every row and the final
update values

## Root cause

Lance 4.0, used by LanceDB 0.30.2, removed a rewritten fragment from the
index bitmap while stale BTree entries for that fragment remained
searchable. The next merge found each target through both the stale
index and the unindexed-fragment scan, producing the ambiguous-match
error. Lance fixed the root cause in lance-format/lance#6563 by applying
the fragment-bitmap allow-list to index results, and the Lance release
pinned by current LanceDB includes that fix. This test preserves the
corrected behavior through the Python API.

## Validation

- `cd python && uv run --extra tests pytest python/tests/test_table.py
-k merge_insert -q` (9 passed)
- `cd python && uv run --extra tests --extra dev ruff format --check
python/tests/test_table.py`
- `cd python && uv run --extra tests --extra dev ruff check
python/tests/test_table.py`

Repository-wide Ruff also reports 20 pre-existing violations in
untouched CI and plugin scripts.

Fixes #3280

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:48:01 +08:00
lancedb-gatefixer[bot] 0ba82873c5 fix(python): cover nullable list v2.2 decoding (#3853)
## Summary

- add a minimized regression for mostly-null `list<float32>` data at the
v2.2 structural page boundary
- verify scans preserve all 64,885 rows, including 64,668 null list
values

## Root cause

Lance 3.0.0 sliced repetition/definition state using top-level row
offsets in the complex all-null decoder. At this page boundary, the list
and validity children were materialized at different lengths. The
current Lance dependency contains the upstream decoder repair; this test
locks that behavior into the LanceDB Python suite without duplicating
decoder logic.

## Validation

- reproduced the attached 1,892,466-row case on `lancedb==0.30.0` with
`expected 1024 got 285`
- verified the full attachment reads on the current branch
- `python/.venv/bin/ruff format --check
python/python/tests/test_table.py`
- `python/.venv/bin/ruff check .`
- `cd python && uv run --extra tests pytest
python/tests/test_table.py::test_read_mostly_null_list_v2_2_page_boundary
-q`
- `cd python && uv run --extra tests pytest python/tests/test_table.py
-q` (137 passed)

Fixes #3194

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:47:29 +08:00
lancedb-gatefixer[bot] 2c06a48bd8 test(python): cover Arrow buffer release after add (#3860)
## Summary

- add deterministic regression coverage that `Table.add()` releases
backing Arrow buffers without cyclic garbage collection
- track the foreign buffer owner rather than RSS, separating live input
retention from allocator high-water behavior
- preserve the bounded-lifetime behavior of the Scannable writer that
superseded the historical preprocessing path

## Root cause

The historical Python preprocessing/write path produced a high allocator
RSS while ingesting very wide IPC batches. The current Scannable writer
releases each input buffer when `Table.add()` completes; remaining RSS
is allocator high-water rather than a live Arrow reference. The resolved
behavior had no regression coverage, so a future native lifetime
regression could silently reintroduce the original failure mode.

## Validation

- `uv run --extra tests --extra dev maturin develop`
- `uv run --project python --extra tests pytest
python/python/tests/test_table.py::test_add
python/python/tests/test_table.py::test_add_releases_arrow_buffers_without_gc
-q`
- `uv run --project python --extra dev ruff format --check
python/python/tests/test_table.py`
- `uv run --project python --extra dev ruff check .`

Fixes #2512

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:45:10 +08:00
lancedb-gatefixer[bot] ac8b28c010 fix(python): support nullable pandas merge input (#3864)
## Summary

- add an end-to-end Python regression for pandas DataFrame inputs merged
into a table created from a Pydantic model
- verify reordered, nullable Arrow source fields can update and insert
into a non-nullable target schema when the values contain no nulls

## Root cause

Lance merge_insert previously compared source schema nullability with
the target, unlike add. The upstream fix now pinned by LanceDB ignores
declared nullability during schema compatibility and validates actual
null values at write time. LanceDB lacked regression coverage for the
full pandas-to-Pydantic path, so this test locks in the correct behavior
without falsifying the input schema nullability.

## Validation

- 5 focused merge-insert tests passed
- Ruff lint passed for the repository
- Ruff format check passed for the changed file
- git diff --check passed

Fixes #2366

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:44:38 +08:00
lancedb-gatefixer[bot] 173f889d2a test(python): cover stale scalar prefilters in hybrid search (#3865)
## Summary

- capture the stale-index state behind the reported fixed-size-binary
panic: the vector and FTS indices cover newer fragments while the BTree
prefilter does not
- verify vector, FTS, and hybrid searches return matches from both
scalar-indexed and unindexed fragments without panicking
- preserve binding-level coverage for the Lance fix in
https://github.com/lance-format/lance/pull/3768, which restricts
incomplete scalar prefilters when search indices are further ahead

The production root cause is in Lance and the current LanceDB dependency
already contains that fix, so this change adds the missing LanceDB
Python regression coverage.

## Validation

- `cd python && uv run --no-sync pytest
python/tests/test_hybrid_query.py::test_hybrid_query_with_stale_fixed_size_binary_prefilter
-q`
- `cd python && uv run --no-sync pytest
python/tests/test_hybrid_query.py -q`
- `python/.venv/bin/ruff check .`
- `python/.venv/bin/ruff format --check
python/python/tests/test_hybrid_query.py`

Fixes #2370

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:44:20 +08:00
lancedb-gatefixer[bot] 798e5364fb test(python): cover VoyageAI text source routing (#3872)
## Summary

- add fast regression coverage for VoyageAI `voyage-3` source embeddings
- verify table text uses `client.embed` and never
`client.multimodal_embed`

## Root cause

The original VoyageAI source-embedding path treated table source values
as images and always invoked the multimodal API. Production routing was
corrected by later merged changes, but the table regression was covered
only by API-gated slow tests. This test locks the corrected text routing
into the regular unit suite.

## Validation

- `cd python && uv run --extra tests pytest
python/tests/test_voyageai_embeddings.py -q`
- `uv run --project python --extra tests --extra dev ruff format --check
python/python/tests/test_voyageai_embeddings.py`
- `uv run --project python --extra tests --extra dev ruff check .`

Fixes #2059

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:42:20 +08:00
lancedb-gatefixer[bot] f1f34dfdd3 fix(python): instruct dimension probe for instructor embeddings (#3874)
## Summary

- pass an Instructor-compatible `[instruction, text]` pair when
detecting embedding dimensions
- add a regression test that verifies the dimension probe uses the
configured source instruction

## Root cause

`InstructorEmbeddingFunction.ndims()` encoded a bare string even though
Instructor models require instruction/text pairs. With affected
`sentence-transformers` versions, the bare input omitted
`instruction_mask` and raised `KeyError` while defining the LanceDB
schema.

## Validation

- `uv run --extra tests pytest python/tests/test_embeddings.py -q` (`14
passed, 9 skipped`)
- `uv run --project python --extra tests --extra dev ruff format --check
python/python/lancedb/embeddings/instructor.py
python/python/tests/test_embeddings.py`
- `uv run --project python --extra tests --extra dev ruff check .`

Fixes #2041

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:41:11 +08:00
lancedb-gatefixer[bot] 123c921c4f test(python): cover sliced nullable table search (#3875)
## Summary

- add a Python regression for vector search over a sliced Arrow table
with nullable scalar columns
- verify the nearest row retains its non-null score values after the
table is written

## Root cause

Lance 0.19.2 deep-copied a validity bitmap without preserving its
non-zero bit offset. For a sliced nullable table, scalar values and
vectors began at the slice while the copied validity bitmap began at the
parent table's first row. That made valid score values appear null even
though the corresponding vector stayed intact. The upstream Lance repair
is already present in the current dependency; this adds a LanceDB-level
guard for the reported create/search path.

## Validation

- reproduced on Python 3.12 with LanceDB 0.16.0, pylance 0.19.2, PyArrow
18.0.0, and Polars 1.14.0
- `uv run --project python --extra dev ruff format --check
python/python/tests/test_table.py`
- `uv run --project python --extra dev ruff check .`
- `cd python && uv run --extra tests pytest
python/tests/test_table.py::test_search_preserves_nulls_from_sliced_arrow_table
-q`

Fixes #1879

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:40:41 +08:00
lancedb-gatefixer[bot] 9e73d440a3 test(python): cover schema-only vector table creation (#3882)
## Summary

- make the existing #1968 regression explicitly assert that schema-only
table creation succeeds
- verify the new table has zero rows and preserves the requested
fixed-size vector schema before accepting subsequent data

## Root cause

In v0.16.0, schema-only table creation sent an empty table through
vector sanitization, which calculated a remainder using `len(data)` and
raised `ZeroDivisionError`. Later refactors removed that runtime path,
but the issue-specific regression only asserted the final row count
after a subsequent add. This change makes the reported operation and its
expected empty-table state explicit so the original defect remains
directly covered.

## Validation

- `uv run --extra tests pytest
python/tests/test_table.py::test_create_table_without_data_with_vector_schema
-q`
- `uv --project python run --extra tests --extra dev ruff format --check
python/python/tests/test_table.py`
- `uv --project python run --extra tests --extra dev ruff check .`
- `git diff --check`

Fixes #1968

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

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

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

## Root cause

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

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

## Validation

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

Fixes #1884

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:38:27 +08:00
lancedb-gatefixer[bot] 16e1967efc fix(python): align wheel ABI with supported versions (#3884)
## Summary

- align the PyO3 runtime and build ABI floor with the declared Python
3.10 minimum
- add a regression test that keeps both ABI features synchronized with
`requires-python`

## Root cause

The Python 3.10 support-floor update originally changed PyO3 to
`abi3-py310`, but a later dependency update reverted both PyO3 features
to `abi3-py39`. Published Windows wheels were consequently tagged
`cp39-abi3` while importing `PyCMethod_New`, a stable-ABI procedure
absent from CPython 3.9.0 and 3.9.1. Windows reports that mismatch as
“The specified procedure could not be found” while loading `_lancedb`.

Restoring `abi3-py310` makes the wheel tag and native imports agree with
the package metadata and prevents future wheels from advertising
unsupported Python 3.9 compatibility.

## Validation

- `uv run --extra tests pytest python/tests/test_package_metadata.py -q`
- `uv run --extra tests --extra dev ruff format --check .`
- `uv run --extra tests --extra dev ruff check .`
- `cargo fmt --all`
- `cargo check --quiet -p lancedb-python`
- `uvx --from maturin==1.12.4 maturin build --profile ci` (built
`lancedb-0.37.1b0-cp310-abi3-manylinux_2_34_x86_64.whl`)

Fixes #2051

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:36:58 +08:00
lancedb-gatefixer[bot] 27dd92c67e test(python): cover debugger-safe connection inspection (#3880)
## Summary

- expand the synchronous debugger regression to enumerate every exposed
connection attribute while the Python background loop is unavailable
- retain direct representation checks for connections and tables

## Root cause

VS Code debugpy suspends Python threads at a breakpoint and inspects
local variables. Connection representation and property access
previously dispatched asynchronous work to LanceDBBackgroundEventLoop
and waited for the suspended loop thread, deadlocking the debugger. The
production safeguards landed in #3620 and #3788; this regression
exercises debugger-style whole-object expansion so a newly exposed
property cannot reintroduce the original failure.

## Validation

- uv run --no-sync pytest
python/tests/test_db.py::test_sync_debugger_inspection_does_not_use_background_loop
python/tests/test_db.py::test_read_consistency_interval_does_not_use_background_loop
-q (2 passed)
- uv run --no-sync pytest python/tests/test_db.py -q (48 passed)
- python/.venv/bin/ruff format --check python/python/tests/test_db.py
- python/.venv/bin/ruff check .
- git diff --check

Fixes #3611

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:36:27 +08:00
lancedb-gatefixer[bot] 9e2e711c7a test(python): cover OpenAI registry variable round-trip (#3863)
## Summary

- replace the synthetic registry-variable metadata test with the OpenAI
embedding function reported in #2387
- verify the resolved API key survives table metadata reconstruction
- assert the OpenAI client receives the resolved key while serialized
metadata retains the variable reference

## Root cause

LanceDB 0.22.0 reconstructed embedding functions from table metadata
with the model constructor, bypassing EmbeddingFunction.create and
leaving the literal $var:api_key placeholder in OpenAI configuration.
The production path was corrected for duplicate #2181 by #2640; this
change gives that fix direct, network-free OpenAI regression coverage
for #2387.

## Validation

- uv run --extra tests pytest python/tests/test_embeddings.py -q (13
passed, 9 skipped)
- uv run --project python --extra dev ruff check .
- uv run --project python --extra dev ruff format --check
python/python/tests/test_embeddings.py
- git diff --check

Fixes #2387

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:36:05 +08:00
lancedb-gatefixer[bot] c3176a47ce fix(python): report unsplittable IVF partition errors (#3846)
## Summary

- add Python regression coverage for an IVF build that cannot form all
requested non-empty partitions
- verify hierarchical k-means returns an actionable RuntimeError instead
of panicking or silently creating a degenerate index
- exercise the current Lance v10.1.0-beta.1 dependency, which contains
the upstream error-return fix

## Root cause

Hierarchical k-means previously guarded a shortfall in generated
clusters with only a debug assertion. Debug builds panicked, while
release builds could silently publish an index with many empty
partitions. The upstream Lance fix now returns a descriptive error and
is already included in the dependency pinned on main; this test locks in
propagation through the LanceDB Python API.

## Validation

- uv run --extra tests pytest python/tests/test_index.py -q (24 passed)
- uv run --extra tests pytest
python/tests/test_index.py::test_create_ivf_index_reports_unsplittable_partitions
-q (1 passed)
- python/.venv/bin/ruff format python/python/tests/test_index.py
- python/.venv/bin/ruff check .
- git diff --check

Fixes #3649

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

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-06 16:13:54 +08:00
lancedb-gatefixer[bot] 7357d63e87 fix(python): guard concurrent table deletes (#3787)
<!-- lance-gatekeeper-fix:v1 agent=5c80c44c083b3b8ad0da595419d468fc
generation=1 -->

## Root cause

The legacy synchronous Python table called `delete` on a shared, mutable
`lance.Dataset`. Concurrent table operations could hold a PyO3 borrow
while delete requested an exclusive borrow, producing `RuntimeError:
Already borrowed`. The current async-backed binding fixes this by
cloning its thread-safe Rust table handle before awaiting, but that
concurrency contract had no regression coverage.

## Fix

- Document why delete must clone the Rust table handle before entering
its async future.
- Add a barrier-synchronized regression test that deletes distinct rows
through one shared table from eight Python threads.
- Verify every delete commits exactly one row, every commit gets a
distinct version, and no rows remain.

## Validation

- `cargo check --quiet --features remote --tests --examples`
- `cargo fmt --all -- --check`
- `uv run --extra tests --extra dev ruff format --check
python/tests/test_table.py`
- `uv run --extra tests --extra dev ruff check
python/tests/test_table.py`
- `uv run --extra tests --extra dev pytest
python/tests/test_table.py::test_concurrent_deletes_are_thread_safe
python/tests/test_table.py::test_delete
python/tests/test_table.py::test_delete_expr
python/tests/test_table.py::test_delete_expr_async -q` (4 passed)
- Manual stress reproduction: 100 concurrent deletes on one table
completed at versions 2–101 with zero rows remaining.

Fixes #530

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-05 15:17:04 -07:00
lancedb-gatefixer[bot] 624a75edf7 fix(python): avoid debugger deadlock during connection inspection (#3788)
## Summary

- cache the immutable read consistency interval on synchronous
connection wrappers
- keep debugger property expansion from dispatching to the background
event loop
- cover direct connections and wrappers reconstructed from native
connections

## Root cause

The debugger expands connection variables by evaluating properties after
suspending all Python threads.
`LanceDBConnection.read_consistency_interval` dispatched a coroutine to
`LanceDBBackgroundEventLoop` and synchronously waited for it, but that
loop thread was also suspended, causing a deadlock.

## Validation

- `uv run --no-sync pytest python/tests/test_db.py -q` (48 passed)
- `ruff format --check python/python/lancedb/db.py
python/python/tests/test_db.py`
- `ruff check .`
- `git diff --check`

Fixes #3773

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

---------

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-08-05 15:15:49 -07:00
Wyatt Alt 8e24dd3828 feat(rust)!: make add_columns a builder (#3778)
Table::add_columns now takes no arguments and returns AddColumnsBuilder,
so calls become .add_columns().transform(t).execute().

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

Matches the builders add, update, and merge_insert already use.
2026-08-04 11:18:22 -07:00
Adityaj0 f79dc017c4 fix: when_not_matched_by_source_delete() doesn't reset a previously-set condition (#3771)
## Summary

`LanceMergeInsertBuilder.when_not_matched_by_source_delete()` didn't
clear a previously-set condition when called again with no argument (or
a different condition type). Per the docstring, `condition=None` means
"delete all unmatched rows," but if the builder had already been
configured with a string/Expr condition, a later no-arg call left the
stale condition in place instead of widening the delete to
unconditional.

Fixes #3767

## Change

Each call now unconditionally sets both
`_when_not_matched_by_source_condition` and
`_when_not_matched_by_source_condition_expr` (one to the new value, the
other to `None`), so the latest call always wins — consistent with every
other setter on this builder (e.g.
`when_matched_update_all(where=...)`).

## Test plan

- [x] New regression test
`test_merge_insert_by_source_delete_reconfigure` in
`python/python/tests/test_table.py`
- [x] `uv run --extra tests pytest
python/tests/test_table.py::test_merge_insert_by_source_delete_reconfigure
python/tests/test_table.py::test_merge_insert_by_source_delete_expr
python/tests/test_table.py::test_merge_insert_by_source_delete_expr_async
-vv` — 3 passed
- [x] `uv run --extra dev ruff format` / `ruff check` — clean

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 15:49:52 -07:00
Adityaj0 e6ae93f52a fix: hybrid search minimum_nprobes(0) silently no-ops instead of raising (#3770)
## Summary

`LanceHybridQueryBuilder._create_query_builders()` checked
`self._minimum_nprobes` for truthiness instead of `is not None` — the
very next line correctly checks `is not None` for
`self._maximum_nprobes`. Since `0` is falsy in Python,
`.minimum_nprobes(0)` on a hybrid query silently dropped the value
instead of forwarding it to the vector sub-query, where it would raise
the same `ValueError` a plain vector query raises for the same input
(`minimum_nprobes must be greater than 0`, validated in
`rust/lancedb/src/query.rs` and covered for the plain-query path by
`test_invalid_nprobes_sync`).

Fixes #3766

## Change

One-line fix: `if self._minimum_nprobes:` → `if self._minimum_nprobes is
not None:`, matching the existing `maximum_nprobes` check right below
it.

## Test plan

- [x] New regression test
`test_hybrid_query_minimum_nprobes_zero_raises` in
`python/python/tests/test_hybrid_query.py`
- [x] `uv run --extra tests pytest python/tests/test_hybrid_query.py
-vv` — 13 passed
- [x] `uv run --extra dev ruff format` / `ruff check` — clean

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 12:54:41 -07:00
Drew Gallardo 3dd9c598e9 feat(remote): add seekable blob range reads (#3750)
## Summary

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

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

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

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

### Behavior

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

## Testing

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 08:38:08 -07:00
Wyatt Alt e3b472c212 feat: connection-level job operations (#3755)
Adds job operations to the connection surface, building on the Job
handle from #3742: job(id), list_jobs, get_job, cancel_job, and
job_history, plus a non-blocking Job.status(). Implemented on the
Database trait (defaulting to NotSupported), the remote backend
(/v1/jobs), and the Python and Node bindings; job_history returns Arrow
batches.

errors() and progress() are not included.

Tested with mocked endpoints in all three languages.

---------

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 07:32:28 -07:00
Cohen Karnell dd2b11eda2 fix(python): log when storage_options is ignored in RemoteDBConnection.open_table (#3743)
`RemoteDBConnection.open_table` accepts `storage_options` and never uses
it:

```python
def open_table(
    self,
    name: str,
    *,
    namespace_path: Optional[List[str]] = None,
    storage_options: Optional[Dict[str, str]] = None,
    index_cache_size: Optional[int] = None,
    ...
) -> Table:
    ...
    if index_cache_size is not None:
        logging.info("index_cache_size is ignored in LanceDb Cloud ...")

    table = LOOP.run(self._conn.open_table(name, namespace_path=namespace_path))
```

The value is never passed down and never mentioned. `index_cache_size`
is ignored on Cloud in the
same way, but it says so.

I checked this at runtime on 0.34.0, not just by reading it: swapping
the inner connection for a
recorder, `open_table("t", storage_options={...})` hands the layer below
`['namespace_path']` and
nothing else, no log record is emitted, and the same probe shows
`index_cache_size` producing its
message as expected.

This adds the matching log line, so the two ignored parameters behave
the same way. `ruff check` and
`ruff format --check` are clean on the file.

A note on severity. This is not a security hole and nothing is exposed.
Someone passing credentials
there gets silence instead of an error, and finds out later.

One thing I am unsure about, and it changes the fix. I have assumed
per-table storage options are
meaningless on Cloud, which is what the `index_cache_size` line next to
it implies about managed
storage. If they are supposed to work, then the right change is to pass
them through to
`self._conn.open_table` instead and this patch is the wrong one. Happy
to redo it that way.

I did not check whether `create_table` or the async connection have the
same gap.
2026-07-30 19:32:31 -07:00
Will Jones 5a1015ba72 docs(python): fill gaps in the Python API reference (#3746)
`docs/src/python/python.md` is the whole Python API reference, but it is
maintained by hand and had drifted from the public API. Anything not
listed there simply doesn't get rendered, so a number of public,
documented, tested APIs were invisible to users — most notably branch
management, where `diff` and `merge` live.

I audited every public symbol reachable from `lancedb` and its
subpackages against the `:::` directives on the page. This adds the
missing ones:

- **Branching** — `Branches`, `AsyncBranches` (`list` / `create` /
`checkout` / `delete` / `diff` / `merge`)
- **Tables** — `TableStatistics` (returned by `Table.stats()`; the
fragment-level stats classes were already listed)
- **Full text queries** — `FullTextQuery`, `MatchQuery`, `PhraseQuery`,
`BoostQuery`, `MultiMatchQuery`, `BooleanQuery`, `FullTextOperator`,
`Occur`
- **Querying** — `LanceEmptyQueryBuilder`, `LanceTakeQueryBuilder`,
`AsyncTakeQuery`
- **Indices** — `Fm` (the FM-index for substring search), `IndexConfig`
- **Blobs** — `blob`, `BlobType`, `BlobFile`
- **Namespaces** — `connect_namespace`, `connect_namespace_async`, and
both namespace connection classes
- **Remote config** — `TlsConfig`, `HeaderProvider`, `OAuthConfig`,
`OAuthFlowType`
- **Rerankers** — the `Reranker` base class plus `JinaReranker`,
`RRFReranker`, `MRRReranker`, `AnswerdotaiRerankers`,
`VoyageAIReranker`, `WatsonxReranker` (5 of 12 were listed)
- **Embeddings** — `get_registry`, `register`, and the 14 embedding
functions that were missing (3 of 17 were listed)
- **PyTorch** — `StreamingDataset` and the permutation API it is built
on
- **Misc** — `Session`, `tokenize`, `FtsToken`, `pydantic.Vector`,
`pydantic.MultiVector`, `instrument_lancedb_metrics`, and the two
exception types

It also repairs cross-references in docstrings that no longer resolve:
links into guide pages that have since moved to lancedb.com
(`querying-an-ann-index`, `experimental-full-text-search`),
`lance.dataset` references with no inventory behind them, and the
relative targets `[Table](Table)` and `[PyArrow Table](pyarrow.Table)`.

Deliberately left out: concrete implementation classes reached through
their abstract base (`LanceTable`, `LanceDBConnection`,
`RemoteDBConnection`), query base classes already covered by
`inherited_members: true`, and internal plumbing such as
`FullTextSearchQuery` and `ColumnOrdering`.

## Testing

The docs job only runs on pushes to `main`, so I built the site locally
and compared against a build of `upstream/main`: every added entry
resolves, and no symbol that was rendered before stopped being rendered
when the four packages moved to automodule. `mkdocs build --strict`
exits 0 on this branch, against 61 warnings on `main`.

## Also in this PR

`lancedb.index`, `lancedb.embeddings`, `lancedb.remote` and
`lancedb.rerankers` are now rendered by a single mkdocstrings directive
each, driven by the module's `__all__`, rather than a hand-maintained
list. These four are where most of the drift was, and `__all__` is
harder to forget than a docs page. `lancedb.embeddings` had no
`__all__`; without one mkdocstrings renders no members at all for a
re-export package, so one is added. AGENTS.md gains a section on how the
page is wired up and how to build the docs locally.

Rendering all that code for the first time surfaced ~100 more build
warnings, which would have made #3707 (turning on `mkdocs build
--strict`) harder to land, so the warning backlog is cleared here too.
97 of the 158 warnings were one systematic false positive — griffe
cannot see the generated `__init__` of a pydantic dataclass, so every
documented parameter looks unknown — switched off via
`warn_unknown_params`. The remaining 61 came from 15 docstrings with
real bugs: prose trailing a `Parameters` section (we were rendering
parameters called `The`, `you` and `To`), types dropped because numpydoc
needs spaces around the colon, `num_partitions, default sqrt(num_rows)`
parsing as a list of names and inventing a `default` parameter, and one
parameter indented five spaces. `mkdocs build --strict` now exits 0.

---

#3747 (the coverage test that keeps this from happening again) is
stacked on this branch, so review it after this one.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 16:50:05 -07:00
Farmer.Chillax 48945d0658 feat(python): add namespace/table exist support (#3460)
In the current LanceDB usage implementation, there is no way to check
whether a table or namespace already exists. This PR introduces the
namespace_exists and table_exists methods to determine the existence of
tables and namespaces.

useage like this:
```
# check table exists
db.table_exists(table_id=['xxx'])

# check namespace exists
db.namespace_exists(namespace_id=['xxx'])
```

fixes: #3419

---------

Signed-off-by: farmer <farmerchillax@outlook.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-30 15:20:47 -07:00
Drew Gallardo 77208fd464 feat(remote): add RemoteTable fetch_blobs HTTP client (#3684)
Remote half of the blob read path. #3578 did local Python. This makes
`RemoteTable` hit the server.

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

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

### Example

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

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

Or pass ids yourself:

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

### Testing

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

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:16:01 -07:00
heart 4dc2d9a0f2 fix(python): avoid async work in sync reprs (#3620)
## Summary

- keep the existing synchronous `connect()` path unchanged
- make `LanceDBConnection.__repr__` and `LanceTable.__repr__`
side-effect-free
- add a regression test that verifies sync reprs do not call the Python
background loop

## Root cause

The freeze is caused by debugger rendering, not by `connect()` itself:

1. debugpy stops at a breakpoint and suspends all Python threads.
2. The debugger renders the new `db_connection` local by calling
`repr()`.
3. `LanceDBConnection.__repr__` reads `read_consistency_interval`.
4. That property calls `LOOP.run(...).result()`.
5. The `LanceDBBackgroundEventLoop` thread is suspended by the debugger,
so `repr()` waits for a thread that cannot run.

This explains why the symptom appears immediately after `connect()`: it
is the first point where a connection object exists in locals and is
automatically rendered. `LanceTable.__repr__` had the same problem
because it also read the connection's consistency interval.

This follows the same principle as #3411: `__repr__` must not trigger
async work or I/O that a debugger assumes is lightweight.

## Evidence

I reproduced the behavior with the real LanceDB classes and debugpy
1.8.21 using a DAP client:

- latest `main` (`ff6ff099`): the debugger reported `allThreadsStopped:
true`, and evaluating `repr(db_connection)` timed out
- this branch (`5755a5ba`): the same evaluation returned
`LanceDBConnection(uri='/tmp/lancedb-debug-repro')` immediately
- setting `PYDEVD_UNBLOCK_THREADS_TIMEOUT=0` also allowed the original
repr path to complete, independently confirming that it was waiting on a
suspended thread

The regression test creates a connection and table, replaces `LOOP.run`
with a function that fails, and verifies that both reprs still work.

## Validation

- `maturin develop --manifest-path python/Cargo.toml`
- `python -m pytest
python/python/tests/test_db.py::test_sync_repr_does_not_use_background_loop
python/python/tests/test_table.py::test_consistency -q` (`4 passed`)
- `ruff check .`
- `ruff format --check python/python/lancedb/db.py
python/python/lancedb/table.py python/python/tests/test_db.py
python/python/tests/test_table.py`
- `git diff --check`

Refs #3611.
2026-07-30 07:56:13 -07:00
LanceDB Robot 1ad6ce3a4e chore: update lance dependency to v10.0.0-beta.7 (#3745)
Updates the Rust workspace Lance dependencies and Java lance-core
dependency to v10.0.0-beta.7. No compatibility fixes were required; full
workspace clippy passed with warnings denied. Lance tag:
https://github.com/lance-format/lance/releases/tag/v10.0.0-beta.7

---------

Co-authored-by: Lu Qiu <luqiujob@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:14:27 -07:00
Yang Cen f7feed48c3 feat(fts): support custom stop-word lists (#3734)
## What

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

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

## Why

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

## How

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

## Validation

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

---------

Co-authored-by: Yang Cen <yangcen@Yangs-Mac-mini.local>
2026-07-29 17:40:12 +08:00