Compare commits

...

63 Commits

Author SHA1 Message Date
Lance Release 715be580d0 Bump version: 0.35.0-beta.0 → 0.35.0-beta.1 2026-07-10 16:12:51 +00:00
Will Jones 0d9c87a079 ci(nodejs): move Windows builds to larger runner and use ThinLTO (#3634)
The `build - aarch64-pc-windows-msvc` node build job (and, marginally,
the x86_64 one) had started hitting `rustc-LLVM ERROR: out of memory`
while linking the `lancedb-nodejs` cdylib — most recently surfaced by
#3526, which adds the goosefs backend (and its tonic/prost gRPC subtree)
to the default node binary.

The peak-memory step is the fat-LTO codegen (`lto=fat`,
`codegen-units=1` from `.cargo/config.toml`), which merges the whole
crate graph into a single LLVM module and runs single-threaded. It
therefore neither parallelizes across cores nor fits in the 16 GB of the
standard `windows-latest` runner as the dependency graph grows.

This PR:

- Moves both `*-pc-windows-msvc` node build jobs to
`windows-2025-8x-x64` (more memory + cores).
- Overrides the release profile to ThinLTO for just these jobs, via
`CARGO_PROFILE_RELEASE_LTO=thin` /
`CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16` in `pre_build`. ThinLTO
parallelizes the cross-module optimization across the runner's cores and
keeps peak memory well under the limit. Scoped so Python wheels and Rust
release builds keep fat LTO.

The larger runner alone would clear the OOM but waste the added cores on
the single-threaded fat-LTO tail; ThinLTO is what makes the extra cores
actually reduce wall-clock and gives durable memory headroom for future
dependency growth.

Tradeoff: ThinLTO can leave a small runtime-perf gap vs fat LTO for the
node native binary, but it recovers most of it and is a common release
configuration.

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

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

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

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

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

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

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

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

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

Fixes #3633.
2026-07-09 12:39:34 -07:00
Pranav Achar ff81428a9c fix(python): flatten_columns raises when flatten=False (#3629)
### Summary

`flatten_columns` raises `ValueError` when called with `flatten=False`,
even though `False` should mean "do not flatten". This is reachable from
the public API — `Query.to_pandas(flatten=...)` and
`to_batches(flatten=...)` type their `flatten` param as
`Optional[Union[int, bool]]` and pass it straight to `flatten_columns`.

### Cause

`bool` is a subclass of `int`, so `isinstance(False, int)` is `True`.
`flatten=False` skips the `flatten is True` check, falls into the
integer branch, and `False <= 0` evaluates to `True`, raising:

```
ValueError: Please specify a positive integer for flatten or the boolean value `True`
```

### Reproduction

```python
import lancedb
db = lancedb.connect("/tmp/db")
t = db.create_table("t", data=[{"id": 1, "vector": [0.1, 0.2]}])
t.search([0.1, 0.2]).to_pandas(flatten=False)   # -> ValueError
```

### Fix

Guard the integer branch with `not isinstance(flatten, bool)` so that
`flatten=False` (and `None`) mean "do not flatten". Behavior is
otherwise unchanged:

- `flatten=True` → flatten all nested levels
- positive `int` → flatten to that depth
- non-positive `int` (e.g. `0`) → still rejected with `ValueError`

Added a regression test in `tests/test_util.py` covering `None`,
`False`, `True`, a positive depth, and `0`.
2026-07-09 11:07:26 -07:00
Unmilan Mukherjee 75c5c83f12 fix(python): resolve Ollama embedding serialization error in create_table (#3583)
This PR fixes a serialization error when using Ollama embeddings in
`create_table`.

The use of `@cached_property` for the Ollama client was causing issues
during serialization/pickling, which is required by certain LanceDB
operations (like when using multiprocessing or certain storage
backends). Switching to a standard `@property` ensures the client is
instantiated when needed without being stored in a way that breaks
serialization.

Verified with the following script:
```python
import lancedb
from lancedb.embeddings import get_registry
import pickle

registry = get_registry().get(\"ollama\")
model = registry(name=\"llama3\")

# This would fail before the fix
pickled = pickle.dumps(model)
unpickled = pickle.loads(pickled)
```

Fixes #2629 (or similar serialization issues reported).

---------

Co-authored-by: Unmilan Mukherjee <Missing-Identity@users.noreply.github.com>
2026-07-09 10:58:41 -07:00
ForwardXu 291e9e37be feat: add Tencent COS and GooseFS object store support via new feature flags (#3526)
## Summary

Closes #3525

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

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

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

## Motivation

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

See the linked issue #3525 for the full discussion.

## Changes

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

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

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

### `python/Cargo.toml`

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

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

### `nodejs/Cargo.toml`

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

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

### `Cargo.lock`

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

## Example Usage

### Rust

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

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

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

### Python

```python
import lancedb

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

## Backwards Compatibility

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

## Testing

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

## Checklist

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

## Related

- Issue: #3525
- Upstream support:
[`lance/tencent`](https://github.com/lance-format/lance),
[`lance/goosefs`](https://github.com/lance-format/lance)
2026-07-08 14:14:39 -07:00
Dan Rammer 6c066530e5 feat: add get_lsm_write_spec to read the installed LSM write spec (#3631)
## Summary

Adds `Table::get_lsm_write_spec` returning `Option<LsmWriteSpec>` — the
read counterpart to the existing `set_lsm_write_spec` /
`unset_lsm_write_spec`. Returns `None` when the MemWAL LSM write path is
not enabled; otherwise reconstructs the spec (mode, shard column,
`num_buckets`, `maintained_indexes`, `writer_config_defaults`) exactly
as installed.

## Changes

- **Rust core (`NativeTable`)** — reconstructs the spec from
`mem_wal_index_details()`, resolving the shard column from its Lance
field id via the dataset schema. This is a raw metadata read, so it is
unaffected by `describe_indices` system-index filtering.
- **Remote (`RemoteTable`)** — reads the `__lance_mem_wal` system index
through `index/list` with `include_system: true` (so the curated
`list_indices` surface stays unchanged), then parses the index `details`
JSON. It matches the index by name and ignores `index_type`, so no
client `IndexType` variant is needed. It uses the **server-resolved
`column` name** from the details (Lance field ids do not travel to the
remote client).
- **Python + TypeScript bindings** — sync and async, mirroring
`set`/`unset`, with round-trip tests (bucket / identity / unsharded,
plus `None` when unset).

## Tests

- Rust: native round-trip unit test + remote mock-endpoint tests
(present + absent). All green (`cargo test --features remote -p
lancedb`).
- Python/TS: round-trip tests added; binding-runtime execution runs in
CI.

## Dependencies for the remote path

The remote path is complete on the client side but depends on two
out-of-repo pieces to work end-to-end:
1. **lance** — emit the server-resolved shard **`column`** name in the
MemWAL index `details` JSON (field ids can't reach the client). See
lance-format/lance#7667.
2. **server** — honor `include_system` on `index/list` so the
`__lance_mem_wal` entry is returned for this read.

Against an older server (no `include_system`), the remote getter
degrades gracefully to `Ok(None)` rather than erroring.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:05:41 -05:00
LanceDB Robot f428c6a76c chore: update lance dependency to v9.0.0-beta.18 (#3632)
Updates LanceDB's Lance dependencies to v9.0.0-beta.18.\n\nThis
refreshes the Rust workspace lockfile and Java lance-core version using
the repository update script. Triggering Lance tag:
https://github.com/lancedb/lance/releases/tag/v9.0.0-beta.18
2026-07-08 04:39:10 -07:00
Omkar Kabde df89c133ca feat(python)!: align Permutation.with_format("torch") with HuggingFace set_format("torch") (#3369)
Closes #3245.

> **BREAKING CHANGE:** `with_format("torch")` no longer returns a list
of stacked row tensors. It now returns per-row dicts so PyTorch's
default `DataLoader` collate stacks them into `{col: tensor(B,)}`.
Switch to `with_format("torch_row")` to keep the old shape.

### What changed

`"torch"` now returns a list of per-row dicts (`[{col: tensor}, ...]`)
at every indexed access path. The default `DataLoader` collate stacks
them into a column-keyed batched dict, no custom `collate_fn` needed.
The old shape is preserved under a new `"torch_row"` literal.
`"torch_col"` is unchanged.

The unbatching lives inside the transform (`batch_to_tensor_dict`), not
`__getitems__`, so the shape survives pickling and works under
`DataLoader(num_workers>0, multiprocessing_context="spawn")`.

### Format comparison

| Format | `iter(batch_size=N)` | `__getitems__([0,1,2])` | `DataLoader`
default collate |
|---|---|---|---|
| `"torch"` (new) | `list[{col: tensor}]` length N | `list[{col:
tensor}]` length 3 | `{col: tensor(B,)}` |
| `"torch_row"` (old `"torch"` behavior) | `list[tensor(n_cols,)]`
length N | `list[tensor(n_cols,)]` length 3 | `tensor(B, n_cols)` |
| `"torch_col"` (unchanged) | `tensor(n_cols, N)` | `tensor(n_cols, 3)`
| needs `collate_fn=lambda x: x` |

Output matches HuggingFace `Dataset.set_format("torch")` on container
shape, keys, and values at every access path. The only divergence:
HuggingFace downcasts `float64` to `torch.float32` by default, LanceDB
preserves dtype. Verified by `scripts/verify_torch_format.py`.

### Migration

```python
# Old default — column names lost, shape was tensor(B, n_cols)
DataLoader(Permutation.identity(table).with_format("torch"))

# New default — column names preserved
DataLoader(Permutation.identity(table).with_format("torch"))     # {col: tensor(B,)}

# Keep old behavior
DataLoader(Permutation.identity(table).with_format("torch_row")) # tensor(B, n_cols)
```
2026-07-07 15:13:09 -07:00
LanceDB Robot ec763521d4 chore: update lance dependency to v9.0.0-beta.17 (#3627)
Updates Lance Rust workspace dependencies and Java lance-core to
v9.0.0-beta.17.

Includes the required PyO3 compatibility fix for the newer dependency
set. Triggering Lance tag:
https://github.com/lance-format/lance/releases/tag/v9.0.0-beta.17

---------

Co-authored-by: Jack Ye <yezhaoqin@gmail.com>
2026-07-07 14:51:33 -07:00
Octopus f8dc2f78ee ci: add CODEOWNERS file for sensitive paths (#3312)
Fixes #3296

## Problem
The repository has no `CODEOWNERS` file, so there is no enforced review
routing for sensitive areas such as release workflows, auth code, and
FFI boundaries. This means changes to critical paths can be merged
without an explicit codeowner review.

## Solution
Add `.github/CODEOWNERS` covering:

- `/.github/workflows/` — release/publish workflows (supply chain risk)
- `/rust/lancedb/src/remote/` — remote client & auth code
- `/python/src/` and `/nodejs/src/` — FFI language boundaries

The listed owners (`@jackye1995`, `@wjones127`, `@Xuanwo`, `@AyushExel`)
are based on recent merge activity. Feel free to adjust to match the
actual team structure or replace with GitHub team handles if preferred.

## Testing
No code change — only adds a metadata file. GitHub will start routing
review requests automatically once this is merged and branch protection
is configured to require codeowner approval.

Co-authored-by: octo-patch <octo-patch@github.com>
2026-07-06 16:33:08 -07:00
LAKSH JAIN 3bcff0165e feat: support date, datetime, bytes, and Decimal literals in expr builder (#3235)
### **Summary**
Closes #3212

Extends the Python `lit()` helper to natively support three additional
types (`date`, `datetime`, and `Decimal`) and implements reflexive
operators for the `Expr` class.

This implementation specifically addresses the blocking feedback
regarding precision loss, CI discovery, and query engine limitations:

* **Logic Refactoring**: Simplified `lit()` by combining `date` and
`datetime` normalization into ISO-8601 strings, ensuring stable SQL
parsing across different engine locales.
* **Precision Preservation**: `decimal.Decimal` objects are now passed
as high-precision strings to the Rust bridge, bypassing intermediate
float conversions and preserving full 128-bit decimal precision for
DataFusion.
* **Averted CI Failures**: Temporarily deferred `bytes` literal support
to a future PR to resolve a known DataFusion `expr_to_sql` limitation
that was crashing the `Doctest` runner.
* **Reflexive Operators**: Added support for "literal-first" arithmetic
and logical operations (e.g., `10 + col('a')` or `True &
col('active')`). Redundant reflexive comparisons (e.g., `__rlt__`) were
pruned as Python's data model handles them automatically.
* **Integration Verification**: Added dedicated integration tests in the
official test directory to ensure the query engine correctly handles the
new types and preserves bit-perfect fidelity.

### **Changes**  
####
[python/python/lancedb/expr.py](file:///c:/Users/Laksh/Documents/lancedb/python/python/lancedb/expr.py)
* Updated `lit()` to handle `date`, `datetime`, and `Decimal` natively.
* Implemented reflexive operators (`__radd__`, `__rand__`, `__rmul__`,
etc.) to support literals on the left-hand side.
* Removed the problematic `bytes` doctest example and `lit()` type
support to unblock CI.

####
[python/src/expr.rs](file:///c:/Users/Laksh/Documents/lancedb/python/src/expr.rs)
* Modified the Rust FFI bridge to extract `Decimal` objects as strings.
* Ensured the `expr_lit` handler is ready to receive normalized temporal
strings.
*   Consolidated imports and added missing operator documentation.

####
[python/python/lancedb/_lancedb.pyi](file:///c:/Users/Laksh/Documents/lancedb/python/python/lancedb/_lancedb.pyi)
* Updated type stubs for `expr_lit` to include `Any` (allowing for
`Decimal`).

### **Testing**  
Added several new advanced test cases in
[python/python/tests/test_expr.py](file:///c:/Users/Laksh/Documents/lancedb/python/python/tests/test_expr.py)
covering:
* **High-precision Decimal preservation**: Verified against 128-bit
boundaries with a "one point off" test case (`1.234567890123456789 <
1.234567890123456790`).
* **Reflexive operator positioning**: Verified successful query
construction with literals on the left.
* **Timezone-aware normalization**: Confirmed stable behavior for
`datetime` objects.
* **Integration Testing**: Confirmed Date32 and Decimal columns return
the correct Python types and values from the engine during `.to_arrow()`
calls.

---------

Co-authored-by: Will Jones <willjones127@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:30:16 -07:00
Weston Pace c6db80dd0b feat: add an elastic dataloader as an iterable dataset (#3509)
# Elastic Streaming Dataloader

## Motivation

Training large models on LanceDB tables today requires loading the
entire dataset
into memory or writing bespoke batching logic. This PR introduces
`StreamingDataset`, a PyTorch `IterableDataset` that streams directly
from a
LanceDB table with two hard guarantees that are difficult to achieve
together:
**elastic determinism** and **resumability**.

## Goals

### Elastic determinism

The dataset partitions the table into a fixed number of *splits*
(controlled by
`num_splits`, `shuffle_seed`, and `epoch`). Samples are yielded by
round-robining
over splits one sample per split per cycle. Because the split structure
is fixed,
the set of samples that makes up each global training step is identical
regardless
of `world_size` or `num_workers`. You can scale your cluster up or down
between
runs and the model sees the same data in the same order — no
re-sharding, no
gradient variance from topology changes.

### Resumability

`state_dict()` / `load_state_dict()` capture how many samples each split
has
consumed. Because all splits are the same size and the round-robin
design keeps
them in lockstep, the state reduces to a single scalar
(`samples_consumed_per_split`)
that is topology-independent. A checkpoint saved with 8 GPUs can resume
correctly
on 4 GPUs or 16 GPUs without any adjustment.

### PyTorch `IterableDataset` / streaming

`StreamingDataset` implements the standard PyTorch `IterableDataset`
interface, so
it drops into any existing `DataLoader` pipeline without modification.
Data is
fetched lazily from Lance in chunks — only the rows needed for the
current batch are
ever in memory.

Compared to the map dataset this takes more work from pytorch and puts
it into the dataset itself (e.g. shuffling, filtering, etc.). We do this
because we cannot achieve things like elastic determinism or
prefiltering otherwise.

### Multi-worker support

DataLoader workers are automatically assigned contiguous sub-blocks of
splits (the
rank's splits are divided evenly across workers). Each worker is
independent:
no shared state, no inter-process coordination. The only constraint is
that
`num_splits` must be divisible by `world_size * num_workers`.

That being said, multi-worker is highly discouraged as it relies on
multiprocessing which is inefficient. Still, we want to support it.

### Filters as prefilters

Filters are applied at *permutation-build time* via
`PermutationBuilder.filter()`,
not re-evaluated on every fetch. The filtered row IDs are stored in the
permutation
table so that subsequent reads see only the matching rows. This allows
us to avoid loading rows that don't match the filter (which is the
default pytorch behavior)

### Prefetching

Two parameters control the I/O pipeline:

- `read_batch_size` (default 64) — number of rows fetched per
`take_offsets` call.
Larger values amortise per-request overhead, which is critical on object
storage
  where a single round-trip can cost ~100 ms.
- `prefetch_batches` (default 4) — number of batches prefetched in
parallel per
split via a `ThreadPoolExecutor`. While the model processes the current
batch,
the next several batches are already in flight, hiding storage latency
behind
  compute.

If set correctly then you can get good performance even with
num_workers=0 (unless you are bottlenecked on transform).

### Transform parallelism

The underlying `Permutation` API supports a `with_transform()` callback
for
decoding, augmentation, and format conversion. Unfortunately, this is
not parallelized. Pytorch typically parallelizes this with num_workers
which is multiprocessing which is highly inefficient. For simple
transforms we should be able to utilize multithreading and Rust based
UDFs. For complex python UDFs we could have a dedicated multiprocessing
pipeline for just the transform. Or we could just utilize
multithreading. In both cases we would exclude the I/O stage from the
multiprocessing because that ends up being very memory hungry and
inefficient.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 05:50:45 -07:00
dependabot[bot] f84190fe12 chore(deps): bump the rust-minor-patch group with 2 updates (#3621)
Bumps the rust-minor-patch group with 2 updates:
[napi](https://github.com/napi-rs/napi-rs) and
[napi-derive](https://github.com/napi-rs/napi-rs).

Updates `napi` from 3.9.4 to 3.10.3
<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.10.3</h2>
<h3>Fixed</h3>
<ul>
<li><em>(napi)</em> preserve the JS error object when cloning an Error
off-thread (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3375">#3375</a>)</li>
</ul>
<h2>napi-v3.10.2</h2>
<h3>Fixed</h3>
<ul>
<li><em>(napi)</em> keep message and cause when cloning a JS-exception
Error off-thread (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3373">#3373</a>)</li>
</ul>
<h2>napi-v3.10.1</h2>
<h3>Fixed</h3>
<ul>
<li><em>(napi)</em> release Error's exception reference via the custom
GC when dropped off-thread. (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3370">#3370</a>)</li>
<li><em>(napi)</em> stop ref exception object in ThreadsafeFunction
sync-throw path on wasm targets (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3369">#3369</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li><em>(napi)</em> share class accessor trampolines (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3364">#3364</a>)</li>
<li>optimize object field raw property access (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3365">#3365</a>)</li>
</ul>
<h2>napi-v3.10.0</h2>
<h3>Added</h3>
<ul>
<li><em>(napi)</em> implement <code>To</code>/<code>FromNapiValue</code>
for <code>OsString</code>, <code>OsStr</code>, <code>Path</code> and
<code>PathBuf</code> (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3339">#3339</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li><em>(napi)</em> route custom-GC Buffer/TypedArray cross-thread drops
through the owning isolate (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3357">#3357</a>)
(<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3360">#3360</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/1ac467e06e71f78b983630926c7908894d08e496"><code>1ac467e</code></a>
chore(napi): release v3.10.3 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3376">#3376</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/9d672f9f9ac4784364548cac55c15444f4d2b1f8"><code>9d672f9</code></a>
fix(napi): preserve the JS error object when cloning an Error off-thread
(<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3375">#3375</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/35476aebcc774a33b7e79e79d6c476db88a50215"><code>35476ae</code></a>
chore(napi): release v3.10.2 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3374">#3374</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/7844c7343f92f3ef45f2756dbba224c342a8467e"><code>7844c73</code></a>
ci: dogfood script-jail <a
href="https://github.com/v0"><code>@​v0</code></a>.2.10 (lifecycle audit
gate + safe install) (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3343">#3343</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/d449ccd8c50ad2268b051458ef919848e72b40a5"><code>d449ccd</code></a>
fix(napi): keep message and cause when cloning a JS-exception Error
off-threa...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/2ec02a67a0ffdbe8dcbe93f7f24d1d79b861216b"><code>2ec02a6</code></a>
chore(deps): update dependency electron to v43 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3361">#3361</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/fd0a99f83015d4b67a591641d9ce66edf08d9740"><code>fd0a99f</code></a>
chore(deps): update dependency <code>@​types/sinon</code> to v22 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3366">#3366</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/745cd8561f9be2781cc04c8ba4564c8f436792c1"><code>745cd85</code></a>
fix: de-flake Windows CI (ava import-from-project EPERM race + cli e2e
timeou...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/2785de583a97e49adea8194090fca2ee12f067c8"><code>2785de5</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3367">#3367</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/441ae7a7b6ddb06a2682a7dd27cf186a8afca9e8"><code>441ae7a</code></a>
fix(napi): release Error's exception reference via the custom GC when
dropped...</li>
<li>Additional commits viewable in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-v3.9.4...napi-v3.10.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `napi-derive` from 3.5.7 to 3.5.9
<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.5.9</h2>
<h3>Other</h3>
<ul>
<li>updated the following local packages: napi-derive-backend</li>
</ul>
<h2>napi-derive-v3.5.8</h2>
<h3>Other</h3>
<ul>
<li>updated the following local packages: napi-derive-backend</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/2785de583a97e49adea8194090fca2ee12f067c8"><code>2785de5</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3367">#3367</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/441ae7a7b6ddb06a2682a7dd27cf186a8afca9e8"><code>441ae7a</code></a>
fix(napi): release Error's exception reference via the custom GC when
dropped...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/cfa3b77ed50dd3639278b219f5d0f630c596cfac"><code>cfa3b77</code></a>
fix(deps): update emnapi to v1.11.2 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3371">#3371</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/65918a6d195fa007985c83baf97a9ce82a95c2cf"><code>65918a6</code></a>
fix(napi): stop ref exception object in ThreadsafeFunction sync-throw
path on...</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/324c5502fb4deaabd6d76253e8a8e380c5a2bbb5"><code>324c550</code></a>
perf(napi): share class accessor trampolines (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3364">#3364</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/80caf6063deb42468f2742bee02cc43ecb2e111d"><code>80caf60</code></a>
perf: optimize object field raw property access (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3365">#3365</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/f72afd58976a83bb0776c6a71171673d94e82226"><code>f72afd5</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3354">#3354</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/4effa4da6247a91048ca3462f2ff8eccdcfabfa4"><code>4effa4d</code></a>
chore(deps): lock file maintenance (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3363">#3363</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/f2bf197f629e491362d1911578c57c33be2e561f"><code>f2bf197</code></a>
chore(deps): lock file maintenance (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3362">#3362</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/962a2f0504517c0f83ff7357100c8b5fc26203af"><code>962a2f0</code></a>
fix(napi): route custom-GC Buffer/TypedArray cross-thread drops through
the o...</li>
<li>Additional commits viewable in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-derive-v3.5.7...napi-derive-v3.5.9">compare
view</a></li>
</ul>
</details>
<br />


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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 11:11:10 -07:00
Weston Pace 122dcd0f66 chore: ignore RUSTSEC-2026-0194 and RUSTSEC-2026-0195 in cargo deny (#3616)
quick-xml < 0.41.0 has two DoS advisories (quadratic attribute-name
check and unbounded namespace allocation in NsReader). All three
versions in our lockfile (0.26.0, 0.38.4, 0.39.4) are below the patched
threshold.

These are pulled in transitively by inferno (dev-only flame-graph dep),
lance-namespace-impls (git dep from lance), and opendal/reqsign (cloud
storage XML parsing). None of these paths expose attacker- controlled
XML; clearing them requires upstream to upgrade to quick-xml >= 0.41.0.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 16:54:34 -07:00
Eric B e6661a7285 fix: handle empty/wrong-length vectors returned by embedding functions (#3192)
## Summary

- When an embedding function returns an empty list (e.g. `[]`) for an
input row — as can happen when a model produces no output for a blank
string — `_append_vector_columns` crashed with `ArrowInvalid: Length of
item not correct: expected N but got array of size 0` because PyArrow
cannot fit a zero-length value into a fixed-size list element.
- The fix adds a validation step in `gen()`, inside
`_append_vector_columns`, that replaces any vector whose length does not
match the expected `ndims` (including empty lists and `None`) with
`None` before `pa.array()` is called.
- `None` is a valid null in a PyArrow fixed-size list array, so the bad
entry flows into `_handle_bad_vectors` and is handled according to the
caller-supplied `on_bad_vectors` policy (`error` / `drop` / `fill` /
`null`) instead of causing an unconditional crash.

## Test plan

- [ ] Added `test_embedding_with_empty_output_vectors` in
`python/python/tests/test_embeddings.py` that uses an embedding function
returning `[]` for empty-string inputs, calls `table.add(...,
on_bad_vectors="drop")`, and asserts no crash and that bad rows are
correctly dropped.
- [ ] Existing `test_embedding_with_bad_results` continues to pass (NaN
vectors still handled correctly).
- [ ] Verified manually that `pa.array([[1.,2.,3.,4.], []],
type=pa.list_(pa.float32(), 4))` raises `ArrowInvalid` without the fix,
and succeeds with `None` in place of `[]`.

Fixes #1672

---------

Co-authored-by: Will Jones <willjones127@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 13:31:16 -07:00
Lance Release 37466a0390 Bump version: 0.31.0-beta.5 → 0.31.0-beta.6 2026-07-02 11:33:53 +00:00
Lance Release bfce8a510d Bump version: 0.34.0-beta.5 → 0.34.0-beta.6 2026-07-02 11:32:45 +00:00
Armaan Sandhu a1261e6299 fix(python): average MRR reciprocal ranks over all rankings (#3599)
## What

`MRRReranker.rerank_multivector` averages each document's reciprocal
ranks over the wrong denominator. It divides by the number of rankings
the document *happens to appear in*, instead of the total number of
rankings being fused.

```python
# python/python/lancedb/rerankers/mrr.py
for result_id, reciprocal_ranks in mrr_score_map.items():
    mean_rr = np.mean(reciprocal_ranks)   # divides by len(present systems)
```

`mrr_score_map[doc]` only accumulates a reciprocal rank for the systems
in which the document was returned, so `np.mean` never accounts for the
systems that missed it.

## Why it's wrong

Mean Reciprocal Rank fusion treats a system that didn't return a
document as a reciprocal rank of `0` and averages across **all**
systems. That's the exact mechanism by which it rewards cross-system
consensus. Dividing by the appearance count removes that, so a document
liked by a single ranking can beat one ranked highly by every ranking.

Concretely, fusing 3 vector rankings:

| Doc | Ranks | Current score | Correct score |
|-----|-------|---------------|---------------|
| A | #1 in 1 system only | `mean([1.0]) = 1.000` | `1.0 / 3 = 0.333` |
| B | #1, #1, #2 across all 3 | `mean([1, 1, .5]) = 0.833` | `2.5 / 3 =
0.833` |

The current code ranks **A above B** - a document two of three rankings
ignored outranks one all three ranked at or near the top.

This also makes `rerank_multivector` inconsistent with `rerank_hybrid`
in the same file, which already treats a missing system as `0`
(`vector_rr = 0.0` / `fts_rr = 0.0`), and with the class docstring
("average of reciprocal ranks across different search results").

## Fix

Divide the summed reciprocal ranks by the total number of rankings:

```python
num_systems = len(vector_results)
...
mean_rr = float(np.sum(reciprocal_ranks)) / num_systems
```

## Tests

Adds `test_mrr_multivector_rewards_consensus`, which asserts the exact
MRR scores and that the consensus document ranks first. It fails on
`main` and passes with this change. Existing reranker tests are
unaffected.
2026-07-01 15:36:56 -07:00
Neo-X7 17c499177f docs(python): add missing parameter documentation for when_matched_update_all (#3536)
Fixes #2493

Added target. prefix requirement to where parameter docstring.
2026-07-01 10:28:58 -07:00
Will Jones d889321b5e fix!: combine repeated where filters with AND instead of replacing (#3585)
BREAKING CHANGE: When passing multiple where clauses to a query, they
now stack instead of replacing the previous filter.

Previously, calling `where`/`only_if` more than once on a query silently
replaced the previous filter, so only the last filter was applied. This
was
surprising and could return rows that an earlier filter should have
excluded.

This implements the alternative suggested in
https://github.com/lancedb/lancedb/pull/3514#issuecomment-4664901580:
instead of
rejecting a second filter, repeated filters are combined with a logical
AND
(`(previous) AND (new)`).

The combination happens in the Rust core (`QueryBase::only_if` and
`only_if_expr`), so it applies to all SDKs at once (Rust, Python async,
and
TypeScript). The Python sync query builder keeps its own filter state,
so it
combines filters in the binding layer as well.

SQL string and expression filters are combined within their own
representation.
When the two representations are mixed, the expression is lowered to SQL
(via
`expr_to_sql_string`) and the filters are combined as SQL strings, so
chaining
`where` works regardless of which form each filter takes.

Fixes #2649

## Tests
- Rust: `cargo test --features remote -p lancedb --lib query`
- Python: `uv run --extra tests pytest python/tests/test_query.py`
- TypeScript: `pnpm test __test__/query.test.ts`

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:11:58 -07:00
Will Jones 8a37f2ad77 feat(rust): re-export arrow and datafusion crates from lancedb (#3576)
lancedb's public API forces downstream crates to construct foreign types
— `RecordBatch`/arrays/builders for `Table::add(...)` (arrow), and
`datafusion_expr::Expr` for `only_if_expr`/`expr_projection`/merge
filters. The required version must exactly match lancedb's internal
arrow/datafusion line, but nothing on the API surface makes that
visible. Drift surfaces only as confusing trait/type errors:

```text
error[E0277]: the trait bound `RecordBatch: Scannable` is not satisfied
  = note: there are multiple different versions of crate `arrow_array` in the dependency graph
```

This re-exports the crates lancedb already pins, so consumers can rely
on a single, guaranteed-matching line via a discoverable import path
instead of declaring their own (potentially mismatched) direct
dependency.

- `lancedb::arrow::{arrow, arrow_array, arrow_buffer, arrow_cast,
arrow_data, arrow_ipc, arrow_ord, arrow_schema, arrow_select}` —
previously only `arrow_schema` was re-exported. `arrow-buffer` is
promoted from a transitive to a direct dependency.
- `lancedb::datafusion` — `Expr` is a first-class part of the query and
merge APIs (`only_if_expr`, `expr_projection`,
`QueryFilter::Datafusion`, `when_matched_update_all_expr`), and
`ExecutionPlan` is returned from `create_plan`.

This follows DataFusion's own precedent of re-exporting `arrow`. The
coupling already exists via the trait/impl bounds — this surfaces it
rather than hiding it behind an `E0277`.

Closes #3575

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

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:10:55 -07:00
Raphael Malikian f94673ae5e ci: update deprecated GitHub Actions to latest versions (Fixes #3577) (#3608)
Fixes #3577

## Problem
GitHub Actions is deprecating Node.js 20 on its runners. Multiple
workflows in lancedb use action versions that target Node.js 20
(`actions/checkout@v4`, `actions/setup-node@v4`, `actions/cache@v4`,
`actions/upload-artifact@v4`, `actions/download-artifact@v4`,
`pnpm/action-setup@v4`). These are being force-run on Node.js 24,
generating deprecation warnings.

## Solution
Updated all deprecated actions to their latest major versions that
support Node.js 24:

| Action | Old Version | New Version |
|--------|------------|-------------|
| `actions/checkout` | @v4 | @v6 |
| `actions/setup-node` | @v4 | @v6 |
| `actions/cache` | @v4 | @v5 |
| `actions/upload-artifact` | @v4 | @v7 |
| `actions/download-artifact` | @v4 | @v8 |
| `pnpm/action-setup` | @v4 | @v6 |

Note: `actions/checkout@v6` and `actions/upload-artifact@v7` are already
used in `pypi-publish.yml` — this PR extends the same versions to all
remaining workflows.

### Files Changed
- `.github/workflows/npm-publish.yml` — Updated checkout, setup-node,
cache, upload-artifact, download-artifact, pnpm
- `.github/workflows/nodejs.yml` — Updated checkout, setup-node, pnpm
- `.github/workflows/python.yml` — Updated checkout
- `.github/workflows/rust.yml` — Updated checkout
- `.github/workflows/java.yml` — Updated checkout
- `.github/workflows/java-publish.yml` — Updated checkout
- `.github/workflows/cargo-publish.yml` — Updated checkout
- `.github/workflows/docs.yml` — Updated checkout, setup-node
- `.github/workflows/dev.yml` — Updated setup-node
- `.github/workflows/codex-fix-ci.yml` — Updated checkout, setup-node,
pnpm
- `.github/workflows/codex-update-lance-dependency.yml` — Updated
checkout, setup-node
- `.github/workflows/license-header-check.yml` — Updated checkout
- `.github/workflows/make-release-commit.yml` — Updated checkout
- `.github/workflows/update_package_lock_run.yml` — Updated checkout
- `.github/workflows/update_package_lock_run_nodejs.yml` — Updated
checkout

## Verification
- All 20 YAML files validated with `yaml.safe_load()` — no syntax errors
- GitHub Actions CI will validate the actual action versions at runtime

## Changelog

| Date | Change | Author |
|------|--------|--------|
| 2026-07-01 | Updated all deprecated Node 20 actions to latest versions
across 15 workflow files | rtmalikian |

---

**Disclosure:** This code was developed with assistance from
DeepSeek-v4-pro (DeepSeek) via Hermes Agent (Nous Research). All changes
were reviewed and verified for correctness.

Signed-off-by: rtmalikian <rtmalikian@gmail.com>
2026-07-01 09:38:26 -07:00
Jack Ye 3b70fc4c9d fix(python): route async namespace connections through rust (#3603)
Summary:
- Route built-in async namespace-backed connections through the Rust
namespace connector.
- Delegate async namespace/table management methods to the inner
AsyncConnection while keeping the custom implementation Python-client
fallback.
- Add regressions for the native async dir path and lazy
namespace_client() construction.

Validated locally with targeted namespace/db/table pytest, full
test_namespace.py, ruff, cargo fmt/check/clippy, and cargo test -p
lancedb-python.
2026-06-30 17:03:23 -07:00
Lance Release 3a7b02119b Bump version: 0.31.0-beta.4 → 0.31.0-beta.5 2026-06-30 22:24:56 +00:00
Lance Release bcbc0da090 Bump version: 0.34.0-beta.4 → 0.34.0-beta.5 2026-06-30 22:23:43 +00:00
Jack Ye 9bead9f53d fix(python): route sync namespace connections through rust (#3598)
Summary:
- Route built-in sync namespace connections through the Rust namespace
connector.
- Keep custom namespace clients on the existing Python fallback.
- Preserve namespace-backed to_lance compatibility with lazy Python
client construction and add regressions.
2026-06-30 14:46:23 -07:00
Jack Ye 0351b77984 feat(remote): monotonic reads via x-lancedb-min-read-version watermark (#3597)
## Summary

Adds per-session monotonic reads for remote (LanceDB Cloud/Enterprise)
tables, preventing successive reads on a handle from moving *backward*
in dataset version when a load balancer routes them to query nodes with
differently-cached views.

Each `RemoteTable` handle tracks the highest dataset version it has
observed in a read response — surfaced by the server via a new
`x-lancedb-version` response header — and sends it back as
`x-lancedb-min-read-version` on subsequent reads (`count_rows`,
`query`). A query node whose cache is behind that version refreshes
before serving; a node already at/beyond it serves from cache at no
extra cost.

The watermark is sourced only from reads (always committed dataset
versions), so unlike the retired `x-lancedb-min-version` it is
unaffected by WAL writes returning WAL entry ids. It is reset on
`checkout_latest()`. Both headers are optional and ignored by older
peers.

Server-side enforcement lives in LanceDB Enterprise. Targets the
`codex/update-lance-9-0-0-beta-8` integration branch to match the
Enterprise submodule pin.
2026-06-30 11:22:00 -07:00
Weston Pace f6c9d31f98 feat: add polars dataframe integration (#3584)
This PR is part cleanup, part feature, part example.

It removes `IntoArrow` and `IntoArrowStream`. There was only one
redundant call site between the two. Once we moved everything to
`Scannable` these traits no longer serve any purpose.

It adds a `Scannable` impl for a polars DataFrame. We used to have this
at one point for `IntoArrow` so this is more like a regression fix than
anything.

It adds an example (and unit test) which ensures we can ingest from a
Polars DataFrame and export to one. LazyFrame support would be a
follow-up (though a pretty straightforward one) but we've never had
proper LazyFrame support before.
2026-06-30 08:28:41 -07:00
Dan Tasse a8f1c5a69f feat: add skill to work with branches better (#3596)
Agents seemed to have trouble finding the right calls to work with
branches (create, list, delete) and passing the right params to get it
to work. We probably don't need a big skill to get it on the right track
but a little nudge seems helpful. Doing a couple simple tasks, it saved
about half the time and tokens, so feels worthwhile. Created with the
Claude skills creator, hence the "skill.md in a bare folder"
organization - happy to move it if that's not the standard anymore.

```
Benchmark results (3 evals, with-skill vs baseline):

┌────────────────┬────────────┬────────────────────┐
│     Metric     │ With skill │   Without skill    │
├────────────────┼────────────┼────────────────────┤
│ Pass rate      │ 3/3 (100%) │ 3/3 (100%)         │
├────────────────┼────────────┼────────────────────┤
│ Avg time       │ 51s        │ 142s (2.8× slower) │
├────────────────┼────────────┼────────────────────┤
│ Avg tokens     │ 19,305     │ 36,513 (47% more)  │
├────────────────┼────────────┼────────────────────┤
│ Avg tool calls │ 5.7        │ 26 (4.5× more)     │
└────────────────┴────────────┴────────────────────┘
```
2026-06-30 09:27:21 -04:00
Jack Ye 10fecdf051 feat(node): expose OAuth connection config (#3587)
Expose the merged Rust OAuth header provider through the Node/TypeScript
connection path.

Includes:
- Native OAuthConfig conversion for napi-rs
- ConnectionOptions.oauthConfig plumbing
- Public TypeScript OAuthConfig and OAuthFlowType exports
- Generated TypeScript API docs for the new config surface
- input-validation and debug-redaction coverage in the Rust binding
layer

Local validation: cargo fmt --all; git diff --check.
2026-06-29 16:55:45 -07:00
Raphael Malikian c9ae93a7fa fix: add missing stacklevel=2 to warnings.warn() calls (Fixes #3589) (#3590)
Fixes #3589

## Problem
Multiple `warnings.warn()` calls across the Python client are missing
the `stacklevel=2` parameter. This causes warning messages to point to
lancedb internal code instead of the user's code that triggered the
warning, making debugging difficult.

## Solution
Add `stacklevel=2` to 7 `warnings.warn()` calls across 4 files:

| File | Warnings Fixed |
|------|---------------|
| `remote/db.py` | `request_thread_pool`, `connection_timeout`,
`read_timeout` deprecation warnings |
| `remote/table.py` | `cleanup_old_versions`, `compact_files`,
`optimize` no-op warnings |
| `table.py` | `data_storage_version`, `enable_v2_manifest_paths`,
`retrain` deprecation warnings |
| `embeddings/colpali.py` | `use_token_pooling` deprecation warning |

## Verification
- All 4 modified files pass `ast.parse()` syntax check
- Only `stacklevel=2` added — no other changes

## Changelog

| Date | Change | Author |
|------|--------|--------|
| 2026-06-27 | Add missing stacklevel=2 to warnings.warn() calls |
rtmalikian |

### Files Changed
- `python/python/lancedb/remote/db.py` — Add stacklevel=2 to 3
deprecation warnings
- `python/python/lancedb/remote/table.py` — Add stacklevel=2 to 3 no-op
warnings
- `python/python/lancedb/table.py` — Add stacklevel=2 to 3 deprecation
warnings
- `python/python/lancedb/embeddings/colpali.py` — Add stacklevel=2 to 1
deprecation warning

### Verification
- Syntax check passed on all modified files

---

**About the Author:** Raphael Malikian — Clinical AI Solutions
Architect. I specialise in building and fixing AI/ML systems for
healthcare, including vector databases, RAG pipelines, and clinical NLP.
If you need help with your project or think I can add value to your
organisation, feel free to reach out — I'd love to connect.

📧 rtmalikian@gmail.com
🔗 GitHub: https://github.com/rtmalikian
🔗 LinkedIn:
http://www.linkedin.com/in/raphael-t-malikian-mbbs-bsc-hons-71075436a

---

**Disclosure:** This code was developed with assistance from
DeepSeek-V4-Pro (DeepSeek) via Hermes Agent (Nous Research). All changes
were reviewed, tested against the actual codebase, and verified for
correctness.

Signed-off-by: rtmalikian <rtmalikian@gmail.com>
2026-06-29 16:36:44 -07:00
Raphael Malikian 05756f0bbf fix(python): raise clear error when permutation API is used on remote tables (Fixes #2934) (#3591)
Fixes #2934

## Problem
Passing a `RemoteTable` to `permutation_builder()` raises a cryptic
`AttributeError`:
```
AttributeError: 'RemoteTable' object has no attribute '_inner'
```
This leaves users confused about what went wrong and why.

## Root Cause
`PermutationBuilder.__init__()` calls `async_permutation_builder(table)`
which accesses `table._inner` — the underlying Rust Lance table object.
`RemoteTable` connects to LanceDB Cloud/Enterprise and does not have a
local `_inner` attribute, making permutations fundamentally unsupported
on remote tables.

## Solution
Added an early check in `PermutationBuilder.__init__()` that verifies
the table has `_inner` before calling the Rust function, raising a clear
`TypeError` with an explanation of why permutations don't work on remote
tables.

## Verification
- Syntax validated with `ast.parse()`
- Structural verification: single call site (`permutation_builder()`),
guard placed before Rust FFI call
- Error message tested with mock: `MockRemoteTable()` correctly triggers
`TypeError`

## Changelog

| Date | Change | Author |
|------|--------|--------|
| 2026-06-28 | Added remote table guard in PermutationBuilder.__init__ |
rtmalikian |

### Files Changed
- python/python/lancedb/permutation.py — Added `hasattr(table,
"_inner")` check with clear error

---

**About the Author:** Raphael Malikian — Clinical AI Solutions
Architect. I specialise in building and fixing AI/ML systems for
healthcare, including vector databases, RAG pipelines, and clinical NLP.
If you need help with your project or think I can add value to your
organisation, feel free to reach out — I'd love to connect.

📧 rtmalikian@gmail.com
🔗 GitHub: https://github.com/rtmalikian
🔗 LinkedIn:
http://www.linkedin.com/in/raphael-t-malikian-mbbs-bsc-hons-71075436a

---

**Disclosure:** This code was developed with assistance from
deepseek-v4-pro (DeepSeek) via Hermes Agent (Nous Research). All changes
were reviewed, tested against the actual codebase, and verified for
correctness.

Signed-off-by: rtmalikian <rtmalikian@gmail.com>
2026-06-29 16:36:01 -07:00
LanceDB Robot 2a0945443e chore: update lance dependency to v9.0.0-beta.10 (#3594)
Updates Lance Rust workspace dependencies and Java lance-core to
v9.0.0-beta.10.

No compatibility code changes were required; clippy and rustfmt passed
after installing the missing runner components.

Lance tag:
https://github.com/lance-format/lance/releases/tag/v9.0.0-beta.10
2026-06-29 15:28:47 -05:00
Jack Ye 39e819b6a7 feat(python): expose OAuth connection config (#3586)
Expose the merged Rust OAuth header provider through the Python async
connection path.

Includes:
- Python OAuthConfig and OAuthFlowType public config objects
- PyO3 conversion into the Rust OAuthConfig
- connect_async(oauth_config=...) plumbing
- repr redaction coverage for client_secret

Local validation: cargo fmt --all; ruff format/check on touched Python
files.
2026-06-29 12:36:35 -07:00
dependabot[bot] 70126943ff chore(deps): bump the rust-minor-patch group across 1 directory with 6 updates (#3588)
Bumps the rust-minor-patch group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [env_logger](https://github.com/rust-cli/env_logger) | `0.11.10` |
`0.11.11` |
| [log](https://github.com/rust-lang/log) | `0.4.32` | `0.4.33` |
| [uuid](https://github.com/uuid-rs/uuid) | `1.23.3` | `1.23.4` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.102` | `1.0.103` |
| [napi](https://github.com/napi-rs/napi-rs) | `3.9.3` | `3.9.4` |
| [napi-derive](https://github.com/napi-rs/napi-rs) | `3.5.6` | `3.5.7`
|


Updates `env_logger` from 0.11.10 to 0.11.11
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/rust-cli/env_logger/releases">env_logger's
releases</a>.</em></p>
<blockquote>
<h2>v0.11.11</h2>
<h2>[0.11.11] - 2026-06-25</h2>
<h3>Internal</h3>
<ul>
<li>Updated <code>env_filter</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md">env_logger's
changelog</a>.</em></p>
<blockquote>
<h2>[0.11.11] - 2026-06-25</h2>
<h3>Internal</h3>
<ul>
<li>Updated <code>env_filter</code></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rust-cli/env_logger/commit/b4d3f2b8dd3f1c3362f07da8f6f4a30c701358cf"><code>b4d3f2b</code></a>
chore: Release</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/cc2b2efcd7454be82ca49f8ac165b3fbc3095ae3"><code>cc2b2ef</code></a>
chore: Release</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/69e27d1e822d8f7e6b788bedffcb00575127553f"><code>69e27d1</code></a>
docs: Update changelog</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/166880db07de228ab22dd32f06b408464e73ac79"><code>166880d</code></a>
Merge pull request <a
href="https://redirect.github.com/rust-cli/env_logger/issues/411">#411</a>
from epage/parse</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/0a580d06e7ac42816e1a84e06fe6417d6973f8e6"><code>0a580d0</code></a>
fix(filter): Remove 'parse' on no_std</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/78d8ef116efbf981e272ad41c0b380298e4b2060"><code>78d8ef1</code></a>
Merge pull request <a
href="https://redirect.github.com/rust-cli/env_logger/issues/404">#404</a>
from cagatay-y/feature/filter-no_std</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/132fe86c8cb8e5df4fca7d71067a8d862a366b95"><code>132fe86</code></a>
feat(filter): Add support for no_std environments</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/4feafa4c3c5baeec6d8646bb73a35246882a731d"><code>4feafa4</code></a>
refactor(env_filter): Fix unreachable pub warning</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/92f8d8d08343c30e60b5f54455d7e16c810fcf11"><code>92f8d8d</code></a>
Merge pull request <a
href="https://redirect.github.com/rust-cli/env_logger/issues/410">#410</a>
from rust-cli/renovate/crate-ci-typos-1.x</li>
<li><a
href="https://github.com/rust-cli/env_logger/commit/4e57784e0a878d9e6510d71ee4f63ec96b8fdcc8"><code>4e57784</code></a>
chore(deps): Update pre-commit hook crate-ci/typos to v1.47.0</li>
<li>Additional commits viewable in <a
href="https://github.com/rust-cli/env_logger/compare/v0.11.10...v0.11.11">compare
view</a></li>
</ul>
</details>
<br />

Updates `log` from 0.4.32 to 0.4.33
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/rust-lang/log/blob/master/CHANGELOG.md">log's
changelog</a>.</em></p>
<blockquote>
<h2>[0.4.33] - 2026-06-20</h2>
<h2>What's Changed</h2>
<ul>
<li>Fixed key comparison by <a
href="https://github.com/matteo-zeggiotti-ok"><code>@​matteo-zeggiotti-ok</code></a>
in <a
href="https://redirect.github.com/rust-lang/log/pull/732">rust-lang/log#732</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/matteo-zeggiotti-ok"><code>@​matteo-zeggiotti-ok</code></a>
made their first contribution in <a
href="https://redirect.github.com/rust-lang/log/pull/732">rust-lang/log#732</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/rust-lang/log/compare/0.4.32...0.4.33">https://github.com/rust-lang/log/compare/0.4.32...0.4.33</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/rust-lang/log/commit/f405739f3a15a3f00680c793e1e1fa7e57d26ba4"><code>f405739</code></a>
Merge pull request <a
href="https://redirect.github.com/rust-lang/log/issues/734">#734</a>
from rust-lang/cargo/0.4.33</li>
<li><a
href="https://github.com/rust-lang/log/commit/6a24abf0835cef62e3d882287c97307dd3ecb403"><code>6a24abf</code></a>
prepare for 0.4.33 release</li>
<li><a
href="https://github.com/rust-lang/log/commit/87e062162e051d54bb553aacae3f0c6c4c213e59"><code>87e0621</code></a>
Merge pull request <a
href="https://redirect.github.com/rust-lang/log/issues/732">#732</a>
from matteo-zeggiotti-ok/fix-key-comparison</li>
<li><a
href="https://github.com/rust-lang/log/commit/a9b57119a631249fc8e881c7ef78e2028aacb823"><code>a9b5711</code></a>
Review: fallback to the &amp;str hash</li>
<li><a
href="https://github.com/rust-lang/log/commit/cc89cc6e41190de36892e33fff48e5f48cf57fa9"><code>cc89cc6</code></a>
Review: fixed other comparisons</li>
<li><a
href="https://github.com/rust-lang/log/commit/920e7dc2811c18a228bf78e818196de950659d85"><code>920e7dc</code></a>
Review: fixed comparison on <code>MaybeStaticStr</code></li>
<li><a
href="https://github.com/rust-lang/log/commit/0d71d3c685f2e23b1ad209b48408efe1205b18b0"><code>0d71d3c</code></a>
Fixed key comparison</li>
<li>See full diff in <a
href="https://github.com/rust-lang/log/compare/0.4.32...0.4.33">compare
view</a></li>
</ul>
</details>
<br />

Updates `uuid` from 1.23.3 to 1.23.4
<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.23.4</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix up name of fuzz script in readme by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/888">uuid-rs/uuid#888</a></li>
<li>document fixes by <a
href="https://github.com/frostyplanet"><code>@​frostyplanet</code></a>
in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/889">uuid-rs/uuid#889</a></li>
<li>Prepare for 1.23.4 release by <a
href="https://github.com/KodrAus"><code>@​KodrAus</code></a> in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/890">uuid-rs/uuid#890</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/frostyplanet"><code>@​frostyplanet</code></a>
made their first contribution in <a
href="https://redirect.github.com/uuid-rs/uuid/pull/889">uuid-rs/uuid#889</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4">https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/uuid-rs/uuid/commit/3296d64a196e0303c486538cdf143080c681ae2e"><code>3296d64</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/890">#890</a> from
uuid-rs/cargo/v1.23.4</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/cba53d0da2089109ea23fd964c8ffd21b0165a49"><code>cba53d0</code></a>
prepare for 1.23.4 release</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/e347af48aab7f7dd6b58a6bb5b578d467660e327"><code>e347af4</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/889">#889</a> from
frostyplanet/main</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/e9bf55c22216c27ff2283a6c427a7b13e025c75e"><code>e9bf55c</code></a>
doc: Fix broken link warnings</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/5351af40a0bc3243a580c40d313f243d2435bad6"><code>5351af4</code></a>
doc: Enable feature flag label for docs.rs</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/1e6a9669e30d53bae50fd52f16b7a1961fda236b"><code>1e6a966</code></a>
Merge pull request <a
href="https://redirect.github.com/uuid-rs/uuid/issues/888">#888</a> from
uuid-rs/KodrAus-patch-1</li>
<li><a
href="https://github.com/uuid-rs/uuid/commit/c9619f639c0e2d5f932fa4e3588aed859f7dc5d0"><code>c9619f6</code></a>
fix up name of fuzz script in readme</li>
<li>See full diff in <a
href="https://github.com/uuid-rs/uuid/compare/v1.23.3...v1.23.4">compare
view</a></li>
</ul>
</details>
<br />

Updates `anyhow` from 1.0.102 to 1.0.103
<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.103</h2>
<ul>
<li>Fix Stacked Borrows violation (UB) in
<code>Error::downcast_mut</code> (<a
href="https://redirect.github.com/dtolnay/anyhow/issues/451">#451</a>,
<a
href="https://redirect.github.com/dtolnay/anyhow/issues/452">#452</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dtolnay/anyhow/commit/5bdb0e24db3994be119d42f18fe2d655e1f68f4a"><code>5bdb0e2</code></a>
Release 1.0.103</li>
<li><a
href="https://github.com/dtolnay/anyhow/commit/e621bd35ddddcd8b2f39d80b9f5938583571a87d"><code>e621bd3</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/anyhow/issues/452">#452</a>
from dtolnay/downcast</li>
<li><a
href="https://github.com/dtolnay/anyhow/commit/6e8c000690151cba99305092024535905b2be162"><code>6e8c000</code></a>
Eliminate pointer-&gt;reference-&gt;pointer during downcast</li>
<li><a
href="https://github.com/dtolnay/anyhow/commit/67c4abd7718b6191768193993270abe8dcdd66bb"><code>67c4abd</code></a>
Add regression test for issue 451</li>
<li><a
href="https://github.com/dtolnay/anyhow/commit/917a16932009c1957f53c2ea325663948add2153"><code>917a169</code></a>
Update actions/upload-artifact@v6 -&gt; v7</li>
<li><a
href="https://github.com/dtolnay/anyhow/commit/d9dc3faf78b8647fdb5b8c5b53abb85e05e13d42"><code>d9dc3fa</code></a>
Update actions/checkout@v6 -&gt; v7</li>
<li><a
href="https://github.com/dtolnay/anyhow/commit/841522b2aa09732fecee40804440d2c35c68c480"><code>841522b</code></a>
Raise minimum tested compiler to rust 1.85</li>
<li>See full diff in <a
href="https://github.com/dtolnay/anyhow/compare/1.0.102...1.0.103">compare
view</a></li>
</ul>
</details>
<br />

Updates `napi` from 3.9.3 to 3.9.4
<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.9.4</h2>
<h3>Other</h3>
<ul>
<li><em>(napi-derive)</em> outline #[napi(object)] field-error
decoration (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3338">#3338</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/9cc199fa348a6ef395eb2cce14e84057dfebcfd8"><code>9cc199f</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3345">#3345</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/b77119e711704cc453949e056b45a4996ea0386c"><code>b77119e</code></a>
chore(release): publish</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/71ce9f6015b5e1bc73fb9b466fd7f7755feb9a42"><code>71ce9f6</code></a>
chore(deps): update actions/cache action to v6 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3349">#3349</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/8c87f474c8901b7c5510a92cdfa2d651de2769ef"><code>8c87f47</code></a>
chore(deps): update <code>@​tybys/wasm-util</code> to 0.10.3 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3348">#3348</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/04e2a7655d070b577a9fccdc5697c0aecc5f8df9"><code>04e2a76</code></a>
chore(deps): update cross-platform-actions/action action to v1.3.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3346">#3346</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/54ecbe4915bfe191110db5979b412e169049031e"><code>54ecbe4</code></a>
chore(deps): update actions/checkout action to v7 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3340">#3340</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/3dd0c309da6fff15efb2db0c0e056ca9eb6a3299"><code>3dd0c30</code></a>
perf(napi-derive): outline #[napi(object)] field-error decoration (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3338">#3338</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/81ac3d98c305cb9fdeec7fdf8d8a1d6ee9faba1f"><code>81ac3d9</code></a>
build(deps): bump undici from 6.26.0 to 6.27.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3342">#3342</a>)</li>
<li>See full diff in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-v3.9.3...napi-v3.9.4">compare
view</a></li>
</ul>
</details>
<br />

Updates `napi-derive` from 3.5.6 to 3.5.7
<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.5.7</h2>
<h3>Other</h3>
<ul>
<li>updated the following local packages: napi-derive-backend</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/9cc199fa348a6ef395eb2cce14e84057dfebcfd8"><code>9cc199f</code></a>
chore: release (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3345">#3345</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/b77119e711704cc453949e056b45a4996ea0386c"><code>b77119e</code></a>
chore(release): publish</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/71ce9f6015b5e1bc73fb9b466fd7f7755feb9a42"><code>71ce9f6</code></a>
chore(deps): update actions/cache action to v6 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3349">#3349</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/8c87f474c8901b7c5510a92cdfa2d651de2769ef"><code>8c87f47</code></a>
chore(deps): update <code>@​tybys/wasm-util</code> to 0.10.3 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3348">#3348</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/04e2a7655d070b577a9fccdc5697c0aecc5f8df9"><code>04e2a76</code></a>
chore(deps): update cross-platform-actions/action action to v1.3.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3346">#3346</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/54ecbe4915bfe191110db5979b412e169049031e"><code>54ecbe4</code></a>
chore(deps): update actions/checkout action to v7 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3340">#3340</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/3dd0c309da6fff15efb2db0c0e056ca9eb6a3299"><code>3dd0c30</code></a>
perf(napi-derive): outline #[napi(object)] field-error decoration (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3338">#3338</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/81ac3d98c305cb9fdeec7fdf8d8a1d6ee9faba1f"><code>81ac3d9</code></a>
build(deps): bump undici from 6.26.0 to 6.27.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3342">#3342</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/ee58383da4d91950e95355dcd8b93885f78f20e5"><code>ee58383</code></a>
chore(napi): release v3.9.3 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3335">#3335</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/c78727667b75807ece2e601ca3e1b2a3f87c7196"><code>c787276</code></a>
fix(napi): sync referred flag when creating a weak ThreadsafeFunction
(<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3337">#3337</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-derive-v3.5.6...napi-derive-v3.5.7">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-06-29 07:44:46 -07:00
Lance Release e01777070d Bump version: 0.31.0-beta.3 → 0.31.0-beta.4 2026-06-29 11:12:18 +00:00
Lance Release 3878adc6dc Bump version: 0.34.0-beta.3 → 0.34.0-beta.4 2026-06-29 11:11:05 +00:00
Jack Ye 3df3043563 feat(rust): add OAuth header provider (#3579)
## Summary

Add the Rust OAuth header provider for remote LanceDB connections.

This supports client credentials and Azure managed identity flows,
handles token caching and refresh, redacts secrets in Debug output, and
wires `ConnectBuilder::oauth_config()` into the remote client while
rejecting ambiguous API-key/header-provider combinations.
2026-06-26 23:57:16 -07:00
Ryan Green 8a5cd74e48 fix: ensure read freshness provider is built into namespace client (#3571)
By default the read freshness provider was not included in the namespace
client, preventing the read freshness headers from being included in the
request. This prevents checkout_latest() from working as expected when
using the namespace client.

This fix ensures the provided is built into the client when the
namespace impl and properties are provided.
2026-06-25 21:47:55 -07:00
Lance Release 448d5ec20f Bump version: 0.31.0-beta.2 → 0.31.0-beta.3 2026-06-25 01:55:06 +00:00
Lance Release 8718345229 Bump version: 0.34.0-beta.2 → 0.34.0-beta.3 2026-06-25 01:53:51 +00:00
LanceDB Robot 026fedc286 chore: update lance dependency to v9.0.0-beta.8 (#3580)
Updates Lance dependencies from v9.0.0-beta.4 to v9.0.0-beta.8.\n\nThis
refreshes the Rust workspace lockfile and the Java lance-core version.
Triggering Lance tag:
https://github.com/lance-format/lance/releases/tag/v9.0.0-beta.8
2026-06-24 18:52:59 -07:00
Jack Ye fe287dc98c fix(remote): support namespace clients with dynamic headers
Bridge LanceDB dynamic header providers into Lance Namespace dynamic context providers for live remote namespace clients.
2026-06-24 15:30:00 -07:00
Jack Ye 411568b72c fix(remote): omit empty api key header (#3573)
## Summary

Skip inserting the x-api-key header when the configured API key is
empty.

This lets bearer-token or other dynamic-header authentication avoid
sending an empty static API key header alongside the real auth header.
2026-06-24 13:25:59 -07:00
LanceDB Robot ebf8d55ede chore: update lance dependency to v9.0.0-beta.4 (#3570)
Bumps the Lance dependencies to v9.0.0-beta.4 and refreshes the
generated lockfile metadata. No compatibility fixes were required beyond
the dependency updates. Triggered by
https://github.com/lance-format/lance/releases/tag/v9.0.0-beta.4
2026-06-24 10:16:29 -05:00
Raphael Malikian 0ba70d96c3 fix: add missing stacklevel=2 to warnings.warn() and fix broken message concatenation (Fixes #3563) (#3564)
Fixes #3563

## Summary

- Add `stacklevel=2` to 10 `warnings.warn()` calls across 4 files
- Fix broken message concatenation in `table.py` where the second string
was incorrectly passed as the `category` parameter

## Problem

Multiple `warnings.warn()` calls in the `python/lancedb/` codebase were
missing the `stacklevel` parameter. Without `stacklevel=2`, warnings
point to library internals instead of the caller's code, making it
impossible for users to identify which of their function calls triggered
the warning.

Additionally, two calls in `table.py` (lines 3411 and 3420) had a more
serious bug: the deprecation message was split across two separate
string arguments, causing the second string to be passed as the
`category` parameter instead of being concatenated with the first
string. This would cause `TypeError` when the warning was triggered.

## Changes

| File | Fixes | Description |
|------|-------|-------------|
| `embeddings/colpali.py` | 1 | Add `stacklevel=2` to
`use_token_pooling` deprecation warning |
| `remote/db.py` | 3 | Add `stacklevel=2` to `request_thread_pool`,
`connection_timeout`, `read_timeout` deprecation warnings |
| `remote/table.py` | 3 | Add `stacklevel=2` to `cleanup_old_versions`,
`compact_files`, `optimize` no-op warnings |
| `table.py` | 3 | Fix broken message concatenation for
`data_storage_version` and `enable_v2_manifest_paths` deprecation
warnings + add `stacklevel=2` to `retrain` deprecation warning |

## Verification

```python
# All warnings.warn() calls now have stacklevel
python3 -c "import ast, os; ..."
# Result: All warnings.warn() calls now have stacklevel!
```

## Changelog

| Date | Change | Author |
|------|--------|--------|
| 2026-06-20 | Fix missing stacklevel=2 in 10 warnings.warn() calls +
fix broken message concatenation | rtmalikian |

### Files Changed
- `python/python/lancedb/embeddings/colpali.py` — Add stacklevel=2
- `python/python/lancedb/remote/db.py` — Add stacklevel=2 to 3
deprecation warnings
- `python/python/lancedb/remote/table.py` — Add stacklevel=2 to 3 no-op
warnings
- `python/python/lancedb/table.py` — Fix broken message concatenation +
add stacklevel=2

### Verification
- AST-based audit confirms all `warnings.warn()` calls now include
`stacklevel=2`
- Syntax check passes for all 4 modified files

---

**About the Author:** Raphael Malikian — Clinical AI Solutions
Architect. I specialise in building and fixing AI/ML systems for
healthcare, including vector databases, RAG pipelines, and clinical NLP.
If you need help with your project or think I can add value to your
organisation, feel free to reach out — I'd love to connect.

📧 rtmalikian@gmail.com
🔗 GitHub: https://github.com/rtmalikian
🔗 LinkedIn:
http://www.linkedin.com/in/raphael-t-malikian-mbbs-bsc-hons-71075436a

---

**Disclosure:** This code was developed with assistance from **Hermes
Agent** (Nous Research). All changes were reviewed, tested against the
actual codebase, and verified for correctness.

Signed-off-by: rtmalikian <rtmalikian@gmail.com>
2026-06-23 13:42:59 -07:00
Lance Release 0749532c3c Bump version: 0.31.0-beta.1 → 0.31.0-beta.2 2026-06-23 16:23:08 +00:00
Lance Release 26481a4b74 Bump version: 0.34.0-beta.1 → 0.34.0-beta.2 2026-06-23 16:21:52 +00:00
dependabot[bot] 08596f1644 chore(deps): bump the rust-minor-patch group with 2 updates (#3565)
Bumps the rust-minor-patch group with 2 updates:
[bytes](https://github.com/tokio-rs/bytes) and
[napi](https://github.com/napi-rs/napi-rs).

Updates `bytes` from 1.11.1 to 1.12.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/bytes/releases">bytes's
releases</a>.</em></p>
<blockquote>
<h2>Bytes v1.12.0</h2>
<h1>1.12.0 (June 18th, 2026)</h1>
<h3>Added</h3>
<ul>
<li>Add <code>BytesMut::extend_from_within()</code> (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/818">#818</a>)</li>
<li>Add <code>BytesMut::try_unsplit()</code> (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/746">#746</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix panic in <code>get_int</code> if <code>nbytes</code> is zero (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/806">#806</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Pass vtable data by value (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/826">#826</a>)</li>
<li>Exclude development scripts from published package (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/810">#810</a>)</li>
</ul>
<h3>Documented</h3>
<ul>
<li>Document that <code>BytesMut::{reserve,try_reserve}</code> doesn't
preserve unused capacity (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/808">#808</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md">bytes's
changelog</a>.</em></p>
<blockquote>
<h1>1.12.0 (June 18th, 2026)</h1>
<h3>Added</h3>
<ul>
<li>Add <code>BytesMut::extend_from_within()</code> (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/818">#818</a>)</li>
<li>Add <code>BytesMut::try_unsplit()</code> (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/746">#746</a>)</li>
</ul>
<h3>Fixed</h3>
<ul>
<li>Fix panic in <code>get_int</code> if <code>nbytes</code> is zero (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/806">#806</a>)</li>
</ul>
<h3>Changed</h3>
<ul>
<li>Pass vtable data by value (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/826">#826</a>)</li>
<li>Exclude development scripts from published package (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/810">#810</a>)</li>
</ul>
<h3>Documented</h3>
<ul>
<li>Document that <code>BytesMut::{reserve,try_reserve}</code> doesn't
preserve unused capacity (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/808">#808</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/tokio-rs/bytes/commit/91402cee605b21bf530ac07ede463599182b5d32"><code>91402ce</code></a>
Release bytes v1.12.0 (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/831">#831</a>)</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/2256e6dc3e82fc87600f2161658572b62bfdd214"><code>2256e6d</code></a>
chore: add safety comments on unsafe blocks (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/827">#827</a>)</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/245adff079eb0cb1a706d35bab5f68b2d51919f6"><code>245adff</code></a>
Pass vtable data by value (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/826">#826</a>)</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/00cc5ff2bd861f7b62603a268ec477c3cead5f1e"><code>00cc5ff</code></a>
Implement <code>BytesMut::extend_from_within</code> (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/818">#818</a>)</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/5b79d316c92b88b836449f8956dc8430c370108b"><code>5b79d31</code></a>
Merge tag 'v1.11.1'</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/804ee6d039bdea017f0f3faf91806c9771d7e555"><code>804ee6d</code></a>
Make try_unsplit method public (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/746">#746</a>)</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/fd426ca0842cf9688def31d9ee4e027692ac6477"><code>fd426ca</code></a>
Exclude development scripts from published package (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/810">#810</a>)</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/b4ed70daee06013a35a13832807e8f244d5419aa"><code>b4ed70d</code></a>
Add test for copy_to_bytes() -&gt; BytesMut avoiding clone (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/809">#809</a>)</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/94e42915a9a4bac7c2fbaa3d94f27bf0eb4dfb14"><code>94e4291</code></a>
Document that <code>BytesMut::{reserve,try_reserve}</code> doesn't
preserve unused capac...</li>
<li><a
href="https://github.com/tokio-rs/bytes/commit/acd1e0ffb8f076225759b8005d04f65ef77cccca"><code>acd1e0f</code></a>
Fix <code>get_int</code> if <code>nbytes</code> is zero (<a
href="https://redirect.github.com/tokio-rs/bytes/issues/806">#806</a>)</li>
<li>See full diff in <a
href="https://github.com/tokio-rs/bytes/compare/v1.11.1...v1.12.0">compare
view</a></li>
</ul>
</details>
<br />

Updates `napi` from 3.9.1 to 3.9.3
<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.9.3</h2>
<h3>Fixed</h3>
<ul>
<li><em>(napi)</em> sync referred flag when creating a weak
ThreadsafeFunction (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3337">#3337</a>)</li>
</ul>
<h3>Other</h3>
<ul>
<li><em>(napi)</em> outline non-generic core of
ThreadsafeFunction::create (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3334">#3334</a>)</li>
</ul>
<h2>napi-v3.9.2</h2>
<h3>Fixed</h3>
<ul>
<li><em>(napi)</em> ReadableStream Reader loses chunks and aborts on
errored streams (<a
href="https://redirect.github.com/napi-rs/napi-rs/pull/3328">#3328</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/ee58383da4d91950e95355dcd8b93885f78f20e5"><code>ee58383</code></a>
chore(napi): release v3.9.3 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3335">#3335</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/c78727667b75807ece2e601ca3e1b2a3f87c7196"><code>c787276</code></a>
fix(napi): sync referred flag when creating a weak ThreadsafeFunction
(<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3337">#3337</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/d4276ca315ef60558427178a53fe251b7de4cb38"><code>d4276ca</code></a>
chore(deps): update dependency oxc-parser to ^0.137.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3336">#3336</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/a0b1831ce596fcca53536240ebe2afc355b8cfba"><code>a0b1831</code></a>
perf(napi): outline non-generic core of ThreadsafeFunction::create (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3334">#3334</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/3759d7b485a4fb95ed4be77c3d383beab38c7b74"><code>3759d7b</code></a>
chore(deps): update rust-lang/crates-io-auth-action action to v1.0.5 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3333">#3333</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/dd41eeb921517f58b26c21ccf8f13e086cf37cea"><code>dd41eeb</code></a>
build(deps): bump protobufjs from 7.6.2 to 7.6.4 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3332">#3332</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/cdd48b3873d1379acc4cf5750a899dcd35e21399"><code>cdd48b3</code></a>
chore(deps): update dependency oxc-parser to ^0.136.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3314">#3314</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/e98762de2c4f6d18d4fa8178916daf7779ca261a"><code>e98762d</code></a>
chore(deps): update yarn monorepo to v4.17.0 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3330">#3330</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/529a78d15c0c7d6ca6d4a732c5e335f9de701879"><code>529a78d</code></a>
chore(napi): release v3.9.2 (<a
href="https://redirect.github.com/napi-rs/napi-rs/issues/3329">#3329</a>)</li>
<li><a
href="https://github.com/napi-rs/napi-rs/commit/88f4b97030d50e0cfa279d2fa7ad8345517200b3"><code>88f4b97</code></a>
fix(napi): ReadableStream Reader loses chunks and aborts on errored
streams (...</li>
<li>Additional commits viewable in <a
href="https://github.com/napi-rs/napi-rs/compare/napi-v3.9.1...napi-v3.9.3">compare
view</a></li>
</ul>
</details>
<br />


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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 09:21:05 -07:00
LanceDB Robot f16da19b78 chore: update lance dependency to v9.0.0-beta.2 (#3569)
Updates LanceDB's Lance dependencies to v9.0.0-beta.2 across the Rust
workspace and Java lance-core dependency.\n\nNo compatibility fixes were
required; clippy and formatting pass after installing the missing
toolchain components on the runner. Triggering Lance tag:
https://github.com/lance-format/lance/releases/tag/v9.0.0-beta.2
2026-06-23 09:20:13 -07:00
Drew Gallardo 41ac32a344 feat(rust): add blob read and materialization APIs (#3562)
This PR is for the Read path against blob v2. #3528 handles declare +
write, and this this adds materialization on local tables.

- blob_columns()
- fetch_blobs(column, row_ids) → bytes
- fetch_blob_files(column, row_ids) → lazy handles
- Pass _rowid from query().with_row_id(). Remote returns NotSupported.
(for now)

### Use cases

search, grab row ids, materialize images:

```rust
let row_ids = /* _rowid from hits */;
let images = table.fetch_blobs("image", &row_ids).await?;
```

Large blobs: open handles, read only what you need:

```rust
let handles = table.fetch_blob_files("image", &row_ids).await?;
let bytes = handles[0].as_ref().unwrap().read().await?;
```

Filter then batch fetch: collect ids from a filter, one call.
Multiple blob columns: image and thumbnail independently.
Row ids from before compact: still resolve.

### Alignment note
Lance `read_blobs` drops null rows. We descriptor-take first, read
non-null ids, re-expand to match input order. Null and zero-length blobs
come back null/None. Bytes path sets `preserve_order(true)`. So I added:

```
TODO(lance): expose selection_index or an aligned execute so we can drop the pre-read.
```

### Tests
`cargo test -p lancedb --test blob_integration`
- 30 tests covering nulls, reorder, dups, cross-fragment bytes + files,
compact, delete, legacy v1 errors.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 06:58:26 -07:00
Drew Gallardo ba1ef34481 feat(rust): add blob v2 schema declaration and write path (#3528)
First Rust PR for #3231. Lance already stores blob v2. This adds the
LanceDB write side.

```rust
let schema = Schema::new(vec![
    Field::new("id", DataType::Int64, false),
    lancedb::blob("image", true),
]);

let table = db.create_table("photos", schema).execute().await?;

table.add(batch_with_large_binary_image_column).execute().await?;
```

Read/materialize and Python are follow-up PRs.

### Testing

- cargo test -p lancedb --test blob_integration
- cargo test -p lancedb blob:: datafusion::blob_coerce
- cargo test -p lancedb (591 passed)
- cargo clippy --features remote --tests

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-19 12:33:15 -07:00
Will Jones 85d870b397 fix: parse RFC 3339 created_at and improve IndexConfig repr (#3558)
The server now serializes an index's `created_at` as an RFC 3339 string
(e.g. `"2026-06-18T21:37:36.637Z"`), but the client deserializer only
accepted a unix timestamp in milliseconds. This caused `list_indices` to
fail with:

```
Failed to parse list_indices response: invalid type: string "2026-06-18T21:37:36.637Z", expected a unix timestamp in milliseconds
```

This PR replaces the fixed millisecond deserializer with a custom one
that accepts both an RFC 3339 string (current server) and a
unix-millisecond integer (legacy deployments), so the client works
against any server version.

It also improves the `IndexConfig` repr in the Python bindings.
Previously it printed only three fields (`Index(FTS, columns=["text"],
name="text_idx")`), hiding the metadata that `list_indices` returns. It
now renders every populated field, omitting any that are `None`. Each
value is valid Python — integer counts use `_` thousands separators and
`created_at` uses the `datetime` repr — so values round-trip. The real
repr is a single line; it's wrapped here for readability:

```python
>>> table.list_indices()
[IndexConfig(
    name="text_idx",
    index_type="FTS",
    columns=["text"],
    index_uuid="aefd3e00-2f95-4bdc-92ac-06de84442bf1",
    type_url="/lance.table.InvertedIndexDetails",
    created_at=datetime.datetime(2026, 6, 18, 21, 37, 36, 637000, tzinfo=datetime.timezone.utc),
    num_indexed_rows=2,
    size_bytes=3_669,
    num_segments=1,
    index_version=1,
    index_details={
        'lance_tokenizer': None,
        'base_tokenizer': 'simple',
        'language': 'English',
        'with_position': False,
        'max_token_length': 40,
        'lower_case': True,
        'stem': True,
        'remove_stop_words': True,
        'custom_stop_words': None,
        'ascii_folding': True,
        'min_ngram_length': 3,
        'max_ngram_length': 3,
        'prefix_only': False,
    },
)]
```

Fixes #3556

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 10:40:56 -07:00
LanceDB Robot c46d59d2ee chore: update lance dependency to v8.0.0-rc.1 (#3557)
Updates LanceDB Lance dependencies to Lance v8.0.0-rc.1.

This includes the Rust workspace Lance crates, Cargo.lock, and Java
lance-core version. Triggering tag:
https://github.com/lance-format/lance/releases/tag/v8.0.0-rc.1
2026-06-19 11:40:38 -05:00
Lance Release 113f187c2d Bump version: 0.31.0-beta.0 → 0.31.0-beta.1 2026-06-19 16:00:59 +00:00
Lance Release 3b279f5705 Bump version: 0.34.0-beta.0 → 0.34.0-beta.1 2026-06-19 15:59:43 +00:00
Ryan Green e1334954d7 fix: overflow using sys.maxsize for k in query with namespace connection (#3561) 2026-06-19 12:57:10 -02:30
LanceDB Robot 2f65a233fe chore: update lance dependency to v8.0.0-beta.19 (#3555)
Updates LanceDB's Lance dependencies from v8.0.0-beta.17 to
v8.0.0-beta.19.

This includes the Rust workspace Lance crates, Cargo.lock refresh, and
Java lance-core version bump. Triggering Lance tag:
https://github.com/lance-format/lance/releases/tag/v8.0.0-beta.19
2026-06-18 14:16:57 -05:00
Lance Release e81356089a Bump version: 0.30.1-beta.2 → 0.31.0-beta.0 2026-06-18 18:43:22 +00:00
137 changed files with 11648 additions and 651 deletions
+137
View File
@@ -0,0 +1,137 @@
---
name: lancedb-branch-ops
description: Branch management for LanceDB tables via the REST API. Use this skill whenever someone wants to create, delete, list, or switch branches on a LanceDB table — or needs to make sure a write (metadata update, index build, etc.) lands on a specific branch instead of main. Invoke it even without the word "branch" if context makes clear they want an experimental copy of a table, want to isolate changes, or want to confirm a mutation didn't touch main. Covers: branches/list, branches/create, branches/delete, and passing "branch" in describe/update_field_metadata/create_index to target a non-main version.
---
## Goal
Manage branches on a LanceDB table: list what exists, create new ones, delete stale ones, and direct read/write operations at a specific branch without touching main.
## Step 0: Establish the connection
Use the `lancedb-connect` skill to resolve the base URL and auth headers (`x-api-key`, `x-lancedb-database`). Skip this only if the connection is already known from the current conversation.
All examples below use `{base_url}` — substitute the resolved endpoint and include the auth headers on every request.
## The branch model (important)
LanceDB branches are named snapshots that diverge from the table's current state at creation time. There is **no checkout command** — you never switch the whole table to a branch. Instead, you **pass `"branch": "<name>"` in the request body** of any operation to target that branch. Omitting the key (or sending an empty body) always targets main.
`branches/list` returns only non-main branches. Main always exists and is not listed.
## 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 -1
View File
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "0.30.1-beta.2"
current_version = "0.32.0-beta.0"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
+21
View File
@@ -0,0 +1,21 @@
# CODEOWNERS
#
# These owners will be the default owners for everything in the repo.
# They will be requested for review when someone opens a pull request.
#
# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# Default owners for everything
* @jackye1995 @wjones127
# Release and publish workflows — changes here can affect supply chain security
/.github/workflows/ @jackye1995 @wjones127 @Xuanwo
# Remote client and auth — sensitive networking and auth code
/rust/lancedb/src/remote/ @jackye1995 @wjones127
# Python FFI boundary
/python/src/ @jackye1995 @wjones127 @AyushExel
# NodeJS FFI boundary
/nodejs/src/ @jackye1995 @wjones127
+11 -9
View File
@@ -34,15 +34,16 @@ 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/'"
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc"
target: x86_64-unknown-linux-gnu
manylinux: ${{ inputs.manylinux }}
args: ${{ inputs.args }}
before-script-linux: |
set -e
curl -L https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-$(uname -m).zip > /tmp/protoc.zip \
&& unzip /tmp/protoc.zip -d /usr/local \
&& rm /tmp/protoc.zip
curl -fsSL https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-x86_64.zip -o /tmp/protoc.zip
unzip /tmp/protoc.zip -d /usr/local
rm /tmp/protoc.zip
/usr/local/bin/protoc --version
- name: Build Arm Manylinux Wheel
if: ${{ inputs.arm-build == 'true' }}
uses: PyO3/maturin-action@v1
@@ -50,13 +51,14 @@ 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/'"
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc"
target: aarch64-unknown-linux-gnu
manylinux: ${{ inputs.manylinux }}
args: ${{ inputs.args }}
before-script-linux: |
set -e
yum install -y clang \
&& curl -L https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-aarch_64.zip > /tmp/protoc.zip \
&& unzip /tmp/protoc.zip -d /usr/local \
&& rm /tmp/protoc.zip
yum install -y clang
curl -fsSL https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-aarch_64.zip -o /tmp/protoc.zip
unzip /tmp/protoc.zip -d /usr/local
rm /tmp/protoc.zip
/usr/local/bin/protoc --version
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
# Only runs on tags that matches the make-release action
if: startsWith(github.ref, 'refs/tags/v')
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust
@@ -47,7 +47,7 @@ jobs:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: ./.github/actions/create-failure-issue
with:
job-results: ${{ toJSON(needs) }}
+3 -3
View File
@@ -36,14 +36,14 @@ jobs:
echo "guidelines = ${{ inputs.guidelines }}"
- name: Checkout Repo
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
fetch-depth: 0
persist-credentials: true
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
# pnpm 11 (used by the nodejs install step below) requires
# Node >= 22.13; use 24 since 22 hits EOL in October.
@@ -82,7 +82,7 @@ jobs:
cache: maven
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 11.1.1
- name: Install Node.js dependencies for TypeScript bindings
@@ -30,13 +30,13 @@ jobs:
echo "tag = ${{ inputs.tag || 'latest' }}"
- name: Checkout Repo LanceDB
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
persist-credentials: true
- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
name: Verify PR title / description conforms to semantic-release
runs-on: ubuntu-latest
steps:
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: "18"
# These rules are disabled because Github will always ensure there
+2 -2
View File
@@ -35,7 +35,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install dependencies needed for ubuntu
run: |
sudo apt install -y protobuf-compiler libssl-dev
@@ -53,7 +53,7 @@ jobs:
python -m pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .
python -m pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -r ../docs/requirements.txt
- name: Set up node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
cache: 'npm'
+2 -2
View File
@@ -32,7 +32,7 @@ jobs:
working-directory: ./java
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Java 8
uses: actions/setup-java@v4
with:
@@ -73,7 +73,7 @@ jobs:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: ./.github/actions/create-failure-issue
with:
job-results: ${{ toJSON(needs) }}
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
working-directory: ./java
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Java 17
uses: actions/setup-java@v4
with:
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install license-header-checker
working-directory: /tmp
run: |
+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
steps:
- name: Output Inputs
run: echo "${{ toJSON(github.event.inputs) }}"
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
+10 -10
View File
@@ -38,14 +38,14 @@ jobs:
CC: gcc-12
CXX: g++-12
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 11.1.1
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
# in October. The library itself still supports Node >= 18
@@ -86,14 +86,14 @@ jobs:
shell: bash
working-directory: nodejs
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 11.1.1
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
name: Setup Node.js 24 for build
with:
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
@@ -130,7 +130,7 @@ jobs:
echo "Run 'pnpm run docs', fix any warnings, and commit the changes."
exit 1
fi
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
name: Setup Node.js ${{ matrix.node-version }} for test
with:
node-version: ${{ matrix.node-version }}
@@ -166,14 +166,14 @@ jobs:
shell: bash
working-directory: nodejs
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
- uses: pnpm/action-setup@v4
- uses: pnpm/action-setup@v6
with:
version: 11.1.1
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
# in October.
+30 -21
View File
@@ -32,7 +32,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -103,7 +103,7 @@ jobs:
features: fp16kernels
pre_build: brew install protobuf
- target: x86_64-pc-windows-msvc
host: windows-latest
host: windows-2025-8x-x64
features: ","
pre_build: |-
choco install --no-progress protoc ninja nasm
@@ -111,12 +111,21 @@ jobs:
# There is an issue where choco doesn't add nasm to the path
export PATH="$PATH:/c/Program Files/NASM"
nasm -v
# Fat LTO of the cdylib is single-threaded and the peak-memory
# step of the build, and had started hitting rustc-LLVM OOM on the
# Windows runners. ThinLTO parallelizes it across the runner's
# cores and keeps peak memory well under the limit.
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
- target: aarch64-pc-windows-msvc
host: windows-latest
host: windows-2025-8x-x64
features: ","
pre_build: |-
choco install --no-progress protoc
rustup target add aarch64-pc-windows-msvc
# See ThinLTO note on the x86_64-pc-windows-msvc target above.
export CARGO_PROFILE_RELEASE_LTO=thin
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
- target: x86_64-unknown-linux-gnu
host: ubuntu-latest
features: fp16kernels
@@ -170,13 +179,13 @@ jobs:
run:
working-directory: nodejs
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 11.1.1
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
# in October.
@@ -190,7 +199,7 @@ jobs:
toolchain: stable
targets: ${{ matrix.settings.target }}
- name: Cache cargo
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: |
~/.cargo/registry/index/
@@ -244,7 +253,7 @@ jobs:
if: ${{ !matrix.settings.docker }}
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: lancedb-${{ matrix.settings.target }}
path: nodejs/dist/*.node
@@ -256,7 +265,7 @@ jobs:
run: pnpm tsc
- name: Upload Generic Artifacts
if: ${{ matrix.settings.target == 'aarch64-apple-darwin' }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: nodejs-dist
path: |
@@ -287,13 +296,13 @@ jobs:
shell: bash
working-directory: nodejs
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 11.1.1
- name: Setup Node.js 24 for install
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
# in October.
@@ -303,18 +312,18 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Node.js ${{ matrix.node }} for test
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
- name: Download artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: lancedb-${{ matrix.settings.target }}
path: nodejs/dist/
# For testing purposes:
# run-id: 13982782871
# github-token: ${{ secrets.GITHUB_TOKEN }} # token with actions:read permissions on target repo
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: nodejs-dist
path: nodejs/dist
@@ -339,13 +348,13 @@ jobs:
needs:
- test-lancedb
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v6
with:
version: 11.1.1
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
@@ -353,14 +362,14 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
with:
name: nodejs-dist
path: nodejs/dist
# For testing purposes:
# run-id: 13982782871
# github-token: ${{ secrets.GITHUB_TOKEN }} # token with actions:read permissions on target repo
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v8
name: Download arch-specific binaries
with:
pattern: lancedb-*
@@ -398,7 +407,7 @@ jobs:
contents: read
issues: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: ./.github/actions/create-failure-issue
with:
job-results: ${{ toJSON(needs) }}
+7 -7
View File
@@ -41,7 +41,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -66,7 +66,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -95,7 +95,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -126,7 +126,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -160,7 +160,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -189,7 +189,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -212,7 +212,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
+7 -7
View File
@@ -40,7 +40,7 @@ jobs:
CC: clang-18
CXX: clang++-18
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -65,7 +65,7 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check advisories bans licenses sources
@@ -78,7 +78,7 @@ jobs:
CC: clang
CXX: clang++
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
# Building without a lock file often requires the latest Rust version since downstream
# dependencies may have updated their minimum Rust version.
- uses: actions-rust-lang/setup-rust-toolchain@v1
@@ -113,7 +113,7 @@ jobs:
CXX: clang++-18
GH_TOKEN: ${{ secrets.SOPHON_READ_TOKEN }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -152,7 +152,7 @@ jobs:
shell: bash
working-directory: rust
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
lfs: true
@@ -181,7 +181,7 @@ jobs:
run:
working-directory: rust/lancedb
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set target
run: rustup target add ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
@@ -210,7 +210,7 @@ jobs:
CC: clang-18
CXX: clang++-18
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
submodules: true
- name: Install dependencies
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: main
persist-credentials: false
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: main
persist-credentials: false
Generated
+352 -88
View File
@@ -157,9 +157,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.102"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
[[package]]
name = "approx"
@@ -1297,15 +1297,6 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]]
name = "bitpacking"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96a7139abd3d9cebf8cd6f920a389cf3dc9576172e32f4563f188cae3c3eb019"
dependencies = [
"crunchy",
]
[[package]]
name = "bitvec"
version = "1.0.1"
@@ -1472,9 +1463,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.1"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
[[package]]
name = "bytes-utils"
@@ -2048,9 +2039,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.18"
version = "0.9.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
dependencies = [
"crossbeam-utils",
]
@@ -3160,6 +3151,12 @@ dependencies = [
"encoding_rs",
]
[[package]]
name = "endian-type"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d"
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@@ -3186,9 +3183,9 @@ dependencies = [
[[package]]
name = "env_filter"
version = "1.0.1"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef"
checksum = "900d271a03799a1ee8d1ca9b19893b48ca674a9284fefcfb85f05e74ed314217"
dependencies = [
"log",
"regex",
@@ -3196,9 +3193,9 @@ dependencies = [
[[package]]
name = "env_logger"
version = "0.11.10"
version = "0.11.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a"
checksum = "de671bd27a75a797dc9ae289ba1e77276e75e2026408aab65185384e2d5cd3f6"
dependencies = [
"anstream",
"anstyle",
@@ -3432,8 +3429,8 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "fsst"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-array",
"rand 0.9.4",
@@ -3788,6 +3785,30 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "goosefs-sdk"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ae079b88ffe7772d12cfc5c40a5a324babb357893d95b5e3a22ae857f236c5f"
dependencies = [
"async-trait",
"bytes",
"dashmap",
"hostname",
"prost",
"prost-types",
"rand 0.9.4",
"reqwest 0.12.28",
"serde",
"thiserror 2.0.18",
"tokio",
"tokio-stream",
"tonic",
"tonic-prost",
"tracing",
"uuid",
]
[[package]]
name = "group"
version = "0.12.1"
@@ -3999,6 +4020,17 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "hostname"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617aaa3557aef3810a6369d0a99fac8a080891b68bd9f9812a1eeda0c0730cbd"
dependencies = [
"cfg-if 1.0.4",
"libc",
"windows-link",
]
[[package]]
name = "http"
version = "0.2.12"
@@ -4159,6 +4191,19 @@ dependencies = [
"webpki-roots 1.0.7",
]
[[package]]
name = "hyper-timeout"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0"
dependencies = [
"hyper 1.9.0",
"hyper-util",
"pin-project-lite",
"tokio",
"tower-service",
]
[[package]]
name = "hyper-util"
version = "0.1.20"
@@ -4682,7 +4727,7 @@ dependencies = [
"jiff",
"nom 8.0.0",
"num-traits",
"ordered-float",
"ordered-float 5.3.0",
"rand 0.9.4",
"serde",
"serde_json",
@@ -4735,8 +4780,8 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a"
[[package]]
name = "lance"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arc-swap",
"arrow",
@@ -4754,7 +4799,6 @@ dependencies = [
"async_cell",
"aws-credential-types",
"aws-sdk-dynamodb",
"bitpacking",
"byteorder",
"bytes",
"chrono",
@@ -4771,8 +4815,9 @@ dependencies = [
"futures",
"half",
"humantime",
"itertools 0.13.0",
"itertools 0.14.0",
"lance-arrow",
"lance-bitpacking",
"lance-core",
"lance-datafusion",
"lance-encoding",
@@ -4810,8 +4855,8 @@ dependencies = [
[[package]]
name = "lance-arrow"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -4820,6 +4865,7 @@ dependencies = [
"arrow-ord",
"arrow-schema",
"arrow-select",
"bytemuck",
"bytes",
"futures",
"getrandom 0.2.17",
@@ -4832,7 +4878,7 @@ dependencies = [
[[package]]
name = "lance-arrow-scalar"
version = "58.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -4846,7 +4892,7 @@ dependencies = [
[[package]]
name = "lance-arrow-stats"
version = "58.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-array",
"arrow-schema",
@@ -4855,18 +4901,19 @@ dependencies = [
[[package]]
name = "lance-bitpacking"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrayref",
"crunchy",
"paste",
"seq-macro",
]
[[package]]
name = "lance-core"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -4878,7 +4925,7 @@ dependencies = [
"datafusion-common",
"datafusion-sql",
"futures",
"itertools 0.13.0",
"itertools 0.14.0",
"lance-arrow",
"lance-derive",
"libc",
@@ -4904,8 +4951,8 @@ dependencies = [
[[package]]
name = "lance-datafusion"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow",
"arrow-array",
@@ -4935,8 +4982,8 @@ dependencies = [
[[package]]
name = "lance-datagen"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow",
"arrow-array",
@@ -4953,8 +5000,8 @@ dependencies = [
[[package]]
name = "lance-derive"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"proc-macro2",
"quote",
@@ -4963,8 +5010,8 @@ dependencies = [
[[package]]
name = "lance-encoding"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-arith",
"arrow-array",
@@ -4980,7 +5027,7 @@ dependencies = [
"futures",
"hex",
"hyperloglogplus",
"itertools 0.13.0",
"itertools 0.14.0",
"lance-arrow",
"lance-bitpacking",
"lance-core",
@@ -4999,8 +5046,8 @@ dependencies = [
[[package]]
name = "lance-file"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-arith",
"arrow-array",
@@ -5030,8 +5077,8 @@ dependencies = [
[[package]]
name = "lance-index"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arc-swap",
"arrow",
@@ -5043,7 +5090,6 @@ dependencies = [
"async-channel",
"async-recursion",
"async-trait",
"bitpacking",
"bitvec",
"bytes",
"chrono",
@@ -5056,11 +5102,12 @@ dependencies = [
"fst",
"futures",
"half",
"itertools 0.13.0",
"itertools 0.14.0",
"jieba-rs",
"jsonb",
"lance-arrow",
"lance-arrow-stats",
"lance-bitpacking",
"lance-core",
"lance-datafusion",
"lance-datagen",
@@ -5096,8 +5143,8 @@ dependencies = [
[[package]]
name = "lance-io"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow",
"arrow-arith",
@@ -5121,6 +5168,7 @@ dependencies = [
"lance-core",
"lance-namespace",
"log",
"metrics",
"moka",
"object_store",
"object_store_opendal",
@@ -5138,8 +5186,8 @@ dependencies = [
[[package]]
name = "lance-linalg"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -5150,12 +5198,13 @@ dependencies = [
"lance-core",
"num-traits",
"rand 0.9.4",
"rayon",
]
[[package]]
name = "lance-namespace"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow",
"async-trait",
@@ -5167,8 +5216,8 @@ dependencies = [
[[package]]
name = "lance-namespace-impls"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow",
"arrow-ipc",
@@ -5191,7 +5240,7 @@ dependencies = [
"lance-table",
"log",
"object_store",
"quick-xml 0.38.4",
"quick-xml 0.40.1",
"rand 0.9.4",
"reqwest 0.12.28",
"roaring",
@@ -5222,15 +5271,15 @@ dependencies = [
[[package]]
name = "lance-select"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-array",
"arrow-buffer",
"arrow-schema",
"byteorder",
"bytes",
"itertools 0.13.0",
"itertools 0.14.0",
"lance-core",
"roaring",
"tracing",
@@ -5238,8 +5287,8 @@ dependencies = [
[[package]]
name = "lance-table"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow",
"arrow-array",
@@ -5278,39 +5327,41 @@ dependencies = [
[[package]]
name = "lance-testing"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"arrow-array",
"arrow-schema",
"criterion",
"lance-arrow",
"num-traits",
"pprof",
"pprof 0.15.0",
"rand 0.9.4",
]
[[package]]
name = "lance-tokenizer"
version = "8.0.0-beta.17"
source = "git+https://github.com/lance-format/lance.git?tag=v8.0.0-beta.17#0f2745d10a0fe5b34a1cf214466bbc0c0d13c90c"
version = "9.0.0-beta.19"
source = "git+https://github.com/lance-format/lance.git?tag=v9.0.0-beta.19#8f0e6d3a7c53438275b134c0ac1afbc80600616e"
dependencies = [
"icu_segmenter",
"jieba-rs",
"lindera",
"rust-stemmers",
"serde",
"stop-words",
"unicode-normalization",
]
[[package]]
name = "lancedb"
version = "0.30.1-beta.2"
version = "0.32.0-beta.0"
dependencies = [
"ahash",
"anyhow",
"arrow",
"arrow-array",
"arrow-buffer",
"arrow-cast",
"arrow-data",
"arrow-ipc",
@@ -5360,12 +5411,15 @@ dependencies = [
"lance-testing",
"lazy_static",
"log",
"metrics",
"metrics-util",
"moka",
"num-traits",
"object_store",
"pin-project",
"polars",
"polars-arrow",
"pprof 0.14.1",
"rand 0.9.4",
"random_word",
"regex",
@@ -5382,13 +5436,14 @@ dependencies = [
"tokenizers",
"tokio",
"url",
"urlencoding",
"uuid",
"walkdir",
]
[[package]]
name = "lancedb-nodejs"
version = "0.30.1-beta.2"
version = "0.32.0-beta.0"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -5413,7 +5468,7 @@ dependencies = [
[[package]]
name = "lancedb-python"
version = "0.33.1-beta.2"
version = "0.35.0-beta.0"
dependencies = [
"arrow",
"async-trait",
@@ -5646,9 +5701,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.32"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a"
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "loom"
@@ -5815,6 +5870,36 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "metrics"
version = "0.24.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2"
dependencies = [
"portable-atomic",
"rapidhash",
]
[[package]]
name = "metrics-util"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376"
dependencies = [
"aho-corasick",
"crossbeam-epoch",
"crossbeam-utils",
"hashbrown 0.15.5",
"indexmap 2.14.0",
"metrics",
"ordered-float 4.6.0",
"quanta",
"radix_trie",
"rand 0.9.4",
"rand_xoshiro",
"sketches-ddsketch",
]
[[package]]
name = "mime"
version = "0.3.17"
@@ -5956,9 +6041,9 @@ dependencies = [
[[package]]
name = "napi"
version = "3.9.1"
version = "3.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad513ff22558f1830b595ea6eb4091da48145d09a222ce157e781896f78be0b9"
checksum = "0c71997d6f7ad4a756966e452426848ac27d3b37a295302d63afbbcce0270f93"
dependencies = [
"bitflags 2.11.1",
"chrono",
@@ -5981,9 +6066,9 @@ checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1"
[[package]]
name = "napi-derive"
version = "3.5.6"
version = "3.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b3f766e04667e6da0e181e2da4f85475d5a6513b7cf6a80bea184e224a5b42"
checksum = "d4ba572deef53e2c386759a8c2014175a62679d74ff83adc205c8bc0e0285727"
dependencies = [
"convert_case",
"ctor 1.0.5",
@@ -5995,9 +6080,9 @@ dependencies = [
[[package]]
name = "napi-derive-backend"
version = "5.0.4"
version = "5.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d5af30503edf933ce7377cf6d4c877a62b0f1107ea05585f1b5e430e88d5baf"
checksum = "ddd961eb2aa8965e3f29722d754f3a86907eb1984e2fbcbe3fe87b9a02d6bfba"
dependencies = [
"convert_case",
"proc-macro2",
@@ -6030,6 +6115,15 @@ dependencies = [
"rawpointer",
]
[[package]]
name = "nibble_vec"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
dependencies = [
"smallvec",
]
[[package]]
name = "nix"
version = "0.26.4"
@@ -6345,7 +6439,9 @@ dependencies = [
"opendal-layer-timeout",
"opendal-service-azblob",
"opendal-service-azdls",
"opendal-service-cos",
"opendal-service-gcs",
"opendal-service-goosefs",
"opendal-service-hf",
"opendal-service-oss",
"opendal-service-s3",
@@ -6473,6 +6569,23 @@ dependencies = [
"opendal-core",
]
[[package]]
name = "opendal-service-cos"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa8cafe9729213375c7331019b0cb756ad3e1aff7f45cd32c45eae91ebde8901"
dependencies = [
"bytes",
"http 1.4.2",
"log",
"opendal-core",
"quick-xml 0.39.4",
"reqsign-core",
"reqsign-file-read-tokio",
"reqsign-tencent-cos",
"serde",
]
[[package]]
name = "opendal-service-gcs"
version = "0.57.0"
@@ -6494,6 +6607,20 @@ dependencies = [
"tokio",
]
[[package]]
name = "opendal-service-goosefs"
version = "0.57.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69e43048bde419947ba826fbdc2f134d6c03f44ebf48bd33a03b72f9fc45fcb4"
dependencies = [
"bytes",
"goosefs-sdk",
"log",
"opendal-core",
"serde",
"tokio",
]
[[package]]
name = "opendal-service-hf"
version = "0.57.0"
@@ -6561,6 +6688,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-float"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951"
dependencies = [
"num-traits",
]
[[package]]
name = "ordered-float"
version = "5.3.0"
@@ -7330,6 +7466,28 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "pprof"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afad4d4df7b31280028245f152d5a575083e2abb822d05736f5e47653e77689f"
dependencies = [
"aligned-vec",
"backtrace",
"cfg-if 1.0.4",
"findshlibs",
"inferno",
"libc",
"log",
"nix",
"once_cell",
"smallvec",
"spin 0.10.0",
"symbolic-demangle",
"tempfile",
"thiserror 1.0.69",
]
[[package]]
name = "pprof"
version = "0.15.0"
@@ -7579,19 +7737,25 @@ dependencies = [
]
[[package]]
name = "quick-xml"
version = "0.26.0"
name = "quanta"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
dependencies = [
"memchr",
"crossbeam-utils",
"libc",
"once_cell",
"raw-cpuid",
"wasi 0.11.1+wasi-snapshot-preview1",
"web-sys",
"winapi",
]
[[package]]
name = "quick-xml"
version = "0.38.4"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd"
dependencies = [
"memchr",
]
@@ -7606,6 +7770,15 @@ dependencies = [
"serde",
]
[[package]]
name = "quick-xml"
version = "0.40.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2474bd2e5029e7ccb6abb2ba48cf2383a333851dedf495901544281590c7da7f"
dependencies = [
"memchr",
]
[[package]]
name = "quinn"
version = "0.11.9"
@@ -7689,6 +7862,16 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "radix_trie"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd"
dependencies = [
"endian-type",
"nibble_vec",
]
[[package]]
name = "rancor"
version = "0.1.1"
@@ -7823,6 +8006,15 @@ version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "973443cf09a9c8656b574a866ab68dfa19f0867d0340648c7d2f6a71b8a8ea68"
[[package]]
name = "rapidhash"
version = "4.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e"
dependencies = [
"rustversion",
]
[[package]]
name = "raw-cpuid"
version = "11.6.0"
@@ -8107,6 +8299,21 @@ dependencies = [
"tokio",
]
[[package]]
name = "reqsign-tencent-cos"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e128f19525861dbded59e1e7c17653a8ed63d573ca04aed708d552dbef5bb32a"
dependencies = [
"anyhow",
"http 1.4.2",
"log",
"percent-encoding",
"reqsign-core",
"serde",
"serde_json",
]
[[package]]
name = "reqwest"
version = "0.12.28"
@@ -8985,6 +9192,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
[[package]]
name = "sketches-ddsketch"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
[[package]]
name = "slab"
version = "0.4.12"
@@ -9205,6 +9418,15 @@ version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51f1e89f093f99e7432c491c382b88a6860a5adbe6bf02574bf0a08efff1978"
[[package]]
name = "stop-words"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d68df56303396bcfb639455b3c166804aeb7994005010aab5e9e8a1277b8871d"
dependencies = [
"serde_json",
]
[[package]]
name = "str_stack"
version = "0.1.1"
@@ -9783,6 +10005,45 @@ dependencies = [
"winnow",
]
[[package]]
name = "tonic"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
dependencies = [
"async-trait",
"base64 0.22.1",
"bytes",
"h2 0.4.14",
"http 1.4.2",
"http-body 1.0.1",
"http-body-util",
"hyper 1.9.0",
"hyper-timeout",
"hyper-util",
"percent-encoding",
"pin-project",
"socket2 0.6.3",
"sync_wrapper",
"tokio",
"tokio-stream",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tonic-prost"
version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0"
dependencies = [
"bytes",
"prost",
"tonic",
]
[[package]]
name = "tower"
version = "0.5.3"
@@ -9791,9 +10052,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
"indexmap 2.14.0",
"pin-project-lite",
"slab",
"sync_wrapper",
"tokio",
"tokio-util",
"tower-layer",
"tower-service",
"tracing",
@@ -10116,9 +10380,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.23.3"
version = "1.23.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7"
checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
dependencies = [
"getrandom 0.4.2",
"js-sys",
+17 -14
View File
@@ -13,24 +13,25 @@ categories = ["database-implementations"]
rust-version = "1.91.0"
[workspace.dependencies]
lance = { "version" = "=8.0.0-beta.17", default-features = false, "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=8.0.0-beta.17", default-features = false, "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=8.0.0-beta.17", default-features = false, "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=8.0.0-beta.17", "tag" = "v8.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
lance = { "version" = "=9.0.0-beta.19", default-features = false, "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=9.0.0-beta.19", default-features = false, "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=9.0.0-beta.19", default-features = false, "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=9.0.0-beta.19", "tag" = "v9.0.0-beta.19", "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 }
arrow-array = "58.0.0"
arrow-buffer = "58.0.0"
arrow-data = "58.0.0"
arrow-ipc = "58.0.0"
arrow-ord = "58.0.0"
@@ -53,6 +54,8 @@ half = { "version" = "2.7.1", default-features = false, features = [
] }
futures = "0"
log = "0.4"
metrics = "0.24"
metrics-util = "0.19"
moka = { version = "0.12", features = ["future"] }
object_store = "0.13.2"
pin-project = "1.0.7"
+12 -30
View File
@@ -51,18 +51,6 @@ ignore = [
# https://rustsec.org/advisories/RUSTSEC-2024-0436
{ id = "RUSTSEC-2024-0436", reason = "transitive via datafusion; awaiting ecosystem migration" },
# encoding: unmaintained. Reached through lindera-dictionary, which is
# required by the native Lindera tokenizer path. Lindera has not migrated
# off this crate yet.
# https://rustsec.org/advisories/RUSTSEC-2021-0153
{ id = "RUSTSEC-2021-0153", reason = "transitive via lindera-dictionary for native Lindera tokenizer" },
# fast-float: unsound and unmaintained. Reached only through polars-arrow
# from the optional Polars integration; replacement requires a Polars
# dependency upgrade.
# https://rustsec.org/advisories/RUSTSEC-2024-0379
{ id = "RUSTSEC-2024-0379", reason = "transitive via polars-arrow; waiting on Polars migration" },
# tantivy: segfault on malformed input due to missing bounds check.
# Pulled in via lance for full-text search. We only feed tantivy
# documents we construct ourselves, not attacker-controlled bytes.
@@ -80,18 +68,6 @@ ignore = [
# https://rustsec.org/advisories/RUSTSEC-2025-0119
{ id = "RUSTSEC-2025-0119", reason = "transitive via hf-hub/indicatif; cosmetic formatting crate" },
# bincode: unmaintained. Reached through lindera and lindera-dictionary,
# which are required by the native Lindera tokenizer path. Lindera has not
# migrated to another serialization format yet.
# https://rustsec.org/advisories/RUSTSEC-2025-0141
{ id = "RUSTSEC-2025-0141", reason = "transitive via lindera/lindera-dictionary for native Lindera tokenizer" },
# lru: soundness issue in IterMut. Reached only through aws-sdk-s3 in
# LanceDB's dev-dependency graph; LanceDB does not use that iterator
# directly. Clearing this requires the AWS SDK chain to update lru.
# https://rustsec.org/advisories/RUSTSEC-2026-0002
{ id = "RUSTSEC-2026-0002", reason = "transitive via aws-sdk-s3 dev-dependency; waiting on AWS SDK lru upgrade" },
# rustls-webpki 0.101.7 (old major line): name-constraint checks for
# URI / wildcard names. Pulled in only via the legacy rustls 0.21 chain
# from aws-smithy-http-client. The 0.103 line we actively use is patched.
@@ -108,17 +84,23 @@ ignore = [
# https://rustsec.org/advisories/RUSTSEC-2026-0104
{ id = "RUSTSEC-2026-0104", reason = "only affects rustls-webpki 0.101 from legacy aws-smithy/rustls 0.21 chain" },
# rand 0.8.5: soundness issue only when ThreadRng reseeds inside a custom
# logger. Reached through several transitive chains. LanceDB does not use
# rand from a custom logger; upgrade once all pinned chains accept 0.8.6+.
# https://rustsec.org/advisories/RUSTSEC-2026-0097
{ id = "RUSTSEC-2026-0097", reason = "transitive rand 0.8.5; LanceDB does not call ThreadRng from custom logging" },
# pyo3 advisories in the Python bindings; tracked pending a patched pyo3 release.
# https://rustsec.org/advisories/RUSTSEC-2026-0176
# https://rustsec.org/advisories/RUSTSEC-2026-0177
{ id = "RUSTSEC-2026-0176", reason = "pyo3 in Python bindings; awaiting patched pyo3 release" },
{ id = "RUSTSEC-2026-0177", reason = "pyo3 in Python bindings; awaiting patched pyo3 release" },
# quick-xml < 0.41.0: quadratic runtime on duplicate attribute names (DoS).
# quick-xml < 0.41.0: unbounded namespace-declaration allocation in NsReader (DoS).
# Pulled in transitively by inferno (dev-only flame-graph dep), lance-namespace-impls
# (git dep from lance), and opendal/reqsign (cloud storage XML parsing). The XML
# parsed by opendal/reqsign comes from trusted cloud-storage endpoints (S3, GCS,
# Azure), not attacker-controlled input. Clearing requires upstream crates to migrate
# to quick-xml >= 0.41.0.
# https://rustsec.org/advisories/RUSTSEC-2026-0194
# https://rustsec.org/advisories/RUSTSEC-2026-0195
{ id = "RUSTSEC-2026-0194", reason = "transitive via inferno/lance/opendal; XML from trusted cloud endpoints, not attacker-controlled" },
{ id = "RUSTSEC-2026-0195", reason = "transitive via inferno/lance/opendal; XML from trusted cloud endpoints, not attacker-controlled" },
]
# ---------------------------------------------------------------------------
+1 -1
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.30.1-beta.2</version>
<version>0.32.0-beta.0</version>
</dependency>
```
+3
View File
@@ -518,6 +518,9 @@ x > 5 OR y = 'test'
Filtering performance can often be improved by creating a scalar index
on the filter column(s).
Calling this multiple times combines the filters with a logical AND rather
than replacing the previous filter.
```
#### Inherited from
+20
View File
@@ -398,6 +398,26 @@ Drop an index from the table.
***
### getLsmWriteSpec()
```ts
abstract getLsmWriteSpec(): Promise<undefined | LsmWriteSpec>
```
Read the [LsmWriteSpec](../interfaces/LsmWriteSpec.md) currently installed on this table.
Resolves to `undefined` when the MemWAL LSM write path is not enabled (no
spec has been set, or it was removed with [Table#unsetLsmWriteSpec](Table.md#unsetlsmwritespec)).
The returned spec — including its `maintainedIndexes` and
`writerConfigDefaults` — mirrors what was passed to
[Table#setLsmWriteSpec](Table.md#setlsmwritespec).
#### Returns
`Promise`&lt;`undefined` \| [`LsmWriteSpec`](../interfaces/LsmWriteSpec.md)&gt;
***
### indexStats()
```ts
+3
View File
@@ -767,6 +767,9 @@ x > 5 OR y = 'test'
Filtering performance can often be improved by creating a scalar index
on the filter column(s).
Calling this multiple times combines the filters with a logical AND rather
than replacing the previous filter.
```
#### Inherited from
+29
View File
@@ -0,0 +1,29 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / OAuthFlowType
# Enumeration: OAuthFlowType
OAuth authentication flow types.
## Enumeration Members
### AzureManagedIdentity
```ts
AzureManagedIdentity: "azure_managed_identity";
```
Azure Managed Identity via IMDS.
***
### ClientCredentials
```ts
ClientCredentials: "client_credentials";
```
Client Credentials grant (service-to-service / M2M).
@@ -0,0 +1,42 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / instrumentLanceDbMetrics
# Function: instrumentLanceDbMetrics()
```ts
function instrumentLanceDbMetrics(meterProvider?): boolean
```
Register LanceDB metrics as OpenTelemetry observable instruments.
Installs a process-global metrics recorder and creates one observable
instrument per LanceDB metric (currently object store request counts, bytes,
latency, errors, and throttles) on the given (or global) `MeterProvider`. The
configured `MetricReader` then collects them on its own schedule.
Counters and gauges map directly to observable counters/gauges. Because
OpenTelemetry has no asynchronous histogram instrument, each histogram is
exported Prometheus-style as cumulative `le` bucket counts (`<name>_bucket`,
with an `le` attribute) plus `<name>_count` and `<name>_sum`.
Requires `@opentelemetry/api` (a dependency) and, to actually export, an
OpenTelemetry SDK such as `@opentelemetry/sdk-metrics`.
## Parameters
* **meterProvider?**: `MeterProvider`
The provider to register instruments on. Defaults to the
global provider from `@opentelemetry/api`.
## Returns
`boolean`
`true` if the recorder is installed and instruments are registered.
`false` if a different `metrics` recorder is already installed in this
process (only one global recorder is permitted), in which case a warning is
emitted and no instruments are created. Calling this more than once is safe;
instruments are created only on the first successful call.
+4
View File
@@ -12,6 +12,7 @@
## Enumerations
- [FullTextQueryType](enumerations/FullTextQueryType.md)
- [OAuthFlowType](enumerations/OAuthFlowType.md)
- [Occur](enumerations/Occur.md)
- [Operator](enumerations/Operator.md)
@@ -85,6 +86,8 @@
- [ListNamespacesResponse](interfaces/ListNamespacesResponse.md)
- [LsmWriteSpec](interfaces/LsmWriteSpec.md)
- [MergeResult](interfaces/MergeResult.md)
- [NativeOAuthConfig](interfaces/NativeOAuthConfig.md)
- [OAuthConfig](interfaces/OAuthConfig.md)
- [OpenTableOptions](interfaces/OpenTableOptions.md)
- [OptimizeOptions](interfaces/OptimizeOptions.md)
- [OptimizeStats](interfaces/OptimizeStats.md)
@@ -128,6 +131,7 @@
- [RecordBatchIterator](functions/RecordBatchIterator.md)
- [connect](functions/connect.md)
- [connectNamespace](functions/connectNamespace.md)
- [instrumentLanceDbMetrics](functions/instrumentLanceDbMetrics.md)
- [makeArrowTable](functions/makeArrowTable.md)
- [packBits](functions/packBits.md)
- [permutationBuilder](functions/permutationBuilder.md)
@@ -64,6 +64,19 @@ client used by manifest-enabled native connections.
***
### oauthConfig?
```ts
optional oauthConfig: NativeOAuthConfig;
```
(For LanceDB cloud only): OAuth configuration for IdP-based
authentication (e.g., Azure Entra ID). When set, token acquisition
and refresh are handled entirely in Rust. TypeScript users should pass
the public `OAuthConfig` type exported from `@lancedb/lancedb`.
***
### readConsistencyInterval?
```ts
@@ -0,0 +1,88 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / NativeOAuthConfig
# Interface: NativeOAuthConfig
OAuth configuration for LanceDB authentication.
This is the generated napi-rs binding shape. TypeScript users should prefer
the public `OAuthConfig` type exported from `@lancedb/lancedb`.
All token acquisition and refresh is handled in the Rust layer.
## Properties
### clientId
```ts
clientId: string;
```
Application / Client ID.
***
### clientSecret?
```ts
optional clientSecret: string;
```
Client secret (required for client_credentials).
***
### flow?
```ts
optional flow: string;
```
Authentication flow: "client_credentials" or "azure_managed_identity"
***
### issuerUrl
```ts
issuerUrl: string;
```
OIDC issuer URL or OAuth authority URL.
For Azure: `https://login.microsoftonline.com/{tenant_id}/v2.0`
***
### managedIdentityClientId?
```ts
optional managedIdentityClientId: string;
```
Client ID for user-assigned managed identity (azure_managed_identity).
***
### refreshBufferSecs?
```ts
optional refreshBufferSecs: number;
```
Seconds before expiry to trigger proactive refresh (default: 300).
Keep this well below the token TTL; if it is greater than or equal to
the TTL, each request refreshes the token.
***
### scopes
```ts
scopes: string[];
```
OAuth scopes to request. For Azure managed identity, exactly one scope
or resource is required. For example: `["api://{app_id}/.default"]`
+111
View File
@@ -0,0 +1,111 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / OAuthConfig
# Interface: OAuthConfig
OAuth configuration for LanceDB authentication.
This is the public TypeScript OAuth configuration type. The generated
`NativeOAuthConfig` type has the same runtime shape but is an implementation
detail of the napi-rs binding.
All token acquisition and refresh is handled in the Rust layer.
This config is passed through to Rust via napi-rs.
## Examples
```typescript
const config: OAuthConfig = {
issuerUrl: "https://login.microsoftonline.com/{tenant}/v2.0",
clientId: "app-id",
clientSecret: "secret",
scopes: ["api://lancedb-api/.default"],
};
```
```typescript
const config: OAuthConfig = {
issuerUrl: "https://login.microsoftonline.com/{tenant}/v2.0",
clientId: "app-id",
scopes: ["api://lancedb-api/.default"],
flow: OAuthFlowType.AzureManagedIdentity,
};
```
## Properties
### clientId
```ts
clientId: string;
```
Application / Client ID.
***
### clientSecret?
```ts
optional clientSecret: string;
```
Client secret (required for ClientCredentials).
***
### flow?
```ts
optional flow: OAuthFlowType;
```
Authentication flow (default: ClientCredentials).
***
### issuerUrl
```ts
issuerUrl: string;
```
OIDC issuer URL or OAuth authority URL.
For Azure: `https://login.microsoftonline.com/{tenant_id}/v2.0`
***
### managedIdentityClientId?
```ts
optional managedIdentityClientId: string;
```
Client ID for user-assigned managed identity (AzureManagedIdentity).
***
### refreshBufferSecs?
```ts
optional refreshBufferSecs: number;
```
Seconds before expiry to trigger proactive refresh (default: 300).
Keep this well below the token TTL; if it is greater than or equal to
the TTL, each request refreshes the token.
***
### scopes
```ts
scopes: string[];
```
OAuth scopes to request.
For Azure managed identity, exactly one scope or resource is required.
For example: `["api://{app_id}/.default"]`
@@ -8,6 +8,14 @@
## Properties
### clumpSize?
```ts
optional clumpSize: number;
```
***
### counts?
```ts
+1 -1
View File
@@ -8,7 +8,7 @@
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.30.1-beta.2</version>
<version>0.32.0-beta.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.30.1-beta.2</version>
<version>0.32.0-beta.0</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>8.0.0-beta.17</lance-core.version>
<lance-core.version>9.0.0-beta.19</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>
+2 -2
View File
@@ -1,7 +1,7 @@
[package]
name = "lancedb-nodejs"
edition.workspace = true
version = "0.30.1-beta.2"
version = "0.32.0-beta.0"
publish = false
license.workspace = true
description.workspace = true
@@ -44,6 +44,6 @@ aws-lc-rs = "=1.16.3"
napi-build = "2.3.1"
[features]
default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface"]
default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface", "lancedb/goosefs", "lancedb/metrics-otel"]
fp16kernels = ["lancedb/fp16kernels"]
remote = ["lancedb/remote"]
+114
View File
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
import {
MeterProvider,
type MetricData,
MetricReader,
} from "@opentelemetry/sdk-metrics";
import * as tmp from "tmp";
import { connect, instrumentLanceDbMetrics } from "../lancedb";
// snapshotLancedbMetrics is internal plumbing (not part of the public API), so
// it is imported from the native module rather than the package entry point.
import { snapshotLancedbMetrics } from "../lancedb/native";
// The metrics recorder is process-global and installed once, so the whole
// bridge is exercised in a single test to avoid cross-test global-state coupling.
// A minimal pull-based reader whose `collect()` we drive directly, invoking the
// observable-instrument callbacks. `@opentelemetry/sdk-metrics` ships no
// in-memory reader, so we subclass the abstract base.
class TestMetricReader extends MetricReader {
protected async onForceFlush(): Promise<void> {
// no-op: collection is driven directly via collect()
}
protected async onShutdown(): Promise<void> {
// no-op: nothing to release
}
}
async function metricsByName(
reader: TestMetricReader,
): Promise<Map<string, MetricData>> {
const collected = await reader.collect();
const result = new Map<string, MetricData>();
for (const scope of collected.resourceMetrics.scopeMetrics) {
for (const metric of scope.metrics) {
result.set(metric.descriptor.name, metric);
}
}
return result;
}
describe("OpenTelemetry metrics bridge", () => {
let tmpDir: tmp.DirResult;
beforeEach(() => {
tmpDir = tmp.dirSync({ unsafeCleanup: true });
});
afterEach(() => tmpDir.removeCallback());
it("snapshot is safe to call regardless of install state", () => {
expect(Array.isArray(snapshotLancedbMetrics())).toBe(true);
});
it("exports object store metrics via observable instruments", async () => {
const reader = new TestMetricReader();
const provider = new MeterProvider({ readers: [reader] });
expect(instrumentLanceDbMetrics(provider)).toBe(true);
// Generate object store activity on the local filesystem (scheme "file").
const db = await connect(tmpDir.name);
const data = Array.from({ length: 256 }, (_, i) => ({ id: i }));
const table = await db.createTable("t", data);
expect(await table.countRows()).toBe(256);
const metrics = await metricsByName(reader);
const requests = metrics.get("lance_object_store_requests_total");
expect(requests).toBeDefined();
// biome-ignore lint/suspicious/noExplicitAny: SDK point shape
const requestPoints = (requests!.dataPoints as any[]) ?? [];
expect(requestPoints.length).toBeGreaterThan(0);
for (const p of requestPoints) {
// Labelled by `operation` and `base` (the store scheme by default).
expect(p.attributes).toHaveProperty("base");
expect(p.attributes).toHaveProperty("operation");
}
const totalRequests = requestPoints.reduce((acc, p) => acc + p.value, 0);
expect(totalRequests).toBeGreaterThan(0);
// Histograms are decomposed into bucket / count / sum observable counters.
const bucket = metrics.get(
"lance_object_store_request_duration_seconds_bucket",
);
expect(bucket).toBeDefined();
// biome-ignore lint/suspicious/noExplicitAny: SDK point shape
const bucketPoints = (bucket!.dataPoints as any[]) ?? [];
expect(bucketPoints.length).toBeGreaterThan(0);
expect(bucketPoints.every((p) => "le" in p.attributes)).toBe(true);
// The implicit +Inf bucket must be present.
expect(bucketPoints.some((p) => p.attributes.le === "+Inf")).toBe(true);
const count = metrics.get(
"lance_object_store_request_duration_seconds_count",
);
expect(count).toBeDefined();
// biome-ignore lint/suspicious/noExplicitAny: SDK point shape
const countPoints = (count!.dataPoints as any[]) ?? [];
expect(countPoints.reduce((acc, p) => acc + p.value, 0)).toBeGreaterThan(0);
const sum = metrics.get("lance_object_store_request_duration_seconds_sum");
expect(sum).toBeDefined();
// biome-ignore lint/suspicious/noExplicitAny: SDK point shape
const sumPoints = (sum!.dataPoints as any[]) ?? [];
expect(sumPoints.reduce((acc, p) => acc + p.value, 0)).toBeGreaterThan(0);
// Unit handling: only `_sum` keeps the histogram's unit (seconds); `_bucket`
// and `_count` observe cumulative counts and are unitless.
expect(sum!.descriptor.unit).toBe("s");
expect(bucket!.descriptor.unit).toBe("");
expect(count!.descriptor.unit).toBe("");
await provider.shutdown();
});
});
+14
View File
@@ -215,6 +215,20 @@ describe("Query orderBy", () => {
expect(results[2].score).toBeCloseTo(4.1, 0.001);
});
it("should combine repeated where clauses with AND", async () => {
const results = await table
.query()
.where("score > 1.0")
.where("score < 3.0")
.orderBy({ columnName: "score" })
.toArray();
// Only rows matching both predicates should be returned, rather than the
// second where() silently replacing the first.
expect(results.length).toBe(2);
expect(results[0].score).toBeCloseTo(1.2, 0.001);
expect(results[1].score).toBeCloseTo(2.8, 0.001);
});
it("should support method chaining with limit", async () => {
const results = await table
.query()
+50
View File
@@ -2992,6 +2992,56 @@ describe("setLsmWriteSpec / unsetLsmWriteSpec", () => {
}),
).rejects.toThrow();
});
it("reads back the installed spec via getLsmWriteSpec", async () => {
const conn = await connect(tmpDir.name);
const table = await makeTable(conn);
await table.setUnenforcedPrimaryKey("id");
// Nothing installed yet.
expect(await table.getLsmWriteSpec()).toBeUndefined();
// A real scalar index is needed to name it as a maintained index.
await table.add([{ id: 1 }, { id: 2 }, { id: 3 }]);
await table.createIndex("id");
const indexName = (await table.listIndices())[0].name;
// Bucket spec round-trips, including maintained indexes and writer config
// defaults. Lance writer-config keys are canonically snake_case.
// biome-ignore lint/style/useNamingConvention: Lance writer-config keys are snake_case
const writerConfigDefaults = { durable_write: "false" };
await table.setLsmWriteSpec({
specType: "bucket",
column: "id",
numBuckets: 4,
maintainedIndexes: [indexName],
writerConfigDefaults,
});
const spec = await table.getLsmWriteSpec();
expect(spec).toBeDefined();
expect(spec?.specType).toBe("bucket");
expect(spec?.column).toBe("id");
expect(spec?.numBuckets).toBe(4);
expect(spec?.maintainedIndexes).toEqual([indexName]);
expect(spec?.writerConfigDefaults).toEqual(writerConfigDefaults);
// After unset, undefined again.
await table.unsetLsmWriteSpec();
expect(await table.getLsmWriteSpec()).toBeUndefined();
// Identity round-trips (column recovered from the schema).
await table.setLsmWriteSpec({ specType: "identity", column: "id" });
const identity = await table.getLsmWriteSpec();
expect(identity?.specType).toBe("identity");
expect(identity?.column).toBe("id");
await table.unsetLsmWriteSpec();
// Unsharded round-trips (no routing column).
await table.setLsmWriteSpec({ specType: "unsharded" });
const unsharded = await table.getLsmWriteSpec();
expect(unsharded?.specType).toBe("unsharded");
expect(unsharded?.column).toBeFalsy();
});
});
describe("LSM merge insert", () => {
+8
View File
@@ -20,6 +20,11 @@ import { HeaderProvider } from "./header";
// Re-export native header provider for use with connectWithHeaderProvider
export { JsHeaderProvider as NativeJsHeaderProvider } from "./native.js";
// OpenTelemetry metrics bridge. Only the high-level entry point is public; the
// underlying recorder/catalog/snapshot functions remain internal plumbing that
// `otel.ts` consumes from the native module.
export { instrumentLanceDbMetrics } from "./otel";
export {
AddColumnsSql,
ConnectionOptions,
@@ -52,6 +57,7 @@ export {
SplitHashOptions,
SplitSequentialOptions,
ShuffleOptions,
OAuthConfig as NativeOAuthConfig,
} from "./native.js";
export {
@@ -130,6 +136,8 @@ export {
TokenResponse,
} from "./header";
export { OAuthConfig, OAuthFlowType } from "./oauth";
export { MergeInsertBuilder, WriteExecutionOptions } from "./merge";
export * as embedding from "./embedding";
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
/**
* OAuth authentication flow types.
*/
export enum OAuthFlowType {
/** Client Credentials grant (service-to-service / M2M). */
ClientCredentials = "client_credentials",
/** Azure Managed Identity via IMDS. */
AzureManagedIdentity = "azure_managed_identity",
}
/**
* OAuth configuration for LanceDB authentication.
*
* This is the public TypeScript OAuth configuration type. The generated
* `NativeOAuthConfig` type has the same runtime shape but is an implementation
* detail of the napi-rs binding.
*
* All token acquisition and refresh is handled in the Rust layer.
* This config is passed through to Rust via napi-rs.
*
* @example Client Credentials (service-to-service):
* ```typescript
* const config: OAuthConfig = {
* issuerUrl: "https://login.microsoftonline.com/{tenant}/v2.0",
* clientId: "app-id",
* clientSecret: "secret",
* scopes: ["api://lancedb-api/.default"],
* };
* ```
*
* @example Azure Managed Identity:
* ```typescript
* const config: OAuthConfig = {
* issuerUrl: "https://login.microsoftonline.com/{tenant}/v2.0",
* clientId: "app-id",
* scopes: ["api://lancedb-api/.default"],
* flow: OAuthFlowType.AzureManagedIdentity,
* };
* ```
*/
export interface OAuthConfig {
/**
* OIDC issuer URL or OAuth authority URL.
* For Azure: `https://login.microsoftonline.com/{tenant_id}/v2.0`
*/
issuerUrl: string;
/** Application / Client ID. */
clientId: string;
/**
* OAuth scopes to request.
* For Azure managed identity, exactly one scope or resource is required.
* For example: `["api://{app_id}/.default"]`
*/
scopes: string[];
/** Authentication flow (default: ClientCredentials). */
flow?: OAuthFlowType;
/** Client secret (required for ClientCredentials). */
clientSecret?: string;
/** Client ID for user-assigned managed identity (AzureManagedIdentity). */
managedIdentityClientId?: string;
/**
* Seconds before expiry to trigger proactive refresh (default: 300).
* Keep this well below the token TTL; if it is greater than or equal to
* the TTL, each request refreshes the token.
*/
refreshBufferSecs?: number;
}
+137
View File
@@ -0,0 +1,137 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
import {
type Attributes,
type MeterProvider,
type ObservableResult,
metrics,
} from "@opentelemetry/api";
import {
lancedbMetricsCatalog,
registerLancedbMetricsRecorder,
snapshotLancedbMetrics,
} from "./native";
let instrumented = false;
/**
* Register LanceDB metrics as OpenTelemetry observable instruments.
*
* Installs a process-global metrics recorder and creates one observable
* instrument per LanceDB metric (currently object store request counts, bytes,
* latency, errors, and throttles) on the given (or global) `MeterProvider`. The
* configured `MetricReader` then collects them on its own schedule.
*
* Counters and gauges map directly to observable counters/gauges. Because
* OpenTelemetry has no asynchronous histogram instrument, each histogram is
* exported Prometheus-style as cumulative `le` bucket counts (`<name>_bucket`,
* with an `le` attribute) plus `<name>_count` and `<name>_sum`.
*
* Requires `@opentelemetry/api` (a dependency) and, to actually export, an
* OpenTelemetry SDK such as `@opentelemetry/sdk-metrics`.
*
* @param meterProvider The provider to register instruments on. Defaults to the
* global provider from `@opentelemetry/api`.
* @returns `true` if the recorder is installed and instruments are registered.
* `false` if a different `metrics` recorder is already installed in this
* process (only one global recorder is permitted), in which case a warning is
* emitted and no instruments are created. Calling this more than once is safe;
* instruments are created only on the first successful call.
*/
export function instrumentLanceDbMetrics(
meterProvider?: MeterProvider,
): boolean {
if (!registerLancedbMetricsRecorder()) {
console.warn(
"Could not install the LanceDB metrics recorder: another `metrics` " +
"recorder is already installed in this process. LanceDB metrics will " +
"not be exported via OpenTelemetry.",
);
return false;
}
if (instrumented) {
return true;
}
const provider = meterProvider ?? metrics.getMeterProvider();
const meter = provider.getMeter("lancedb");
const scalarCallback = (metricName: string) => (result: ObservableResult) => {
for (const point of snapshotLancedbMetrics()) {
if (point.name === metricName && point.value != null) {
result.observe(point.value, point.attributes);
}
}
};
const bucketCallback = (metricName: string) => (result: ObservableResult) => {
for (const point of snapshotLancedbMetrics()) {
if (point.name !== metricName || point.buckets == null) {
continue;
}
for (const bucket of point.buckets) {
const attributes: Attributes = {
...point.attributes,
le: bucket.le,
};
result.observe(bucket.cumulativeCount, attributes);
}
}
};
const fieldCallback =
(metricName: string, field: "count" | "sum") =>
(result: ObservableResult) => {
for (const point of snapshotLancedbMetrics()) {
if (point.name !== metricName) {
continue;
}
const value = point[field];
if (value != null) {
result.observe(value, point.attributes);
}
}
};
for (const desc of lancedbMetricsCatalog()) {
const unit = desc.unit ?? "";
if (desc.kind === "counter") {
const counter = meter.createObservableCounter(desc.name, {
unit,
description: desc.description,
});
counter.addCallback(scalarCallback(desc.name));
} else if (desc.kind === "gauge") {
const gauge = meter.createObservableGauge(desc.name, {
unit,
description: desc.description,
});
gauge.addCallback(scalarCallback(desc.name));
} else if (desc.kind === "histogram") {
// `_bucket` and `_count` observe cumulative sample counts, not the
// histogram's measured quantity, so they are unitless; only `_sum`
// carries the histogram's unit.
const bucket = meter.createObservableCounter(`${desc.name}_bucket`, {
description: `${desc.description} (cumulative buckets)`,
});
bucket.addCallback(bucketCallback(desc.name));
const count = meter.createObservableCounter(`${desc.name}_count`, {
description: `${desc.description} (count)`,
});
count.addCallback(fieldCallback(desc.name, "count"));
const sum = meter.createObservableCounter(`${desc.name}_sum`, {
unit,
description: `${desc.description} (sum)`,
});
sum.addCallback(fieldCallback(desc.name, "sum"));
}
}
instrumented = true;
return true;
}
+3
View File
@@ -362,6 +362,9 @@ export class StandardQueryBase<
*
* Filtering performance can often be improved by creating a scalar index
* on the filter column(s).
*
* Calling this multiple times combines the filters with a logical AND rather
* than replacing the previous filter.
*/
where(predicate: string): this {
this.doCall((inner: NativeQueryType) => inner.onlyIf(predicate));
+20
View File
@@ -585,6 +585,17 @@ export abstract class Table {
* @returns {Promise<void>}
*/
abstract unsetLsmWriteSpec(): Promise<void>;
/**
* Read the {@link LsmWriteSpec} currently installed on this table.
*
* Resolves to `undefined` when the MemWAL LSM write path is not enabled (no
* spec has been set, or it was removed with {@link Table#unsetLsmWriteSpec}).
* The returned spec — including its `maintainedIndexes` and
* `writerConfigDefaults` — mirrors what was passed to
* {@link Table#setLsmWriteSpec}.
* @returns {Promise<LsmWriteSpec | undefined>}
*/
abstract getLsmWriteSpec(): Promise<LsmWriteSpec | undefined>;
/**
* Drain and close any cached MemWAL shard writers held for this table.
*
@@ -1091,6 +1102,15 @@ export class LocalTable extends Table {
return await this.inner.unsetLsmWriteSpec();
}
async getLsmWriteSpec(): Promise<LsmWriteSpec | undefined> {
// The native binding types `specType` as a plain `string`; narrow it back
// to the public union. The Rust `From` impl only ever emits one of the
// three valid values, so the cast is safe.
return ((await this.inner.getLsmWriteSpec()) ?? undefined) as
| LsmWriteSpec
| undefined;
}
async closeLsmWriters(): Promise<void> {
return await this.inner.closeLsmWriters();
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-darwin-arm64",
"version": "0.30.1-beta.2",
"version": "0.32.0-beta.0",
"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.30.1-beta.2",
"version": "0.32.0-beta.0",
"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.30.1-beta.2",
"version": "0.32.0-beta.0",
"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.30.1-beta.2",
"version": "0.32.0-beta.0",
"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.30.1-beta.2",
"version": "0.32.0-beta.0",
"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.30.1-beta.2",
"version": "0.32.0-beta.0",
"os": [
"win32"
],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-win32-x64-msvc",
"version": "0.30.1-beta.2",
"version": "0.32.0-beta.0",
"os": ["win32"],
"cpu": ["x64"],
"main": "lancedb.win32-x64-msvc.node",
+73 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@lancedb/lancedb",
"version": "0.30.1-beta.2",
"version": "0.32.0-beta.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@lancedb/lancedb",
"version": "0.30.1-beta.2",
"version": "0.32.0-beta.0",
"cpu": [
"x64",
"arm64"
@@ -18,6 +18,7 @@
"win32"
],
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
@@ -27,6 +28,7 @@
"@biomejs/biome": "^1.7.3",
"@jest/globals": "^29.7.0",
"@napi-rs/cli": "3.7.0",
"@opentelemetry/sdk-metrics": "^1.30.0",
"@types/axios": "^0.14.0",
"@types/jest": "^29.1.2",
"@types/node": "22.7.4",
@@ -4148,6 +4150,75 @@
"@octokit/openapi-types": "^27.0.0"
}
},
"node_modules/@opentelemetry/api": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
"integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
"license": "Apache-2.0",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/core": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz",
"integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "1.28.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/resources": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz",
"integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.30.1",
"@opentelemetry/semantic-conventions": "1.28.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-metrics": {
"version": "1.30.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.30.1.tgz",
"integrity": "sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "1.30.1",
"@opentelemetry/resources": "1.30.1"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/semantic-conventions": {
"version": "1.28.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz",
"integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+3 -1
View File
@@ -11,7 +11,7 @@
"ann"
],
"private": false,
"version": "0.30.1-beta.2",
"version": "0.32.0-beta.0",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
@@ -44,6 +44,7 @@
"@biomejs/biome": "^1.7.3",
"@jest/globals": "^29.7.0",
"@napi-rs/cli": "3.7.0",
"@opentelemetry/sdk-metrics": "^1.30.0",
"@types/axios": "^0.14.0",
"@types/jest": "^29.1.2",
"@types/node": "22.7.4",
@@ -92,6 +93,7 @@
"version": "napi version"
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"reflect-metadata": "^0.2.2"
},
"optionalDependencies": {
+53
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@opentelemetry/api':
specifier: ^1.9.0
version: 1.9.1
apache-arrow:
specifier: '>=15.0.0 <=18.1.0'
version: 18.1.0
@@ -33,6 +36,9 @@ importers:
'@napi-rs/cli':
specifier: 3.7.0
version: 3.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.7.4)
'@opentelemetry/sdk-metrics':
specifier: ^1.30.0
version: 1.30.1(@opentelemetry/api@1.9.1)
'@types/axios':
specifier: ^0.14.0
version: 0.14.4
@@ -1307,6 +1313,32 @@ packages:
'@octokit/types@16.0.0':
resolution: {integrity: sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==}
'@opentelemetry/api@1.9.1':
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
engines: {node: '>=8.0.0'}
'@opentelemetry/core@1.30.1':
resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/resources@1.30.1':
resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/sdk-metrics@1.30.1':
resolution: {integrity: sha512-q9zcZ0Okl8jRgmy7eNW3Ku1XSgg3sDLa5evHZpCwjspw7E8Is4K/haRPDJrBcX3YSn/Y7gUvFnByNYEKQNbNog==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.3.0 <1.10.0'
'@opentelemetry/semantic-conventions@1.28.0':
resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==}
engines: {node: '>=14'}
'@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
@@ -4925,6 +4957,27 @@ snapshots:
dependencies:
'@octokit/openapi-types': 27.0.0
'@opentelemetry/api@1.9.1': {}
'@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/semantic-conventions': 1.28.0
'@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions': 1.28.0
'@opentelemetry/sdk-metrics@1.30.1(@opentelemetry/api@1.9.1)':
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.1)
'@opentelemetry/semantic-conventions@1.28.0': {}
'@protobufjs/aspromise@1.1.2':
optional: true
+6
View File
@@ -112,6 +112,12 @@ impl Connection {
builder = builder.client_config(rust_config);
if let Some(oauth_config) = options.oauth_config {
let config: lancedb::remote::oauth::OAuthConfig =
oauth_config.try_into().default_error()?;
builder = builder.oauth_config(config);
}
if let Some(api_key) = options.api_key {
builder = builder.api_key(&api_key);
}
+6
View File
@@ -12,6 +12,7 @@ mod header;
mod index;
mod iterator;
pub mod merge;
pub mod otel;
pub mod permutation;
mod query;
pub mod remote;
@@ -65,6 +66,11 @@ pub struct ConnectionOptions {
/// (For LanceDB cloud only): the host to use for LanceDB cloud. Used
/// for testing purposes.
pub host_override: Option<String>,
/// (For LanceDB cloud only): OAuth configuration for IdP-based
/// authentication (e.g., Azure Entra ID). When set, token acquisition
/// and refresh are handled entirely in Rust. TypeScript users should pass
/// the public `OAuthConfig` type exported from `@lancedb/lancedb`.
pub oauth_config: Option<remote::OAuthConfig>,
}
#[napi(object)]
+4 -6
View File
@@ -3,7 +3,7 @@
use std::time::Duration;
use lancedb::{arrow::IntoArrow, ipc::ipc_file_to_batches, table::merge::MergeInsertBuilder};
use lancedb::{ipc::ipc_file_to_batches, table::merge::MergeInsertBuilder};
use napi::bindgen_prelude::*;
use napi_derive::napi;
@@ -66,11 +66,9 @@ impl NativeMergeInsertBuilder {
#[napi(catch_unwind)]
pub async fn execute(&self, buf: Buffer) -> napi::Result<MergeResult> {
let data = ipc_file_to_batches(buf.to_vec())
.and_then(IntoArrow::into_arrow)
.map_err(|e| {
napi::Error::from_reason(format!("Failed to read IPC file: {}", convert_error(&e)))
})?;
let data = ipc_file_to_batches(buf.to_vec()).map_err(|e| {
napi::Error::from_reason(format!("Failed to read IPC file: {}", convert_error(&e)))
})?;
let this = self.clone();
+119
View File
@@ -0,0 +1,119 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
//! Node.js bindings over [`lancedb::metrics_otel`].
//!
//! The aggregation, catalog, and histogram bucketing all live in the LanceDB
//! core crate; this module only converts the core snapshot types into napi
//! objects and exposes the three entry points to JavaScript, where
//! `lancedb/otel.ts` bridges them into the user's OpenTelemetry `MeterProvider`.
use std::collections::HashMap;
use lancedb::metrics_otel::{MetricPoint as CoreMetricPoint, MetricValue};
use napi_derive::napi;
/// One cumulative histogram bucket: all samples with value `<= le`.
#[napi(object)]
pub struct MetricBucket {
/// The inclusive upper bound of the bucket, or `"+Inf"` for the final bucket.
pub le: String,
/// Cumulative number of samples less than or equal to `le`.
pub cumulative_count: f64,
}
/// One aggregated metric data point. For counters and gauges only `value` is
/// set; for histograms `buckets` (cumulative `le` counts), `count`, and `sum`
/// are set.
#[napi(object)]
pub struct MetricPoint {
pub name: String,
pub kind: String,
pub attributes: HashMap<String, String>,
pub value: Option<f64>,
pub buckets: Option<Vec<MetricBucket>>,
pub count: Option<f64>,
pub sum: Option<f64>,
}
impl From<CoreMetricPoint> for MetricPoint {
fn from(point: CoreMetricPoint) -> Self {
let kind = point.kind.as_str().to_string();
let (value, buckets, count, sum) = match point.value {
MetricValue::Scalar(v) => (Some(v), None, None, None),
MetricValue::Histogram {
buckets,
count,
sum,
} => (
None,
Some(
buckets
.into_iter()
// Counts stay well within the f64-exact integer range
// (2^53), so this cast is lossless in practice and keeps
// the values plain JS numbers for OpenTelemetry.
.map(|(le, cumulative_count)| MetricBucket {
le,
cumulative_count: cumulative_count as f64,
})
.collect(),
),
Some(count as f64),
Some(sum),
),
};
Self {
name: point.name,
kind,
attributes: point.attributes,
value,
buckets,
count,
sum,
}
}
}
/// A described metric, used by the JavaScript layer to create instruments up front.
#[napi(object)]
pub struct MetricDescription {
pub name: String,
pub kind: String,
pub unit: Option<String>,
pub description: String,
}
/// Install the LanceDB metrics recorder as the process-global `metrics` recorder.
///
/// Returns `true` if the recorder is installed (now or previously). Returns
/// `false` if a *different* recorder is already installed — `metrics` allows
/// only one global recorder per process, so LanceDB cannot coexist with another.
#[napi]
pub fn register_lancedb_metrics_recorder() -> bool {
lancedb::metrics_otel::register_metrics_recorder()
}
/// The catalog of described LanceDB metrics. Empty until the recorder is installed.
#[napi]
pub fn lancedb_metrics_catalog() -> Vec<MetricDescription> {
lancedb::metrics_otel::metrics_catalog()
.into_iter()
.map(|desc| MetricDescription {
name: desc.name,
kind: desc.kind.as_str().to_string(),
unit: desc.unit,
description: desc.description,
})
.collect()
}
/// A point-in-time snapshot of every recorded metric. Empty until the recorder
/// is installed.
#[napi]
pub fn snapshot_lancedb_metrics() -> Vec<MetricPoint> {
lancedb::metrics_otel::snapshot_metrics()
.into_iter()
.map(MetricPoint::from)
.collect()
}
+7 -1
View File
@@ -16,6 +16,7 @@ pub struct SplitRandomOptions {
pub counts: Option<Vec<i64>>,
pub fixed: Option<i64>,
pub seed: Option<i64>,
pub clump_size: Option<i64>,
pub split_names: Option<Vec<String>>,
}
@@ -125,10 +126,15 @@ impl PermutationBuilder {
};
let seed = options.seed.map(|s| s as u64);
let clump_size = options.clump_size.map(|c| c as u64);
self.modify(|builder| {
builder.with_split_strategy(
SplitStrategy::Random { seed, sizes },
SplitStrategy::Random {
seed,
sizes,
clump_size,
},
options.split_names.clone(),
)
})
+121
View File
@@ -3,6 +3,7 @@
use std::collections::HashMap;
use lancedb::error::Error;
use napi_derive::*;
/// Timeout configuration for remote HTTP client.
@@ -140,6 +141,84 @@ impl From<TlsConfig> for lancedb::remote::TlsConfig {
}
}
/// OAuth configuration for LanceDB authentication.
///
/// This is the generated napi-rs binding shape. TypeScript users should prefer
/// the public `OAuthConfig` type exported from `@lancedb/lancedb`.
///
/// All token acquisition and refresh is handled in the Rust layer.
#[napi(object)]
#[derive(Clone)]
pub struct OAuthConfig {
/// OIDC issuer URL or OAuth authority URL.
/// For Azure: `https://login.microsoftonline.com/{tenant_id}/v2.0`
pub issuer_url: String,
/// Application / Client ID.
pub client_id: String,
/// OAuth scopes to request. For Azure managed identity, exactly one scope
/// or resource is required. For example: `["api://{app_id}/.default"]`
pub scopes: Vec<String>,
/// Authentication flow: "client_credentials" or "azure_managed_identity"
pub flow: Option<String>,
/// Client secret (required for client_credentials).
pub client_secret: Option<String>,
/// Client ID for user-assigned managed identity (azure_managed_identity).
pub managed_identity_client_id: Option<String>,
/// Seconds before expiry to trigger proactive refresh (default: 300).
/// Keep this well below the token TTL; if it is greater than or equal to
/// the TTL, each request refreshes the token.
pub refresh_buffer_secs: Option<u32>,
}
impl std::fmt::Debug for OAuthConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OAuthConfig")
.field("issuer_url", &self.issuer_url)
.field("client_id", &self.client_id)
.field("scopes", &self.scopes)
.field("flow", &self.flow)
.field(
"client_secret",
&self.client_secret.as_deref().map(|_| "<redacted>"),
)
.field(
"managed_identity_client_id",
&self.managed_identity_client_id,
)
.field("refresh_buffer_secs", &self.refresh_buffer_secs)
.finish()
}
}
impl TryFrom<OAuthConfig> for lancedb::remote::oauth::OAuthConfig {
type Error = Error;
fn try_from(config: OAuthConfig) -> Result<Self, Self::Error> {
use lancedb::remote::oauth::OAuthFlow;
let flow = match config.flow.as_deref().unwrap_or("client_credentials") {
"client_credentials" => OAuthFlow::ClientCredentials,
"azure_managed_identity" => OAuthFlow::AzureManagedIdentity {
client_id: config.managed_identity_client_id,
},
other => {
return Err(Error::InvalidInput {
message: format!("Unknown OAuth flow type: {other}"),
});
}
};
Ok(Self {
issuer_url: config.issuer_url,
client_id: config.client_id,
client_secret: config.client_secret,
scopes: config.scopes,
flow,
refresh_buffer_secs: config.refresh_buffer_secs.map(|v| v as u64),
})
}
}
impl From<ClientConfig> for lancedb::remote::ClientConfig {
fn from(config: ClientConfig) -> Self {
Self {
@@ -156,3 +235,45 @@ impl From<ClientConfig> for lancedb::remote::ClientConfig {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unknown_oauth_flow_returns_invalid_input() {
let config = OAuthConfig {
issuer_url: "https://issuer.example.com".to_string(),
client_id: "client-id".to_string(),
scopes: vec!["scope".to_string()],
flow: Some("typo".to_string()),
client_secret: None,
managed_identity_client_id: None,
refresh_buffer_secs: None,
};
let err = lancedb::remote::oauth::OAuthConfig::try_from(config).unwrap_err();
assert!(matches!(
err,
Error::InvalidInput { message }
if message == "Unknown OAuth flow type: typo"
));
}
#[test]
fn test_oauth_config_debug_redacts_client_secret() {
let config = OAuthConfig {
issuer_url: "https://issuer.example.com".to_string(),
client_id: "client-id".to_string(),
scopes: vec!["scope".to_string()],
flow: Some("client_credentials".to_string()),
client_secret: Some("super-secret".to_string()),
managed_identity_client_id: None,
refresh_buffer_secs: None,
};
let debug = format!("{config:?}");
assert!(!debug.contains("super-secret"));
assert!(debug.contains("client_secret: Some(\"<redacted>\")"));
}
}
+51
View File
@@ -411,6 +411,16 @@ impl Table {
.default_error()
}
#[napi(catch_unwind)]
pub async fn get_lsm_write_spec(&self) -> napi::Result<Option<LsmWriteSpec>> {
let spec = self
.inner_ref()?
.get_lsm_write_spec()
.await
.default_error()?;
Ok(spec.map(LsmWriteSpec::from))
}
#[napi(catch_unwind)]
pub async fn close_lsm_writers(&self) -> napi::Result<()> {
self.inner_ref()?.close_lsm_writers().await.default_error()
@@ -728,6 +738,47 @@ impl TryFrom<LsmWriteSpec> for lancedb::table::LsmWriteSpec {
}
}
impl From<lancedb::table::LsmWriteSpec> for LsmWriteSpec {
fn from(spec: lancedb::table::LsmWriteSpec) -> Self {
use lancedb::table::LsmWriteSpec as Native;
match spec {
Native::Bucket {
column,
num_buckets,
maintained_indexes,
writer_config_defaults,
} => Self {
spec_type: "bucket".to_string(),
column: Some(column),
num_buckets: Some(num_buckets),
maintained_indexes: Some(maintained_indexes),
writer_config_defaults: Some(writer_config_defaults),
},
Native::Identity {
column,
maintained_indexes,
writer_config_defaults,
} => Self {
spec_type: "identity".to_string(),
column: Some(column),
num_buckets: None,
maintained_indexes: Some(maintained_indexes),
writer_config_defaults: Some(writer_config_defaults),
},
Native::Unsharded {
maintained_indexes,
writer_config_defaults,
} => Self {
spec_type: "unsharded".to_string(),
column: None,
num_buckets: None,
maintained_indexes: Some(maintained_indexes),
writer_config_defaults: Some(writer_config_defaults),
},
}
}
}
/// Statistics about a compaction operation.
#[napi(object)]
#[derive(Clone, Debug)]
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "0.34.0-beta.0"
current_version = "0.35.0-beta.1"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "lancedb-python"
version = "0.34.0-beta.0"
version = "0.35.0-beta.1"
publish = false
edition.workspace = true
description = "Python bindings for LanceDB"
@@ -47,6 +47,6 @@ pyo3-build-config = { version = "0.28", features = [
] }
[features]
default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface"]
default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface", "lancedb/cos", "lancedb/goosefs", "lancedb/metrics-otel"]
fp16kernels = ["lancedb/fp16kernels"]
remote = ["lancedb/remote"]
@@ -0,0 +1,135 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Benchmark for StreamingDataset throughput.
Sweeps read_batch_size from 1 to 16384 to show how amortising the per-request
overhead scales. Each row at each chunk size is timed via the real
StreamingDataset so the numbers reflect production code.
Run with:
cd python
uv run --extra tests benchmarks/bench_streaming_dataloader.py
Optional env vars:
BENCH_NUM_ROWS — total rows in the table (default 49152 = 24 × 2048)
BENCH_NUM_SPLITS — number of splits (default 24)
BENCH_STEPS — round-robin cycles to time per chunk size (default 100)
BENCH_ROW_BYTES — approximate bytes per row padded with a binary column
(default 4096, mimics a small embedding/image patch)
"""
import os
import time
import tempfile
import pyarrow as pa
import lancedb
from lancedb.streaming import StreamingDataset
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
NUM_SPLITS = int(os.environ.get("BENCH_NUM_SPLITS", 24))
# Default: 2048 rows per split so every chunk size up to 16Ki has ≥1 full
# chunk (except 16Ki itself which gets a single full-split fetch — still valid).
NUM_ROWS = int(os.environ.get("BENCH_NUM_ROWS", NUM_SPLITS * 2048))
STEPS = int(os.environ.get("BENCH_STEPS", 100))
ROW_BYTES = int(os.environ.get("BENCH_ROW_BYTES", 4096))
assert NUM_ROWS % NUM_SPLITS == 0, "NUM_ROWS must be divisible by NUM_SPLITS"
CHUNK_SIZES = [1, 4, 16, 64, 256, 1024, 4096, 16384]
# ---------------------------------------------------------------------------
# Table helpers
# ---------------------------------------------------------------------------
def make_table(db_path: str) -> lancedb.table.Table:
db = lancedb.connect(db_path)
payload = b"x" * ROW_BYTES
data = pa.table(
{
"id": pa.array(range(NUM_ROWS), type=pa.int32()),
"payload": pa.array([payload] * NUM_ROWS, type=pa.large_binary()),
}
)
return db.create_table("bench", data, mode="overwrite")
# ---------------------------------------------------------------------------
# Timing
# ---------------------------------------------------------------------------
def bench_chunk(table, chunk_size: int, steps: int) -> tuple[int, float]:
"""Return (rows_drained, elapsed_seconds) for one timed run."""
total_rows = steps * NUM_SPLITS
ds = StreamingDataset(
table, num_splits=NUM_SPLITS, shuffle_seed=42, read_batch_size=chunk_size
)
count = 0
t0 = time.perf_counter()
for _ in ds:
count += 1
if count >= total_rows:
break
return count, time.perf_counter() - t0
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
rows_per_split = NUM_ROWS // NUM_SPLITS
print("Benchmark config:")
print(
f" NUM_ROWS={NUM_ROWS} NUM_SPLITS={NUM_SPLITS} "
f"rows/split={rows_per_split} STEPS={STEPS} ROW_BYTES={ROW_BYTES}"
)
print(f" ~{NUM_ROWS * ROW_BYTES / 1024 / 1024:.1f} MB total table size")
print()
with tempfile.TemporaryDirectory() as tmp:
print("Creating table...", flush=True)
table = make_table(tmp)
cols = (
f"{'chunk':>6} {'rows':>6} {'elapsed':>8} {'rows/s':>10} {'ms/step':>9}"
)
print(f"\n{cols}")
print("-" * 52)
for chunk in CHUNK_SIZES:
# Warm-up pass (one step's worth of rows)
warmup_ds = StreamingDataset(
table, num_splits=NUM_SPLITS, shuffle_seed=42, read_batch_size=chunk
)
warmup_count = 0
for _ in warmup_ds:
warmup_count += 1
if warmup_count >= NUM_SPLITS:
break
drained, elapsed = bench_chunk(table, chunk, STEPS)
rows_per_sec = drained / elapsed if elapsed > 0 else float("inf")
ms_per_step = elapsed / STEPS * 1000
print(
f"{chunk:>6} {drained:>6} {elapsed:>7.3f}s "
f"{rows_per_sec:>10.0f} {ms_per_step:>8.1f}ms"
)
print()
print("Done.")
if __name__ == "__main__":
main()
+5
View File
@@ -47,6 +47,10 @@ repository = "https://github.com/lancedb/lancedb"
pylance = [
"pylance>=5.0.0b5",
]
# A library only needs the OpenTelemetry API; the application supplies and
# configures the SDK (the actual exporter/reader). See
# https://opentelemetry.io/docs/languages/python/instrumentation/
otel = ["opentelemetry-api"]
tests = [
"aiohttp>=3.9.0",
"boto3>=1.28.57",
@@ -61,6 +65,7 @@ tests = [
"pylance>=5.0.0b5",
"requests>=2.31.0",
"datafusion>=52,<53",
"opentelemetry-sdk>=1.30.0",
]
dev = [
"ruff>=0.3.0",
+16 -2
View File
@@ -89,6 +89,8 @@ def connect(
If presented, connect to LanceDB cloud.
Otherwise, connect to a database on file system or cloud storage.
Can be set via environment variable `LANCEDB_API_KEY`.
OAuth configuration is currently supported only by ``connect_async``;
synchronous LanceDB Cloud connections require an API key.
region: str, default "us-east-1"
The region to use for LanceDB Cloud.
host_override: str, optional
@@ -147,8 +149,14 @@ def connect(
For object storage, use a URI prefix:
>>> db = lancedb.connect("s3://my-bucket/lancedb",
... storage_options={"aws_access_key_id": "***"})
>>> db = lancedb.connect( # doctest: +SKIP
... "s3://my-bucket/lancedb",
... storage_options={
... "aws_access_key_id": "***",
... "aws_secret_access_key": "***",
... "aws_region": "us-east-1",
... },
... )
For tests and temporary data, use an in-memory database:
@@ -340,6 +348,7 @@ async def connect_async(
session: Optional[Session] = None,
manifest_enabled: bool = False,
namespace_client_properties: Optional[Dict[str, str]] = None,
oauth_config=None,
) -> AsyncConnection:
"""Connect to a LanceDB database.
@@ -389,6 +398,10 @@ async def connect_async(
namespace_client_properties : dict, optional
Additional directory namespace client properties to use with
``manifest_enabled=True``.
oauth_config : OAuthConfig, optional
OAuth configuration for LanceDB Cloud/Enterprise. This is supported by
``connect_async`` only; synchronous ``connect`` uses API key
authentication for ``db://`` URIs.
Examples
--------
@@ -435,6 +448,7 @@ async def connect_async(
session,
manifest_enabled,
namespace_client_properties,
oauth_config,
)
)
+43 -2
View File
@@ -1,4 +1,5 @@
from datetime import datetime, timedelta
from datetime import date, datetime, timedelta
from decimal import Decimal
from typing import Dict, List, Optional, Tuple, Any, TypedDict, Union, Literal
import pyarrow as pa
@@ -29,6 +30,25 @@ IvfHnswPq: type[HnswPq] = HnswPq
IvfHnswSq: type[HnswSq] = HnswSq
IvfHnswFlat: type[HnswFlat] = HnswFlat
class MetricPoint:
name: str
kind: str
attributes: Dict[str, str]
value: Optional[float]
buckets: Optional[List[Tuple[str, int]]]
count: Optional[int]
sum: Optional[float]
class MetricDescription:
name: str
kind: str
unit: Optional[str]
description: str
def register_lancedb_metrics_recorder() -> bool: ...
def lancedb_metrics_catalog() -> List[MetricDescription]: ...
def snapshot_lancedb_metrics() -> List[MetricPoint]: ...
class PyExpr:
"""A type-safe DataFusion expression node (Rust-side handle)."""
@@ -53,7 +73,9 @@ class PyExpr:
def to_sql(self) -> str: ...
def expr_col(name: str) -> PyExpr: ...
def expr_lit(value: Union[bool, int, float, str, bytes]) -> PyExpr: ...
def expr_lit(
value: Union[bool, int, float, str, bytes, date, datetime, Decimal],
) -> PyExpr: ...
def expr_func(name: str, args: List[PyExpr]) -> PyExpr: ...
class Session:
@@ -226,6 +248,7 @@ class Table:
async def set_unenforced_primary_key(self, columns: List[str]) -> None: ...
async def set_lsm_write_spec(self, spec: LsmWriteSpec) -> None: ...
async def unset_lsm_write_spec(self) -> None: ...
async def get_lsm_write_spec(self) -> Optional[LsmWriteSpec]: ...
async def close_lsm_writers(self) -> None: ...
@property
def tags(self) -> Tags: ...
@@ -280,6 +303,24 @@ async def connect(
session: Optional[Session],
manifest_enabled: bool = False,
namespace_client_properties: Optional[Dict[str, str]] = None,
oauth_config: Optional[Any] = None,
) -> Connection: ...
def connect_namespace(
namespace_client_impl: str,
namespace_client_properties: Dict[str, str],
read_consistency_interval: Optional[float] = None,
storage_options: Optional[Dict[str, str]] = None,
session: Optional[Session] = None,
namespace_client_pushdown_operations: Optional[List[str]] = None,
) -> Connection: ...
def connect_namespace_client(
namespace_client: Any,
read_consistency_interval: Optional[float] = None,
storage_options: Optional[Dict[str, str]] = None,
session: Optional[Session] = None,
namespace_client_pushdown_operations: Optional[List[str]] = None,
namespace_client_impl: Optional[str] = None,
namespace_client_properties: Optional[Dict[str, str]] = None,
) -> Connection: ...
class RecordBatchStream:
@@ -81,6 +81,7 @@ class ColPaliEmbeddings(EmbeddingFunction):
warnings.warn(
"use_token_pooling is deprecated, use pooling_strategy=None instead",
DeprecationWarning,
stacklevel=2,
)
self.pooling_strategy = None
+11 -1
View File
@@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
from functools import cached_property
from typing import TYPE_CHECKING, List, Optional, Sequence, Union
from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Union
import numpy as np
@@ -56,6 +56,16 @@ class OllamaEmbeddings(TextEmbeddingFunction):
embeddings = self._compute_embedding(texts)
return list(embeddings)
def __getstate__(self) -> dict[str, Any]:
state = super().__getstate__()
state["__dict__"] = {
k: v for k, v in state["__dict__"].items() if k != "_ollama_client"
}
return state
def __setstate__(self, state: dict[str, Any]) -> None:
super().__setstate__(state)
@cached_property
def _ollama_client(self) -> "ollama.Client":
ollama = attempt_import_or_raise("ollama")
+17 -3
View File
@@ -19,6 +19,8 @@ operators::
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
from typing import Iterable, Union
import pyarrow as pa
@@ -63,7 +65,7 @@ def _coerce(value: "ExprLike") -> "Expr":
# Type alias used in annotations.
ExprLike = Union["Expr", bool, int, float, str, bytes]
ExprLike = Union["Expr", bool, int, float, str, bytes, date, datetime, Decimal]
class Expr:
@@ -118,10 +120,18 @@ class Expr:
"""Logical AND (``expr_a & expr_b``)."""
return Expr(self._inner.and_(_coerce(other)._inner))
def __rand__(self, other: ExprLike) -> "Expr":
"""Right-hand logical AND (``True & expr``)."""
return Expr(_coerce(other)._inner.and_(self._inner))
def __or__(self, other: "Expr") -> "Expr":
"""Logical OR (``expr_a | expr_b``)."""
return Expr(self._inner.or_(_coerce(other)._inner))
def __ror__(self, other: ExprLike) -> "Expr":
"""Right-hand logical OR (``False | expr``)."""
return Expr(_coerce(other)._inner.or_(self._inner))
def __invert__(self) -> "Expr":
"""Logical NOT (``~expr``)."""
return Expr(self._inner.not_())
@@ -266,13 +276,14 @@ def col(name: str) -> Expr:
return Expr(expr_col(name))
def lit(value: Union[bool, int, float, str, bytes]) -> Expr:
def lit(value: Union[bool, int, float, str, bytes, date, datetime, Decimal]) -> Expr:
"""Create a literal (constant) value expression.
Parameters
----------
value:
A Python ``bool``, ``int``, ``float``, ``str``, or ``bytes``.
A Python ``bool``, ``int``, ``float``, ``str``, ``bytes``, ``date``,
``datetime``, or ``Decimal``.
Examples
--------
@@ -280,6 +291,9 @@ def lit(value: Union[bool, int, float, str, bytes]) -> Expr:
>>> col("price") * lit(1.1)
Expr((price * 1.1))
"""
if not isinstance(value, (bool, int, float, str, bytes, date, datetime, Decimal)):
raise TypeError(f"Unsupported literal type: {type(value).__name__}")
return Expr(expr_lit(value))
+9
View File
@@ -51,6 +51,15 @@ class LanceMergeInsertBuilder(object):
If there are multiple matches then the behavior is undefined.
Currently this causes multiple copies of the row to be created
but that behavior is subject to change.
Parameters
----------
where: Optional[str], default None
An optional filter to limit which rows are updated. Column
references in this expression must be prefixed with "target."
to refer to the existing table data. For example, to only
update rows where the existing color is red, use:
``where="target.color = 'red'"``
"""
self._when_matched_update_all = True
self._when_matched_update_all_condition = where
+263 -170
View File
@@ -38,15 +38,13 @@ from lance_namespace_urllib3_client.models.query_table_request_vector import (
QueryTableRequestVector,
)
from lance_namespace_urllib3_client.models.string_fts_query import StringFtsQuery
from lance_namespace.errors import TableNotFoundError
from lancedb._lancedb import connect_namespace_client as _connect_namespace_client
from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError
from lancedb._lancedb import (
connect_namespace as _connect_namespace,
connect_namespace_client as _connect_namespace_client,
)
from lancedb.background_loop import LOOP
from lancedb.db import AsyncConnection, DBConnection
from lancedb.namespace_utils import (
_normalize_create_namespace_mode,
_normalize_drop_namespace_mode,
_normalize_drop_namespace_behavior,
)
from lance_namespace import (
LanceNamespace,
connect as namespace_connect,
@@ -55,13 +53,6 @@ from lance_namespace import (
DropNamespaceResponse,
ListNamespacesResponse,
ListTablesResponse,
ListTablesRequest,
DescribeNamespaceRequest,
DropTableRequest,
RenameTableRequest,
ListNamespacesRequest,
CreateNamespaceRequest,
DropNamespaceRequest,
)
from lancedb.table import AsyncTable, LanceTable, Table
from lancedb.util import validate_table_name
@@ -71,6 +62,9 @@ from lancedb.embeddings import EmbeddingFunctionConfig
from ._lancedb import Session
_MAX_QUERY_K = 2**31 - 1
def _query_to_namespace_request(
table_id: List[str],
query: "Query",
@@ -148,7 +142,8 @@ def _query_to_namespace_request(
if query.limit is not None:
k = query.limit
elif query.vector is None and query.full_text_query is None:
k = sys.maxsize
# limit k to max i32 value to avoid client overflows
k = _MAX_QUERY_K
else:
k = 10
@@ -369,6 +364,23 @@ def _convert_pyarrow_schema_to_json(schema: pa.Schema) -> JsonArrowSchema:
return JsonArrowSchema(fields=fields, metadata=meta)
def _builds_namespace_natively(
namespace_client_impl: Optional[str],
namespace_client_properties: Optional[Dict[str, str]],
) -> bool:
"""Whether ``connect_namespace_client`` builds the namespace client natively
in Rust (installing the read-freshness context provider) rather than wrapping
the pre-built Python client.
Must mirror Rust ``build_namespace_natively`` in ``python/src/connection.rs``.
"""
return namespace_client_impl == "rest" and bool(namespace_client_properties)
def _supports_native_namespace(namespace_client_impl: str) -> bool:
return namespace_client_impl in {"dir", "rest"}
class LanceNamespaceDBConnection(DBConnection):
"""
A LanceDB connection that uses a namespace for table management.
@@ -379,7 +391,7 @@ class LanceNamespaceDBConnection(DBConnection):
def __init__(
self,
namespace_client: LanceNamespace,
namespace_client: Optional[LanceNamespace] = None,
*,
read_consistency_interval: Optional[timedelta] = None,
storage_options: Optional[Dict[str, str]] = None,
@@ -387,6 +399,7 @@ class LanceNamespaceDBConnection(DBConnection):
namespace_client_pushdown_operations: Optional[List[str]] = None,
namespace_client_impl: Optional[str] = None,
namespace_client_properties: Optional[Dict[str, str]] = None,
_inner: Optional[AsyncConnection] = None,
):
"""
Initialize a namespace-based LanceDB connection.
@@ -428,23 +441,36 @@ class LanceNamespaceDBConnection(DBConnection):
)
self._namespace_client_impl = namespace_client_impl
self._namespace_client_properties = namespace_client_properties
self._inner = AsyncConnection(
_connect_namespace_client(
namespace_client,
read_consistency_interval=(
read_consistency_interval.total_seconds()
if read_consistency_interval is not None
else None
),
storage_options=self.storage_options or None,
session=session,
namespace_client_pushdown_operations=(
list(self._namespace_client_pushdown_operations)
),
namespace_client_impl=namespace_client_impl,
namespace_client_properties=namespace_client_properties,
)
# When the namespace connection or client is built natively in Rust, the
# underlying Rust table performs QueryTable pushdown through the
# read-freshness context provider, which the pure-Python ``query_table``
# path bypasses.
self._route_pushdown_to_rust = _inner is not None or _builds_namespace_natively(
namespace_client_impl, namespace_client_properties
)
if _inner is not None:
self._inner = _inner
else:
if namespace_client is None:
raise ValueError("namespace_client is required without a native _inner")
self._inner = AsyncConnection(
_connect_namespace_client(
namespace_client,
read_consistency_interval=(
read_consistency_interval.total_seconds()
if read_consistency_interval is not None
else None
),
storage_options=self.storage_options or None,
session=session,
namespace_client_pushdown_operations=(
list(self._namespace_client_pushdown_operations)
),
namespace_client_impl=namespace_client_impl,
namespace_client_properties=namespace_client_properties,
)
)
self._uri = self._inner.uri
@override
def serialize(self) -> str:
@@ -490,11 +516,11 @@ class LanceNamespaceDBConnection(DBConnection):
)
if namespace_path is None:
namespace_path = []
request = ListTablesRequest(
id=namespace_path, page_token=page_token, limit=limit
return LOOP.run(
self._inner.table_names(
namespace_path=namespace_path, start_after=page_token, limit=limit
)
)
response = self._namespace_client.list_tables(request)
return response.tables if response.tables else []
@override
def create_table(
@@ -539,6 +565,7 @@ class LanceNamespaceDBConnection(DBConnection):
namespace_path=namespace_path,
namespace_client=self._namespace_client,
pushdown_operations=self._namespace_client_pushdown_operations,
route_pushdown_to_rust=self._route_pushdown_to_rust,
_async=async_table,
)
@@ -564,8 +591,8 @@ class LanceNamespaceDBConnection(DBConnection):
index_cache_size=index_cache_size,
)
)
except RuntimeError as e:
if "Table not found" in str(e):
except (RuntimeError, ValueError) as e:
if "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)}")
raise
@@ -576,6 +603,7 @@ class LanceNamespaceDBConnection(DBConnection):
namespace_path=namespace_path,
namespace_client=self._namespace_client,
pushdown_operations=self._namespace_client_pushdown_operations,
route_pushdown_to_rust=self._route_pushdown_to_rust,
_async=async_table,
)
if branch is not None:
@@ -586,12 +614,9 @@ class LanceNamespaceDBConnection(DBConnection):
@override
def drop_table(self, name: str, namespace_path: Optional[List[str]] = None):
# Use namespace drop_table directly
if namespace_path is None:
namespace_path = []
table_id = namespace_path + [name]
request = DropTableRequest(id=table_id)
self._namespace_client.drop_table(request)
LOOP.run(self._inner.drop_table(name, namespace_path=namespace_path))
@override
def rename_table(
@@ -605,14 +630,19 @@ class LanceNamespaceDBConnection(DBConnection):
cur_namespace_path = []
if new_namespace_path is None:
new_namespace_path = []
cur_table_id = cur_namespace_path + [cur_name]
new_namespace_id = new_namespace_path if new_namespace_path else None
request = RenameTableRequest(
id=cur_table_id,
new_table_name=new_name,
new_namespace_id=new_namespace_id,
)
self._namespace_client.rename_table(request)
try:
LOOP.run(
self._inner.rename_table(
cur_name,
new_name,
cur_namespace_path=cur_namespace_path,
new_namespace_path=new_namespace_path,
)
)
except RuntimeError as e:
if "rename_table not implemented" in str(e):
raise NotImplementedError("rename_table not implemented") from e
raise
@override
def drop_database(self):
@@ -624,8 +654,7 @@ class LanceNamespaceDBConnection(DBConnection):
def drop_all_tables(self, namespace_path: Optional[List[str]] = None):
if namespace_path is None:
namespace_path = []
for table_name in self.table_names(namespace_path=namespace_path):
self.drop_table(table_name, namespace_path=namespace_path)
LOOP.run(self._inner.drop_all_tables(namespace_path=namespace_path))
@override
def list_namespaces(
@@ -655,13 +684,10 @@ class LanceNamespaceDBConnection(DBConnection):
"""
if namespace_path is None:
namespace_path = []
request = ListNamespacesRequest(
id=namespace_path, page_token=page_token, limit=limit
)
response = self._namespace_client.list_namespaces(request)
return ListNamespacesResponse(
namespaces=response.namespaces if response.namespaces else [],
page_token=response.page_token,
return LOOP.run(
self._inner.list_namespaces(
namespace_path=namespace_path, page_token=page_token, limit=limit
)
)
@override
@@ -689,14 +715,12 @@ class LanceNamespaceDBConnection(DBConnection):
CreateNamespaceResponse
Response containing the properties of the created namespace.
"""
request = CreateNamespaceRequest(
id=namespace_path,
mode=_normalize_create_namespace_mode(mode),
properties=properties,
)
response = self._namespace_client.create_namespace(request)
return CreateNamespaceResponse(
properties=response.properties if hasattr(response, "properties") else None
return LOOP.run(
self._inner.create_namespace(
namespace_path=namespace_path,
mode=mode,
properties=properties,
)
)
@override
@@ -724,20 +748,18 @@ class LanceNamespaceDBConnection(DBConnection):
DropNamespaceResponse
Response containing properties and transaction_id if applicable.
"""
request = DropNamespaceRequest(
id=namespace_path,
mode=_normalize_drop_namespace_mode(mode),
behavior=_normalize_drop_namespace_behavior(behavior),
)
response = self._namespace_client.drop_namespace(request)
return DropNamespaceResponse(
properties=(
response.properties if hasattr(response, "properties") else None
),
transaction_id=(
response.transaction_id if hasattr(response, "transaction_id") else None
),
)
try:
return LOOP.run(
self._inner.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(
@@ -756,11 +778,7 @@ class LanceNamespaceDBConnection(DBConnection):
DescribeNamespaceResponse
Response containing the namespace properties.
"""
request = DescribeNamespaceRequest(id=namespace_path)
response = self._namespace_client.describe_namespace(request)
return DescribeNamespaceResponse(
properties=response.properties if hasattr(response, "properties") else None
)
return LOOP.run(self._inner.describe_namespace(namespace_path))
@override
def list_tables(
@@ -790,13 +808,10 @@ class LanceNamespaceDBConnection(DBConnection):
"""
if namespace_path is None:
namespace_path = []
request = ListTablesRequest(
id=namespace_path, page_token=page_token, limit=limit
)
response = self._namespace_client.list_tables(request)
return ListTablesResponse(
tables=response.tables if response.tables else [],
page_token=response.page_token,
return LOOP.run(
self._inner.list_tables(
namespace_path=namespace_path, page_token=page_token, limit=limit
)
)
def _lance_table_from_uri(
@@ -852,6 +867,18 @@ class LanceNamespaceDBConnection(DBConnection):
LanceNamespace
The namespace client for this connection.
"""
if self._namespace_client is None:
if (
self._namespace_client_impl is None
or self._namespace_client_properties is None
):
raise ValueError(
"Cannot construct a Python namespace client without "
"namespace implementation properties"
)
self._namespace_client = namespace_connect(
self._namespace_client_impl, self._namespace_client_properties
)
return self._namespace_client
@@ -865,12 +892,15 @@ class AsyncLanceNamespaceDBConnection:
def __init__(
self,
namespace_client: LanceNamespace,
namespace_client: Optional[LanceNamespace] = None,
*,
read_consistency_interval: Optional[timedelta] = None,
storage_options: Optional[Dict[str, str]] = None,
session: Optional[Session] = None,
namespace_client_pushdown_operations: Optional[List[str]] = None,
namespace_client_impl: Optional[str] = None,
namespace_client_properties: Optional[Dict[str, str]] = None,
_inner: Optional[AsyncConnection] = None,
):
"""
Initialize an async namespace-based LanceDB connection.
@@ -896,6 +926,12 @@ class AsyncLanceNamespaceDBConnection:
namespace.create_table() instead of using declare_table + local write.
Default is None (no pushdown, all operations run locally).
namespace_client_impl : Optional[str]
The namespace implementation name used to create this connection.
Required (with ``namespace_client_properties``) for the Rust client to
be built natively and install the read-freshness provider.
namespace_client_properties : Optional[Dict[str, str]]
The namespace properties used to create this connection.
"""
self._namespace_client = namespace_client
self.read_consistency_interval = read_consistency_interval
@@ -904,23 +940,37 @@ class AsyncLanceNamespaceDBConnection:
self._namespace_client_pushdown_operations = set(
namespace_client_pushdown_operations or []
)
self._inner = AsyncConnection(
_connect_namespace_client(
namespace_client,
read_consistency_interval=(
read_consistency_interval.total_seconds()
if read_consistency_interval is not None
else None
),
storage_options=self.storage_options or None,
session=session,
namespace_client_pushdown_operations=(
list(self._namespace_client_pushdown_operations)
),
namespace_client_impl=None,
namespace_client_properties=None,
)
self._namespace_client_impl = namespace_client_impl
self._namespace_client_properties = namespace_client_properties
# See LanceNamespaceDBConnection: when Rust owns the namespace
# connection/client, its table performs QueryTable pushdown through the
# read-freshness provider, so defer to it rather than the urllib3 client
# path (which omits x-lancedb-min-timestamp).
self._route_pushdown_to_rust = _inner is not None or _builds_namespace_natively(
namespace_client_impl, namespace_client_properties
)
if _inner is not None:
self._inner = _inner
else:
if namespace_client is None:
raise ValueError("namespace_client is required without a native _inner")
self._inner = AsyncConnection(
_connect_namespace_client(
namespace_client,
read_consistency_interval=(
read_consistency_interval.total_seconds()
if read_consistency_interval is not None
else None
),
storage_options=self.storage_options or None,
session=session,
namespace_client_pushdown_operations=(
list(self._namespace_client_pushdown_operations)
),
namespace_client_impl=namespace_client_impl,
namespace_client_properties=namespace_client_properties,
)
)
async def table_names(
self,
@@ -944,11 +994,9 @@ class AsyncLanceNamespaceDBConnection:
)
if namespace_path is None:
namespace_path = []
request = ListTablesRequest(
id=namespace_path, page_token=page_token, limit=limit
return await self._inner.table_names(
namespace_path=namespace_path, start_after=page_token, limit=limit
)
response = self._namespace_client.list_tables(request)
return response.tables if response.tables else []
async def create_table(
self,
@@ -988,6 +1036,7 @@ class AsyncLanceNamespaceDBConnection:
namespace_path=namespace_path,
namespace_client=self._namespace_client,
pushdown_operations=self._namespace_client_pushdown_operations,
route_pushdown_to_rust=self._route_pushdown_to_rust,
)
async def open_table(
@@ -1010,8 +1059,8 @@ class AsyncLanceNamespaceDBConnection:
storage_options=storage_options,
index_cache_size=index_cache_size,
)
except RuntimeError as e:
if "Table not found" in str(e):
except (RuntimeError, ValueError) as e:
if "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)}")
raise
@@ -1025,15 +1074,14 @@ class AsyncLanceNamespaceDBConnection:
namespace_path=namespace_path,
namespace_client=self._namespace_client,
pushdown_operations=self._namespace_client_pushdown_operations,
route_pushdown_to_rust=self._route_pushdown_to_rust,
)
async def drop_table(self, name: str, namespace_path: Optional[List[str]] = None):
"""Drop a table from the namespace."""
if namespace_path is None:
namespace_path = []
table_id = namespace_path + [name]
request = DropTableRequest(id=table_id)
self._namespace_client.drop_table(request)
await self._inner.drop_table(name, namespace_path=namespace_path)
async def rename_table(
self,
@@ -1047,14 +1095,17 @@ class AsyncLanceNamespaceDBConnection:
cur_namespace_path = []
if new_namespace_path is None:
new_namespace_path = []
cur_table_id = cur_namespace_path + [cur_name]
new_namespace_id = new_namespace_path if new_namespace_path else None
request = RenameTableRequest(
id=cur_table_id,
new_table_name=new_name,
new_namespace_id=new_namespace_id,
)
self._namespace_client.rename_table(request)
try:
await self._inner.rename_table(
cur_name,
new_name,
cur_namespace_path=cur_namespace_path,
new_namespace_path=new_namespace_path,
)
except RuntimeError as e:
if "rename_table not implemented" in str(e):
raise NotImplementedError("rename_table not implemented") from e
raise
async def drop_database(self):
"""Deprecated method."""
@@ -1066,9 +1117,7 @@ class AsyncLanceNamespaceDBConnection:
"""Drop all tables in the namespace."""
if namespace_path is None:
namespace_path = []
table_names = await self.table_names(namespace_path=namespace_path)
for table_name in table_names:
await self.drop_table(table_name, namespace_path=namespace_path)
await self._inner.drop_all_tables(namespace_path=namespace_path)
async def list_namespaces(
self,
@@ -1097,13 +1146,8 @@ class AsyncLanceNamespaceDBConnection:
"""
if namespace_path is None:
namespace_path = []
request = ListNamespacesRequest(
id=namespace_path, page_token=page_token, limit=limit
)
response = self._namespace_client.list_namespaces(request)
return ListNamespacesResponse(
namespaces=response.namespaces if response.namespaces else [],
page_token=response.page_token,
return await self._inner.list_namespaces(
namespace_path=namespace_path, page_token=page_token, limit=limit
)
async def create_namespace(
@@ -1130,15 +1174,11 @@ class AsyncLanceNamespaceDBConnection:
CreateNamespaceResponse
Response containing the properties of the created namespace.
"""
request = CreateNamespaceRequest(
id=namespace_path,
mode=_normalize_create_namespace_mode(mode),
return await self._inner.create_namespace(
namespace_path=namespace_path,
mode=mode,
properties=properties,
)
response = self._namespace_client.create_namespace(request)
return CreateNamespaceResponse(
properties=response.properties if hasattr(response, "properties") else None
)
async def drop_namespace(
self,
@@ -1164,20 +1204,16 @@ class AsyncLanceNamespaceDBConnection:
DropNamespaceResponse
Response containing properties and transaction_id if applicable.
"""
request = DropNamespaceRequest(
id=namespace_path,
mode=_normalize_drop_namespace_mode(mode),
behavior=_normalize_drop_namespace_behavior(behavior),
)
response = self._namespace_client.drop_namespace(request)
return DropNamespaceResponse(
properties=(
response.properties if hasattr(response, "properties") else None
),
transaction_id=(
response.transaction_id if hasattr(response, "transaction_id") else None
),
)
try:
return await self._inner.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
async def describe_namespace(
self, namespace_path: List[str]
@@ -1195,11 +1231,7 @@ class AsyncLanceNamespaceDBConnection:
DescribeNamespaceResponse
Response containing the namespace properties.
"""
request = DescribeNamespaceRequest(id=namespace_path)
response = self._namespace_client.describe_namespace(request)
return DescribeNamespaceResponse(
properties=response.properties if hasattr(response, "properties") else None
)
return await self._inner.describe_namespace(namespace_path)
async def list_tables(
self,
@@ -1228,13 +1260,8 @@ class AsyncLanceNamespaceDBConnection:
"""
if namespace_path is None:
namespace_path = []
request = ListTablesRequest(
id=namespace_path, page_token=page_token, limit=limit
)
response = self._namespace_client.list_tables(request)
return ListTablesResponse(
tables=response.tables if response.tables else [],
page_token=response.page_token,
return await self._inner.list_tables(
namespace_path=namespace_path, page_token=page_token, limit=limit
)
async def namespace_client(self) -> LanceNamespace:
@@ -1248,6 +1275,18 @@ class AsyncLanceNamespaceDBConnection:
LanceNamespace
The namespace client for this connection.
"""
if self._namespace_client is None:
if (
self._namespace_client_impl is None
or self._namespace_client_properties is None
):
raise ValueError(
"Cannot construct a Python namespace client without "
"namespace implementation properties"
)
self._namespace_client = namespace_connect(
self._namespace_client_impl, self._namespace_client_properties
)
return self._namespace_client
@@ -1298,6 +1337,32 @@ def connect_namespace(
LanceNamespaceDBConnection
A namespace-based connection to LanceDB
"""
if _supports_native_namespace(namespace_client_impl):
inner = AsyncConnection(
_connect_namespace(
namespace_client_impl,
namespace_client_properties,
read_consistency_interval=(
read_consistency_interval.total_seconds()
if read_consistency_interval is not None
else None
),
storage_options=storage_options,
session=session,
namespace_client_pushdown_operations=namespace_client_pushdown_operations,
)
)
return LanceNamespaceDBConnection(
namespace_client=None,
read_consistency_interval=read_consistency_interval,
storage_options=storage_options,
session=session,
namespace_client_pushdown_operations=namespace_client_pushdown_operations,
namespace_client_impl=namespace_client_impl,
namespace_client_properties=namespace_client_properties,
_inner=inner,
)
namespace_client = namespace_connect(
namespace_client_impl, namespace_client_properties
)
@@ -1373,6 +1438,32 @@ def connect_namespace_async(
... tables = await db.table_names()
... table = await db.create_table("my_table", schema=schema)
"""
if _supports_native_namespace(namespace_client_impl):
inner = AsyncConnection(
_connect_namespace(
namespace_client_impl,
namespace_client_properties,
read_consistency_interval=(
read_consistency_interval.total_seconds()
if read_consistency_interval is not None
else None
),
storage_options=storage_options,
session=session,
namespace_client_pushdown_operations=namespace_client_pushdown_operations,
)
)
return AsyncLanceNamespaceDBConnection(
namespace_client=None,
read_consistency_interval=read_consistency_interval,
storage_options=storage_options,
session=session,
namespace_client_pushdown_operations=namespace_client_pushdown_operations,
namespace_client_impl=namespace_client_impl,
namespace_client_properties=namespace_client_properties,
_inner=inner,
)
namespace_client = namespace_connect(
namespace_client_impl, namespace_client_properties
)
@@ -1383,4 +1474,6 @@ def connect_namespace_async(
storage_options=storage_options,
session=session,
namespace_client_pushdown_operations=namespace_client_pushdown_operations,
namespace_client_impl=namespace_client_impl,
namespace_client_properties=namespace_client_properties,
)
+170
View File
@@ -0,0 +1,170 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Bridge LanceDB's internal metrics into OpenTelemetry.
LanceDB (through Lance core) publishes metrics (currently object store request
counts, bytes, latency, errors, and throttles) through the Rust ``metrics``
facade. This module installs a process-global recorder that aggregates them and
registers OpenTelemetry observable instruments that report the aggregated values
into the user's ``MeterProvider``.
The bridge is generic: every metric LanceDB describes is surfaced automatically,
with no per-metric Python code. Histograms have no asynchronous OpenTelemetry
instrument, so each is exported Prometheus-style as cumulative ``le`` buckets
plus ``_count`` and ``_sum`` observable counters.
"""
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Optional
from ._lancedb import (
lancedb_metrics_catalog,
register_lancedb_metrics_recorder,
snapshot_lancedb_metrics,
)
if TYPE_CHECKING:
from opentelemetry.metrics import MeterProvider
_INSTRUMENTED = False
def instrument_lancedb_metrics(
meter_provider: Optional["MeterProvider"] = None,
) -> bool:
"""Register LanceDB metrics as OpenTelemetry observable instruments.
Installs a process-global metrics recorder and creates one observable
instrument per LanceDB metric on the given (or global) ``MeterProvider``. The
user's configured ``MetricReader`` then collects them on its own schedule.
Counters and gauges map directly to observable counters/gauges. Each
histogram is exported as cumulative ``le`` bucket counts (``<name>_bucket``,
with an ``le`` attribute) plus ``<name>_count`` and ``<name>_sum``.
Parameters
----------
meter_provider : opentelemetry.metrics.MeterProvider, optional
The provider to register instruments on. Defaults to the global provider
from ``opentelemetry.metrics.get_meter_provider()``.
Returns
-------
bool
``True`` if the recorder is installed and instruments are registered.
``False`` if a different ``metrics`` recorder is already installed in
this process (``metrics`` permits only one global recorder), in which
case a warning is emitted and no instruments are created.
Notes
-----
Requires the OpenTelemetry API (``pip install lancedb[otel]``) and, to
actually export, an OpenTelemetry SDK (``pip install opentelemetry-sdk``)
configured by the application. Calling this more than once is safe;
instruments are created only on the first successful call.
"""
global _INSTRUMENTED
try:
from opentelemetry.metrics import Observation, get_meter_provider
except ImportError as exc:
raise ImportError(
"instrument_lancedb_metrics requires the OpenTelemetry API/SDK. "
"Install it with `pip install lancedb[otel]` or "
"`pip install opentelemetry-sdk`."
) from exc
if not register_lancedb_metrics_recorder():
warnings.warn(
"Could not install the LanceDB metrics recorder: another `metrics` "
"recorder is already installed in this process. LanceDB metrics will "
"not be exported via OpenTelemetry.",
stacklevel=2,
)
return False
if _INSTRUMENTED:
return True
provider = meter_provider or get_meter_provider()
meter = provider.get_meter("lancedb")
def scalar_callback(metric_name: str):
def callback(_options):
return [
Observation(point.value, point.attributes)
for point in snapshot_lancedb_metrics()
if point.name == metric_name and point.value is not None
]
return callback
def bucket_callback(metric_name: str):
def callback(_options):
observations = []
for point in snapshot_lancedb_metrics():
if point.name != metric_name or point.buckets is None:
continue
for le, cumulative in point.buckets:
attributes = dict(point.attributes)
attributes["le"] = le
observations.append(Observation(cumulative, attributes))
return observations
return callback
def field_callback(metric_name: str, field: str):
def callback(_options):
observations = []
for point in snapshot_lancedb_metrics():
if point.name != metric_name:
continue
value = getattr(point, field)
if value is not None:
observations.append(Observation(value, point.attributes))
return observations
return callback
for desc in lancedb_metrics_catalog():
unit = desc.unit or ""
if desc.kind == "counter":
meter.create_observable_counter(
desc.name,
callbacks=[scalar_callback(desc.name)],
unit=unit,
description=desc.description,
)
elif desc.kind == "gauge":
meter.create_observable_gauge(
desc.name,
callbacks=[scalar_callback(desc.name)],
unit=unit,
description=desc.description,
)
elif desc.kind == "histogram":
# `_bucket` and `_count` observe cumulative sample counts, not the
# histogram's measured quantity, so they are unitless; only `_sum`
# carries the histogram's unit.
meter.create_observable_counter(
f"{desc.name}_bucket",
callbacks=[bucket_callback(desc.name)],
description=f"{desc.description} (cumulative buckets)",
)
meter.create_observable_counter(
f"{desc.name}_count",
callbacks=[field_callback(desc.name, "count")],
description=f"{desc.description} (count)",
)
meter.create_observable_counter(
f"{desc.name}_sum",
callbacks=[field_callback(desc.name, "sum")],
unit=unit,
description=f"{desc.description} (sum)",
)
_INSTRUMENTED = True
return True
+24 -4
View File
@@ -11,7 +11,7 @@ import pyarrow as pa
from ._lancedb import async_permutation_builder, PermutationReader
from .table import LanceTable, Table
from .background_loop import LOOP
from .util import batch_to_tensor, batch_to_tensor_rows
from .util import batch_to_tensor, batch_to_tensor_dict, batch_to_tensor_rows
from typing import Any, Callable, Iterator, Literal, Optional, TYPE_CHECKING, Union
if TYPE_CHECKING:
@@ -48,6 +48,14 @@ class PermutationBuilder:
By default, the permutation builder will create a single split that contains all
rows in the same order as the base table.
"""
if not hasattr(table, "_inner"):
raise TypeError(
f"PermutationBuilder requires a local LanceTable, "
f"got {type(table).__name__}. "
"The permutation API is not supported on remote tables. "
"Remote tables connect to LanceDB Cloud or Enterprise and do not have "
"direct access to the underlying Lance dataset needed for permutations."
)
self._async = async_permutation_builder(table)
def split_random(
@@ -57,6 +65,7 @@ class PermutationBuilder:
counts: Optional[list[int]] = None,
fixed: Optional[int] = None,
seed: Optional[int] = None,
clump_size: Optional[int] = None,
split_names: Optional[list[str]] = None,
) -> "PermutationBuilder":
"""
@@ -79,6 +88,9 @@ class PermutationBuilder:
Rows will be randomly assigned to splits. The optional seed can be provided to
make the assignment deterministic.
If clump_size is provided, rows are shuffled as contiguous groups of that size,
preserving I/O locality while still randomising the split assignment.
The optional split_names can be provided to name the splits. If not provided,
the splits can only be referenced by their index.
"""
@@ -87,6 +99,7 @@ class PermutationBuilder:
counts=counts,
fixed=fixed,
seed=seed,
clump_size=clump_size,
split_names=split_names,
)
return self
@@ -933,6 +946,7 @@ class Permutation:
"pandas",
"arrow",
"torch",
"torch_row",
"torch_col",
"polars",
],
@@ -948,15 +962,19 @@ class Permutation:
- "python_col" - the batch will be a dict of lists (one entry per column)
- "pandas" - the batch will be a pandas DataFrame
- "arrow" - the batch will be a pyarrow RecordBatch
- "torch" - the batch will be a list of tensors, one per row
- "torch" - the batch will be a list of per-row dicts mapping column
name to a 0-D torch tensor. Works with the default
``torch.utils.data.DataLoader`` collate, which stacks the per-row
dicts back into a dict of batched tensors.
- "torch_row" - the batch will be a list of tensors, one per row
- "torch_col" - the batch will be a 2D torch tensor (first dim indexes columns)
- "polars" - the batch will be a polars DataFrame
Conversion may or may not involve a data copy. Lance uses Arrow internally
and so it is able to zero-copy to the arrow and polars formats.
Conversion to torch_col will be zero-copy but will only support a subset of data
types (numeric types).
Conversion to torch and torch_col will be zero-copy but will only support a
subset of data types (numeric types).
Conversion to numpy and/or pandas will typically be zero-copy for numeric
types. Conversion of strings, lists, and structs will require creating python
@@ -977,6 +995,8 @@ class Permutation:
elif format == "arrow":
return self.with_transform(Transforms.arrow2arrow)
elif format == "torch":
return self.with_transform(batch_to_tensor_dict)
elif format == "torch_row":
return self.with_transform(batch_to_tensor_rows)
elif format == "torch_col":
return self.with_transform(batch_to_tensor)
+36 -2
View File
@@ -119,6 +119,27 @@ def _filter_to_sql(filter: Optional[Union[str, Expr]]) -> Optional[str]:
return filter
def _combine_where(
existing: Optional[Union[str, Expr]], new: Union[str, Expr]
) -> Union[str, Expr]:
"""Combine a new filter with an existing one using a logical AND.
Calling ``where`` more than once composes the filters with AND instead of
replacing the previous filter. Two :class:`~lancedb.expr.Expr` filters are
combined as an expression; otherwise both filters are lowered to SQL strings
and combined as SQL.
"""
if existing is None:
return new
existing_is_expr = isinstance(existing, Expr)
new_is_expr = isinstance(new, Expr)
if existing_is_expr and new_is_expr:
return existing & new
existing_sql = existing.to_sql() if existing_is_expr else existing
new_sql = new.to_sql() if new_is_expr else new
return f"({existing_sql}) AND ({new_sql})"
def _projection_to_scanner_kwargs(
columns: Optional[
Union[
@@ -1148,8 +1169,13 @@ class LanceQueryBuilder(ABC):
-------
LanceQueryBuilder
The LanceQueryBuilder object.
Notes
-----
Calling this multiple times combines the filters with a logical AND
rather than replacing the previous filter.
"""
self._where = where
self._where = _combine_where(self._where, where)
self._postfilter = not prefilter
return self
@@ -1693,8 +1719,13 @@ class LanceVectorQueryBuilder(LanceQueryBuilder):
-------
LanceQueryBuilder
The LanceQueryBuilder object.
Notes
-----
Calling this multiple times combines the filters with a logical AND
rather than replacing the previous filter.
"""
self._where = where
self._where = _combine_where(self._where, where)
if prefilter is not None:
self._postfilter = not prefilter
return self
@@ -2894,6 +2925,9 @@ class AsyncStandardQuery(AsyncQueryBase):
Filtering performance can often be improved by creating a scalar index
on the filter column(s).
Calling this multiple times combines the filters with a logical AND
rather than replacing the previous filter.
"""
if isinstance(predicate, Expr):
self._inner.where_expr(predicate._inner)
+3
View File
@@ -9,6 +9,7 @@ from typing import List, Optional
from lancedb import __version__
from .header import HeaderProvider
from .oauth import OAuthConfig, OAuthFlowType
__all__ = [
"TimeoutConfig",
@@ -16,6 +17,8 @@ __all__ = [
"TlsConfig",
"ClientConfig",
"HeaderProvider",
"OAuthConfig",
"OAuthFlowType",
]
+3
View File
@@ -124,6 +124,7 @@ class RemoteDBConnection(DBConnection):
"request_thread_pool is no longer used and will be removed in "
"a future release.",
DeprecationWarning,
stacklevel=2,
)
if connection_timeout is not None:
@@ -132,6 +133,7 @@ class RemoteDBConnection(DBConnection):
"release. Please use client_config.timeout_config.connect_timeout "
"instead.",
DeprecationWarning,
stacklevel=2,
)
client_config.timeout_config.connect_timeout = timedelta(
seconds=connection_timeout
@@ -142,6 +144,7 @@ class RemoteDBConnection(DBConnection):
"read_timeout is deprecated and will be removed in a future release. "
"Please use client_config.timeout_config.read_timeout instead.",
DeprecationWarning,
stacklevel=2,
)
client_config.timeout_config.read_timeout = timedelta(seconds=read_timeout)
+75
View File
@@ -0,0 +1,75 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
from dataclasses import dataclass, field
from enum import Enum
from typing import List, Optional
class OAuthFlowType(str, Enum):
"""OAuth authentication flow types."""
CLIENT_CREDENTIALS = "client_credentials"
"""Client Credentials grant (service-to-service / M2M)."""
AZURE_MANAGED_IDENTITY = "azure_managed_identity"
"""Azure Managed Identity via IMDS."""
@dataclass
class OAuthConfig:
"""OAuth configuration for LanceDB authentication.
All token acquisition and refresh is handled in the Rust layer.
This config is passed through to Rust via PyO3.
Parameters
----------
issuer_url : str
OIDC issuer URL or OAuth authority URL.
For Azure: ``https://login.microsoftonline.com/{tenant_id}/v2.0``
client_id : str
Application / Client ID.
scopes : List[str]
OAuth scopes to request.
For Azure managed identity, exactly one scope or resource is required.
For example: ``["api://{app_id}/.default"]``
flow : OAuthFlowType
Authentication flow to use. Default: CLIENT_CREDENTIALS.
client_secret : Optional[str]
Client secret (required for CLIENT_CREDENTIALS).
managed_identity_client_id : Optional[str]
Client ID for user-assigned managed identity (AZURE_MANAGED_IDENTITY).
refresh_buffer_secs : Optional[int]
Seconds before expiry to trigger proactive refresh (default: 300).
Keep this well below the token TTL; if it is greater than or equal to
the TTL, each request refreshes the token.
Examples
--------
Client Credentials (service-to-service):
>>> config = OAuthConfig(
... issuer_url="https://login.microsoftonline.com/{tenant}/v2.0",
... client_id="app-id",
... client_secret="secret",
... scopes=["api://lancedb-api/.default"],
... )
Azure Managed Identity:
>>> config = OAuthConfig(
... issuer_url="https://login.microsoftonline.com/{tenant}/v2.0",
... client_id="app-id",
... scopes=["api://lancedb-api/.default"],
... flow=OAuthFlowType.AZURE_MANAGED_IDENTITY,
... )
"""
issuer_url: str
client_id: str
scopes: List[str]
flow: OAuthFlowType = OAuthFlowType.CLIENT_CREDENTIALS
client_secret: Optional[str] = field(default=None, repr=False)
managed_identity_client_id: Optional[str] = None
refresh_buffer_secs: Optional[int] = None
+10 -3
View File
@@ -845,7 +845,8 @@ class RemoteTable(Table):
"""
warnings.warn(
"cleanup_old_versions() is a no-op on LanceDB Cloud. "
"Tables are automatically cleaned up and optimized."
"Tables are automatically cleaned up and optimized.",
stacklevel=2,
)
pass
@@ -857,7 +858,8 @@ class RemoteTable(Table):
"""
warnings.warn(
"compact_files() is a no-op on LanceDB Cloud. "
"Tables are automatically compacted and optimized."
"Tables are automatically compacted and optimized.",
stacklevel=2,
)
pass
@@ -874,7 +876,8 @@ class RemoteTable(Table):
"""
warnings.warn(
"optimize() is a no-op on LanceDB Cloud. "
"Indices are optimized automatically."
"Indices are optimized automatically.",
stacklevel=2,
)
pass
@@ -909,6 +912,10 @@ class RemoteTable(Table):
"""Not supported on LanceDB Cloud."""
return LOOP.run(self._table.unset_lsm_write_spec())
def get_lsm_write_spec(self) -> Optional["LsmWriteSpec"]:
"""Read the installed LsmWriteSpec, or ``None``."""
return LOOP.run(self._table.get_lsm_write_spec())
def close_lsm_writers(self) -> None:
"""No-op on LanceDB Cloud (no local shard writers)."""
return LOOP.run(self._table.close_lsm_writers())
+8 -1
View File
@@ -156,9 +156,16 @@ class MRRReranker(Reranker):
reciprocal_rank = 1.0 / rank
mrr_score_map[result_id].append(reciprocal_rank)
# MRR averages the reciprocal rank across *all* ranking systems, treating
# a system in which a document does not appear as a reciprocal rank of 0.
# We therefore divide by the total number of systems, not by the number of
# systems the document happens to appear in -- otherwise a document found
# by a single ranking would outrank one ranked highly by every system,
# defeating the purpose of fusing the rankings.
num_systems = len(vector_results)
final_mrr_scores = {}
for result_id, reciprocal_ranks in mrr_score_map.items():
mean_rr = np.mean(reciprocal_ranks)
mean_rr = float(np.sum(reciprocal_ranks)) / num_systems
final_mrr_scores[result_id] = mean_rr
combined = pa.concat_tables(vector_results, **self._concat_tables_args)
+607
View File
@@ -0,0 +1,607 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Elastic streaming dataloader for PyTorch.
Provides StreamingDataset, a PyTorch IterableDataset that guarantees:
- **Elastic determinism**: for a fixed (num_splits, shuffle_seed, epoch) the set
of samples that forms each global training step is identical regardless of
world_size or num_workers.
- **Resumability**: state_dict / load_state_dict capture per-split consumption
counts so training can resume from an exact mid-epoch position even when the
distributed topology changes between runs.
"""
import ctypes
import logging
import os
import random
import threading
import time
from collections import deque
from concurrent.futures import ThreadPoolExecutor
from multiprocessing import RawArray
from typing import Any, Callable, Iterator, Optional
from torch.utils.data import IterableDataset, get_worker_info
from .permutation import (
Permutation,
Transforms,
permutation_builder,
_table_from_pickle_state,
_table_to_pickle_state,
)
logger = logging.getLogger(__name__)
# Multiplier used to combine shuffle_seed and epoch into a single permutation
# seed. Chosen to be a large prime so different (seed, epoch) pairs produce
# distinct seeds for any practically encountered epoch count.
_EPOCH_PRIME = 100003
DEFAULT_READ_BATCH_SIZE = 64
DEFAULT_PREFETCH_BATCHES = 4
class StreamingDataset(IterableDataset):
"""An elastic, resumable PyTorch IterableDataset backed by a LanceDB table.
The table is partitioned into ``num_splits`` fixed splits using a
deterministic random shuffle controlled by ``shuffle_seed`` and ``epoch``.
Each rank is assigned a contiguous block of splits, and within a rank each
DataLoader worker is assigned a contiguous sub-block. Samples are yielded
by round-robining over the assigned splits, one sample per split per cycle.
Internally ``__iter__`` runs a two-stage pipeline:
- **Stage 1 (I/O)**: one thread pool with ``num_splits * prefetch_batches``
workers fetches raw ``RecordBatch`` objects from LanceDB in parallel
across all splits and places them in a per-split raw-batch queue.
- **Stage 2 (transform)**: a second thread pool with ``os.cpu_count()``
workers picks up raw batches, applies the transform, and places the
results in a per-split cooked-row queue.
The main thread round-robins over the cooked queues, yielding one row per
split per cycle.
Parameters
----------
table:
LanceDB table to stream from.
num_splits:
Number of fixed splits to partition the table into. Must be divisible
by ``world_size``. When used with DataLoader workers it must also be
divisible by ``world_size * num_workers``. Defaults to ``world_size``.
If the row count (after any ``filter``) is not evenly divisible by
``num_splits``, the surplus rows at most ``num_splits - 1`` per epoch
are silently dropped to keep all splits the same length.
shuffle:
Whether to randomly assign rows to splits. When ``True`` (the
default) rows are shuffled using ``shuffle_seed`` and ``epoch``.
When ``False`` rows are divided into splits sequentially in storage
order, which can be useful for deterministic debugging or evaluation.
shuffle_seed:
Base seed for the random permutation. Combined with ``epoch`` so
each epoch produces a different ordering. Pass ``None`` to generate
a random seed at construction time.
epoch:
Current training epoch. Combined with ``shuffle_seed`` so that each
epoch produces a different sample ordering.
rank:
This process's rank in the distributed training group.
world_size:
Total number of processes in the distributed training group.
read_batch_size:
Number of rows fetched from each split in a single ``take_offsets``
call. Larger values amortise per-request overhead (critical on object
storage) at the cost of higher memory usage per split buffer. Defaults
to ``DEFAULT_READ_BATCH_SIZE`` (64).
prefetch_batches:
Number of I/O batches to keep in flight per split. Higher values
overlap storage latency with transform and training compute at the cost
of more memory and threads. Defaults to ``DEFAULT_PREFETCH_BATCHES``
(4).
columns:
Optional list of column names to read. When set, only those columns
are fetched from storage; all others are omitted. ``None`` (the
default) reads every column.
shuffle_clump_size:
When set, rows are shuffled in contiguous groups of this size rather
than individually. Larger clumps improve I/O locality (important on
object storage) at the cost of reduced randomness. ``None`` (the
default) shuffles rows individually.
filter:
Optional SQL filter expression (e.g. ``"label = 'dog'"``). Only rows
that satisfy the predicate are included in the permutation. The filter
is applied during permutation construction so split sizes reflect the
filtered row count.
transform:
Optional callable applied to each ``pyarrow.RecordBatch`` before rows
are yielded. Receives one batch at a time and must return an iterable
whose length equals the number of rows in the batch. When ``None``
(the default) rows are returned as plain Python dicts.
worker_info_override:
If set, used in place of ``torch.utils.data.get_worker_info()`` to
determine the DataLoader worker assignment. Intended for unit tests
that need to simulate multiple workers without spawning real processes.
If both this and the real worker info are non-None a warning is logged
and the override takes precedence.
"""
def __init__(
self,
table,
*,
num_splits: Optional[int] = None,
shuffle: bool = True,
shuffle_seed: Optional[int] = 0,
epoch: int = 0,
rank: int = 0,
world_size: int = 1,
read_batch_size: int = DEFAULT_READ_BATCH_SIZE,
prefetch_batches: int = DEFAULT_PREFETCH_BATCHES,
columns: Optional[list[str]] = None,
shuffle_clump_size: Optional[int] = None,
filter: Optional[str] = None,
transform: Optional[Callable] = None,
connection_factory: Optional[Callable[[str], Any]] = None,
worker_info_override=None,
):
super().__init__()
if num_splits is None:
num_splits = world_size
if shuffle_seed is None:
shuffle_seed = random.randrange(2**32)
if num_splits % world_size != 0:
raise ValueError(
f"num_splits ({num_splits}) must be divisible by "
f"world_size ({world_size})"
)
self._table = table
self._num_splits = num_splits
self._shuffle = shuffle
self._shuffle_seed = shuffle_seed
self._epoch = epoch
self._rank = rank
self._world_size = world_size
self._read_batch_size = read_batch_size
self._prefetch_batches = prefetch_batches
self._columns = columns
self._shuffle_clump_size = shuffle_clump_size
self._filter = filter
self._transform = transform
self._connection_factory = connection_factory
self._worker_info_override = worker_info_override
# Live references to pipeline state, set only while __iter__ is running
# in the same process. Used by the observability properties when the
# DataLoader runs with num_workers=0.
self._raw_batches_ref: Optional[list[deque]] = None
self._cooked_ref: Optional[list[deque]] = None
self._fetch_head_ref: Optional[list[int]] = None
self._split_sizes_ref: Optional[list[int]] = None
self._local_consumed_ref: Optional[list[int]] = None
# Shared-memory counters written by __iter__ (which may run in a
# DataLoader worker process) and read by the observability properties
# in the main process. RawArray is picklable via the forkserver
# reduction protocol so it survives the dataset pickle round-trip.
# Layout: [unscanned_rows, raw_rows, cooked_rows, consumed_rows,
# bytes_loaded, fetch_time_us, transform_time_us]
self._worker_stats: RawArray = RawArray(ctypes.c_int64, 7)
# Cumulative bytes of Arrow buffer data fetched across all iterations.
self._bytes_loaded: int = 0
# Cumulative seconds spent in LanceDB I/O and in transform functions.
self._fetch_time: float = 0.0
self._transform_time: float = 0.0
# Number of samples each split has already been consumed. At global
# step boundaries all splits have consumed this many samples, so a
# single scalar captures the topology-independent checkpoint state.
self._resume_offset: int = 0
# Build the permutation table once, deterministically.
builder = permutation_builder(table)
if filter is not None:
builder = builder.filter(filter)
if shuffle:
perm_seed = shuffle_seed + epoch * _EPOCH_PRIME
self._perm_table = builder.split_random(
fixed=num_splits, seed=perm_seed, clump_size=shuffle_clump_size
).execute()
else:
self._perm_table = builder.split_sequential(fixed=num_splits).execute()
# Contiguous block of global split indices assigned to this rank.
splits_per_rank = num_splits // world_size
rank_start = rank * splits_per_rank
self._rank_splits: list[int] = list(
range(rank_start, rank_start + splits_per_rank)
)
def _resolve_my_splits(self) -> list[int]:
"""Return the split indices this instance should read in __iter__."""
torch_worker_info = get_worker_info()
if self._worker_info_override is not None:
if torch_worker_info is not None:
logger.warning(
"worker_info_override is set but get_worker_info() also returned a "
"non-None value; ignoring the real torch worker info and using the "
"override instead. This may lead to duplicated or incorrect data "
"from the dataset."
)
worker_info = self._worker_info_override
else:
worker_info = torch_worker_info
if worker_info is None:
return self._rank_splits
num_workers: int = worker_info.num_workers
worker_id: int = worker_info.id
n_rank_splits = len(self._rank_splits)
if n_rank_splits % num_workers != 0:
raise ValueError(
f"Number of rank splits ({n_rank_splits}) must be divisible by "
f"num_workers ({num_workers})"
)
splits_per_worker = n_rank_splits // num_workers
start = worker_id * splits_per_worker
return self._rank_splits[start : start + splits_per_worker]
def __iter__(self) -> Iterator[dict[str, Any]]:
if self._raw_batches_ref is not None:
raise RuntimeError(
"StreamingDataset does not support concurrent iteration. "
"Only one active iterator per dataset instance is allowed."
)
my_splits = self._resolve_my_splits()
if not my_splits:
return
# Set identity transform on each Permutation so __getitems__ returns
# the raw RecordBatch. Stage 2 applies the real transform.
permutations: list[Permutation] = []
for split_idx in my_splits:
perm = Permutation.from_tables(
self._table, self._perm_table, split=split_idx
)
if self._columns is not None:
perm = perm.select_columns(self._columns)
perm = perm.with_transform(lambda batch: batch)
if self._resume_offset > 0:
perm = perm.with_skip(self._resume_offset)
permutations.append(perm)
n = len(permutations)
split_sizes = [perm.num_rows for perm in permutations]
initial_offset = self._resume_offset
local_consumed = [0] * n
batch_size = self._read_batch_size
max_prefetch = self._prefetch_batches
cpu_workers = os.cpu_count() or 1
final_transform = (
self._transform if self._transform is not None else Transforms.arrow2python
)
# Per-split pipeline state.
fetch_head = [0] * n
io_pending = [deque() for _ in range(n)] # Future[RecordBatch]
raw_batches = [deque() for _ in range(n)] # RecordBatch — fetched, awaiting tx
tx_pending = [deque() for _ in range(n)] # Future[list[Any]]
cooked = [deque() for _ in range(n)] # rows ready to yield
# Limit simultaneous transforms to cpu_workers across all splits.
tx_semaphore = threading.Semaphore(cpu_workers)
# ── Stage 1 helpers ───────────────────────────────────────────────────
def _io_call(perm, indices):
t0 = time.perf_counter()
batch = perm.__getitems__(indices)
self._bytes_loaded += batch.nbytes
self._fetch_time += time.perf_counter() - t0
return batch
def _submit_io(i: int) -> None:
remaining = split_sizes[i] - fetch_head[i]
if remaining <= 0:
return
fetch = min(batch_size, remaining)
start = fetch_head[i]
fetch_head[i] += fetch
perm_i = permutations[i]
indices = list(range(start, start + fetch))
io_pending[i].append(io_pool.submit(_io_call, perm_i, indices))
def _fill_io(i: int) -> None:
while len(io_pending[i]) < max_prefetch and fetch_head[i] < split_sizes[i]:
_submit_io(i)
def _drain_io(i: int) -> None:
"""Move completed I/O futures into raw_batches non-blockingly."""
while io_pending[i] and io_pending[i][0].done():
raw_batches[i].append(io_pending[i].popleft().result())
# ── Stage 2 helpers ───────────────────────────────────────────────────
def _tx_call_guarded(batch):
try:
t0 = time.perf_counter()
result = final_transform(batch)
self._transform_time += time.perf_counter() - t0
return result
finally:
tx_semaphore.release()
def _try_submit_tx(i: int) -> None:
"""Submit transforms for raw_batches[i] up to available capacity."""
while raw_batches[i] and tx_semaphore.acquire(blocking=False):
batch = raw_batches[i].popleft()
tx_pending[i].append(tx_pool.submit(_tx_call_guarded, batch))
def _drain_tx(i: int) -> None:
"""Move completed transform futures into cooked non-blockingly."""
while tx_pending[i] and tx_pending[i][0].done():
cooked[i].extend(tx_pending[i].popleft().result())
# ── Combined advance ──────────────────────────────────────────────────
def _advance(i: int) -> None:
"""Non-blocking pipeline pump for split i."""
_drain_io(i)
_drain_tx(i)
_try_submit_tx(i)
_fill_io(i)
def _ensure_cooked(i: int) -> None:
"""Ensure cooked[i] has at least one row, blocking if necessary."""
_advance(i)
while not cooked[i]:
if tx_pending[i]:
# Wait for the oldest in-flight transform.
cooked[i].extend(tx_pending[i].popleft().result())
_advance(i)
elif raw_batches[i]:
# Acquire a transform slot (may block briefly if all
# cpu_workers are busy with other splits).
tx_semaphore.acquire()
batch = raw_batches[i].popleft()
tx_pending[i].append(tx_pool.submit(_tx_call_guarded, batch))
elif io_pending[i]:
# Block on the oldest in-flight I/O fetch.
raw_batches[i].append(io_pending[i].popleft().result())
_advance(i)
else:
break # split exhausted
# ── Main loop ─────────────────────────────────────────────────────────
with ThreadPoolExecutor(max_workers=n * max_prefetch) as io_pool:
with ThreadPoolExecutor(max_workers=cpu_workers) as tx_pool:
self._raw_batches_ref = raw_batches
self._cooked_ref = cooked
self._fetch_head_ref = fetch_head
self._split_sizes_ref = split_sizes
self._local_consumed_ref = local_consumed
try:
for i in range(n):
_fill_io(i)
while True:
# Stop when any split is exhausted (all exhaust
# simultaneously: equal split sizes + round-robin).
if any(local_consumed[i] >= split_sizes[i] for i in range(n)):
break
for i in range(n):
_ensure_cooked(i)
row = cooked[i].popleft()
local_consumed[i] += 1
_advance(i)
# After the last split in each cycle: update the
# global offset and refresh the shared-memory stats
# so the main process can observe pipeline depth
# even when __iter__ runs in a worker process.
if i == n - 1:
self._resume_offset = initial_offset + local_consumed[i]
ws = self._worker_stats
ws[0] = sum(
split_sizes[j] - fetch_head[j] for j in range(n)
)
ws[1] = sum(
batch.num_rows for q in raw_batches for batch in q
)
ws[2] = sum(len(q) for q in cooked)
ws[3] = sum(local_consumed)
ws[4] = self._bytes_loaded
ws[5] = int(self._fetch_time * 1_000_000)
ws[6] = int(self._transform_time * 1_000_000)
yield row
finally:
self._raw_batches_ref = None
self._cooked_ref = None
self._fetch_head_ref = None
self._split_sizes_ref = None
self._local_consumed_ref = None
@property
def bytes_loaded(self) -> int:
"""Cumulative bytes of raw Arrow buffer data fetched from storage.
Measured on the ``RecordBatch`` before any transform is applied, so
the value reflects actual I/O rather than the size of transformed
output. Accumulates across multiple iterations of the same dataset
instance and is never reset automatically.
"""
if self._raw_batches_ref is not None:
return self._bytes_loaded
return int(self._worker_stats[4])
@property
def fetch_time(self) -> float:
"""Cumulative seconds spent waiting for data from LanceDB.
Measured per batch in the Stage 1 I/O threads as the total elapsed
time of the ``take_offsets`` call. Accumulates across all splits and
all iterations.
"""
if self._raw_batches_ref is not None:
return self._fetch_time
return self._worker_stats[5] / 1_000_000
@property
def transform_time(self) -> float:
"""Cumulative seconds spent applying the transform.
Measured per batch in the Stage 2 transform threads as the elapsed
time inside the transform callable (or the default ``arrow2python``
conversion when no transform is set). Accumulates across all splits
and all iterations.
"""
if self._raw_batches_ref is not None:
return self._transform_time
return self._worker_stats[6] / 1_000_000
@property
def raw_queue_depth(self) -> int:
"""Number of raw rows waiting for a transform thread across all splits.
A persistently non-zero value means Stage 2 (transform) is the
bottleneck: I/O is completing faster than transforms can consume
batches. Returns 0 when not iterating.
"""
if self._raw_batches_ref is not None:
return sum(batch.num_rows for q in self._raw_batches_ref for batch in q)
return int(self._worker_stats[1])
@property
def prefetch_queue_depth(self) -> int:
"""Number of rows transformed and ready to yield across all splits.
Counts rows whose transform has completed and are sitting in memory
waiting for the main thread rows that can be handed off with no
I/O or CPU wait. Returns 0 when not iterating.
"""
if self._cooked_ref is not None:
return sum(len(q) for q in self._cooked_ref)
return int(self._worker_stats[2])
@property
def unscanned_rows(self) -> int:
"""Number of rows not yet submitted to the I/O stage across all splits.
Decreases as the I/O stage submits fetch requests. When this reaches
zero all data has been requested from storage (though it may not have
arrived yet). Returns 0 when not iterating.
"""
if self._fetch_head_ref is not None:
return sum(
size - head
for size, head in zip(self._split_sizes_ref, self._fetch_head_ref)
)
return int(self._worker_stats[0])
@property
def consumed_rows(self) -> int:
"""Number of rows already yielded to the caller across all splits.
Monotonically increases throughout iteration. Returns 0 when not
iterating.
"""
if self._local_consumed_ref is not None:
return sum(self._local_consumed_ref)
return int(self._worker_stats[3])
def __getstate__(self):
"""Support pickling for multi-worker DataLoader (forkserver / spawn).
The live LanceDB table object contains non-picklable connection state
(sockets, Rust-backed PyO3 objects). If a ``connection_factory`` was
supplied only the table name is serialised; the factory is called in
the worker to reopen the connection without embedding any credentials.
Without a factory the table's own picklable reopen state is captured
via ``_table_to_pickle_state`` (mirrors the ``Permutation`` approach).
"""
state = self.__dict__.copy()
# _table: replace with reconnect info (credentials must not be embedded).
state["_table_name"] = self._table.name
if self._connection_factory is not None:
state["_table"] = None
else:
state["_table"] = _table_to_pickle_state(self._table)
# _perm_table: always in-memory; serialise as Arrow data (mirrors
# how Permutation.__getstate__ handles its permutation_table).
state["_perm_table"] = (
self._perm_table.name,
self._perm_table.to_arrow(),
)
for key in (
"_raw_batches_ref",
"_cooked_ref",
"_fetch_head_ref",
"_split_sizes_ref",
"_local_consumed_ref",
):
state[key] = None
return state
def __setstate__(self, state):
"""Reconnect to LanceDB after unpickling in a worker process."""
from . import connect as _connect
table_name = state.pop("_table_name")
table_state = state.pop("_table")
perm_name, perm_data = state.pop("_perm_table")
self.__dict__.update(state)
if self._connection_factory is not None:
self._table = self._connection_factory(table_name)
else:
self._table = _table_from_pickle_state(table_state)
self._perm_table = _connect("memory://").create_table(perm_name, perm_data)
def state_dict(self) -> dict:
"""Snapshot the dataset's consumption state.
The returned dict is topology-independent: at global step boundaries
every split has been consumed the same number of times (by the
round-robin design), so the per-split count is a single uniform value
that is identical across all ranks and DataLoader workers.
"""
return {
"shuffle_seed": self._shuffle_seed,
"num_splits": self._num_splits,
"epoch": self._epoch,
"samples_consumed_per_split": [self._resume_offset] * self._num_splits,
}
def load_state_dict(self, state: dict) -> None:
"""Resume from a previously snapshotted state.
Raises ``ValueError`` if ``num_splits`` or ``shuffle_seed`` differ
from the checkpoint, since a different split structure or shuffle order
makes mid-epoch resumption meaningless.
"""
if state["num_splits"] != self._num_splits:
raise ValueError(
f"num_splits mismatch: checkpoint has {state['num_splits']}, "
f"current dataset has {self._num_splits}"
)
if state["shuffle_seed"] != self._shuffle_seed:
raise ValueError(
f"shuffle_seed mismatch: checkpoint has {state['shuffle_seed']}, "
f"current dataset has {self._shuffle_seed}"
)
consumed = state["samples_consumed_per_split"]
# All entries are equal at step boundaries; use the first.
if isinstance(consumed, list):
self._resume_offset = consumed[0] if consumed else 0
else:
self._resume_offset = int(consumed)
+84 -11
View File
@@ -651,6 +651,16 @@ def _append_vector_columns(
col_data = func.compute_source_embeddings_with_retry(
batch[conf.source_column]
)
# Replace vectors with wrong length (including empty lists
# returned for inputs like empty strings) with None so that
# _handle_bad_vectors can process them according to the
# on_bad_vectors policy instead of crashing when PyArrow
# tries to cast them into a fixed-size list array.
expected_ndims = conf.function.ndims()
col_data = [
v if v is not None and len(v) == expected_ndims else None
for v in col_data
]
if no_vector_column:
batch = batch.append_column(
schema.field(vector_column),
@@ -2022,6 +2032,7 @@ class LanceTable(Table):
namespace_client: Optional[Any] = None,
managed_versioning: Optional[bool] = None,
pushdown_operations: Optional[set] = None,
route_pushdown_to_rust: bool = False,
_async: AsyncTable = None,
):
if namespace_path is None:
@@ -2031,6 +2042,14 @@ class LanceTable(Table):
self._location = location # Store location for use in _dataset_path
self._namespace_client = namespace_client
self._pushdown_operations = pushdown_operations or set()
# When the connection built the namespace client natively (e.g. an
# enterprise "rest" connection), the underlying Rust table already
# executes QueryTable pushdown itself -- and, unlike this Python urllib3
# path, it routes through the read-freshness context provider that emits
# the ``x-lancedb-min-timestamp`` header. So we must defer pushdown to
# Rust instead of calling the Python ``namespace_client.query_table``
# directly, or reads silently bypass read-freshness (stale results).
self._route_pushdown_to_rust = route_pushdown_to_rust
if _async is not None:
self._table = _async
else:
@@ -2133,12 +2152,19 @@ class LanceTable(Table):
branch = self.current_branch()
version = None if branch is not None else self.version
if self._namespace_client is not None:
namespace_client = self._namespace_client
if namespace_client is None:
conn_uri = getattr(self._conn, "uri", "")
if get_uri_scheme(conn_uri) == "namespace":
namespace_client = self._conn.namespace_client()
self._namespace_client = namespace_client
if namespace_client is not None:
table_id = self._namespace_path + [self.name]
ds = lance.dataset(
version=version,
storage_options=self._conn.storage_options,
namespace_client=self._namespace_client,
namespace_client=namespace_client,
table_id=table_id,
**kwargs,
)
@@ -2241,6 +2267,7 @@ class LanceTable(Table):
namespace_path=self._namespace_path,
namespace_client=self._namespace_client,
pushdown_operations=self._pushdown_operations,
route_pushdown_to_rust=self._route_pushdown_to_rust,
location=self._location,
_async=async_table,
)
@@ -2391,8 +2418,11 @@ class LanceTable(Table):
Returns
-------
pa.Table"""
if _should_push_down_query_table(
self._namespace_client, self._pushdown_operations
if (
_should_push_down_query_table(
self._namespace_client, self._pushdown_operations
)
and not self._route_pushdown_to_rust
):
return self._execute_query(Query()).read_all()
@@ -3344,6 +3374,7 @@ class LanceTable(Table):
location: Optional[str] = None,
namespace_client: Optional[Any] = None,
pushdown_operations: Optional[set] = None,
route_pushdown_to_rust: bool = False,
):
"""
Create a new table.
@@ -3406,21 +3437,24 @@ class LanceTable(Table):
self._location = location
self._namespace_client = namespace_client
self._pushdown_operations = pushdown_operations or set()
self._route_pushdown_to_rust = route_pushdown_to_rust
if data_storage_version is not None:
warnings.warn(
"setting data_storage_version directly on create_table is deprecated. ",
"setting data_storage_version directly on create_table is deprecated. "
"Use database_options instead.",
DeprecationWarning,
stacklevel=2,
)
if storage_options is None:
storage_options = {}
storage_options["new_table_data_storage_version"] = data_storage_version
if enable_v2_manifest_paths is not None:
warnings.warn(
"setting enable_v2_manifest_paths directly on create_table is ",
"setting enable_v2_manifest_paths directly on create_table is "
"deprecated. Use database_options instead.",
DeprecationWarning,
stacklevel=2,
)
if storage_options is None:
storage_options = {}
@@ -3517,6 +3551,7 @@ class LanceTable(Table):
_should_push_down_query_table(
self._namespace_client, self._pushdown_operations
)
and not self._route_pushdown_to_rust
and self.current_branch() is None
):
from lancedb.namespace import _execute_server_side_query
@@ -3724,6 +3759,11 @@ class LanceTable(Table):
[`AsyncTable.unset_lsm_write_spec`][lancedb.AsyncTable.unset_lsm_write_spec]."""
return LOOP.run(self._table.unset_lsm_write_spec())
def get_lsm_write_spec(self) -> Optional["LsmWriteSpec"]:
"""Read the installed LsmWriteSpec, or ``None``. See
[`AsyncTable.get_lsm_write_spec`][lancedb.AsyncTable.get_lsm_write_spec]."""
return LOOP.run(self._table.get_lsm_write_spec())
def close_lsm_writers(self) -> None:
"""Close cached MemWAL shard writers. See
[`AsyncTable.close_lsm_writers`][lancedb.AsyncTable.close_lsm_writers]."""
@@ -3995,7 +4035,16 @@ def _handle_bad_vector_column(
dim = _infer_vector_dim(vec_arr)
if dim is None:
return data
has_wrong_dim = pc.not_equal(pc.list_value_length(vec_arr), dim)
is_null = pc.is_null(vec_arr)
# pc.list_value_length returns null for null list entries, so
# pc.not_equal(null, dim) also returns null. Use or_kleene so that
# True OR null = True (Kleene three-valued logic), ensuring null vectors
# are counted as wrong-dim.
has_wrong_dim = pc.or_kleene(
is_null,
pc.not_equal(pc.list_value_length(vec_arr), dim),
)
has_bad_vectors = pc.any(has_nan).as_py() or pc.any(has_wrong_dim).as_py()
@@ -4258,6 +4307,7 @@ class AsyncTable:
namespace_path: Optional[List[str]] = None,
namespace_client: Optional[Any] = None,
pushdown_operations: Optional[set] = None,
route_pushdown_to_rust: bool = False,
):
"""Create a new AsyncTable object.
@@ -4270,6 +4320,9 @@ class AsyncTable:
self._namespace_path = namespace_path or []
self._namespace_client = namespace_client
self._pushdown_operations = pushdown_operations or set()
# See LanceTable.__init__: defer QueryTable pushdown to Rust (which emits
# the read-freshness header) for natively-built namespace clients.
self._route_pushdown_to_rust = route_pushdown_to_rust
def _set_namespace_context(
self,
@@ -4277,10 +4330,12 @@ class AsyncTable:
namespace_path: Optional[List[str]] = None,
namespace_client: Optional[Any] = None,
pushdown_operations: Optional[set] = None,
route_pushdown_to_rust: bool = False,
) -> "AsyncTable":
self._namespace_path = namespace_path or []
self._namespace_client = namespace_client
self._pushdown_operations = pushdown_operations or set()
self._route_pushdown_to_rust = route_pushdown_to_rust
return self
def __repr__(self):
@@ -4367,6 +4422,17 @@ class AsyncTable:
"""
await self._inner.unset_lsm_write_spec()
async def get_lsm_write_spec(self) -> Optional["LsmWriteSpec"]:
"""Read the LsmWriteSpec currently installed on this table.
Returns ``None`` when the MemWAL LSM write path is not enabled (no
spec has been set, or it was removed with `unset_lsm_write_spec`).
The returned spec including its ``maintained_indexes`` and
``writer_config_defaults`` mirrors what was passed to
`set_lsm_write_spec`.
"""
return await self._inner.get_lsm_write_spec()
async def close_lsm_writers(self) -> None:
"""Drain and close any cached MemWAL shard writers for this table.
@@ -4490,8 +4556,11 @@ class AsyncTable:
-------
pa.Table
"""
if _should_push_down_query_table(
self._namespace_client, self._pushdown_operations
if (
_should_push_down_query_table(
self._namespace_client, self._pushdown_operations
)
and not self._route_pushdown_to_rust
):
return (await self._execute_query(Query())).read_all()
@@ -5175,8 +5244,11 @@ class AsyncTable:
batch_size: Optional[int] = None,
timeout: Optional[timedelta] = None,
) -> pa.RecordBatchReader:
if _should_push_down_query_table(
self._namespace_client, self._pushdown_operations
if (
_should_push_down_query_table(
self._namespace_client, self._pushdown_operations
)
and not self._route_pushdown_to_rust
):
from lancedb.namespace import _execute_server_side_query
@@ -5662,6 +5734,7 @@ class AsyncTable:
"The 'retrain' parameter is deprecated and will be removed in a "
"future version.",
DeprecationWarning,
stacklevel=2,
)
return await self._inner.optimize(
+39 -1
View File
@@ -177,7 +177,10 @@ def flatten_columns(tbl: pa.Table, flatten: Optional[Union[int, bool]] = None):
continue
else:
break
elif isinstance(flatten, int):
# `bool` is a subclass of `int`, so guard against it explicitly: `flatten=False`
# (and `None`) must mean "do not flatten" rather than falling into the integer
# branch and raising on the `flatten <= 0` check.
elif isinstance(flatten, int) and not isinstance(flatten, bool):
if flatten <= 0:
raise ValueError(
"Please specify a positive integer for flatten or the boolean "
@@ -515,3 +518,38 @@ def batch_to_tensor_rows(batch: pa.RecordBatch):
stacked = torch.tensor(numpy.column_stack(columns))
rows = list(stacked.unbind(dim=0))
return rows
def batch_to_tensor_dict(batch: pa.RecordBatch):
"""
Convert a PyArrow RecordBatch to a list of per-row dicts of PyTorch Tensors.
Each column is first converted to a 1-D tensor (zero-copy via DLPack), then
sliced per row. The result is a list whose length is ``batch.num_rows`` and
whose items are dicts keyed by column name. This shape composes directly
with PyTorch's default ``DataLoader`` collate, which stacks the per-row
dicts back into a dict of batched tensors.
Fails if torch is not installed.
Fails if a column's data type is not supported by PyTorch.
Parameters
----------
batch : pa.RecordBatch
The record batch to convert.
Returns
-------
list[dict[str, torch.Tensor]]
One per-row dict per row in the batch. Each dict maps column name to a
0-D tensor view into the column.
"""
torch = attempt_import_or_raise("torch", "torch")
tensors = {
name: torch.from_dlpack(col)
for name, col in zip(batch.schema.names, batch.columns)
}
return [
{name: tensor[i] for name, tensor in tensors.items()}
for i in range(batch.num_rows)
]
File diff suppressed because it is too large Load Diff
+60
View File
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import os
import pickle
from typing import List, Optional, Union
from unittest.mock import MagicMock, patch
@@ -242,6 +243,49 @@ def test_embedding_with_bad_results(tmp_path):
assert tbl["vector"].null_count == 1
def test_embedding_with_empty_output_vectors(tmp_path):
"""Regression test for issue #1672.
When an embedding function returns an empty list (e.g. for empty-string
inputs), _append_vector_columns used to crash because PyArrow cannot cast
[] into a fixed-size list element. The fix replaces wrong-length vectors
with None before building the Arrow array so that _handle_bad_vectors can
process them normally.
"""
@register("empty-vec-embedding")
class EmptyVecEmbeddingFunction(TextEmbeddingFunction):
def ndims(self):
return 128
def generate_embeddings(self, texts: Union[List[str], np.ndarray]) -> list:
# Simulate a model that returns an empty list for blank inputs
return [
[] if text.strip() == "" else np.random.randn(self.ndims()).tolist()
for text in texts
]
db = lancedb.connect(tmp_path)
registry = EmbeddingFunctionRegistry.get_instance()
model = registry.get("empty-vec-embedding").create()
class Schema(LanceModel):
text: str = model.SourceField()
vector: Vector(model.ndims()) = model.VectorField()
table = db.create_table("test_empty_vec", schema=Schema, mode="overwrite")
# Should not crash; the row with the empty string should be dropped
table.add(
[{"text": "hello world"}, {"text": ""}, {"text": "foo"}],
on_bad_vectors="drop",
)
assert len(table) == 2
texts = table.to_arrow()["text"].to_pylist()
assert "" not in texts
def test_with_existing_vectors(tmp_path):
@register("mock-embedding")
class MockEmbeddingFunction(TextEmbeddingFunction):
@@ -548,6 +592,22 @@ def test_openai_no_retry_on_401(mock_sleep):
assert mock_sleep.call_count == 0
def test_ollama_embeddings_pickle():
"""OllamaEmbeddings must pickle even after the cached client is created."""
registry = get_registry()
model = registry.get("ollama").create(name="nomic-embed-text")
# Simulate accessing the cached client, which stores it on the instance.
model.__dict__["_ollama_client"] = MagicMock()
pickled = pickle.dumps(model)
restored = pickle.loads(pickled)
assert restored.name == "nomic-embed-text"
assert restored.host == "http://localhost:11434"
assert "_ollama_client" not in restored.__dict__
def test_url_retrieve_downloads_image():
"""
Embedding functions like open-clip, siglip, and jinaai use url_retrieve()
@@ -3,10 +3,14 @@
"""Tests for the type-safe expression builder API."""
import pytest
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
import pyarrow as pa
import pytest
import lancedb
from lancedb.expr import Expr, col, lit, func
from lancedb.expr import Expr, col, func, lit
# ── unit tests for Expr construction ─────────────────────────────────────────
@@ -54,6 +58,28 @@ class TestExprConstruction:
with pytest.raises(Exception):
func("not_a_real_function", col("x"))
def test_lit_date(self):
e = lit(date(2024, 1, 1))
assert isinstance(e, Expr)
def test_lit_datetime(self):
# Naive datetime
e = lit(datetime(2024, 1, 1, 10, 0))
assert isinstance(e, Expr)
def test_lit_datetime_tz(self):
# Timezone-aware datetime
tz = timezone(timedelta(hours=5))
dt = datetime(2024, 1, 1, 10, 0, tzinfo=tz)
e = lit(dt)
assert isinstance(e, Expr)
def test_lit_decimal_precision(self):
# High precision Decimal that would be rounded if converted to float
d = Decimal("1.234567890123456789")
e = lit(d)
assert isinstance(e, Expr)
class TestExprOperators:
def test_eq_operator(self):
@@ -142,6 +168,20 @@ class TestExprOperators:
assert isinstance(e, Expr)
assert e.to_sql() == "(name = 'alice')"
def test_reflexive_comparisons(self):
# 10 < col("age") swaps to col("age") > 10
assert (10 < col("age")).to_sql() == "(age > 10)"
assert (10 <= col("age")).to_sql() == "(age >= 10)"
assert (10 > col("age")).to_sql() == "(age < 10)"
assert (10 >= col("age")).to_sql() == "(age <= 10)"
assert (10 == col("age")).to_sql() == "(age = 10)"
assert (10 != col("age")).to_sql() == "(age <> 10)"
def test_reflexive_logical(self):
# True & Expr calls Expr.__rand__(True)
assert (True & (col("age") > 18)).to_sql() == "(true AND (age > 18))"
assert (False | (col("age") > 18)).to_sql() == "(false OR (age > 18))"
class TestExprBytesLiteral:
def test_bytes_to_sql(self):
@@ -282,6 +322,40 @@ class TestExprRepr:
{e: 1}
class TestExprReflexive:
def test_reflexive_eq(self):
e = 1 == col("x")
assert isinstance(e, Expr)
assert e.to_sql() == "(x = 1)"
def test_reflexive_ne(self):
e = 1 != col("x")
assert isinstance(e, Expr)
assert e.to_sql() == "(x <> 1)"
def test_reflexive_lt(self):
# 1 < x => (x > 1)
e = 1 < col("x")
assert isinstance(e, Expr)
assert e.to_sql() == "(x > 1)"
def test_reflexive_gt(self):
# 1 > x => (x < 1)
e = 1 > col("x")
assert isinstance(e, Expr)
assert e.to_sql() == "(x < 1)"
def test_reflexive_and(self):
e = True & col("active")
assert isinstance(e, Expr)
assert e.to_sql() == "(true AND active)"
def test_reflexive_or(self):
e = False | col("inactive")
assert isinstance(e, Expr)
assert e.to_sql() == "(false OR inactive)"
# ── integration tests: end-to-end query against a real table ─────────────────
@@ -432,6 +506,72 @@ class TestColNamingIntegration:
assert sorted(result["upper_name"].to_pylist()) == ["ALICE", "BOB", "CHARLIE"]
@pytest.fixture
def type_check_table(tmp_path):
"""Fixture that creates a table with Date32 and Decimal128 columns."""
db = lancedb.connect(str(tmp_path))
schema = pa.schema(
[
("date", pa.date32()),
("decimal", pa.decimal128(10, 2)),
("binary", pa.binary()),
]
)
data = pa.table(
{
"date": [date(2024, 1, 1), date(2024, 1, 2)],
"decimal": [Decimal("10.50"), Decimal("20.75")],
"binary": [b"\x01", b"\x02"],
},
schema=schema,
)
return db.create_table("extended_types", data)
class TestExtendedTypeIntegration:
"""Integration tests verifying that typed literals work correctly in filters."""
def test_date_integration(self, type_check_table):
"""Verify that Date32 literals are correctly parsed and filtered."""
result = (
type_check_table.search()
.where(col("date") == lit(date(2024, 1, 1)))
.to_arrow()
)
assert result.num_rows == 1
assert result["date"][0].as_py() == date(2024, 1, 1)
def test_decimal_integration(self, tmp_path):
"""A Decimal literal must retain full 128-bit precision in a filter.
1.234567890123456789 and 1.234567890123456790 differ only in the last
digit and are indistinguishable once truncated to f64. The filter
therefore returns the single expected row only if ``lit(Decimal)``
produces a true ``Decimal128`` scalar rather than being coerced to f64.
"""
low = Decimal("1.234567890123456789")
high = Decimal("1.234567890123456790")
db = lancedb.connect(str(tmp_path / "decimal_precision"))
schema = pa.schema([("val", pa.decimal128(19, 18))])
table = db.create_table(
"decimal_precision",
pa.table({"val": [low, high]}, schema=schema),
)
result = table.search().where(col("val") < lit(high)).to_arrow()
assert result.num_rows == 1
assert result["val"][0].as_py() == low
def test_binary_integration(self, type_check_table):
"""Verify that Binary literals are correctly filtered."""
result = (
type_check_table.search().where(col("binary") == lit(b"\x01")).to_arrow()
)
assert result.num_rows == 1
assert result["binary"][0].as_py() == b"\x01"
# ── bytes / binary column integration tests ───────────────────────────────────
+36 -5
View File
@@ -91,7 +91,9 @@ async def test_create_scalar_index(some_table: AsyncTable):
# Can recreate if replace=True
await some_table.create_index("id", replace=True)
indices = await some_table.list_indices()
assert str(indices) == '[Index(BTree, columns=["id"], name="id_idx")]'
assert str(indices).startswith(
'[IndexConfig(name="id_idx", index_type="BTree", columns=["id"]'
)
assert len(indices) == 1
assert indices[0].index_type == "BTree"
assert indices[0].columns == ["id"]
@@ -106,6 +108,27 @@ async def test_create_scalar_index(some_table: AsyncTable):
assert len(indices) == 0
@pytest.mark.asyncio
async def test_index_config_repr(db_async):
# Use >= 1000 rows so the thousands separator in the repr is exercised.
nrows = 1500
table = await db_async.create_table(
"repr_table", pa.Table.from_pydict({"id": list(range(nrows))})
)
await table.create_index("id", config=BTree())
indices = await table.list_indices()
assert len(indices) == 1
r = repr(indices[0])
assert r.startswith('IndexConfig(name="id_idx", index_type="BTree", columns=["id"]')
# Integer counts use `_` thousands separators (valid Python int syntax).
assert "num_indexed_rows=1_500" in r
assert "num_unindexed_rows=0" in r
# created_at renders as a datetime so the value round-trips.
assert "created_at=datetime.datetime(" in r
assert r.endswith(")")
@pytest.mark.asyncio
async def test_create_nested_scalar_index_lists_canonical_paths(db_async):
metadata_type = pa.struct(
@@ -198,7 +221,9 @@ async def test_create_nested_scalar_index_lists_canonical_paths(db_async):
async def test_create_fixed_size_binary_index(some_table: AsyncTable):
await some_table.create_index("fsb", config=BTree())
indices = await some_table.list_indices()
assert str(indices) == '[Index(BTree, columns=["fsb"], name="fsb_idx")]'
assert str(indices).startswith(
'[IndexConfig(name="fsb_idx", index_type="BTree", columns=["fsb"]'
)
assert len(indices) == 1
assert indices[0].index_type == "BTree"
assert indices[0].columns == ["fsb"]
@@ -247,7 +272,9 @@ async def test_create_bitmap_index(some_table: AsyncTable):
async def test_create_label_list_index(some_table: AsyncTable):
await some_table.create_index("tags", config=LabelList())
indices = await some_table.list_indices()
assert str(indices) == '[Index(LabelList, columns=["tags"], name="tags_idx")]'
assert str(indices).startswith(
'[IndexConfig(name="tags_idx", index_type="LabelList", columns=["tags"]'
)
plan = await some_table.query().where("array_has(tags, 'tag0')").explain_plan()
assert "ScalarIndexQuery" in plan
@@ -262,7 +289,9 @@ async def test_create_large_list_label_list_index(db_async):
await table.create_index("tags", config=LabelList())
indices = await table.list_indices()
assert str(indices) == '[Index(LabelList, columns=["tags"], name="tags_idx")]'
assert str(indices).startswith(
'[IndexConfig(name="tags_idx", index_type="LabelList", columns=["tags"]'
)
plan = await table.query().where("array_has(tags, 'shared')").explain_plan()
assert "ScalarIndexQuery" in plan
@@ -299,7 +328,9 @@ async def test_create_label_list_index_rejects_list_struct(db_async):
async def test_full_text_search_index(some_table: AsyncTable):
await some_table.create_index("tags", config=FTS(with_position=False))
indices = await some_table.list_indices()
assert str(indices) == '[Index(FTS, columns=["tags"], name="tags_idx")]'
assert str(indices).startswith(
'[IndexConfig(name="tags_idx", index_type="FTS", columns=["tags"]'
)
await some_table.prewarm_index("tags_idx")
@@ -11,6 +11,7 @@ import lancedb
import pyarrow as pa
import pytest
from lancedb._lancedb import LsmWriteSpec
from lancedb.index import BTree
SCHEMA = pa.schema(
[
@@ -136,3 +137,76 @@ def test_lsm_write_spec_identity_and_writer_config_defaults():
s = s.with_writer_config_defaults({"durable_write": "false"})
assert s.writer_config_defaults == {"durable_write": "false"}
assert "durable_write" in repr(s)
def test_get_lsm_write_spec(tmp_path):
_db, table = _make_table(tmp_path)
table.set_unenforced_primary_key("id")
# None when nothing is installed.
assert table.get_lsm_write_spec() is None
# A real scalar index is needed to name it as a maintained index.
table.create_index("id", config=BTree())
idx_name = table.list_indices()[0].name
# Bucket spec round-trips, including maintained indexes and writer config
# defaults.
table.set_lsm_write_spec(
LsmWriteSpec.bucket("id", 4)
.with_maintained_indexes([idx_name])
.with_writer_config_defaults({"durable_write": "false"})
)
spec = table.get_lsm_write_spec()
assert spec is not None
assert spec.spec_type == "bucket"
assert spec.column == "id"
assert spec.num_buckets == 4
assert spec.maintained_indexes == [idx_name]
assert spec.writer_config_defaults == {"durable_write": "false"}
# After unset, None again.
table.unset_lsm_write_spec()
assert table.get_lsm_write_spec() is None
# Identity round-trips (column recovered from the schema).
table.set_lsm_write_spec(LsmWriteSpec.identity("id"))
spec = table.get_lsm_write_spec()
assert spec.spec_type == "identity"
assert spec.column == "id"
table.unset_lsm_write_spec()
# Unsharded round-trips (no routing column).
table.set_lsm_write_spec(LsmWriteSpec.unsharded())
spec = table.get_lsm_write_spec()
assert spec.spec_type == "unsharded"
assert spec.column is None
@pytest.mark.asyncio
async def test_async_get_lsm_write_spec(tmp_path):
db = await lancedb.connect_async(
tmp_path, read_consistency_interval=timedelta(seconds=0)
)
table = await db.create_table(
"t",
pa.RecordBatchReader.from_batches(SCHEMA, [_batch(["seed"], [0])]),
)
assert await table.get_lsm_write_spec() is None
# A real scalar index is needed to name it as a maintained index.
await table.create_index("id", config=BTree())
idx_name = (await table.list_indices())[0].name
await table.set_lsm_write_spec(
LsmWriteSpec.bucket("id", 8).with_maintained_indexes([idx_name])
)
spec = await table.get_lsm_write_spec()
assert spec is not None
assert spec.spec_type == "bucket"
assert spec.column == "id"
assert spec.num_buckets == 8
assert spec.maintained_indexes == [idx_name]
await table.unset_lsm_write_spec()
assert await table.get_lsm_write_spec() is None
+137 -5
View File
@@ -5,11 +5,12 @@
import tempfile
import shutil
import sys
import importlib
import pytest
import pyarrow as pa
import lancedb
from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError
from lancedb.namespace import _MAX_QUERY_K
from lancedb.table import AsyncTable, LanceTable
@@ -65,6 +66,9 @@ def _namespace_lance_table(namespace_client: _NamespaceClient) -> LanceTable:
table._namespace_path = ["geneva"]
table._namespace_client = namespace_client
table._pushdown_operations = {"QueryTable"}
# This test exercises the Python-side pushdown path (non-native client), so
# pushdown is not routed to Rust.
table._route_pushdown_to_rust = False
return table
@@ -100,6 +104,40 @@ class TestNamespaceConnection:
assert isinstance(db, lancedb.LanceNamespaceDBConnection)
assert len(list(db.table_names())) == 0
def test_sync_builtin_namespace_uses_rust_without_python_client(self, monkeypatch):
"""Built-in sync namespace connections should not construct or call the
Python namespace client for normal namespace/table management."""
namespace_module = importlib.import_module("lancedb.namespace")
def fail_namespace_connect(*args, **kwargs):
raise AssertionError("Python namespace client should not be constructed")
monkeypatch.setattr(
namespace_module, "namespace_connect", fail_namespace_connect
)
db = lancedb.connect_namespace("dir", {"root": self.temp_dir})
assert isinstance(db, lancedb.LanceNamespaceDBConnection)
assert db._namespace_client is None
assert db._route_pushdown_to_rust is True
db.create_namespace(["test_ns"])
assert "test_ns" in db.list_namespaces().namespaces
schema = pa.schema([pa.field("id", pa.int64())])
table = db.create_table("test_table", schema=schema, namespace_path=["test_ns"])
assert table.namespace == ["test_ns"]
assert "test_table" in db.table_names(namespace_path=["test_ns"])
assert "test_table" in db.list_tables(namespace_path=["test_ns"]).tables
opened = db.open_table("test_table", namespace_path=["test_ns"])
assert opened.namespace == ["test_ns"]
db.drop_table("test_table", namespace_path=["test_ns"])
assert db.list_tables(namespace_path=["test_ns"]).tables == []
db.drop_namespace(["test_ns"])
assert "test_ns" not in db.list_namespaces().namespaces
def test_create_table_through_namespace(self):
"""Test creating a table through namespace."""
db = lancedb.connect_namespace("dir", {"root": self.temp_dir})
@@ -561,6 +599,61 @@ class TestAsyncNamespaceConnection:
table_names = await db.table_names()
assert len(list(table_names)) == 0
async def test_async_builtin_namespace_uses_rust_without_python_client(
self, monkeypatch
):
"""Built-in async namespace connections should not construct or call the
Python namespace client for normal namespace/table management."""
namespace_module = importlib.import_module("lancedb.namespace")
def fail_namespace_connect(*args, **kwargs):
raise AssertionError("Python namespace client should not be constructed")
monkeypatch.setattr(
namespace_module, "namespace_connect", fail_namespace_connect
)
db = lancedb.connect_namespace_async("dir", {"root": self.temp_dir})
assert isinstance(db, lancedb.AsyncLanceNamespaceDBConnection)
assert db._namespace_client is None
assert db._route_pushdown_to_rust is True
await db.create_namespace(["test_ns"])
assert "test_ns" in (await db.list_namespaces()).namespaces
schema = pa.schema([pa.field("id", pa.int64())])
table = await db.create_table(
"test_table", schema=schema, namespace_path=["test_ns"]
)
assert table._namespace_path == ["test_ns"]
assert table._namespace_client is None
assert table._route_pushdown_to_rust is True
assert "test_table" in await db.table_names(namespace_path=["test_ns"])
assert "test_table" in (await db.list_tables(namespace_path=["test_ns"])).tables
opened = await db.open_table("test_table", namespace_path=["test_ns"])
assert opened._namespace_path == ["test_ns"]
await db.drop_table("test_table", namespace_path=["test_ns"])
assert (await db.list_tables(namespace_path=["test_ns"])).tables == []
await db.drop_namespace(["test_ns"])
assert "test_ns" not in (await db.list_namespaces()).namespaces
async def test_async_namespace_client_is_lazy(self):
"""namespace_client() should still return the backing client on demand."""
pytest.importorskip("lance")
from lance.namespace import DirectoryNamespace
db = lancedb.connect_namespace_async("dir", {"root": self.temp_dir})
assert db._namespace_client is None
ns_client = await db.namespace_client()
assert isinstance(ns_client, DirectoryNamespace)
namespace_id = ns_client.namespace_id().replace("\\\\", "\\")
assert str(self.temp_dir) in namespace_id
assert db._namespace_client is ns_client
# Async connect via namespace helper is not enabled yet.
async def test_create_table_async(self):
@@ -805,6 +898,39 @@ class TestPushdownOperations:
db = lancedb.connect_namespace("dir", {"root": self.temp_dir})
assert len(db._namespace_client_pushdown_operations) == 0
def test_route_pushdown_to_rust_for_native_rest(self):
"""A natively-built rest connection must defer QueryTable pushdown to
Rust so reads carry the x-lancedb-min-timestamp read-freshness header."""
db = lancedb.connect_namespace(
"rest",
{"uri": "http://localhost:12345"},
namespace_client_pushdown_operations=["QueryTable"],
)
assert db._route_pushdown_to_rust is True
def test_route_pushdown_to_rust_for_native_dir(self):
"""The sync dir connection is natively built and defers QueryTable
pushdown to Rust."""
db = lancedb.connect_namespace("dir", {"root": self.temp_dir})
assert db._route_pushdown_to_rust is True
def test_async_route_pushdown_to_rust_for_native_rest(self):
"""The async connection must not silently bypass the read-freshness fix:
a natively-built rest connection defers pushdown to Rust (regression test
for the async path omitting the freshness header)."""
db = lancedb.connect_namespace_async(
"rest",
{"uri": "http://localhost:12345"},
namespace_client_pushdown_operations=["QueryTable"],
)
assert db._route_pushdown_to_rust is True
def test_async_route_pushdown_to_rust_for_native_dir(self):
"""The async dir connection is natively built and defers QueryTable
pushdown to Rust."""
db = lancedb.connect_namespace_async("dir", {"root": self.temp_dir})
assert db._route_pushdown_to_rust is True
def test_lance_table_to_arrow_uses_query_pushdown(self):
namespace_client = _NamespaceClient()
table = _namespace_lance_table(namespace_client)
@@ -816,10 +942,13 @@ class TestPushdownOperations:
["geneva", "hist"],
["geneva", "hist"],
]
# Unlimited reads cap k at i32::MAX (the namespace query_table `k`
# field is i32); sys.maxsize would overflow the Rust binding.
assert [request.k for request in namespace_client.requests] == [
sys.maxsize,
sys.maxsize,
_MAX_QUERY_K,
_MAX_QUERY_K,
]
assert all(r.k <= 2**31 - 1 for r in namespace_client.requests)
@pytest.mark.asyncio
@@ -874,10 +1003,13 @@ class TestAsyncPushdownOperations:
["geneva", "hist"],
["geneva", "hist"],
]
# Unlimited reads cap k at i32::MAX (the namespace query_table `k`
# field is i32); sys.maxsize would overflow the Rust binding.
assert [request.k for request in namespace_client.requests] == [
sys.maxsize,
sys.maxsize,
_MAX_QUERY_K,
_MAX_QUERY_K,
]
assert all(r.k <= 2**31 - 1 for r in namespace_client.requests)
def test_local_table_to_arrow_and_to_pandas_are_unchanged(tmp_path):
+34 -7
View File
@@ -935,14 +935,41 @@ def test_transform_fn(mem_db):
try:
import torch
torch_result = list(
permutation.with_format("torch").iter(10, skip_last_batch=False)
# "torch" returns a list of per-row dicts. Default DataLoader collate
# stacks the per-row dicts back into a dict of batched tensors.
torch_perm = permutation.with_format("torch")
torch_batch = list(torch_perm.iter(10, skip_last_batch=False))[0]
assert isinstance(torch_batch, list)
assert len(torch_batch) == 10
assert isinstance(torch_batch[0], dict)
assert set(torch_batch[0].keys()) == {"id", "value"}
assert isinstance(torch_batch[0]["id"], torch.Tensor)
assert torch_batch[0]["id"].dtype == torch.int64
rows = torch_perm.__getitems__([0, 1, 2])
assert isinstance(rows, list)
assert len(rows) == 3
assert isinstance(rows[0], dict)
assert set(rows[0].keys()) == {"id", "value"}
assert isinstance(rows[0]["id"], torch.Tensor)
# "torch_row" returns a list of tensors, one per row.
torch_rows = list(
permutation.with_format("torch_row").iter(10, skip_last_batch=False)
)[0]
assert isinstance(torch_result, list)
assert len(torch_result) == 10
assert isinstance(torch_result[0], torch.Tensor)
assert torch_result[0].shape == (2,)
assert torch_result[0].dtype == torch.int64
assert isinstance(torch_rows, list)
assert len(torch_rows) == 10
assert isinstance(torch_rows[0], torch.Tensor)
assert torch_rows[0].shape == (2,)
assert torch_rows[0].dtype == torch.int64
# "torch_col" stacks columns into a single 2D tensor.
torch_col = list(
permutation.with_format("torch_col").iter(10, skip_last_batch=False)
)[0]
assert isinstance(torch_col, torch.Tensor)
assert torch_col.shape == (2, 10)
assert torch_col.dtype == torch.int64
except ImportError:
# Skip check if torch is not installed
pass
+55
View File
@@ -502,6 +502,61 @@ def test_with_row_id(table: lancedb.table.Table):
assert rs["_rowid"].to_pylist() == [0, 1]
def test_where_repeated_combines_with_and(table: lancedb.table.Table):
# Calling where() more than once should AND the filters together instead of
# silently replacing the previous one (regression test for #2649).
builder = table.search().where("id >= 1").where("id < 2")
assert builder._where == "(id >= 1) AND (id < 2)"
ids = [row["id"] for row in builder.limit(10).to_list()]
assert ids == [1]
def test_where_repeated_combines_expr(table: lancedb.table.Table):
from lancedb.expr import col, lit
builder = table.search().where(col("id") >= lit(1)).where(col("id") < lit(2))
ids = [row["id"] for row in builder.limit(10).to_list()]
assert ids == [1]
def test_where_mixed_filter_kinds_combines(table: lancedb.table.Table):
# Mixing a SQL string filter with an expression filter lowers the
# expression to SQL and combines them as SQL strings.
from lancedb.expr import col, lit
builder = table.search().where("id >= 1").where(col("id") < lit(2))
ids = [row["id"] for row in builder.limit(10).to_list()]
assert ids == [1]
@pytest.mark.asyncio
async def test_where_repeated_combines_with_and_async(table_async: AsyncTable):
ids = [
row["id"]
for row in (
await table_async.query().where("id >= 1").where("id < 2").to_list()
)
]
assert ids == [1]
@pytest.mark.asyncio
async def test_where_mixed_filter_kinds_combines_async(table_async: AsyncTable):
from lancedb.expr import col, lit
ids = [
row["id"]
for row in (
await table_async.query()
.where("id >= 1")
.where(col("id") < lit(2))
.to_list()
)
]
assert ids == [1]
def test_distance_range(table: lancedb.table.Table):
q = [0, 0]
rs = table.search(q).to_arrow()
+5 -3
View File
@@ -412,10 +412,12 @@ def test_remote_permutation_is_picklable():
content_len = int(request.headers.get("Content-Length"))
body = json.loads(request.rfile.read(content_len))
if "filter" in body:
match = re.search(r"_rowoffset in \((.*?)\)", body["filter"])
offsets = [int(offset.strip()) for offset in match.group(1).split(",")]
match = re.search(
r"_rowoffset\s+in\s+\((.*?)\)", body["filter"], re.IGNORECASE
)
offsets = [int(o.strip()) for o in match.group(1).split(",")]
else:
offsets = rows
offsets = list(range(len(rows)))
table = pa.table({"a": [rows[offset] for offset in offsets]})
request.send_response(200)
+32
View File
@@ -350,6 +350,38 @@ def test_mrr_reranker_empty_input():
reranker.rerank_multivector([])
def test_mrr_multivector_rewards_consensus():
# Reciprocal ranks must be averaged across *all* ranking systems, treating a
# missing system as 0. A document ranked first by every system must outrank a
# document ranked first by only one of them.
reranker = MRRReranker()
def ranking(row_ids):
return pa.table({"_rowid": pa.array(row_ids, type=pa.int64())})
# Doc 1 is rank 1 in only the first system; doc 2 is rank 1 in two systems
# and rank 2 in the third (strong cross-system consensus).
rs1 = ranking([1, 2, 3])
rs2 = ranking([2, 3, 4])
rs3 = ranking([2, 5, 6])
result = reranker.rerank_multivector([rs1, rs2, rs3])
scores = {
row_id: score
for row_id, score in zip(
result["_rowid"].to_pylist(),
result["_relevance_score"].to_pylist(),
)
}
# sum of reciprocal ranks / number of systems
assert scores[1] == pytest.approx(1.0 / 3)
assert scores[2] == pytest.approx((0.5 + 1.0 + 1.0) / 3)
assert scores[2] > scores[1]
# The consensus document ranks first overall.
assert result["_rowid"].to_pylist()[0] == 2
def test_rrf_reranker_distance():
data = pa.table(
{
+36 -19
View File
@@ -7,7 +7,8 @@ Tests for S3 bucket names containing dots.
Related issue: https://github.com/lancedb/lancedb/issues/1898
These tests validate the early error checking for S3 bucket names with dots.
No actual S3 connection is made - validation happens before connection.
When validation succeeds, eager namespace initialization may still fail later
because these tests intentionally do not provide real S3 credentials.
"""
import pytest
@@ -20,6 +21,13 @@ BUCKET_WITH_DOTS_AND_AWS_REGION = ("s3://my.bucket.name", {"aws_region": "us-eas
BUCKET_WITHOUT_DOTS = "s3://my-bucket/path"
def assert_not_rejected_for_bucket_dots(connect):
try:
connect()
except ValueError as err:
assert "contains dots" not in str(err)
class TestS3BucketWithDotsSync:
"""Tests for connect()."""
@@ -27,19 +35,22 @@ class TestS3BucketWithDotsSync:
with pytest.raises(ValueError, match="contains dots"):
lancedb.connect(BUCKET_WITH_DOTS)
def test_bucket_with_dots_and_region_passes(self):
def test_bucket_with_dots_and_region_is_not_rejected(self):
uri, opts = BUCKET_WITH_DOTS_AND_REGION
db = lancedb.connect(uri, storage_options=opts)
assert db is not None
assert_not_rejected_for_bucket_dots(
lambda: lancedb.connect(uri, storage_options=opts)
)
def test_bucket_with_dots_and_aws_region_passes(self):
def test_bucket_with_dots_and_aws_region_is_not_rejected(self):
uri, opts = BUCKET_WITH_DOTS_AND_AWS_REGION
db = lancedb.connect(uri, storage_options=opts)
assert db is not None
assert_not_rejected_for_bucket_dots(
lambda: lancedb.connect(uri, storage_options=opts)
)
def test_bucket_without_dots_passes(self):
db = lancedb.connect(BUCKET_WITHOUT_DOTS)
assert db is not None
def test_bucket_without_dots_is_not_rejected(self):
assert_not_rejected_for_bucket_dots(
lambda: lancedb.connect(BUCKET_WITHOUT_DOTS)
)
class TestS3BucketWithDotsAsync:
@@ -51,18 +62,24 @@ class TestS3BucketWithDotsAsync:
await lancedb.connect_async(BUCKET_WITH_DOTS)
@pytest.mark.asyncio
async def test_bucket_with_dots_and_region_passes(self):
async def test_bucket_with_dots_and_region_is_not_rejected(self):
uri, opts = BUCKET_WITH_DOTS_AND_REGION
db = await lancedb.connect_async(uri, storage_options=opts)
assert db is not None
try:
await lancedb.connect_async(uri, storage_options=opts)
except ValueError as err:
assert "contains dots" not in str(err)
@pytest.mark.asyncio
async def test_bucket_with_dots_and_aws_region_passes(self):
async def test_bucket_with_dots_and_aws_region_is_not_rejected(self):
uri, opts = BUCKET_WITH_DOTS_AND_AWS_REGION
db = await lancedb.connect_async(uri, storage_options=opts)
assert db is not None
try:
await lancedb.connect_async(uri, storage_options=opts)
except ValueError as err:
assert "contains dots" not in str(err)
@pytest.mark.asyncio
async def test_bucket_without_dots_passes(self):
db = await lancedb.connect_async(BUCKET_WITHOUT_DOTS)
assert db is not None
async def test_bucket_without_dots_is_not_rejected(self):
try:
await lancedb.connect_async(BUCKET_WITHOUT_DOTS)
except ValueError as err:
assert "contains dots" not in str(err)
+10
View File
@@ -1137,6 +1137,16 @@ def test_namespace_open_table_with_branch_version(tmp_path):
assert db.open_table("t", namespace_path=["ns1"], branch="exp").count_rows() == 3
def test_namespace_root_table_to_lance_uses_namespace_client(tmp_path):
pytest.importorskip("lance") # "dir" impl is lance.namespace.DirectoryNamespace
db = lancedb.connect_namespace("dir", {"root": str(tmp_path)})
table = db.create_table("t", [{"i": 0}])
assert table._namespace_client is None
assert table.to_lance().count_rows() == 1
assert table._namespace_client is not None
@pytest.mark.asyncio
async def test_async_namespace_open_table_with_branch_version(tmp_path):
pytest.importorskip("lance") # "dir" impl is lance.namespace.DirectoryNamespace
+36 -4
View File
@@ -148,15 +148,47 @@ def test_permutation_dataloader(mem_db):
for batch in dataloader:
assert batch["a"].size(0) == 10
permutation = permutation.with_format("torch")
dataloader = torch.utils.data.DataLoader(permutation, batch_size=10, shuffle=True)
# "torch" produces a list of per-row dicts per batch. The default
# DataLoader collate stacks the per-row dicts back into a batched dict.
torch_perm = permutation.with_format("torch")
batch = next(torch_perm.iter(10, skip_last_batch=False))
assert isinstance(batch, list)
assert len(batch) == 10
assert isinstance(batch[0], dict)
assert isinstance(batch[0]["a"], torch.Tensor)
rows = torch_perm.__getitems__([0, 1, 2])
assert isinstance(rows, list)
assert len(rows) == 3
assert isinstance(rows[0], dict)
assert isinstance(rows[0]["a"], torch.Tensor)
dataloader = torch.utils.data.DataLoader(torch_perm, batch_size=10, shuffle=True)
for batch in dataloader:
assert isinstance(batch, dict)
assert batch["a"].shape == (10,)
# Spawn-based workers exercise the pickle round-trip path: the new
# transform-as-list shape must survive pickling so workers produce the
# same per-row dicts the parent does.
spawn_loader = torch.utils.data.DataLoader(
torch_perm,
batch_size=10,
num_workers=2,
multiprocessing_context="spawn",
)
for batch in spawn_loader:
assert isinstance(batch, dict)
assert batch["a"].shape == (10,)
# "torch_row" returns a list of row tensors. Works with the default
# DataLoader collate (stacks rows into 2D).
row_perm = permutation.with_format("torch_row")
dataloader = torch.utils.data.DataLoader(row_perm, batch_size=10, shuffle=True)
for batch in dataloader:
assert batch.size(0) == 10
assert batch.size(1) == 1
permutation = permutation.with_format("torch_col")
col_perm = permutation.with_format("torch_col")
dataloader = torch.utils.data.DataLoader(
permutation, collate_fn=lambda x: x, batch_size=10, shuffle=True
col_perm, collate_fn=lambda x: x, batch_size=10, shuffle=True
)
for batch in dataloader:
assert batch.size(0) == 1
+33 -1
View File
@@ -25,10 +25,42 @@ import pandas as pd
import polars as pl
import pytest
import lancedb
from lancedb.util import get_uri_scheme, join_uri, value_to_sql
from lancedb.util import flatten_columns, get_uri_scheme, join_uri, value_to_sql
from utils import exception_output
def _struct_table() -> pa.Table:
return pa.table(
{
"id": [1, 2],
"nested": pa.array([{"a": 1, "b": 2}, {"a": 3, "b": 4}]),
}
)
def test_flatten_columns():
tbl = _struct_table()
# None / False mean "do not flatten": the struct column is preserved.
# `False` is a regression guard: because bool is a subclass of int it used
# to fall into the integer branch and raise ValueError (see issue).
for no_flatten in (None, False):
result = flatten_columns(tbl, no_flatten)
assert result.column_names == ["id", "nested"]
# True flattens all nested levels.
flattened = flatten_columns(tbl, True)
assert flattened.column_names == ["id", "nested.a", "nested.b"]
# A positive integer flattens up to that depth.
flattened = flatten_columns(tbl, 1)
assert flattened.column_names == ["id", "nested.a", "nested.b"]
# Non-positive integers are still rejected.
with pytest.raises(ValueError):
flatten_columns(tbl, 0)
def test_normalize_uri():
uris = [
"relative/path",
+115 -12
View File
@@ -539,7 +539,7 @@ impl Connection {
}
#[pyfunction]
#[pyo3(signature = (uri, api_key=None, region=None, host_override=None, read_consistency_interval=None, client_config=None, storage_options=None, session=None, manifest_enabled=false, namespace_client_properties=None))]
#[pyo3(signature = (uri, api_key=None, region=None, host_override=None, read_consistency_interval=None, client_config=None, storage_options=None, session=None, manifest_enabled=false, namespace_client_properties=None, oauth_config=None))]
#[allow(clippy::too_many_arguments)]
pub fn connect(
py: Python<'_>,
@@ -553,6 +553,7 @@ pub fn connect(
session: Option<crate::session::Session>,
manifest_enabled: bool,
namespace_client_properties: Option<HashMap<String, String>>,
oauth_config: Option<crate::oauth::PyOAuthConfig>,
) -> PyResult<Bound<'_, PyAny>> {
future_into_py(py, async move {
let mut builder = lancedb::connect(&uri);
@@ -582,6 +583,11 @@ pub fn connect(
if let Some(client_config) = client_config {
builder = builder.client_config(client_config.into());
}
if let Some(oauth_config) = oauth_config {
let config: lancedb::remote::oauth::OAuthConfig =
oauth_config.try_into().infer_error()?;
builder = builder.oauth_config(config);
}
if let Some(session) = session {
builder = builder.session(session.inner.clone());
}
@@ -610,24 +616,38 @@ pub fn connect_namespace_client(
namespace_client_impl: Option<String>,
namespace_client_properties: Option<HashMap<String, String>>,
) -> PyResult<Connection> {
let namespace_client = extract_namespace_arc(py, namespace_client)?;
let read_consistency_interval = read_consistency_interval.map(Duration::from_secs_f64);
let namespace_client_pushdown_operations =
parse_namespace_client_pushdown_operations(namespace_client_pushdown_operations)?;
let ns_impl = namespace_client_impl.unwrap_or_else(|| "python".to_string());
let ns_properties = namespace_client_properties.unwrap_or_default();
let storage_options = storage_options.unwrap_or_default();
let session = session.map(|s| s.inner.clone());
let database = LanceNamespaceDatabase::from_namespace_client(
namespace_client,
ns_impl,
ns_properties,
storage_options,
read_consistency_interval,
session,
namespace_client_pushdown_operations,
);
// Prefer building the namespace natively from (impl, properties) so the
// read-freshness provider installed
let database = if build_namespace_natively(namespace_client_impl.as_deref(), &ns_properties) {
let ns_impl = namespace_client_impl.expect("impl present per build_namespace_natively");
crate::runtime::block_on(LanceNamespaceDatabase::connect(
&ns_impl,
ns_properties,
storage_options,
read_consistency_interval,
session,
namespace_client_pushdown_operations,
))
.infer_error()?
} else {
let namespace_client = extract_namespace_arc(py, namespace_client)?;
LanceNamespaceDatabase::from_namespace_client(
namespace_client,
namespace_client_impl.unwrap_or_else(|| "python".to_string()),
ns_properties,
storage_options,
read_consistency_interval,
session,
namespace_client_pushdown_operations,
)
};
Ok(Connection::new(LanceConnection::new(
Arc::new(database),
@@ -635,6 +655,56 @@ pub fn connect_namespace_client(
)))
}
#[pyfunction]
#[pyo3(signature = (
namespace_client_impl,
namespace_client_properties,
read_consistency_interval=None,
storage_options=None,
session=None,
namespace_client_pushdown_operations=None,
))]
#[allow(clippy::too_many_arguments)]
pub fn connect_namespace(
namespace_client_impl: String,
namespace_client_properties: HashMap<String, String>,
read_consistency_interval: Option<f64>,
storage_options: Option<HashMap<String, String>>,
session: Option<crate::session::Session>,
namespace_client_pushdown_operations: Option<Vec<String>>,
) -> PyResult<Connection> {
let read_consistency_interval = read_consistency_interval.map(Duration::from_secs_f64);
let namespace_client_pushdown_operations =
parse_namespace_client_pushdown_operations(namespace_client_pushdown_operations)?;
let mut builder =
lancedb::connect_namespace(&namespace_client_impl, namespace_client_properties)
.pushdown_operations(namespace_client_pushdown_operations);
if let Some(storage_options) = storage_options {
builder = builder.storage_options(storage_options);
}
if let Some(read_consistency_interval) = read_consistency_interval {
builder = builder.read_consistency_interval(read_consistency_interval);
}
if let Some(session) = session {
builder = builder.session(session.inner.clone());
}
Ok(Connection::new(
crate::runtime::block_on(builder.execute()).infer_error()?,
))
}
/// Whether to build the namespace natively (from impl + properties) instead of
/// wrapping a pre-built client. Native construction is required for the
/// read-freshness provider to be installed
fn build_namespace_natively(
namespace_client_impl: Option<&str>,
namespace_client_properties: &HashMap<String, String>,
) -> bool {
matches!(namespace_client_impl, Some("rest")) && !namespace_client_properties.is_empty()
}
#[derive(FromPyObject)]
pub struct PyClientConfig {
user_agent: String,
@@ -733,3 +803,36 @@ impl From<PyClientConfig> for lancedb::remote::ClientConfig {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn props(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn native_build_only_for_rest_with_properties() {
let rest = props(&[("uri", "http://localhost:10024")]);
// rest + non-empty properties -> build natively (installs the
// read-freshness provider so checkout_latest() busts the server cache).
assert!(build_namespace_natively(Some("rest"), &rest));
// dir is local (no server cache) -> wrap the pre-built client unchanged.
assert!(!build_namespace_natively(
Some("dir"),
&props(&[("root", "/tmp")])
));
// No impl: only a pre-built client was handed in -> wrap it as-is.
assert!(!build_namespace_natively(None, &rest));
// rest but no properties: nothing to build a connection from -> wrap.
assert!(!build_namespace_natively(Some("rest"), &HashMap::new()));
}
}
+60 -8
View File
@@ -7,12 +7,14 @@
//! build type-safe filter / projection expressions that map directly to
//! DataFusion [`Expr`] nodes, bypassing SQL string parsing.
use std::ops::{Add, Div, Mul, Not, Sub};
use arrow::{datatypes::DataType, pyarrow::PyArrowType};
use datafusion_common::ScalarValue;
use lancedb::expr::{
DfExpr, col as ldb_col, contains, expr_cast, is_in, lit as df_lit, lower, upper,
};
use pyo3::types::PyBytes;
use pyo3::types::{PyBytes, PyDate, PyDateTime};
use pyo3::{Bound, PyAny, PyResult, exceptions::PyValueError, prelude::*, pyfunction};
/// A type-safe DataFusion expression.
@@ -63,30 +65,30 @@ impl PyExpr {
Self(self.0.clone().or(other.0.clone()))
}
/// Logical NOT.
fn not_(&self) -> Self {
use std::ops::Not;
Self(self.0.clone().not())
}
// ── arithmetic ───────────────────────────────────────────────────────────
/// Add expressions.
fn add(&self, other: &Self) -> Self {
use std::ops::Add;
Self(self.0.clone().add(other.0.clone()))
}
/// Subtract expressions.
fn sub(&self, other: &Self) -> Self {
use std::ops::Sub;
Self(self.0.clone().sub(other.0.clone()))
}
/// Multiply expressions.
fn mul(&self, other: &Self) -> Self {
use std::ops::Mul;
Self(self.0.clone().mul(other.0.clone()))
}
/// Divide expressions.
fn div(&self, other: &Self) -> Self {
use std::ops::Div;
Self(self.0.clone().div(other.0.clone()))
}
@@ -153,7 +155,8 @@ pub fn expr_col(name: &str) -> PyExpr {
/// Create a literal value expression.
///
/// Supported Python types: `bool`, `int`, `float`, `str`, `bytes`.
/// Supported Python types: `bool`, `int`, `float`, `str`, `bytes`, `date`,
/// `datetime`, `Decimal`.
#[pyfunction]
pub fn expr_lit(value: Bound<'_, PyAny>) -> PyResult<PyExpr> {
// bool must be checked before int because bool is a subclass of int in Python
@@ -163,6 +166,19 @@ pub fn expr_lit(value: Bound<'_, PyAny>) -> PyResult<PyExpr> {
if let Ok(i) = value.extract::<i64>() {
return Ok(PyExpr(df_lit(i)));
}
// Decimal must be checked before f64: Python's Decimal implements __float__,
// so value.extract::<f64>() would succeed and silently truncate the value to
// f64, losing precision. Build a Decimal128 scalar to preserve it instead.
if value.get_type().name()? == "Decimal" {
let s = value.call_method0("__str__")?.extract::<String>()?;
// Parse the decimal string into an i128 value, precision, and scale.
let (val, precision, scale) = parse_decimal(&s)?;
return Ok(PyExpr(df_lit(ScalarValue::Decimal128(
Some(val),
precision,
scale,
))));
}
if let Ok(f) = value.extract::<f64>() {
return Ok(PyExpr(df_lit(f)));
}
@@ -173,12 +189,48 @@ pub fn expr_lit(value: Bound<'_, PyAny>) -> PyResult<PyExpr> {
let bytes = value.extract::<Vec<u8>>()?;
return Ok(PyExpr(df_lit(ScalarValue::Binary(Some(bytes)))));
}
// datetime.datetime is a subclass of datetime.date, so it must be checked first.
if let Ok(dt) = value.cast::<PyDateTime>() {
let ts: f64 = dt.call_method0("timestamp")?.extract()?;
let micros = (ts * 1_000_000.0).round() as i64;
return Ok(PyExpr(df_lit(ScalarValue::TimestampMicrosecond(
Some(micros),
None,
))));
}
if let Ok(d) = value.cast::<PyDate>() {
let ordinal: i32 = d.call_method0("toordinal")?.extract()?;
let days = ordinal - 719163; // Unix epoch is 1970-01-01
return Ok(PyExpr(df_lit(ScalarValue::Date32(Some(days)))));
}
Err(PyValueError::new_err(format!(
"unsupported literal type: {}. Supported: bool, int, float, str, bytes",
"unsupported literal type: {}. Supported: bool, int, float, str, bytes, date, datetime, Decimal",
value.get_type().name()?
)))
}
fn parse_decimal(s: &str) -> PyResult<(i128, u8, i8)> {
let s = s.trim();
let dot_pos = s.find('.');
let scale = if let Some(pos) = dot_pos {
(s.len() - pos - 1) as i8
} else {
0
};
let digits = s.replace('.', "");
let val = digits
.parse::<i128>()
.map_err(|e| PyValueError::new_err(format!("failed to parse decimal digits: {}", e)))?;
// Precision is total number of digits
let precision = digits.trim_start_matches('-').len() as u8;
Ok((val, precision, scale))
}
/// Call an arbitrary registered SQL function by name.
///
/// See `lancedb::expr::func` for the list of supported function names.
+64 -5
View File
@@ -319,11 +319,53 @@ pub struct IndexConfig {
#[pymethods]
impl IndexConfig {
pub fn __repr__(&self) -> String {
format!(
"Index({}, columns={:?}, name=\"{}\")",
self.index_type, self.columns, self.name
)
pub fn __repr__(&self, py: Python<'_>) -> String {
let mut fields = vec![
format!("name={:?}", self.name),
format!("index_type={:?}", self.index_type),
format!("columns={:?}", self.columns),
];
if let Some(v) = &self.index_uuid {
fields.push(format!("index_uuid={:?}", v));
}
if let Some(v) = &self.type_url {
fields.push(format!("type_url={:?}", v));
}
if let Some(v) = self.created_at {
// Render the datetime's own Python repr so the value round-trips,
// falling back to RFC 3339 if the conversion ever fails.
let rendered = v
.into_pyobject(py)
.ok()
.and_then(|obj| obj.into_any().repr().ok())
.map(|r| r.to_string())
.unwrap_or_else(|| v.to_rfc3339());
fields.push(format!("created_at={}", rendered));
}
if let Some(v) = self.num_indexed_rows {
fields.push(format!("num_indexed_rows={}", fmt_thousands(v)));
}
if let Some(v) = self.num_unindexed_rows {
fields.push(format!("num_unindexed_rows={}", fmt_thousands(v)));
}
if let Some(v) = self.size_bytes {
fields.push(format!("size_bytes={}", fmt_thousands(v)));
}
if let Some(v) = self.num_segments {
fields.push(format!("num_segments={}", v));
}
if let Some(v) = self.index_version {
fields.push(format!("index_version={}", v));
}
if let Some(v) = &self.index_details {
let details = v
.bind(py)
.repr()
.map(|r| r.to_string())
.unwrap_or_else(|_| "<unavailable>".to_string());
fields.push(format!("index_details={}", details));
}
format!("IndexConfig({})", fields.join(", "))
}
// For backwards-compatibility with the old sync SDK, we also support getting
@@ -352,6 +394,23 @@ impl IndexConfig {
}
}
/// Format an integer with `_` thousands separators, e.g. `24_500_213`.
///
/// Underscores are valid Python int-literal syntax, so the repr stays
/// copy-pasteable and machine-parseable while remaining readable.
fn fmt_thousands(n: u64) -> String {
let digits = n.to_string();
let bytes = digits.as_bytes();
let mut out = String::with_capacity(digits.len() + digits.len() / 3);
for (i, b) in bytes.iter().enumerate() {
if i > 0 && (bytes.len() - i).is_multiple_of(3) {
out.push('_');
}
out.push(*b as char);
}
out
}
fn parse_index_details(py: Python<'_>, s: String) -> Py<PyAny> {
let json = py.import("json").expect("json module is always available");
match json.call_method1("loads", (s.as_str(),)) {

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