Compare commits

..

1 Commits

Author SHA1 Message Date
Lance Release 0eead75e05 Bump version: 0.30.1-beta.1 → 0.30.1-beta.2 2026-06-04 06:05:10 +00:00
208 changed files with 2919 additions and 30799 deletions
-81
View File
@@ -1,81 +0,0 @@
---
name: lancedb
description: Use when writing, reviewing, debugging, or documenting LanceDB pipelines in Python or TypeScript, especially code that should work across local LanceDB OSS tables and remote LanceDB Enterprise/Cloud tables. Helps avoid non-portable full-table materialization, choose idiomatic query/search patterns, and apply LanceDB performance defaults for ingestion, indexing, filtering, and diagnostics.
---
# Building LanceDB Pipelines
Use this skill to produce LanceDB pipelines that are portable between local and remote tables (for LanceDB Enterprise/Cloud) and idiomatic for the selected SDK.
## LanceDB Table Modes
LanceDB has two common execution modes:
- **Local table**: embedded, open source, in-process LanceDB. The client opens data from a local path or object storage URI and executes queries in the application process.
- **Remote table**: LanceDB Enterprise/Cloud table opened through a `db://...` URI. The data may be very large, commonly backed by object storage, and queried through a remote service.
Do NOT assume local-only table helpers exist on remote tables. If the user asks for LanceDB Enterprise, Cloud, `db://...`, production remote access, or a remote table, focus on the remote table path: use `search()` / `query()`, keep reads bounded with `select()` and `limit()`, and avoid table-level full materialization APIs.
## Workflow
1. Identify the SDK: Python, TypeScript, or both.
2. Identify the table mode: local/embedded OSS, remote Enterprise/Cloud, or portable across both. If the user says "LanceDB Enterprise", choose the remote table path.
3. Read the matching language branch before writing or changing code:
- Python patterns: `references/python/patterns.md`
- Python API quick reference: `references/python/api_reference.md`
- Python performance guidance: `references/python/performance.md`
- TypeScript patterns: `references/typescript/patterns.md`
- TypeScript API quick reference: `references/typescript/api_reference.md`
- TypeScript performance guidance: `references/typescript/performance.md`
- Column metadata authoring (both SDKs): `references/column_metadata.md`
- Branch operations (both SDKs): `references/branch_ops.md`
4. Start with `patterns.md` for the selected SDK. Read `api_reference.md` when choosing method names or return collectors. Read `performance.md` when the task involves ingestion, indexing, filtering, query tuning, diagnostics, or large datasets. Read `column_metadata.md` when the task is documenting, tagging, classifying, or grouping table columns (field descriptions, `lancedb:tag:*` tags, logical column families). Read `branch_ops.md` when the task involves branch lifecycle (list/create/delete), writing to a non-main branch, or verifying a change stayed off main.
5. For Python schemas, favor Pydantic models and validate records before writing. Use PyArrow schemas when Arrow-native, streaming, or highly dynamic data makes them materially better suited.
6. Prefer `search()` or `query()` builders with explicit `select()` and `limit()` for reads.
7. Avoid table-level full materialization in remote or portable code. This is the main local-vs-remote read pitfall.
8. After a successful embedded OSS ingestion, call `table.optimize()`. Do not call it for Enterprise/Cloud; remote maintenance is automatic.
9. For remote Enterprise/Cloud writes, never drop-then-reuse or `mode="overwrite"` the same table name — see "Enterprise: never drop-then-reuse the same table name" below. This is the main local-vs-remote write pitfall.
10. If reviewing an existing file or repo, run `scripts/check_materialization.py` on the relevant paths and inspect each finding before editing.
11. Cross-check unfamiliar or non-trivial API claims against the source tree instead of relying on memory.
## Core Portability Rule
Do not write code that assumes a local table API will exist on a remote table. Remote tables can be very large, so whole-table materialization helpers are intentionally unavailable or unsafe.
This does **not** mean result conversion is forbidden. Bounded query/search result collection is normal:
- Python: `table.search(...).select([...]).limit(10).to_pandas()`
- TypeScript: `await table.search(...).select([...]).limit(10).toArray()`
The unsafe pattern is table-level or unbounded collection, plus local-only dataset escape hatches in remote code:
- Python: `table.to_pandas()`, `table.to_arrow()`, `table.to_polars()`; `table.to_lance()` is local/OSS-only dataset access, not materialization
- TypeScript: `await table.toArrow()`, `await table.query().toArray()` without `limit()`
## Enterprise: never drop-then-reuse the same table name
LanceDB Enterprise/Cloud splits a **control plane** (DDL: create/drop/rename) from a **data plane** (query nodes that serve reads). Query nodes cache the resolved dataset for a table name for up to `table_cache_ttl`**default 300 seconds (5 minutes)**. After you drop or overwrite a table, the control plane updates immediately but the data plane keeps serving the *old* dataset until that cache entry expires. During the window the two planes disagree.
The failure this causes: you `drop_table("t")` then immediately `create_table("t", ...)` (or `create_table("t", ..., mode="overwrite")`). The DDL returns success, but every query against `t` returns **`500 Internal Server Error`** (the query node resolves the stale/deleted dataset), and a fresh `describe` may still show the *old* schema/version. It looks like your write silently failed; it didn't — the name is cached.
**`mode="overwrite"` has the same problem** — it is a drop+create of the same name under the hood.
Rules for portable Enterprise ingestion:
1. **Never reuse a table name you just dropped/overwrote within the cache TTL.** Do not use `mode="overwrite"` to replace an existing Enterprise table in place.
2. To (re)load data, **write to a fresh table name** (e.g. `<table>_v2`, or a run-stamped suffix). A brand-new name has no cached data-plane entry, so writes and reads work immediately.
3. Before creating, `list_tables()` and **fail loudly if the name already exists** rather than overwriting — prompt for a new name.
4. To land on a specific final name that is currently occupied by an old table: drop the old table, **wait out the TTL (~5 min), then `rename_table(fresh_name, final_name)`**. Renaming onto a name whose old dataset is still cached hits the same race, so the wait is mandatory. `rename_table` is a supported control-plane op.
5. When you hand a table name back to a human, tell them which step still needs the propagation wait (usually: "the old `t` was dropped; run the rename in ~5 minutes").
This is Enterprise/Cloud-specific. Local/OSS tables have no separate data plane, so `mode="overwrite"` and immediate same-name reuse are fine there.
## Script
Run the scanner when reviewing or modifying an existing codebase:
```bash
python skills/lancedb/scripts/check_materialization.py path/to/file_or_dir
```
The script reports likely unsafe full-table materialization in Python and TypeScript. Treat results as review prompts, not automatic proof of a bug.
@@ -1,117 +0,0 @@
# Branch Operations
Manage branches on a LanceDB table: list what exists, create new ones, delete stale ones, and direct read/write operations at a specific branch without touching main. Use for branch lifecycle tasks, experimental/isolated table versions, targeting an operation at a non-main branch, or confirming a mutation did not affect main.
Works on local/OSS and remote Enterprise/Cloud tables.
## The branch model (important)
Branches are isolated, writable lines of history forked from another branch (or a specific version). Writes on a branch never affect `main`.
There is **no global "switch branch" state** — you never repoint the whole table at a branch. Instead, **operations are scoped by which table handle you use**:
- The handle you got from `open_table(name)` / `openTable(name)` targets `main`.
- `branches.create(...)` and `branches.checkout(...)` return a **new table handle scoped to that branch**. Every read/write on that handle (add, update, `update_field_metadata`, `create_index`, search, …) lands on the branch.
- The original main handle is unaffected — keep it around to verify isolation.
`branches.list()` returns only non-main branches. Main always exists and is not listed.
## Python
`table.branches` is a property returning the branch manager; `table.current_branch()` tells you what a handle is scoped to (`None` = main).
```python
table = db.open_table("products") # scoped to main
# list — dict of name -> metadata (parent_branch, parent_version, ...); {} = only main
table.branches.list()
# create: forks from main by default and returns a handle scoped to the new branch
exp = table.branches.create("experiment-reindex")
exp = table.branches.create("exp2", from_ref="main", from_version=None) # optional fork point
# checkout an existing branch -> branch-scoped handle
wip = table.branches.checkout("wip-branch")
# with version= it pins to that version (read-only detached view); omit to track latest, writable
# operate on the branch simply by using its handle
wip.update_field_metadata(
{"path": "category", "metadata": {"lancedb:description": "Product category label."}}
)
wip.create_scalar_index("category")
# delete: removes only the branch pointer; main and row data remain intact
table.branches.delete("stale-2024")
# alternatively, open a branch handle directly from the connection
wip = db.open_table("products", branch="wip-branch")
exp.current_branch() # "experiment-reindex"
table.current_branch() # None (main)
```
Async: same shape — `table.branches` returns `AsyncBranches`; `await table.branches.create(...)` etc.
## TypeScript
`table.branches()` is an **async method** returning the `Branches` manager; `table.currentBranch()` returns the scoped branch or `null` for main.
```typescript
const table = await db.openTable("products"); // scoped to main
const branches = await table.branches();
// list — Record<string, BranchContents>; {} = only main
await branches.list();
// create: forks from main by default, returns a Table scoped to the new branch
const exp = await branches.create("experiment-reindex");
const exp2 = await branches.create("exp2", "main" /* fromRef */, undefined /* fromVersion */);
// checkout an existing branch -> branch-scoped Table
const wip = await branches.checkout("wip-branch");
// with a version arg it pins (read-only detached view); omit to track latest, writable
// operate on the branch simply by using its handle
await wip.updateFieldMetadata([
{ path: "category", metadata: { "lancedb:description": "Product category label." } },
]);
await wip.createIndex("category");
// delete: removes only the branch pointer; main and row data remain intact
await branches.delete("stale-2024");
// alternatively, open a branch handle directly from the connection
const wip2 = await db.openTable("products", { branch: "wip-branch" });
exp.currentBranch(); // "experiment-reindex"
table.currentBranch(); // null (main)
```
## Verifying isolation
After writing to a branch, confirm the change did NOT land on main by reading through both handles:
```python
wip = table.branches.checkout("wip-branch")
wip.update_field_metadata({"path": "category", "metadata": {"lancedb:description": "..."}})
assert b"lancedb:description" in (wip.schema.field("category").metadata or {})
assert b"lancedb:description" not in (table.schema.field("category").metadata or {}) # main untouched
```
Two handles on the same branch see each other's writes (e.g. `table.branches.create("exp")` and `db.open_table(name, branch="exp")`); main stays isolated.
## Quick reference
| Goal | Python | TypeScript |
|------|--------|------------|
| List branches (non-main) | `table.branches.list()` | `await (await table.branches()).list()` |
| Create branch (off main) | `table.branches.create(name)` → branch handle | `await branches.create(name)` → branch `Table` |
| Create from a fork point | `table.branches.create(name, from_ref=..., from_version=...)` | `await branches.create(name, fromRef, fromVersion)` |
| Get a branch handle | `table.branches.checkout(name)` or `db.open_table(t, branch=name)` | `await branches.checkout(name)` or `await db.openTable(t, { branch: name })` |
| Pin to a branch version (read-only) | `table.branches.checkout(name, version=v)` | `await branches.checkout(name, v)` |
| Delete branch | `table.branches.delete(name)` | `await branches.delete(name)` |
| Which branch is this handle on? | `table.current_branch()` (`None` = main) | `table.currentBranch()` (`null` = main) |
| Target main | use the original (non-branch) handle | use the original (non-branch) handle |
Branch names must be non-empty; empty names raise a validation error.
@@ -1,183 +0,0 @@
# Column Metadata Authoring
Write column-level descriptions, tags, and logical groupings onto a LanceDB table's schema. Use this when the user wants to document, annotate, tag, or classify what their table columns ARE (embeddings vs labels vs eval metrics, model provenance, version families, etc.).
Works on local/OSS and remote Enterprise/Cloud tables alike — read the schema through the table handle, write through `update_field_metadata` (Python) / `updateFieldMetadata` (TypeScript).
## Metadata key conventions
All metadata uses namespaced keys:
| Key | Purpose | Example value |
|-----|---------|---------------|
| `lancedb:description` | Human-readable explanation of what the column contains | `"CLIP ViT-L/14 image embedding, L2-normalized (768-dim)"` |
| `lancedb:tag:<name>` | Flexible key-value tag; the suffix names the tag category | `lancedb:tag:field_type: "embedding"`, `lancedb:tag:model: "clip"`, `lancedb:tag:project_id: "foo"` |
| `lancedb:logical-column` | Logical group/family this column belongs to | `"clip_features"` |
Tags are open-ended — use whatever key suffix and value make sense given the user's intent. The tag suffix should describe *what is being classified* (e.g., `field_type`, `model`, `project_id`) and the value describes *how*. Multiple tags on the same column are fine — each is a separate key. All values are strings.
## Step 1: Read the schema and existing metadata
Read existing metadata before writing, to avoid redundant updates.
Python — `table.schema` (sync property; async: `await table.schema()`) returns a `pyarrow.Schema`. **Arrow field metadata is bytes-keyed in Python**:
```python
schema = table.schema
for field in schema:
meta = field.metadata or {} # dict[bytes, bytes], e.g. {b"lancedb:description": b"..."}
print(field.name, field.type, field.nullable, meta)
```
TypeScript — `await table.schema()` returns an Arrow `Schema`; field metadata is a `Map<string, string>`:
```typescript
const schema = await table.schema();
for (const field of schema.fields) {
console.log(field.name, field.type, field.nullable, field.metadata); // Map
// field.metadata.get("lancedb:description")
}
```
For struct/nested fields, recurse into the field's children and address them as dot-paths (e.g., `parent.child`).
If the user hasn't specified which columns to update, work with all columns.
## Step 2: Generate metadata
Decide what to generate based on the user's request.
### Descriptions (`lancedb:description`)
Base descriptions on:
- The column name and Arrow type (e.g., `FixedSizeList` of floats → likely an embedding)
- User-supplied context (upstream pipeline, sample values, domain knowledge)
- Name patterns: `_embedding`/`_vec`/`_embed` → vector; `_label`/`_class` → label; `_score`/`_eval`/`_metric` → evaluation metric
Be specific and concise. Good: `"Sentence-BERT embedding of the query text (768-dim)."` Not: `"An embedding column."`
### Tags (`lancedb:tag:<name>`)
Choose tag key names that match what the user asked to annotate. Common patterns:
- Semantic field type → `lancedb:tag:field_type: "embedding"` / `"text"` / `"image"` / `"label"` / `"eval"` / `"id"` / `"metadata"`
- Model or source → `lancedb:tag:model: "clip"` / `"bert"` / `"vit"`
- Project affiliation → `lancedb:tag:project_id: "<name>"`
- Version → `lancedb:tag:version: "v3"` (and `lancedb:tag:latest: "true"` for the newest)
Use Arrow type as a hint: `FixedSizeList` + float → embedding; `Utf8`/`LargeUtf8` → text; `Binary` → image or blob.
### Logical groupings (`lancedb:logical-column`)
Look for naming patterns across columns:
- `clip_v1`, `clip_v2`, `clip_v3` → logical column `"clip"`, latest is `v3`
- `text_embed_20240101`, `text_embed_20240601` → logical column `"text_embed"`, latest is the most recent date suffix
Write `lancedb:logical-column` on all members of a group. Mark the newest with `lancedb:tag:latest: "true"` (in addition to its version tag).
## Step 3: Write the metadata
Each update names a field by dot-path and carries a metadata map. Semantics (identical in both SDKs):
- **Merge by default** (`replace` omitted/false) — preserves existing metadata the user didn't ask to change
- `replace: true` swaps the field's entire metadata map — only if the user explicitly asks to overwrite
- A value of `None`/`null` deletes that specific key
- Batch all field updates into a single call when possible
- Returns the new table version
Python (sync and async take one dict per field, as varargs):
```python
res = table.update_field_metadata(
{
"path": "clip_v3",
"metadata": {
"lancedb:description": "CLIP ViT-L/14 image embedding, L2-normalized (1024-dim).",
"lancedb:tag:field_type": "embedding",
"lancedb:tag:model": "clip",
"lancedb:tag:version": "v3",
"lancedb:tag:latest": "true",
"lancedb:logical-column": "clip",
},
},
{
"path": "clip_v2",
"metadata": {
"lancedb:description": "CLIP ViT-B/32 image embedding (768-dim), superseded by v3.",
"lancedb:tag:field_type": "embedding",
"lancedb:tag:model": "clip",
"lancedb:tag:version": "v2",
"lancedb:logical-column": "clip",
},
},
)
print(res.version) # new table version
# merge semantics: add a key, delete one via None, keep the rest
table.update_field_metadata(
{"path": "clip_v2", "metadata": {"lancedb:tag:archived": "true", "lancedb:tag:latest": None}}
)
```
(`replace_field_metadata` is deprecated — use `update_field_metadata`.)
TypeScript (takes an array of `FieldMetadataUpdate`):
```typescript
const res = await table.updateFieldMetadata([
{
path: "clip_v3",
metadata: {
"lancedb:description": "CLIP ViT-L/14 image embedding, L2-normalized (1024-dim).",
"lancedb:tag:field_type": "embedding",
"lancedb:tag:model": "clip",
"lancedb:tag:version": "v3",
"lancedb:tag:latest": "true",
"lancedb:logical-column": "clip",
},
},
{
path: "clip_v2",
metadata: {
"lancedb:description": "CLIP ViT-B/32 image embedding (768-dim), superseded by v3.",
"lancedb:tag:field_type": "embedding",
"lancedb:tag:model": "clip",
"lancedb:tag:version": "v2",
"lancedb:logical-column": "clip",
},
},
]);
console.log(res.version); // new table version
// merge semantics: add a key, delete one via null, keep the rest
await table.updateFieldMetadata([
{ path: "clip_v2", metadata: { "lancedb:tag:archived": "true", "lancedb:tag:latest": null } },
]);
```
## Step 4: Confirm
Report back:
- Which columns were updated and what was written
- The new table version number (from the result)
- Any columns skipped (e.g., already had up-to-date metadata)
## Quick examples
**"Write descriptions for all columns in the `product_embeddings` table"**
1. Read `table.schema` → all fields + existing metadata
2. Generate a `lancedb:description` for each column based on name + type
3. One `update_field_metadata` call with all descriptions
4. Report
**"Tag the columns in `model_outputs` with their field type and model"**
1. Read the schema
2. For each field, classify by name + Arrow type → set `lancedb:tag:field_type` and `lancedb:tag:model` where applicable
3. Write in one batched call
4. Report
**"Group the feature columns in `training_features` into logical families and mark the latest version"**
1. Read the schema
2. Find version patterns → assign `lancedb:logical-column` and `lancedb:tag:version`; mark newest with `lancedb:tag:latest: "true"`
3. Write in one batched call
4. Show the grouping
@@ -1,131 +0,0 @@
# Python API Reference
Quick method reference for Python LanceDB code. Cross-check source for non-trivial claims.
## Connect
```python
import lancedb
db = lancedb.connect("./camelot-db") # local/OSS
db = lancedb.connect("db://my-db", api_key=api_key, region=region) # remote
```
**Place the local database directory next to the script/entrypoint that opens it** (i.e. resolve the path relative to the script, `Path(__file__).parent / "camelot-db"`), not buried under a shared `data/` folder. The Lance dataset is the database, not a data file — keeping it beside its code makes ownership obvious and paths stable regardless of the working directory the script is launched from.
**Do not name the directory `lancedb`** (e.g. `./lancedb`, `./data/lancedb`). It collides with the imported `lancedb` package name, which is confusing to read and easy to shadow in scripts. Give it a name derived from the repo or dataset with a clear prefix/suffix — for example `./<dataset>-db`, `./<repo>_lancedb`, or `./vectordb`.
Async:
```python
db = await lancedb.connect_async("./camelot-db")
```
## Table Reads
| Task | Preferred API |
| --- | --- |
| Vector search | `table.search(query_vector).limit(k)` |
| Full scan with filters/projection (sync) | `table.search().where(...).select(...).limit(...)` |
| Full scan with filters/projection (async) | `table.query().where(...).select(...).limit(...)` |
| Filter | `.where("col > 10")` |
| Projection | `.select(["id", "text"])` |
| Bound result count | `.limit(20)` |
| Collect bounded result as Python objects (default, no extra deps) | `.to_list()` on query/search result |
| Collect bounded result as Arrow (default, `pyarrow` always available) | `.to_arrow()` on query/search result |
| Collect bounded result as pandas (only if project uses pandas) | `.to_pandas()` on query/search result |
| Collect bounded result as Polars (only if project uses polars) | `.to_polars()` on query/search result |
## Sync vs Async Scan API
The plain-scan entry point differs between the sync and async clients. **Verified against `lancedb` 0.34.0** — re-check if the pinned version changes:
- **Sync** (`lancedb.connect(...)`): the table has **no `.query()` method**. Use `.search()` with no argument for a plain scan; it returns a query builder that supports `.where()`, `.select()`, `.limit()`, and the `.to_list()` / `.to_arrow()` / `.to_pandas()` / `.to_polars()` collectors.
```python
rows = table.search().where("status = 'ready'").select(["id", "text"]).limit(20).to_list()
```
- **Async** (`lancedb.connect_async(...)`): the table has **both** `.query()` and `.search()`. Use `.query()` for a plain scan.
```python
rows = await async_table.query().where("status = 'ready'").select(["id", "text"]).limit(20).to_list()
```
Do not call `table.query()` on a sync table — it raises `AttributeError`.
## Local vs Remote Table Methods
| API | Local table | Remote table | Agent guidance |
| --- | --- | --- | --- |
| `table.search(...)` | Yes | Yes | Preferred read path (sync + async) |
| `table.query()` | Async only | Async only | Sync scan path is `table.search()`; `.query()` is the async scan builder |
| `table.to_pandas()` | Yes | No / unsafe for portability | Avoid in portable code |
| `table.to_arrow()` | Yes | No / unsafe for portability | Avoid in portable code |
| `table.to_polars()` | Yes | No / unsafe for portability | Avoid in portable code |
| `table.to_lance()` | Yes | No | Local/OSS escape hatch only |
## Indexes
Use `create_index(...)` for vector indexes and modern index configs. Use scalar indexes for filtered or merge keys.
Common calls:
```python
table.create_index("vector")
table.create_scalar_index("status")
table.create_fts_index("text")
```
Check source docs before specifying advanced index config names or parameters.
## Filtering And Recall Knobs
```python
table.search(query_vector).where("status = 'ready'") # pre-filter by default
table.search(query_vector).where("status = 'ready'", prefilter=False)
table.search(query_vector).limit(10).refine_factor(20)
table.search(query_vector).limit(10).nprobes(50)
```
Use post-filtering only when fewer than `limit` results are acceptable.
## Diagnostics
```python
print(table.search(query_vector).where("year > 2000").limit(10).analyze_plan())
print(table.index_stats("vector_idx"))
```
Use these before changing indexes or search tuning.
## Column (Field) Metadata
```python
schema = table.schema # sync property; async: await table.schema()
meta = schema.field("category").metadata # dict[bytes, bytes] — Arrow metadata is bytes-keyed
res = table.update_field_metadata( # varargs: one dict per field; works local + remote
{"path": "category", "metadata": {"lancedb:description": "...", "lancedb:tag:field_type": "label"}}
)
res.version # new table version
```
Merges by default; a `None` value deletes that key; `"replace": True` swaps the whole map. Nested fields use dot-paths (`"a.b.c"`). `replace_field_metadata` is deprecated. See `references/column_metadata.md` for key conventions (`lancedb:description`, `lancedb:tag:<name>`, `lancedb:logical-column`) and the authoring workflow.
## Branches
```python
table.branches.list() # non-main branches; {} = only main
exp = table.branches.create("exp") # fork off main -> handle scoped to the branch
wip = table.branches.checkout("wip") # existing branch -> scoped handle (version= pins read-only)
wip = db.open_table("t", branch="wip") # or open scoped directly
table.branches.delete("stale") # removes only the branch pointer
table.current_branch() # None = main
```
There is no global switch — scoping is per table handle: any read/write on a branch handle lands on that branch; the original handle keeps targeting main. See `references/branch_ops.md` for the model and isolation checks.
## Maintenance
```python
table.optimize()
```
Call this after every successful local/OSS ingestion. It handles compaction, cleanup of old versions according to retention, and index optimization. Do not add this for LanceDB Enterprise/Cloud remote tables; Enterprise handles compaction and cleanup automatically from cluster configuration.
@@ -1,173 +0,0 @@
# Python Patterns
Use these patterns when writing Python code with `lancedb`.
## Before Writing Code
Choose the output type from what the project actually depends on. **Do not assume `pandas` or `polars` is installed** — they are heavy dependencies that many LanceDB projects do not use. `pyarrow`, by contrast, ships as a LanceDB dependency and is always available, so it is a safe default to lean on.
Default output (after applying `select()` and `limit()`):
- **Python objects**: `.to_list()` — a list of dicts, no extra dependencies. Prefer this for scripts, examples, and agent-generated code unless there is a reason to do otherwise.
- **PyArrow**: `.to_arrow()` — a `pyarrow.Table`, when the surrounding code is Arrow-native or you need columnar/zero-copy handoff.
Only reach for a DataFrame when the project *already* declares that dependency:
- Pandas projects (pandas in `pyproject.toml`/requirements): `.to_pandas()`.
- Polars projects (polars declared): `.to_polars()`.
If unsure, check the dependency manifest or the imports in surrounding files. When in doubt, use `.to_list()` or `.to_arrow()`.
## Schema Design and Validation
Favor `LanceModel` and Pydantic validation for Python schemas. They keep field
types readable, validate source records before a write, and map directly to a
LanceDB schema. Use `Vector(dimension)` for fixed-size vectors:
```python
from lancedb.pydantic import LanceModel, Vector
class Document(LanceModel):
id: int
text: str
vector: Vector(384, nullable=False)
rows = [Document.model_validate(row) for row in source_rows]
table = db.create_table("documents", schema=Document)
table.add(rows)
```
Use PyArrow schemas instead when the pipeline is already Arrow-native, needs
record-batch streaming, or has runtime schema requirements that would make a
Pydantic model harder to understand. Declare Pydantic as a direct project
dependency when application code imports it, even if LanceDB also depends on it.
## Recommended Patterns
### Bounded search or query
Use this for application reads, examples, notebooks, and agent-generated scripts:
```python
results = (
table.search(query_vector)
.where("status = 'ready'")
.select(["id", "text"])
.limit(20)
.to_list() # or .to_arrow(); .to_pandas()/.to_polars() only if the project uses them
)
```
Why: `search()` works across local and remote tables and on both the sync and async clients. `select()` avoids fetching unused columns. `limit()` prevents accidental full-table reads. `.to_list()` and `.to_arrow()` avoid assuming pandas/polars is installed (see "Before Writing Code").
For a **plain scan** (no query vector), the entry point differs by client:
```python
# Sync client: no .query() method — use .search() with no argument.
rows = table.search().where("status = 'ready'").select(["id", "text"]).limit(20).to_list()
# Async client: use .query().
rows = await async_table.query().where("status = 'ready'").select(["id", "text"]).limit(20).to_list()
```
`table.query()` on a sync table raises `AttributeError` (verified on `lancedb` 0.34.0). See the "Sync vs Async Scan API" section in `api_reference.md`.
### Bounded query result conversion
It is fine to collect bounded query/search results:
```python
arrow_table = table.search().select(["id"]).limit(100).to_arrow() # sync plain scan
rows = table.search(query_vector).limit(10).to_list()
df = table.search(query_vector).limit(10).to_pandas() # only if pandas is a project dep
```
### Local-only Lance dataset API
`table.to_lance()` does not itself materialize the full dataset. It returns the underlying `lance.LanceDataset`, making the table accessible through the PyLance dataset API. Use it when the task is explicitly local/OSS and needs Lance dataset methods not exposed by LanceDB:
```python
# Local/OSS only: RemoteTable does not expose table.to_lance().
ds = table.to_lance()
for batch in ds.to_batches(columns=["id", "text"], batch_size=10_000):
process(batch)
```
### Async Python
Keep the same shape and bound the result before collecting:
```python
results = await (
async_table.query()
.where("status = 'ready'")
.select(["id", "text"])
.limit(20)
.to_list() # or .to_arrow()
)
```
## Anti-Patterns
**Avoid the following anti-patterns in your code.**
### Table-level full materialization
Avoid whole-table collectors in portable or large-table code:
```python
df = table.to_pandas()
arrow_table = table.to_arrow()
polars_df = table.to_polars()
```
Why: local tables expose these whole-table collectors, but remote tables intentionally do not — a remote production table can be far larger than a local development table, so it is easy to accidentally pull the entire table into memory.
`table.to_lance()` is different: it is not a full materialization call, but it is still local/OSS-only and should not appear in code meant to run against remote Enterprise tables.
### Unbounded result collection
Avoid query/search collection without a meaningful limit:
```python
rows = table.search().to_list() # unbounded plain scan
rows = table.search(query_vector).to_list() # unbounded vector search
```
Prefer `select(...).limit(...)` before collecting; for large reads, stream in batches instead.
### Per-row writes
Avoid loops that write one row per call:
```python
for row in rows:
table.add([row]) # one commit + fragment per row
```
Each `add()` creates a new version and fragment. Pass the whole batch in a single call, or chunk very large inputs:
```python
table.add(rows) # single commit
# for very large inputs, add batches of several thousand rows
```
After the final successful write to an embedded OSS table, call
`table.optimize()`. Skip this for Enterprise/Cloud tables because their
maintenance is automatic.
### Drop-then-reuse the same table name (Enterprise/Cloud)
Avoid dropping or overwriting a remote table and then reusing that name right away:
```python
db.drop_table("my_table")
table = db.create_table("my_table", data=rows) # reads 500 for ~5 min
table = db.create_table("my_table", data=rows, mode="overwrite") # same problem
```
Why: Enterprise/Cloud splits DDL (control plane) from query serving (data plane). The data plane caches the dataset behind a table name for up to `table_cache_ttl` (default 300s / 5 min), so after a drop/overwrite the DDL succeeds but queries against the reused name return `500 Internal Server Error` until the cache expires — and a fresh `describe` may still show the old schema. Instead, write to a **fresh name**, use `list_tables()` and fail if it already exists, then `rename_table(fresh, final)` onto the final name only after the old table's drop has propagated (~5 min). See the "Enterprise: never drop-then-reuse the same table name" section in `SKILL.md`. Local/OSS tables have no separate data plane — overwrite freely there.
### Guessing performance fixes
Avoid changing `nprobes`, `refine_factor`, or index types before checking the query plan and index stats. Diagnose first, then tune one knob at a time.
@@ -1,131 +0,0 @@
# Python Performance Guidance
Use this when writing Python code that ingests data, queries large tables, builds indexes, or investigates latency.
## Ingestion
### Recommended: validate schemas and records with Pydantic
Favor `LanceModel` for readable Python schema definitions and validate source
records before writing. Use PyArrow directly for Arrow-native or streaming
pipelines where it is the clearer representation.
```python
from lancedb.pydantic import LanceModel, Vector
class Document(LanceModel):
id: int
text: str
vector: Vector(384, nullable=False)
rows = [Document.model_validate(row) for row in source_rows]
table = db.create_table("documents", schema=Document)
table.add(rows)
```
### Recommended: bulk ingestion for materialized data
```python
table.add(arrow_table)
table.add(df)
table.add(pa.dataset("data/", format="parquet"))
```
For very large initial loads, create the table empty first, then call `add(...)`. Passing data directly to `create_table(name, data)` can skip the auto-parallel write path.
### Recommended: iterator ingestion for generated or streamed data
```python
def batches():
for raw in source:
vectors = model.encode(raw["text"])
yield pa.RecordBatch.from_pydict({**raw, "vector": vectors})
table.add(batches())
```
Use chunks of several thousand rows or more when practical. Tiny batches and per-row writes create many small fragments.
### Anti-pattern: per-row `add()`
```python
for row in rows:
table.add([row])
```
Each call creates a version and fragment. This slows ingestion and later queries.
## Indexing
- Build a vector index once brute-force vector search becomes too slow. As a rule of thumb, local brute force is fine below roughly 100K vectors; beyond that, build an index.
- Use `IVF_PQ` as the general-purpose default. Enterprise builds this automatically.
- Use scalar indexes for filtered columns and merge/upsert keys.
- Use `BTREE` for mostly distinct numeric/string/temporal columns, `BITMAP` for booleans and low-cardinality columns, and `LABEL_LIST` for list membership queries.
- Keep full-text defaults unless phrase queries require position data.
## Querying
Always be explicit:
```python
table.search(query_vector).select(["id", "title"]).limit(20)
```
- `select()` reduces bytes read and transferred.
- `limit()` prevents accidental full-table materialization.
- Pre-filtering is the default and guarantees returned rows satisfy the predicate.
- Use post-filtering only when fewer than `limit` results are acceptable.
## Recall Tuning
Tune one knob at a time:
- Quantized indexes: raise `refine_factor` to rescore more candidates on full vectors.
- HNSW-backed indexes: raise `ef`; start around `1.5 * k`, increase toward `10 * k` if recall is short.
- IVF candidate breadth: `nprobes` is auto-tuned; override only when a selective pre-filter leaves too few neighbors.
## Maintenance
After every successful embedded OSS/local ingestion, call `table.optimize()`.
Do not add this to LanceDB Enterprise/Cloud remote table code; remote compaction
and cleanup are handled automatically based on the Enterprise cluster
configuration.
Why local maintenance is needed:
- Frequent writes can create many small fragments. Queries then need to scan across more files, which can increase latency.
- Updates, deletes, and appends create new table versions. Old versions are retained for time travel and rollback, which can grow disk usage.
- Indexes may have newly added rows that are not yet fully optimized into the index structure.
For local/OSS tables, run `optimize()` after the final successful ingestion
write. Also run it after later batches of update/delete operations or on a
regular maintenance schedule:
```python
table.optimize()
```
If the user wants more aggressive local disk cleanup, pass a shorter cleanup retention window:
```python
from datetime import timedelta
table.optimize(cleanup_older_than=timedelta(days=1))
```
Do not use very short cleanup windows when the application depends on time travel, rollback, or old versions.
## Diagnostics
Before changing code or indexes, inspect:
```python
print(table.search(query_vector).where("year > 2000").limit(10).analyze_plan())
print(table.index_stats("vector_idx"))
```
Look for high scan bytes, missing indexes, fragmented data, and unindexed rows.
## Python Multiprocessing
When using multiprocessing, use `spawn` rather than `fork`. LanceDB is multi-threaded internally, and `fork` plus a multi-threaded process is unsafe.
@@ -1,105 +0,0 @@
# TypeScript API Reference
Quick method reference for TypeScript LanceDB code. Cross-check source for non-trivial claims.
## Connect
```typescript
import * as lancedb from "@lancedb/lancedb";
const db = await lancedb.connect("./camelot-db");
```
**Place the local database directory next to the script/entrypoint that opens it** (resolve the path relative to the module, e.g. via `import.meta.dirname` / `__dirname`), not buried under a shared `data/` folder. The Lance dataset is the database, not a data file — keeping it beside its code makes ownership obvious and paths stable regardless of the working directory the script is launched from.
**Do not name the directory `lancedb`** (e.g. `./lancedb`, `./data/lancedb`). It collides with the imported `lancedb` package/namespace, which is confusing to read. Give it a name derived from the repo or dataset with a clear prefix/suffix — for example `./<dataset>-db`, `./<repo>_lancedb`, or `./vectordb`.
Remote connections use `db://...` plus Enterprise/Cloud credentials and deployment settings. Check current source/docs for exact connection options.
## Table Reads
| Task | Preferred API |
| --- | --- |
| Vector search | `table.search(queryVector).limit(k)` |
| Full scan with filters/projection | `table.query().where(...).select(...).limit(...)` |
| Filter | `.where("col > 10")` |
| Projection | `.select(["id", "text"])` |
| Bound result count | `.limit(20)` |
| Collect bounded result as objects | `.toArray()` on query/search result |
| Collect bounded result as Arrow | `.toArrow()` on query/search result |
| Stream result batches | `for await (const batch of table.query()...)` |
## Local vs Remote Safety
| API | Agent guidance |
| --- | --- |
| `table.search(...)` | Preferred read path |
| `table.query()` | Preferred scan/filter path |
| `await table.toArrow()` | Avoid in portable or large-table code |
| `await table.query().toArray()` with no `limit()` | Avoid; unbounded collection |
| `await table.query().toArrow()` with no `limit()` | Avoid; unbounded collection |
## Indexes
```typescript
await table.createIndex("vector");
await table.createIndex("status");
```
Use vector indexes for large vector search workloads and scalar indexes for filtered columns or merge/upsert keys. Check source/docs before specifying advanced index options.
## Filtering And Recall Knobs
```typescript
await table.search(queryVector).where("status = 'ready'").limit(10).toArray();
await table.search(queryVector).limit(10).refineFactor(20).toArray();
await table.search(queryVector).limit(10).nprobes(50).toArray();
await table.search(queryVector).limit(10).ef(100).toArray();
await table.search(queryVector).where("status = 'ready'").postfilter().limit(10).toArray();
```
Use `postfilter()` only when fewer than `limit` results are acceptable.
## Diagnostics
```typescript
console.log(await table.search(queryVector).where("year > 2000").limit(10).analyzePlan());
console.log(await table.indexStats("vector_idx"));
```
Use these before changing indexes or search tuning.
## Column (Field) Metadata
```typescript
const schema = await table.schema();
const meta = schema.fields.find((f) => f.name === "category")?.metadata; // Map<string, string>
const res = await table.updateFieldMetadata([
{ path: "category", metadata: { "lancedb:description": "...", "lancedb:tag:field_type": "label" } },
]);
res.version; // new table version
```
Merges by default; a `null` value deletes that key; `replace: true` swaps the whole map. Nested fields use dot-paths (`"a.b.c"`). See `references/column_metadata.md` for key conventions (`lancedb:description`, `lancedb:tag:<name>`, `lancedb:logical-column`) and the authoring workflow.
## Branches
```typescript
const branches = await table.branches(); // async manager
await branches.list(); // non-main branches; {} = only main
const exp = await branches.create("exp"); // fork off main -> Table scoped to the branch
const wip = await branches.checkout("wip"); // existing branch -> scoped Table (version arg pins read-only)
const wip2 = await db.openTable("t", { branch: "wip" }); // or open scoped directly
await branches.delete("stale"); // removes only the branch pointer
table.currentBranch(); // null = main
```
There is no global switch — scoping is per table handle: any read/write on a branch handle lands on that branch; the original handle keeps targeting main. See `references/branch_ops.md` for the model and isolation checks.
## Maintenance
```typescript
await table.optimize();
```
Call this after every successful local/OSS ingestion. It handles compaction, cleanup of old versions according to retention, and index optimization. Do not add this for LanceDB Enterprise/Cloud remote tables; Enterprise handles compaction and cleanup automatically from cluster configuration.
@@ -1,100 +0,0 @@
# TypeScript Patterns
Use these patterns when writing TypeScript code with `@lancedb/lancedb`.
## Recommended Patterns
### Bounded query
Use this for application reads, scripts, and examples:
```typescript
const rows = await table
.query()
.where("status = 'ready'")
.select(["id", "text"])
.limit(20)
.toArray();
```
### Bounded vector search
```typescript
const rows = await table
.search(queryVector)
.select(["id", "text"])
.limit(20)
.toArray();
```
### Batch streaming for larger reads
When the task needs many rows, avoid collecting everything at once:
```typescript
for await (const batch of table
.query()
.where("status = 'ready'")
.select(["id", "text"])
.limit(10_000)) {
process(batch);
}
```
## Anti-Patterns
**Avoid the following anti-patterns in your code.**
### Table-level full materialization
Avoid whole-table collectors in portable or large-table code:
```typescript
const tableArrow = await table.toArrow();
```
Why: local tables expose these whole-table collectors, but remote tables intentionally do not — a remote production table can be far larger than a local development table, so it is easy to accidentally pull the entire table into memory.
### Unbounded result collection
Avoid query/search collection without a meaningful limit:
```typescript
const rows = await table.query().toArray(); // unbounded plain scan
const rows = await table.search(queryVector).toArray(); // unbounded vector search
```
Prefer `select(...).limit(...)` before collecting; for large reads, stream in batches instead.
### Per-row writes
Avoid loops that write one row per call:
```typescript
for (const row of rows) {
await table.add([row]); // one commit + fragment per row
}
```
Each `add()` creates a new version and fragment. Pass the whole batch in a single call, or chunk very large inputs:
```typescript
await table.add(rows); // single commit
// for very large inputs, add in chunks of several thousand rows
```
### Drop-then-reuse the same table name (Enterprise/Cloud)
Avoid dropping or overwriting a remote table and then reusing that name right away:
```typescript
await db.dropTable("my_table");
const table = await db.createTable("my_table", rows); // reads 500 for ~5 min
const table = await db.createTable("my_table", rows, { mode: "overwrite" }); // same problem
```
Why: Enterprise/Cloud splits DDL (control plane) from query serving (data plane). The data plane caches the dataset behind a table name for up to `table_cache_ttl` (default 300s / 5 min), so after a drop/overwrite the DDL succeeds but queries against the reused name return `500 Internal Server Error` until the cache expires — and a fresh `describe` may still show the old schema. Instead, write to a **fresh name**, use `tableNames()` and fail if it already exists, then `renameTable(fresh, final)` onto the final name only after the old table's drop has propagated (~5 min). See the "Enterprise: never drop-then-reuse the same table name" section in `SKILL.md`. Local/OSS tables have no separate data plane — overwrite freely there.
### Guessing performance fixes
Avoid changing `nprobes`, `refineFactor`, `ef`, or index settings before checking `analyzePlan()` and `indexStats(...)`. Diagnose first, then tune one knob at a time.
@@ -1,78 +0,0 @@
# TypeScript Performance Guidance
Use this when writing TypeScript code that ingests data, queries large tables, builds indexes, or investigates latency.
## Ingestion
- Prefer bulk or batched writes.
- Avoid per-row write loops; they create many small commits/fragments.
- For generated data, accumulate reasonable batches before adding.
- For file-backed data, prefer APIs that stream from Arrow/Parquet-style inputs when available.
## Indexing
- Build a vector index once brute-force vector search becomes too slow. As a rule of thumb, local brute force is fine below roughly 100K vectors; beyond that, build an index.
- Use the general-purpose vector index defaults unless the task has explicit recall/latency requirements.
- Build scalar indexes for filtered columns and merge/upsert keys.
- Use full-text index phrase options only when phrase queries require them.
## Querying
Always be explicit:
```typescript
await table.search(queryVector).select(["id", "title"]).limit(20).toArray();
```
- `select()` reduces bytes read and transferred.
- `limit()` prevents accidental full-table collection.
- Pre-filtering is the default behavior. Use `postfilter()` only when fewer than `limit` results are acceptable.
## Recall Tuning
Tune one knob at a time:
- Quantized indexes: raise `refineFactor(...)` to rescore more candidates on full vectors.
- HNSW-backed indexes: raise `ef(...)`; start around `1.5 * k`, increase toward `10 * k` if recall is short.
- IVF candidate breadth: `nprobes(...)` is usually auto-tuned; override only when a selective pre-filter leaves too few neighbors.
## Maintenance
After every successful embedded OSS/local ingestion, call `table.optimize()`.
Do not add this to LanceDB Enterprise/Cloud remote table code; remote compaction
and cleanup are handled automatically based on the Enterprise cluster
configuration.
Why local maintenance is needed:
- Frequent writes can create many small fragments. Queries then need to scan across more files, which can increase latency.
- Updates, deletes, and appends create new table versions. Old versions are retained for time travel and rollback, which can grow disk usage.
- Indexes may have newly added rows that are not yet fully optimized into the index structure.
For local/OSS tables, run `optimize()` after the final successful ingestion
write. Also run it after later batches of update/delete operations or on a
regular maintenance schedule:
```typescript
await table.optimize();
```
If the user wants more aggressive local disk cleanup, pass a shorter cleanup retention window:
```typescript
const olderThan = new Date(Date.now() - 24 * 60 * 60 * 1000);
await table.optimize({ cleanupOlderThan: olderThan });
```
Do not use very short cleanup windows when the application depends on time travel, rollback, or old versions.
## Diagnostics
Before changing code or indexes, inspect:
```typescript
console.log(await table.search(queryVector).where("year > 2000").limit(10).analyzePlan());
console.log(await table.indexStats("vector_idx"));
```
Look for high scan cost, missing indexes, fragmented data, and unindexed rows.
@@ -1,135 +0,0 @@
#!/usr/bin/env python3
"""Scan Python and TypeScript for likely unsafe LanceDB materialization."""
from __future__ import annotations
import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path
PY_FULL_TABLE = re.compile(r"\b\w+\.(to_pandas|to_arrow|to_polars)\s*\(")
TS_TABLE_TO_ARROW = re.compile(r"\b\w+\.toArrow\s*\(")
TS_QUERY_COLLECTOR = re.compile(r"\.query\s*\(\s*\)[\s\S]*?\.to(Array|Arrow)\s*\(")
@dataclass
class Finding:
path: Path
line: int
message: str
text: str
def iter_files(paths: list[Path]) -> list[Path]:
files: list[Path] = []
for path in paths:
if path.is_dir():
files.extend(
p
for p in path.rglob("*")
if p.suffix in {".py", ".ts", ".tsx"} and "node_modules" not in p.parts
)
elif path.suffix in {".py", ".ts", ".tsx"}:
files.append(path)
return sorted(set(files))
def line_number(text: str, offset: int) -> int:
return text.count("\n", 0, offset) + 1
def scan_python(path: Path, text: str) -> list[Finding]:
findings: list[Finding] = []
for match in PY_FULL_TABLE.finditer(text):
line_start = text.rfind("\n", 0, match.start()) + 1
line_end = text.find("\n", match.start())
if line_end == -1:
line_end = len(text)
line = text[line_start:line_end].strip()
if ".search(" in line or ".query(" in line:
continue
findings.append(
Finding(
path,
line_number(text, match.start()),
f"Review Python `{match.group(1)}()` call; table-level materialization is not portable to remote tables.",
line,
)
)
return findings
def statement_around(text: str, start: int, end: int) -> str:
before = max(text.rfind(";", 0, start), text.rfind("\n\n", 0, start))
after_candidates = [pos for pos in (text.find(";", end), text.find("\n\n", end)) if pos != -1]
after = min(after_candidates) if after_candidates else len(text)
return text[before + 1 : after].strip()
def scan_typescript(path: Path, text: str) -> list[Finding]:
findings: list[Finding] = []
for match in TS_TABLE_TO_ARROW.finditer(text):
stmt = statement_around(text, match.start(), match.end())
if ".query(" in stmt or ".search(" in stmt:
continue
findings.append(
Finding(
path,
line_number(text, match.start()),
"Review TypeScript `table.toArrow()`-style call; table-level materialization is not portable for large/remote tables.",
stmt.splitlines()[0].strip(),
)
)
for match in TS_QUERY_COLLECTOR.finditer(text):
stmt = statement_around(text, match.start(), match.end())
if ".limit(" in stmt:
continue
findings.append(
Finding(
path,
line_number(text, match.start()),
"Review unbounded TypeScript query collection; add `limit()` or stream batches.",
stmt.splitlines()[0].strip(),
)
)
return findings
def scan_file(path: Path) -> list[Finding]:
text = path.read_text(encoding="utf-8", errors="replace")
if path.suffix == ".py":
return scan_python(path, text)
if path.suffix in {".ts", ".tsx"}:
return scan_typescript(path, text)
return []
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="+", type=Path)
parser.add_argument(
"--no-fail", action="store_true", help="Always exit 0 after reporting findings."
)
args = parser.parse_args()
findings: list[Finding] = []
for path in iter_files(args.paths):
findings.extend(scan_file(path))
for finding in findings:
print(f"{finding.path}:{finding.line}: {finding.message}")
print(f" {finding.text}")
if findings:
print(
f"\n{len(findings)} finding(s). Review manually; bounded query result conversion may be OK."
)
return 0 if args.no_fail or not findings else 1
if __name__ == "__main__":
sys.exit(main())
+1 -3
View File
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "0.32.0-beta.2"
current_version = "0.30.1-beta.2"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
@@ -23,8 +23,6 @@ allow_dirty = true
commit = true
message = "Bump version: {current_version} → {new_version}"
commit_args = ""
# bump-my-version >=1.4.0 rejects pre_commit_hooks containing shell syntax unless opted in.
allow_shell_hooks = true
# Java maven files
pre_commit_hooks = [
+9 -11
View File
@@ -34,16 +34,15 @@ runs:
maturin-version: "1.12.4"
command: build
working-directory: python
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc"
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/'"
target: x86_64-unknown-linux-gnu
manylinux: ${{ inputs.manylinux }}
args: ${{ inputs.args }}
before-script-linux: |
set -e
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
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
- name: Build Arm Manylinux Wheel
if: ${{ inputs.arm-build == 'true' }}
uses: PyO3/maturin-action@v1
@@ -51,14 +50,13 @@ runs:
maturin-version: "1.12.4"
command: build
working-directory: python
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/' -e PROTOC=/usr/local/bin/protoc"
docker-options: "-e PIP_EXTRA_INDEX_URL='https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/'"
target: aarch64-unknown-linux-gnu
manylinux: ${{ inputs.manylinux }}
args: ${{ inputs.args }}
before-script-linux: |
set -e
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
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
+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@v6
- uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust
@@ -47,7 +47,7 @@ jobs:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- 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@v6
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
fetch-depth: 0
persist-credentials: true
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
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@v6
uses: pnpm/action-setup@v4
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@v6
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: true
- name: Set up Node.js
uses: actions/setup-node@v6
uses: actions/setup-node@v4
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@v6
- uses: actions/setup-node@v4
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@v6
uses: actions/checkout@v4
- 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@v6
uses: actions/setup-node@v4
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@v6
uses: actions/checkout@v4
- name: Set up Java 8
uses: actions/setup-java@v4
with:
@@ -73,7 +73,7 @@ jobs:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- 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@v6
uses: actions/checkout@v4
- 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@v6
uses: actions/checkout@v4
- 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@v6
- uses: actions/checkout@v4
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@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v4
with:
version: 11.1.1
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
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@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v4
with:
version: 11.1.1
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
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@v6
- uses: actions/setup-node@v4
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@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
- uses: pnpm/action-setup@v6
- uses: pnpm/action-setup@v4
with:
version: 11.1.1
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
# in October.
+21 -30
View File
@@ -32,7 +32,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
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-2025-8x-x64
host: windows-latest
features: ","
pre_build: |-
choco install --no-progress protoc ninja nasm
@@ -111,21 +111,12 @@ 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-2025-8x-x64
host: windows-latest
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
@@ -179,13 +170,13 @@ jobs:
run:
working-directory: nodejs
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/action-setup@v4
with:
version: 11.1.1
- name: Setup node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
# in October.
@@ -199,7 +190,7 @@ jobs:
toolchain: stable
targets: ${{ matrix.settings.target }}
- name: Cache cargo
uses: actions/cache@v5
uses: actions/cache@v4
with:
path: |
~/.cargo/registry/index/
@@ -253,7 +244,7 @@ jobs:
if: ${{ !matrix.settings.docker }}
shell: bash
- name: Upload artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: lancedb-${{ matrix.settings.target }}
path: nodejs/dist/*.node
@@ -265,7 +256,7 @@ jobs:
run: pnpm tsc
- name: Upload Generic Artifacts
if: ${{ matrix.settings.target == 'aarch64-apple-darwin' }}
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: nodejs-dist
path: |
@@ -296,13 +287,13 @@ jobs:
shell: bash
working-directory: nodejs
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/action-setup@v4
with:
version: 11.1.1
- name: Setup Node.js 24 for install
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
# in October.
@@ -312,18 +303,18 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup Node.js ${{ matrix.node }} for test
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- name: Download artifacts
uses: actions/download-artifact@v8
uses: actions/download-artifact@v4
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@v8
- uses: actions/download-artifact@v4
with:
name: nodejs-dist
path: nodejs/dist
@@ -348,13 +339,13 @@ jobs:
needs:
- test-lancedb
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v6
uses: pnpm/action-setup@v4
with:
version: 11.1.1
- name: Setup node
uses: actions/setup-node@v6
uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
@@ -362,14 +353,14 @@ jobs:
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
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@v8
- uses: actions/download-artifact@v4
name: Download arch-specific binaries
with:
pattern: lancedb-*
@@ -407,7 +398,7 @@ jobs:
contents: read
issues: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- 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@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -66,7 +66,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -95,7 +95,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -126,7 +126,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -160,7 +160,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -189,7 +189,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -212,7 +212,7 @@ jobs:
shell: bash
working-directory: python
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
+11 -27
View File
@@ -40,7 +40,7 @@ jobs:
CC: clang-18
CXX: clang++-18
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -65,7 +65,7 @@ jobs:
timeout-minutes: 10
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- 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@v6
- uses: actions/checkout@v4
# 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@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -125,26 +125,10 @@ jobs:
- uses: rui314/setup-mold@v1
- name: Make Swap
run: |
swapfile=/swapfile
min_swap_bytes=$((15 * 1024 * 1024 * 1024))
active_swap_bytes="$(sudo swapon --show=NAME,SIZE --bytes --noheadings | awk '$1 == "/swapfile" { print $2 }')"
if [ -n "$active_swap_bytes" ]; then
if [ "$active_swap_bytes" -ge "$min_swap_bytes" ]; then
echo "/swapfile is already active with enough space; skipping swap creation"
exit 0
fi
echo "/swapfile is already active but smaller than 16G; using /mnt/lancedb-swapfile"
swapfile=/mnt/lancedb-swapfile
fi
if sudo swapon --show=NAME --noheadings | grep -Fxq "$swapfile"; then
echo "$swapfile is already active; skipping swap creation"
exit 0
fi
sudo rm -f "$swapfile"
sudo fallocate -l 16G "$swapfile"
sudo chmod 600 "$swapfile"
sudo mkswap "$swapfile"
sudo swapon "$swapfile"
sudo fallocate -l 16G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
- name: Build
run: cargo build --profile ci --all-features --tests --locked --examples
- name: Run feature tests
@@ -168,7 +152,7 @@ jobs:
shell: bash
working-directory: rust
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
lfs: true
@@ -197,7 +181,7 @@ jobs:
run:
working-directory: rust/lancedb
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
- name: Set target
run: rustup target add ${{ matrix.target }}
- uses: Swatinem/rust-cache@v2
@@ -226,7 +210,7 @@ jobs:
CC: clang-18
CXX: clang++-18
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
submodules: true
- name: Install dependencies
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: main
persist-credentials: false
-1
View File
@@ -27,7 +27,6 @@ python/dist
*.so
*.dylib
*.dll
*.pdb
## Javascript
*.node
Generated
+386 -990
View File
File diff suppressed because it is too large Load Diff
+23 -26
View File
@@ -13,25 +13,24 @@ categories = ["database-implementations"]
rust-version = "1.91.0"
[workspace.dependencies]
lance = { "version" = "=9.1.0-beta.2", default-features = false, "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=9.1.0-beta.2", default-features = false, "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=9.1.0-beta.2", default-features = false, "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=9.1.0-beta.2", "tag" = "v9.1.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
lance = { "version" = "=7.2.0-beta.3", default-features = false, "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=7.2.0-beta.3", default-features = false, "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=7.2.0-beta.3", default-features = false, "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=7.2.0-beta.3", "tag" = "v7.2.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
ahash = "0.8"
# Note that this one does not include pyarrow
arrow = { version = "58.0.0", optional = false }
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"
@@ -39,23 +38,21 @@ arrow-schema = "58.0.0"
arrow-select = "58.0.0"
arrow-cast = "58.0.0"
async-trait = "0"
datafusion = { version = "54.0.0", default-features = false }
datafusion-catalog = "54.0.0"
datafusion-common = { version = "54.0.0", default-features = false }
datafusion-execution = "54.0.0"
datafusion-expr = "54.0.0"
datafusion-functions = "54.0.0"
datafusion-physical-plan = "54.0.0"
datafusion-physical-expr = "54.0.0"
datafusion-sql = "54.0.0"
datafusion = { version = "53.0.0", default-features = false }
datafusion-catalog = "53.0.0"
datafusion-common = { version = "53.0.0", default-features = false }
datafusion-execution = "53.0.0"
datafusion-expr = "53.0.0"
datafusion-functions = "53.0.0"
datafusion-physical-plan = "53.0.0"
datafusion-physical-expr = "53.0.0"
datafusion-sql = "53.0.0"
env_logger = "0.11"
half = { "version" = "2.7.1", default-features = false, features = [
"num-traits",
] }
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"
+29 -25
View File
@@ -51,6 +51,18 @@ 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.
@@ -68,6 +80,18 @@ 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.
@@ -84,23 +108,11 @@ 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" },
# 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" },
# 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" },
]
# ---------------------------------------------------------------------------
@@ -135,14 +147,6 @@ allow = [
"CDLA-Permissive-2.0",
]
confidence-threshold = 0.8
# Per-crate license exceptions: allow a license for a specific crate only,
# rather than globally via the `allow` list above.
exceptions = [
# CDDL-1.0 (copyleft) is pulled in only as a dev/profiling dependency via
# `inferno` -> `pprof` -> `lance-testing`; it is a test dependency that we
# do not distribute, so scope the allowance to `inferno` alone.
{ allow = ["CDDL-1.0"], crate = "inferno" },
]
# Crates whose license cannot be determined from Cargo metadata but whose
# license we've manually confirmed from upstream. Keep this list minimal.
[[licenses.clarify]]
+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.32.0-beta.2</version>
<version>0.30.1-beta.2</version>
</dependency>
```
-43
View File
@@ -1,43 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / BranchContents
# Class: BranchContents
## Constructors
### new BranchContents()
```ts
new BranchContents(): BranchContents
```
#### Returns
[`BranchContents`](BranchContents.md)
## Properties
### manifestSize
```ts
manifestSize: number;
```
***
### parentBranch?
```ts
optional parentBranch: string;
```
***
### parentVersion
```ts
parentVersion: number;
```
-96
View File
@@ -1,96 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / Branches
# Class: Branches
Branch manager for a [Table](Table.md).
Unlike tags, `create` and `checkout` return a new [Table](Table.md) handle scoped
to the branch; writes on it do not affect `main`.
## Methods
### checkout()
```ts
checkout(name, version?): Promise<Table>
```
Check out an existing branch and return a handle scoped to it.
With `version` set, the returned handle is pinned to that version of the
branch (a read-only, detached view); otherwise it tracks the branch's
latest and stays writable.
#### Parameters
* **name**: `string`
* **version?**: `number`
#### Returns
`Promise`&lt;[`Table`](Table.md)&gt;
***
### create()
```ts
create(
name,
fromRef?,
fromVersion?): Promise<Table>
```
Create a branch and return a handle scoped to it.
#### Parameters
* **name**: `string`
Name of the new branch.
* **fromRef?**: `string`
Source branch to fork from. Defaults to `main`.
* **fromVersion?**: `number`
A specific version on `fromRef`. Defaults to latest.
#### Returns
`Promise`&lt;[`Table`](Table.md)&gt;
***
### delete()
```ts
delete(name): Promise<void>
```
Delete a branch.
#### Parameters
* **name**: `string`
#### Returns
`Promise`&lt;`void`&gt;
***
### list()
```ts
list(): Promise<Record<string, BranchContents>>
```
List all branches, mapping name to branch metadata.
#### Returns
`Promise`&lt;`Record`&lt;`string`, [`BranchContents`](BranchContents.md)&gt;&gt;
-18
View File
@@ -57,24 +57,6 @@ block size may be added in the future.
***
### fm()
```ts
static fm(): Index
```
Create an FM-Index.
An FM-Index is a scalar index on string or binary columns that accelerates
substring search, i.e. `contains(col, 'needle')`. Unlike the tokenized
full-text-search index, it matches arbitrary substrings of the raw bytes.
#### Returns
[`Index`](Index.md)
***
### fts()
```ts
+1 -10
View File
@@ -33,7 +33,7 @@ protected inner: Query | Promise<Query>;
### analyzePlan()
```ts
analyzePlan(distributedMetrics?): Promise<string>
analyzePlan(): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
@@ -41,12 +41,6 @@ Executes the query and returns the physical query plan annotated with runtime me
This is useful for debugging and performance analysis, as it shows how the query was executed
and includes metrics such as elapsed time, rows processed, and I/O statistics.
#### Parameters
* **distributedMetrics?**: [`AnalyzePlanDistributedMetrics`](../type-aliases/AnalyzePlanDistributedMetrics.md)
How distributed worker metrics are displayed for remote query plans.
Defaults to `"aggregate"`.
#### Returns
`Promise`&lt;`string`&gt;
@@ -524,9 +518,6 @@ 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
+1 -7
View File
@@ -38,7 +38,7 @@ protected inner: NativeQueryType | Promise<NativeQueryType>;
### analyzePlan()
```ts
analyzePlan(distributedMetrics?): Promise<string>
analyzePlan(): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
@@ -46,12 +46,6 @@ Executes the query and returns the physical query plan annotated with runtime me
This is useful for debugging and performance analysis, as it shows how the query was executed
and includes metrics such as elapsed time, rows processed, and I/O statistics.
#### Parameters
* **distributedMetrics?**: [`AnalyzePlanDistributedMetrics`](../type-aliases/AnalyzePlanDistributedMetrics.md)
How distributed worker metrics are displayed for remote query plans.
Defaults to `"aggregate"`.
#### Returns
`Promise`&lt;`string`&gt;
-80
View File
@@ -110,23 +110,6 @@ containing the new version number of the table after altering the columns.
***
### branches()
```ts
abstract branches(): Promise<Branches>
```
Get the branch manager for this table.
Branches are isolated, writable lines of history forked from another
branch (or version). Writes on a branch do not affect `main`.
#### Returns
`Promise`&lt;[`Branches`](Branches.md)&gt;
***
### checkout()
```ts
@@ -295,23 +278,6 @@ await table.createIndex("my_float_col");
***
### currentBranch()
```ts
abstract currentBranch(): null | string
```
The branch this table handle is scoped to, or `null` for the main branch.
A handle returned by [Branches.create](Branches.md#create) or [Branches.checkout](Branches.md#checkout)
reports the branch it targets; a handle opened normally reports `null`.
#### Returns
`null` \| `string`
***
### delete()
```ts
@@ -398,26 +364,6 @@ 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
@@ -934,32 +880,6 @@ Return the table as an arrow table
***
### tokenize()
```ts
abstract tokenize(query, options): Promise<FtsToken[]>
```
Tokenize a full-text search query using the tokenizer configured on an FTS index.
Specify exactly one of `column` or `indexName`.
Model-backed tokenizers such as `jieba/*` and `lindera/*` are rebuilt in
the client process from index metadata. For remote tables, this means the
same tokenizer model files must also exist locally.
#### Parameters
* **query**: `string`
* **options**: [`TokenizeTableOptions`](../type-aliases/TokenizeTableOptions.md)
#### Returns
`Promise`&lt;[`FtsToken`](../interfaces/FtsToken.md)[]&gt;
***
### unsetLsmWriteSpec()
```ts
+1 -7
View File
@@ -29,7 +29,7 @@ protected inner: TakeQuery | Promise<TakeQuery>;
### analyzePlan()
```ts
analyzePlan(distributedMetrics?): Promise<string>
analyzePlan(): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
@@ -37,12 +37,6 @@ Executes the query and returns the physical query plan annotated with runtime me
This is useful for debugging and performance analysis, as it shows how the query was executed
and includes metrics such as elapsed time, rows processed, and I/O statistics.
#### Parameters
* **distributedMetrics?**: [`AnalyzePlanDistributedMetrics`](../type-aliases/AnalyzePlanDistributedMetrics.md)
How distributed worker metrics are displayed for remote query plans.
Defaults to `"aggregate"`.
#### Returns
`Promise`&lt;`string`&gt;
+1 -10
View File
@@ -51,7 +51,7 @@ addQueryVector(vector): VectorQuery
### analyzePlan()
```ts
analyzePlan(distributedMetrics?): Promise<string>
analyzePlan(): Promise<string>
```
Executes the query and returns the physical query plan annotated with runtime metrics.
@@ -59,12 +59,6 @@ Executes the query and returns the physical query plan annotated with runtime me
This is useful for debugging and performance analysis, as it shows how the query was executed
and includes metrics such as elapsed time, rows processed, and I/O statistics.
#### Parameters
* **distributedMetrics?**: [`AnalyzePlanDistributedMetrics`](../type-aliases/AnalyzePlanDistributedMetrics.md)
How distributed worker metrics are displayed for remote query plans.
Defaults to `"aggregate"`.
#### Returns
`Promise`&lt;`string`&gt;
@@ -773,9 +767,6 @@ 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
@@ -1,29 +0,0 @@
[**@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).
@@ -1,42 +0,0 @@
[**@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.
-26
View File
@@ -1,26 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / tokenize
# Function: tokenize()
```ts
function tokenize(query, options?): Promise<FtsToken[]>
```
Tokenize a full-text search query using an explicit tokenizer.
This does not require a table or FTS index. The tokenizer options match
[Index.fts](../classes/Index.md#fts).
## Parameters
* **query**: `string`
* **options?**: `Partial`&lt;[`TokenizeOptions`](../interfaces/TokenizeOptions.md)&gt;
## Returns
`Promise`&lt;[`FtsToken`](../interfaces/FtsToken.md)[]&gt;
-12
View File
@@ -12,7 +12,6 @@
## Enumerations
- [FullTextQueryType](enumerations/FullTextQueryType.md)
- [OAuthFlowType](enumerations/OAuthFlowType.md)
- [Occur](enumerations/Occur.md)
- [Operator](enumerations/Operator.md)
@@ -20,8 +19,6 @@
- [BooleanQuery](classes/BooleanQuery.md)
- [BoostQuery](classes/BoostQuery.md)
- [BranchContents](classes/BranchContents.md)
- [Branches](classes/Branches.md)
- [Connection](classes/Connection.md)
- [HeaderProvider](classes/HeaderProvider.md)
- [Index](classes/Index.md)
@@ -72,7 +69,6 @@
- [FragmentStatistics](interfaces/FragmentStatistics.md)
- [FragmentSummaryStats](interfaces/FragmentSummaryStats.md)
- [FtsOptions](interfaces/FtsOptions.md)
- [FtsToken](interfaces/FtsToken.md)
- [FullTextQuery](interfaces/FullTextQuery.md)
- [FullTextSearchOptions](interfaces/FullTextSearchOptions.md)
- [HnswPqOptions](interfaces/HnswPqOptions.md)
@@ -87,8 +83,6 @@
- [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)
@@ -108,7 +102,6 @@
- [TimeoutConfig](interfaces/TimeoutConfig.md)
- [TlsConfig](interfaces/TlsConfig.md)
- [TokenResponse](interfaces/TokenResponse.md)
- [TokenizeOptions](interfaces/TokenizeOptions.md)
- [UpdateFieldMetadataResult](interfaces/UpdateFieldMetadataResult.md)
- [UpdateOptions](interfaces/UpdateOptions.md)
- [UpdateResult](interfaces/UpdateResult.md)
@@ -118,8 +111,6 @@
## Type Aliases
- [AnalyzePlanDistributedMetrics](type-aliases/AnalyzePlanDistributedMetrics.md)
- [BaseTokenizer](type-aliases/BaseTokenizer.md)
- [Data](type-aliases/Data.md)
- [DataLike](type-aliases/DataLike.md)
- [FieldLike](type-aliases/FieldLike.md)
@@ -129,15 +120,12 @@
- [RecordBatchLike](type-aliases/RecordBatchLike.md)
- [SchemaLike](type-aliases/SchemaLike.md)
- [TableLike](type-aliases/TableLike.md)
- [TokenizeTableOptions](type-aliases/TokenizeTableOptions.md)
## Functions
- [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)
- [tokenize](functions/tokenize.md)
@@ -64,19 +64,6 @@ 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
+1 -5
View File
@@ -23,7 +23,7 @@ whether to remove punctuation
### baseTokenizer?
```ts
optional baseTokenizer: BaseTokenizer;
optional baseTokenizer: "raw" | "simple" | "whitespace" | "ngram";
```
The tokenizer to use when building the index.
@@ -37,10 +37,6 @@ The following tokenizers are available:
"raw" - Raw tokenizer. This tokenizer does not split the text into tokens and indexes the entire text as a single token.
"icu" - ICU dictionary-based word segmentation.
"icu/split" - ICU segmentation with simple-style delimiter splitting.
***
### language?
-29
View File
@@ -1,29 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / FtsToken
# Interface: FtsToken
Token produced by the tokenizer configured on a full-text search index.
## Properties
### position
```ts
position: number;
```
Token position used by full-text query matching.
***
### text
```ts
text: string;
```
Token text after tokenizer filters have been applied.
-109
View File
@@ -23,31 +23,6 @@ be more columns to represent composite indices.
***
### createdAt?
```ts
optional createdAt: Date;
```
When the index was created.
`undefined` for remote tables or indices created before timestamps were tracked.
***
### indexDetails?
```ts
optional indexDetails: any;
```
Index-type-specific details parsed as a JavaScript object.
Falls back to a raw string if JSON parsing fails. `undefined` for
remote tables or when details are unavailable.
***
### indexType
```ts
@@ -58,30 +33,6 @@ The type of the index
***
### indexUuid?
```ts
optional indexUuid: string;
```
The UUID of the first segment of the index.
`undefined` for remote tables, which do not yet surface this.
***
### indexVersion?
```ts
optional indexVersion: number;
```
The on-disk index format version.
`undefined` for remote tables.
***
### name
```ts
@@ -89,63 +40,3 @@ name: string;
```
The name of the index
***
### numIndexedRows?
```ts
optional numIndexedRows: number;
```
The number of rows indexed, across all segments.
`undefined` for remote tables.
***
### numSegments?
```ts
optional numSegments: number;
```
The number of segments that make up the index.
`undefined` for remote tables.
***
### numUnindexedRows?
```ts
optional numUnindexedRows: number;
```
The number of rows not yet covered by this index.
`undefined` for remote tables.
***
### sizeBytes?
```ts
optional sizeBytes: number;
```
The total size in bytes of all index files across all segments.
`undefined` for remote tables or indices without size tracking.
***
### typeUrl?
```ts
optional typeUrl: string;
```
The protobuf type URL, a precise type identifier for the index.
`undefined` for remote tables.
+11
View File
@@ -30,6 +30,17 @@ The type of the index
***
### loss?
```ts
optional loss: number;
```
The KMeans loss value of the index,
it is only present for vector indices.
***
### numIndexedRows
```ts
@@ -1,88 +0,0 @@
[**@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
@@ -1,111 +0,0 @@
[**@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,18 +8,6 @@
## Properties
### branch?
```ts
optional branch: string;
```
Open the table scoped to this branch instead of the default branch.
Reads and writes on the returned table operate in the branch's context.
***
### ~~indexCacheSize?~~
```ts
@@ -55,17 +43,3 @@ Options already set on the connection will be inherited by the table,
but can be overridden here.
The available options are described at https://docs.lancedb.com/storage/
***
### version?
```ts
optional version: number;
```
Open the table pinned to this version, producing a read-only view.
Composes with [OpenTableOptions.branch](OpenTableOptions.md#branch): when both are set, opens
that branch at the version; otherwise opens `main` at the version. Call
`checkoutLatest` to return to a writable state.
@@ -8,14 +8,6 @@
## Properties
### clumpSize?
```ts
optional clumpSize: number;
```
***
### counts?
```ts
-109
View File
@@ -1,109 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / TokenizeOptions
# Interface: TokenizeOptions
Options for tokenizing a full-text search query without a table index.
## Properties
### asciiFolding?
```ts
optional asciiFolding: boolean;
```
Whether to fold ASCII characters.
***
### baseTokenizer?
```ts
optional baseTokenizer: BaseTokenizer;
```
The tokenizer to use. The default is "simple".
***
### language?
```ts
optional language: string;
```
Language for stemming and stop words.
***
### lowercase?
```ts
optional lowercase: boolean;
```
Whether to lowercase tokens.
***
### maxTokenLength?
```ts
optional maxTokenLength: number;
```
Maximum token length; tokens longer than this are ignored.
***
### ngramMaxLength?
```ts
optional ngramMaxLength: number;
```
N-gram maximum length.
***
### ngramMinLength?
```ts
optional ngramMinLength: number;
```
N-gram minimum length.
***
### prefixOnly?
```ts
optional prefixOnly: boolean;
```
Whether to only emit token prefixes for the n-gram tokenizer.
***
### removeStopWords?
```ts
optional removeStopWords: boolean;
```
Whether to remove stop words.
***
### stem?
```ts
optional stem: boolean;
```
Whether to stem tokens.
@@ -1,11 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / AnalyzePlanDistributedMetrics
# Type Alias: AnalyzePlanDistributedMetrics
```ts
type AnalyzePlanDistributedMetrics: "aggregate" | "per_worker" | "full";
```
-19
View File
@@ -1,19 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / BaseTokenizer
# Type Alias: BaseTokenizer
```ts
type BaseTokenizer:
| "simple"
| "whitespace"
| "raw"
| "ngram"
| "icu"
| "icu/split"
| `jieba/${string}`
| `lindera/${string}`;
```
@@ -1,11 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / TokenizeTableOptions
# Type Alias: TokenizeTableOptions
```ts
type TokenizeTableOptions: object | object;
```
+1 -1
View File
@@ -8,7 +8,7 @@
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.32.0-beta.2</version>
<version>0.30.1-beta.2</version>
<relativePath>../pom.xml</relativePath>
</parent>
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.32.0-beta.2</version>
<version>0.30.1-beta.2</version>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>LanceDB Java SDK Parent POM</description>
@@ -28,7 +28,7 @@
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<arrow.version>15.0.0</arrow.version>
<lance-core.version>9.1.0-beta.2</lance-core.version>
<lance-core.version>7.2.0-beta.1</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>
+3 -7
View File
@@ -1,7 +1,7 @@
[package]
name = "lancedb-nodejs"
edition.workspace = true
version = "0.32.0-beta.2"
version = "0.30.1-beta.2"
publish = false
license.workspace = true
description.workspace = true
@@ -25,12 +25,8 @@ lancedb = { path = "../rust/lancedb", default-features = false }
lance-namespace.workspace = true
napi = { version = "3.8.3", default-features = false, features = [
"napi9",
"async",
"chrono_date",
"serde-json",
"async"
] }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
serde_json = "1"
napi-derive = "3.5.2"
# Prevent dynamic linking of lzma, which comes from datafusion
lzma-sys = { version = "0.1", features = ["static"] }
@@ -44,6 +40,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", "lancedb/goosefs", "lancedb/metrics-otel"]
default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface"]
fp16kernels = ["lancedb/fp16kernels"]
remote = ["lancedb/remote"]
-114
View File
@@ -1,114 +0,0 @@
// 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,20 +215,6 @@ 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()
-34
View File
@@ -191,40 +191,6 @@ describe("remote connection", () => {
);
});
it("supports version time-travel and branches on remote", async () => {
await withMockDatabase(
(req, res) => {
const body = req.url?.includes("/branches/list")
? JSON.stringify({
branches: {
exp: { parentVersion: 1, createAt: 1, manifestSize: 1 },
},
})
: JSON.stringify({ name: "t", version: 2, schema: { fields: [] } });
res.writeHead(200, { "Content-Type": "application/json" }).end(body);
},
async (db) => {
// version-only (and "main" + version) time-travel the main chain
const v2 = await db.openTable("t", undefined, { version: 2 });
expect(v2.currentBranch()).toBeNull();
const mainV2 = await db.openTable("t", undefined, {
branch: "main",
version: 2,
});
expect(mainV2.currentBranch()).toBeNull();
// a non-main branch opens a handle scoped to that branch
const exp = await db.openTable("t", undefined, { branch: "exp" });
expect(exp.currentBranch()).toBe("exp");
const expV2 = await db.openTable("t", undefined, {
branch: "exp",
version: 2,
});
expect(expV2.currentBranch()).toBe("exp");
},
);
});
describe("TlsConfig", () => {
it("should create TlsConfig with all fields", () => {
const tlsConfig: TlsConfig = {
+25 -491
View File
@@ -16,7 +16,6 @@ import {
PhraseQuery,
Table,
connect,
tokenize,
} from "../lancedb";
import {
Table as ArrowTable,
@@ -86,140 +85,6 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
await expect(table.countRows()).resolves.toBe(3);
});
it("should support branches", async () => {
await table.add([{ id: 1 }]);
expect(await table.countRows()).toBe(1);
expect(table.currentBranch()).toBeNull();
// fork an isolated, writable branch from main
const branch = await (await table.branches()).create("exp");
expect(branch.currentBranch()).toBe("exp");
expect(await branch.countRows()).toBe(1);
await branch.add([{ id: 2 }]);
expect(await branch.countRows()).toBe(2);
// main is untouched by branch writes
expect(await table.countRows()).toBe(1);
// listed, with main (null) as the parent
const list = await (await table.branches()).list();
expect(Object.keys(list)).toContain("exp");
expect(list["exp"].parentBranch).toBeNull();
// fromRef="main" is equivalent to the default
await (await table.branches()).create("exp2", "main");
const list2 = await (await table.branches()).list();
expect(list2["exp2"].parentBranch).toBeNull();
// checkout returns a handle scoped to the branch's latest
const checkedOut = await (await table.branches()).checkout("exp");
expect(checkedOut.currentBranch()).toBe("exp");
expect(await checkedOut.countRows()).toBe(2);
// delete removes it
await (await table.branches()).delete("exp");
await (await table.branches()).delete("exp2");
const after = await (await table.branches()).list();
expect(Object.keys(after)).not.toContain("exp");
});
it("should open a branch via open_table", async () => {
const db = await connect(tmpDir.name);
await table.add([{ id: 1 }]);
const branch = await (await table.branches()).create("exp");
await branch.add([{ id: 2 }]);
// open_table(..., { branch }) returns a handle scoped to the branch
const opened = await db.openTable("some_table", undefined, {
branch: "exp",
});
expect(await opened.countRows()).toBe(2);
// opening without branch still tracks main
expect(await (await db.openTable("some_table")).countRows()).toBe(1);
});
it("should open a branch at a version isolated from main and HEAD", async () => {
const db = await connect(tmpDir.name);
// main: a single fork-point row
const t = await db.createTable("bv_table", [{ id: 0 }]);
const mainV1 = await t.version();
// fork "exp", then advance exp AND main independently past the fork so
// they diverge while sharing version numbers
const exp = await (await t.branches()).create("exp");
await exp.add([{ id: 1 }]); // exp: {0, 1}
const expV2 = await exp.version();
await exp.add([{ id: 2 }]); // exp HEAD: {0, 1, 2}
await t.add([{ id: 100 }, { id: 101 }, { id: 102 }]); // main HEAD: {0,100,101,102}
expect(await t.version()).toBe(expV2);
// open exp at the shared version: the data must be exp's, not main's.
// count alone cannot prove this (main@v2 also exists), so assert
// provenance by content.
const pinned = await db.openTable("bv_table", undefined, {
branch: "exp",
version: expV2,
});
expect(await pinned.countRows()).toBe(2); // not exp HEAD (3), not main@v2 (4)
expect(await pinned.countRows("id = 1")).toBe(1); // exp's post-fork row
expect(await pinned.countRows("id = 100")).toBe(0); // main's rows invisible
// the same coordinate is reachable directly via branches().checkout(name, version)
const pinnedDirect = await (await t.branches()).checkout("exp", expV2);
expect(await pinnedDirect.countRows()).toBe(2);
// the HEADs are unaffected
expect(
await (
await db.openTable("bv_table", undefined, { branch: "exp" })
).countRows(),
).toBe(3);
expect(await (await db.openTable("bv_table")).countRows()).toBe(4);
// version-only (no branch) time-travels main itself: its fork-point
// version holds only main's first row, and the shared version number
// resolves to main's data, not the branch's ("opens main at the version")
const oldMain = await db.openTable("bv_table", undefined, {
version: mainV1,
});
expect(await oldMain.countRows()).toBe(1);
const sharedOnMain = await db.openTable("bv_table", undefined, {
version: expV2,
});
expect(await sharedOnMain.countRows()).toBe(4); // main@v2, not exp@v2 (2)
// detached head: writing to a pinned version is rejected
await expect(pinned.add([{ id: 9 }])).rejects.toThrow(
/cannot be modified/,
);
// a nonexistent version is rejected -- on main, and on a branch (a
// distinct resolution path, on the branch's manifests)
await expect(
db.openTable("bv_table", undefined, { version: 9999 }),
).rejects.toThrow();
await expect(
db.openTable("bv_table", undefined, { branch: "exp", version: 9999 }),
).rejects.toThrow();
// checkoutLatest re-attaches the pinned handle to the BRANCH's HEAD
// (writable again), not main's HEAD (4), and not staying pinned (2)
await pinned.checkoutLatest();
expect(await pinned.countRows()).toBe(3); // exp HEAD
await pinned.add([{ id: 3 }]);
expect(await pinned.countRows()).toBe(4); // writable again
});
it("rejects invalid branch inputs", async () => {
const branches = await table.branches();
await expect(branches.create("")).rejects.toThrow("non-empty");
await expect(branches.checkout("")).rejects.toThrow("non-empty");
await expect(branches.delete("")).rejects.toThrow("non-empty");
await expect(branches.create("bad", "main", -1)).rejects.toThrow(
"non-negative",
);
});
it("should show table stats", async () => {
await table.add([{ id: 1 }, { id: 2 }]);
await table.add([{ id: 1 }]);
@@ -850,15 +715,13 @@ describe("When creating an index", () => {
expect(fs.readdirSync(indexDir)).toHaveLength(1);
const indices = await tbl.listIndices();
expect(indices.length).toBe(1);
expect(indices[0]).toEqual(
expect.objectContaining({
name: "vec_idx",
indexType: "IvfPq",
columns: ["vec"],
}),
);
expect(indices[0]).toEqual({
name: "vec_idx",
indexType: "IvfPq",
columns: ["vec"],
});
const stats = await tbl.indexStats("vec_idx");
expect(stats).toBeDefined();
expect(stats?.loss).toBeDefined();
// Search without specifying the column
let rst = await tbl
@@ -918,22 +781,10 @@ describe("When creating an index", () => {
expect(indices2.length).toBe(0);
});
it("should preserve canonical nested field paths across index lifecycle", async () => {
it("should create and search a nested vector index", async () => {
const db = await connect(tmpDir.name);
const nestedSchema = new Schema([
new Field("rowId", new Int32(), true),
new Field("row-id", new Int32(), true),
new Field("userId", new Int32(), true),
new Field(
"metadata",
new Struct([new Field("user_id", new Int32(), true)]),
true,
),
new Field(
"MetaData",
new Struct([new Field("userId", new Int32(), true)]),
true,
),
new Field("id", new Int32(), true),
new Field(
"image",
new Struct([
@@ -945,146 +796,27 @@ describe("When creating an index", () => {
]),
true,
),
new Field(
"payload",
new Struct([new Field("text", new Utf8(), true)]),
true,
),
new Field(
"meta-data",
new Struct([new Field("user-id", new Int32(), true)]),
true,
),
new Field(
"literal",
new Struct([new Field("a.b", new Int32(), true)]),
true,
),
]);
const nestedTable = await db.createTable(
"nested_field_index_lifecycle",
"nested_vector",
makeArrowTable(
Array.from({ length: 300 }, (_, rowId) => ({
rowId,
"row-id": rowId,
userId: rowId,
metadata: { ["user_id"]: rowId },
["MetaData"]: { userId: rowId },
image: { embedding: [rowId, rowId + 1] },
payload: { text: `document ${rowId}` },
"meta-data": { "user-id": rowId },
literal: { "a.b": rowId },
Array.from({ length: 300 }, (_, id) => ({
id,
image: { embedding: [id, id + 1] },
})),
{ schema: nestedSchema },
),
);
await nestedTable.createIndex("rowId", {
config: Index.btree(),
name: "row_id_idx",
});
await nestedTable.createIndex("`row-id`", {
config: Index.btree(),
name: "row_dash_id_idx",
});
await nestedTable.createIndex("userId", {
config: Index.btree(),
name: "top_user_id_idx",
});
await nestedTable.createIndex("metadata.user_id", {
config: Index.btree(),
name: "nested_user_id_idx",
});
await nestedTable.createIndex("MetaData.userId", {
config: Index.btree(),
name: "mixed_case_metadata_user_id_idx",
});
await nestedTable.createIndex("`meta-data`.`user-id`", {
config: Index.btree(),
name: "escaped_names_idx",
});
await nestedTable.createIndex("literal.`a.b`", {
config: Index.btree(),
name: "literal_dot_idx",
});
await nestedTable.createIndex("image.embedding", {
name: "image_embedding_idx",
});
await nestedTable.createIndex("payload.text", {
config: Index.fts({ withPosition: false }),
name: "payload_text_idx",
});
const indices = await nestedTable.listIndices();
expect(indices).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "row_id_idx",
indexType: "BTree",
columns: ["rowId"],
}),
expect.objectContaining({
name: "row_dash_id_idx",
indexType: "BTree",
columns: ["`row-id`"],
}),
expect.objectContaining({
name: "top_user_id_idx",
indexType: "BTree",
columns: ["userId"],
}),
expect.objectContaining({
name: "nested_user_id_idx",
indexType: "BTree",
columns: ["metadata.user_id"],
}),
expect.objectContaining({
name: "mixed_case_metadata_user_id_idx",
indexType: "BTree",
columns: ["MetaData.userId"],
}),
expect.objectContaining({
name: "escaped_names_idx",
indexType: "BTree",
columns: ["`meta-data`.`user-id`"],
}),
expect.objectContaining({
name: "literal_dot_idx",
indexType: "BTree",
columns: ["literal.`a.b`"],
}),
expect.objectContaining({
name: "image_embedding_idx",
indexType: "IvfPq",
columns: ["image.embedding"],
}),
expect.objectContaining({
name: "payload_text_idx",
indexType: "FTS",
columns: ["payload.text"],
}),
]),
);
const stats = await nestedTable.indexStats(
"mixed_case_metadata_user_id_idx",
);
expect(stats?.numIndexedRows).toEqual(300);
expect(stats?.indexType).toEqual("BTREE");
const filtered = await nestedTable
.query()
.where("MetaData.userId = 42")
.limit(1)
.toArray();
expect(filtered[0].MetaData.userId).toEqual(42);
const escapedFiltered = await nestedTable
.query()
.where("`row-id` = 43")
.limit(1)
.toArray();
expect(escapedFiltered[0]["row-id"]).toEqual(43);
expect(indices).toContainEqual({
name: "image_embedding_idx",
indexType: "IvfPq",
columns: ["image.embedding"],
});
const explicit = await nestedTable
.query()
@@ -1097,37 +829,7 @@ describe("When creating an index", () => {
.nearestTo([0.0, 1.0])
.limit(1)
.toArray();
expect(inferred[0].rowId).toEqual(explicit[0].rowId);
await nestedTable.add([
{
rowId: 300,
"row-id": 300,
userId: 300,
metadata: { ["user_id"]: 300 },
["MetaData"]: { userId: 300 },
image: { embedding: [300.0, 301.0] },
payload: { text: "document 300" },
"meta-data": { "user-id": 300 },
literal: { "a.b": 300 },
},
]);
await nestedTable.optimize();
const indicesAfterOptimize = await nestedTable.listIndices();
expect(indicesAfterOptimize).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "mixed_case_metadata_user_id_idx",
indexType: "BTree",
columns: ["MetaData.userId"],
}),
expect.objectContaining({
name: "image_embedding_idx",
indexType: "IvfPq",
columns: ["image.embedding"],
}),
]),
);
expect(inferred[0].id).toEqual(explicit[0].id);
});
it("should report multiple nested vector candidates", async () => {
@@ -1261,13 +963,11 @@ describe("When creating an index", () => {
expect(fs.readdirSync(indexDir)).toHaveLength(1);
const indices = await tbl.listIndices();
expect(indices.length).toBe(1);
expect(indices[0]).toEqual(
expect.objectContaining({
name: "vec_idx",
indexType: "IvfHnswSq",
columns: ["vec"],
}),
);
expect(indices[0]).toEqual({
name: "vec_idx",
indexType: "IvfHnswSq",
columns: ["vec"],
});
// Search without specifying the column
let rst = await tbl
@@ -1440,20 +1140,6 @@ describe("When creating an index", () => {
expect(fs.readdirSync(indexDir)).toHaveLength(1);
});
test("create an FM index", async () => {
// FM-Index accelerates substring search on a string/binary column.
const db = await connect(tmpDir.name);
const fmTbl = await db.createTable("fm_table", [
{ id: 0, text: "hello world" },
{ id: 1, text: "foo bar" },
]);
await fmTbl.createIndex("text", {
config: Index.fm(),
});
const indexDir = path.join(tmpDir.name, "fm_table.lance", "_indices");
expect(fs.readdirSync(indexDir)).toHaveLength(1);
});
test("should be able to get index stats", async () => {
await tbl.createIndex("id");
@@ -1464,6 +1150,7 @@ describe("When creating an index", () => {
expect(stats?.distanceType).toBeUndefined();
expect(stats?.indexType).toEqual("BTREE");
expect(stats?.numIndices).toEqual(1);
expect(stats?.loss).toBeUndefined();
});
test("when getting stats on non-existent index", async () => {
@@ -1613,35 +1300,6 @@ describe("When creating an index", () => {
expect(rst64Query.toString()).toEqual(rst64Search.toString());
expect(rst64Query.numRows).toBe(2);
});
it("should expose rich metadata fields on IndexConfig", async () => {
await tbl.createIndex("id", { config: Index.btree() });
await tbl.createIndex("vec");
const indicesByName = Object.fromEntries(
(await tbl.listIndices()).map((idx) => [idx.name, idx]),
);
const scalarIdx = indicesByName["id_idx"];
expect(scalarIdx).toBeDefined();
expect(typeof scalarIdx.indexUuid).toBe("string");
expect(scalarIdx.numIndexedRows).toBe(300);
expect(scalarIdx.numUnindexedRows).toBe(0);
expect(scalarIdx.numSegments).toBeGreaterThanOrEqual(1);
expect(scalarIdx.sizeBytes).toBeGreaterThan(0);
// Use toString check to avoid cross-realm instanceof failures with native Date objects
expect(Object.prototype.toString.call(scalarIdx.createdAt)).toBe(
"[object Date]",
);
expect((scalarIdx.createdAt as Date).getTime()).toBeGreaterThan(0);
expect(typeof scalarIdx.indexDetails).toBe("object");
const vectorIdx = indicesByName["vec_idx"];
expect(vectorIdx).toBeDefined();
expect(typeof vectorIdx.indexUuid).toBe("string");
expect(vectorIdx.numIndexedRows).toBe(300);
expect(typeof vectorIdx.indexDetails).toBe("object");
});
});
describe("When querying a table", () => {
@@ -2308,75 +1966,6 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
expect(results2[0].text).toBe(data[1].text);
});
test("tokenizes FTS queries by column or index name", async () => {
const db = await connect(tmpDir.name);
const data = [
{
text: "Running in cafés",
japanese: "Hello, こんにちは世界!",
vector: [0.1, 0.2, 0.3],
},
];
const table = await db.createTable("test", data);
await table.createIndex("text", {
config: Index.fts({ baseTokenizer: "simple" }),
});
await table.createIndex("japanese", {
config: Index.fts({
baseTokenizer: "icu",
stem: false,
removeStopWords: false,
}),
name: "japanese_icu_idx",
});
await expect(table.tokenize("hello", {} as never)).rejects.toThrow(
"Specify exactly one",
);
await expect(
table.tokenize("hello", {
column: "text",
indexName: "text_idx",
} as never),
).rejects.toThrow("Specify exactly one");
const simpleTokens = await table.tokenize("Running in cafés", {
column: "text",
});
expect(simpleTokens).toEqual([
{ text: "run", position: 0 },
{ text: "cafe", position: 2 },
]);
const icuTokens = await table.tokenize("Hello, こんにちは世界!", {
indexName: "japanese_icu_idx",
});
expect(icuTokens).toEqual([
{ text: "hello", position: 0 },
{ text: "こんにちは", position: 1 },
{ text: "世界", position: 2 },
]);
const directSimpleTokens = await tokenize("Running in cafés", {
baseTokenizer: "simple",
});
expect(directSimpleTokens).toEqual([
{ text: "run", position: 0 },
{ text: "cafe", position: 2 },
]);
const directIcuTokens = await tokenize("Hello, こんにちは世界!", {
baseTokenizer: "icu",
stem: false,
removeStopWords: false,
});
expect(directIcuTokens).toEqual([
{ text: "hello", position: 0 },
{ text: "こんにちは", position: 1 },
{ text: "世界", position: 2 },
]);
});
test("full text search fast search", async () => {
const db = await connect(tmpDir.name);
const data = [{ text: "hello world", vector: [0.1, 0.2, 0.3], id: 1 }];
@@ -2775,13 +2364,8 @@ describe("when calling analyzePlan", () => {
.fill(1)
.map(() => Math.random());
const plan = await table.query().nearestTo(queryVec).analyzePlan();
console.log("Query Plan:\n", plan); // <--- Print the plan
expect(plan).toMatch("AnalyzeExec");
const fullPlan = await table
.query()
.nearestTo(queryVec)
.analyzePlan("full");
expect(fullPlan).toMatch("AnalyzeExec");
});
});
@@ -3067,56 +2651,6 @@ 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", () => {
+1 -28
View File
@@ -84,20 +84,6 @@ export interface CreateTableOptions {
}
export interface OpenTableOptions {
/**
* Open the table scoped to this branch instead of the default branch.
*
* Reads and writes on the returned table operate in the branch's context.
*/
branch?: string;
/**
* Open the table pinned to this version, producing a read-only view.
*
* Composes with {@link OpenTableOptions.branch}: when both are set, opens
* that branch at the version; otherwise opens `main` at the version. Call
* `checkoutLatest` to return to a writable state.
*/
version?: number;
/**
* Configuration for object storage.
*
@@ -497,20 +483,7 @@ export class LocalConnection extends Connection {
options?.indexCacheSize,
);
let table: Table = new LocalTable(innerTable);
// "main" is the default branch, so treat it as no branch. On a real branch,
// scope and pin in one step (yielding "version V of branch B"); otherwise
// pin the version, if any, against main.
const branch =
options?.branch != null && options.branch !== "main"
? options.branch
: undefined;
if (branch != null) {
table = await (await table.branches()).checkout(branch, options?.version);
} else if (options?.version != null) {
await table.checkout(options.version);
}
return table;
return new LocalTable(innerTable);
}
async cloneTable(
-79
View File
@@ -13,21 +13,13 @@ import {
Connection as LanceDbConnection,
JsHeaderProvider as NativeJsHeaderProvider,
Session,
tokenize as nativeTokenize,
} from "./native.js";
import { HeaderProvider } from "./header";
import type { BaseTokenizer } from "./indices";
import type { FtsToken } from "./table";
// 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,
@@ -46,7 +38,6 @@ export {
FragmentSummaryStats,
Tags,
TagContents,
BranchContents,
MergeResult,
AddResult,
AddColumnsResult,
@@ -60,7 +51,6 @@ export {
SplitHashOptions,
SplitSequentialOptions,
ShuffleOptions,
OAuthConfig as NativeOAuthConfig,
} from "./native.js";
export {
@@ -93,7 +83,6 @@ export {
QueryBase,
VectorQuery,
TakeQuery,
AnalyzePlanDistributedMetrics,
QueryExecutionOptions,
ColumnOrdering,
FullTextSearchOptions,
@@ -118,19 +107,15 @@ export {
HnswPqOptions,
HnswSqOptions,
FtsOptions,
BaseTokenizer,
} from "./indices";
export {
Table,
Branches,
AddDataOptions,
UpdateOptions,
OptimizeOptions,
Version,
WriteProgress,
FtsToken,
TokenizeTableOptions,
LsmWriteSpec,
ColumnAlteration,
FieldMetadataUpdate,
@@ -143,8 +128,6 @@ export {
TokenResponse,
} from "./header";
export { OAuthConfig, OAuthFlowType } from "./oauth";
export { MergeInsertBuilder, WriteExecutionOptions } from "./merge";
export * as embedding from "./embedding";
@@ -162,68 +145,6 @@ export {
} from "./arrow";
export { IntoSql, packBits } from "./util";
/**
* Options for tokenizing a full-text search query without a table index.
*/
export interface TokenizeOptions {
/**
* The tokenizer to use. The default is "simple".
*/
baseTokenizer?: BaseTokenizer;
/** Language for stemming and stop words. */
language?: string;
/** Maximum token length; tokens longer than this are ignored. */
maxTokenLength?: number;
/** Whether to lowercase tokens. */
lowercase?: boolean;
/** Whether to stem tokens. */
stem?: boolean;
/** Whether to remove stop words. */
removeStopWords?: boolean;
/** Whether to fold ASCII characters. */
asciiFolding?: boolean;
/** N-gram minimum length. */
ngramMinLength?: number;
/** N-gram maximum length. */
ngramMaxLength?: number;
/** Whether to only emit token prefixes for the n-gram tokenizer. */
prefixOnly?: boolean;
}
/**
* Tokenize a full-text search query using an explicit tokenizer.
*
* This does not require a table or FTS index. The tokenizer options match
* {@link Index.fts}.
*/
export async function tokenize(
query: string,
options?: Partial<TokenizeOptions>,
): Promise<FtsToken[]> {
return await nativeTokenize(
query,
options?.baseTokenizer,
options?.language,
options?.maxTokenLength,
options?.lowercase,
options?.stem,
options?.removeStopWords,
options?.asciiFolding,
options?.ngramMinLength,
options?.ngramMaxLength,
options?.prefixOnly,
);
}
/**
* Connect to a LanceDB instance at the given URI.
*
+1 -26
View File
@@ -486,16 +486,6 @@ export interface IvfFlatOptions {
sampleRate?: number;
}
export type BaseTokenizer =
| "simple"
| "whitespace"
| "raw"
| "ngram"
| "icu"
| "icu/split"
| `jieba/${string}`
| `lindera/${string}`;
/**
* Options to create a full text search index
*/
@@ -519,12 +509,8 @@ export interface FtsOptions {
* "whitespace" - Whitespace tokenizer. This tokenizer splits the text into tokens using whitespace as a delimiter.
*
* "raw" - Raw tokenizer. This tokenizer does not split the text into tokens and indexes the entire text as a single token.
*
* "icu" - ICU dictionary-based word segmentation.
*
* "icu/split" - ICU segmentation with simple-style delimiter splitting.
*/
baseTokenizer?: BaseTokenizer;
baseTokenizer?: "simple" | "whitespace" | "raw" | "ngram";
/**
* language for stemming and stop words
@@ -716,17 +702,6 @@ export class Index {
return new Index(LanceDbIndex.labelList());
}
/**
* Create an FM-Index.
*
* An FM-Index is a scalar index on string or binary columns that accelerates
* substring search, i.e. `contains(col, 'needle')`. Unlike the tokenized
* full-text-search index, it matches arbitrary substrings of the raw bytes.
*/
static fm() {
return new Index(LanceDbIndex.fm());
}
/**
* Create a full text search index
*
-76
View File
@@ -1,76 +0,0 @@
// 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
@@ -1,137 +0,0 @@
// 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 -15
View File
@@ -79,8 +79,6 @@ export interface QueryExecutionOptions {
timeoutMs?: number;
}
export type AnalyzePlanDistributedMetrics = "aggregate" | "per_worker" | "full";
export interface ColumnOrdering {
columnName: string;
ascending?: boolean;
@@ -313,20 +311,13 @@ export class QueryBase<
* KNNVectorDistance: metric=l2, metrics=[output_rows=1, elapsed_compute=114.333µs, output_batches=1]
* LanceScan: uri=/path/to/data, projection=[vector], row_id=true, row_addr=false, ordered=false, metrics=[output_rows=1, elapsed_compute=103.626µs, bytes_read=549, iops=2, requests=2]
*
* @param distributedMetrics - How distributed worker metrics are displayed for remote query plans.
* Defaults to `"aggregate"`.
* @returns A query execution plan with runtime metrics for each step.
*/
async analyzePlan(
distributedMetrics?: AnalyzePlanDistributedMetrics,
): Promise<string> {
const distributedMetricsMode = distributedMetrics ?? "aggregate";
async analyzePlan(): Promise<string> {
if (this.inner instanceof Promise) {
return this.inner.then((inner) =>
inner.analyzePlan(distributedMetricsMode),
);
return this.inner.then((inner) => inner.analyzePlan());
} else {
return this.inner.analyzePlan(distributedMetricsMode);
return this.inner.analyzePlan();
}
}
@@ -371,9 +362,6 @@ 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));
-144
View File
@@ -25,12 +25,10 @@ import {
AddColumnsSql,
AddResult,
AlterColumnsResult,
BranchContents,
DeleteResult,
DropColumnsResult,
IndexConfig,
IndexStatistics,
Branches as NativeBranches,
OptimizeStats,
TableStatistics,
Tags,
@@ -158,26 +156,6 @@ export interface Version {
metadata: Record<string, string>;
}
/** Token produced by the tokenizer configured on a full-text search index. */
export interface FtsToken {
/** Token text after tokenizer filters have been applied. */
text: string;
/** Token position used by full-text query matching. */
position: number;
}
export type TokenizeTableOptions =
| {
/** FTS-indexed column whose tokenizer should be used. */
column: string;
indexName?: never;
}
| {
/** Name of the FTS index whose tokenizer should be used. */
indexName: string;
column?: never;
};
/**
* Specification selecting Lance's MemWAL LSM-style write path for
* `mergeInsert`.
@@ -605,17 +583,6 @@ 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.
*
@@ -686,22 +653,6 @@ export abstract class Table {
*/
abstract tags(): Promise<Tags>;
/**
* Get the branch manager for this table.
*
* Branches are isolated, writable lines of history forked from another
* branch (or version). Writes on a branch do not affect `main`.
*/
abstract branches(): Promise<Branches>;
/**
* The branch this table handle is scoped to, or `null` for the main branch.
*
* A handle returned by {@link Branches.create} or {@link Branches.checkout}
* reports the branch it targets; a handle opened normally reports `null`.
*/
abstract currentBranch(): string | null;
/**
* Restore the table to the currently checked out version
*
@@ -736,19 +687,6 @@ export abstract class Table {
abstract optimize(options?: Partial<OptimizeOptions>): Promise<OptimizeStats>;
/** List all indices that have been created with {@link Table.createIndex} */
abstract listIndices(): Promise<IndexConfig[]>;
/**
* Tokenize a full-text search query using the tokenizer configured on an FTS index.
*
* Specify exactly one of `column` or `indexName`.
*
* Model-backed tokenizers such as `jieba/*` and `lindera/*` are rebuilt in
* the client process from index metadata. For remote tables, this means the
* same tokenizer model files must also exist locally.
*/
abstract tokenize(
query: string,
options: TokenizeTableOptions,
): Promise<FtsToken[]>;
/** Return the table as an arrow table */
abstract toArrow(): Promise<ArrowTable>;
@@ -1135,15 +1073,6 @@ 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();
}
@@ -1179,14 +1108,6 @@ export class LocalTable extends Table {
return await this.inner.tags();
}
async branches(): Promise<Branches> {
return new Branches(await this.inner.branches());
}
currentBranch(): string | null {
return this.inner.currentBranch() ?? null;
}
async optimize(options?: Partial<OptimizeOptions>): Promise<OptimizeStats> {
let cleanupOlderThanMs;
if (
@@ -1206,17 +1127,6 @@ export class LocalTable extends Table {
return await this.inner.listIndices();
}
async tokenize(
query: string,
options: TokenizeTableOptions,
): Promise<FtsToken[]> {
return await this.inner.tokenize(
query,
options?.column,
options?.indexName,
);
}
async toArrow(): Promise<ArrowTable> {
return await this.query().toArrow();
}
@@ -1328,57 +1238,3 @@ export interface FieldMetadataUpdate {
/** If true, replace the field's entire metadata map instead of merging. */
replace?: boolean;
}
/**
* Branch manager for a {@link Table}.
*
* Unlike tags, `create` and `checkout` return a new {@link Table} handle scoped
* to the branch; writes on it do not affect `main`.
*/
export class Branches {
#inner: NativeBranches;
/**
* Construct a Branches manager. Internal use only.
* @hidden
*/
constructor(inner: NativeBranches) {
this.#inner = inner;
}
/** List all branches, mapping name to branch metadata. */
async list(): Promise<Record<string, BranchContents>> {
return await this.#inner.list();
}
/**
* Create a branch and return a handle scoped to it.
*
* @param name Name of the new branch.
* @param fromRef Source branch to fork from. Defaults to `main`.
* @param fromVersion A specific version on `fromRef`. Defaults to latest.
*/
async create(
name: string,
fromRef?: string,
fromVersion?: number,
): Promise<Table> {
return new LocalTable(await this.#inner.create(name, fromRef, fromVersion));
}
/**
* Check out an existing branch and return a handle scoped to it.
*
* With `version` set, the returned handle is pinned to that version of the
* branch (a read-only, detached view); otherwise it tracks the branch's
* latest and stays writable.
*/
async checkout(name: string, version?: number): Promise<Table> {
return new LocalTable(await this.#inner.checkout(name, version));
}
/** Delete a branch. */
async delete(name: string): Promise<void> {
return await this.#inner.delete(name);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-darwin-arm64",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.2",
"os": ["darwin"],
"cpu": ["arm64"],
"main": "lancedb.darwin-arm64.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-arm64-gnu",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.2",
"os": ["linux"],
"cpu": ["arm64"],
"main": "lancedb.linux-arm64-gnu.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-arm64-musl",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.2",
"os": ["linux"],
"cpu": ["arm64"],
"main": "lancedb.linux-arm64-musl.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-x64-gnu",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.2",
"os": ["linux"],
"cpu": ["x64"],
"main": "lancedb.linux-x64-gnu.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-x64-musl",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.2",
"os": ["linux"],
"cpu": ["x64"],
"main": "lancedb.linux-x64-musl.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-win32-arm64-msvc",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.2",
"os": [
"win32"
],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-win32-x64-msvc",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.2",
"os": ["win32"],
"cpu": ["x64"],
"main": "lancedb.win32-x64-msvc.node",
+7 -78
View File
@@ -1,12 +1,12 @@
{
"name": "@lancedb/lancedb",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@lancedb/lancedb",
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.1",
"cpu": [
"x64",
"arm64"
@@ -18,7 +18,6 @@
"win32"
],
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
@@ -27,8 +26,7 @@
"@aws-sdk/client-s3": "3.1003.0",
"@biomejs/biome": "^1.7.3",
"@jest/globals": "^29.7.0",
"@napi-rs/cli": "3.7.0",
"@opentelemetry/sdk-metrics": "^1.30.0",
"@napi-rs/cli": "3.5.1",
"@types/axios": "^0.14.0",
"@types/jest": "^29.1.2",
"@types/node": "22.7.4",
@@ -2944,9 +2942,9 @@
}
},
"node_modules/@napi-rs/cli": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-3.7.0.tgz",
"integrity": "sha512-3d3+rmxlOIV/G1zPWeX4PCxuYnhcCQM2BvY9rtimC8RO0dFR9gtYP+Grov+WoduZtfWRj5N1XvytWeRxxCk5zw==",
"version": "3.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-3.5.1.tgz",
"integrity": "sha512-XBfLQRDcB3qhu6bazdMJsecWW55kR85l5/k0af9BIBELXQSsCFU0fzug7PX8eQp6vVdm7W/U3z6uP5WmITB2Gw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2956,7 +2954,7 @@
"@octokit/rest": "^22.0.1",
"clipanion": "^4.0.0-rc.4",
"colorette": "^2.0.20",
"emnapi": "^1.10.0",
"emnapi": "^1.7.1",
"es-toolkit": "^1.41.0",
"js-yaml": "^4.1.0",
"obug": "^2.0.0",
@@ -4150,75 +4148,6 @@
"@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",
+2 -4
View File
@@ -11,7 +11,7 @@
"ann"
],
"private": false,
"version": "0.32.0-beta.2",
"version": "0.30.1-beta.2",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
@@ -43,8 +43,7 @@
"@aws-sdk/client-s3": "3.1003.0",
"@biomejs/biome": "^1.7.3",
"@jest/globals": "^29.7.0",
"@napi-rs/cli": "3.7.0",
"@opentelemetry/sdk-metrics": "^1.30.0",
"@napi-rs/cli": "3.5.1",
"@types/axios": "^0.14.0",
"@types/jest": "^29.1.2",
"@types/node": "22.7.4",
@@ -93,7 +92,6 @@
"version": "napi version"
},
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"reflect-metadata": "^0.2.2"
},
"optionalDependencies": {
+5 -58
View File
@@ -8,9 +8,6 @@ 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
@@ -34,11 +31,8 @@ importers:
specifier: ^29.7.0
version: 29.7.0
'@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)
specifier: 3.5.1
version: 3.5.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.7.4)
'@types/axios':
specifier: ^0.14.0
version: 0.14.4
@@ -893,8 +887,8 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@napi-rs/cli@3.7.0':
resolution: {integrity: sha512-3d3+rmxlOIV/G1zPWeX4PCxuYnhcCQM2BvY9rtimC8RO0dFR9gtYP+Grov+WoduZtfWRj5N1XvytWeRxxCk5zw==}
'@napi-rs/cli@3.5.1':
resolution: {integrity: sha512-XBfLQRDcB3qhu6bazdMJsecWW55kR85l5/k0af9BIBELXQSsCFU0fzug7PX8eQp6vVdm7W/U3z6uP5WmITB2Gw==}
engines: {node: '>= 16'}
hasBin: true
peerDependencies:
@@ -1313,32 +1307,6 @@ 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==}
@@ -4614,7 +4582,7 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@napi-rs/cli@3.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.7.4)':
'@napi-rs/cli@3.5.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@22.7.4)':
dependencies:
'@inquirer/prompts': 8.4.3(@types/node@22.7.4)
'@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
@@ -4957,27 +4925,6 @@ 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,12 +112,6 @@ 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);
}
+1 -70
View File
@@ -4,16 +4,13 @@
use std::sync::Mutex;
use lancedb::index::Index as LanceDbIndex;
use lancedb::index::scalar::{BTreeIndexBuilder, FmIndexBuilder, FtsIndexBuilder};
use lancedb::index::scalar::{BTreeIndexBuilder, FtsIndexBuilder};
use lancedb::index::vector::{
IvfFlatIndexBuilder, IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder,
IvfRqIndexBuilder,
};
use lancedb::tokenize as lancedb_tokenize;
use napi_derive::napi;
use crate::error::NapiErrorExt;
use crate::table::FtsToken;
use crate::util::parse_distance_type;
#[napi]
@@ -33,65 +30,6 @@ impl Index {
}
}
#[napi(catch_unwind)]
#[allow(dead_code, clippy::too_many_arguments)]
pub fn tokenize(
query: String,
base_tokenizer: Option<String>,
language: Option<String>,
max_token_length: Option<u32>,
lower_case: Option<bool>,
stem: Option<bool>,
remove_stop_words: Option<bool>,
ascii_folding: Option<bool>,
ngram_min_length: Option<u32>,
ngram_max_length: Option<u32>,
prefix_only: Option<bool>,
) -> napi::Result<Vec<FtsToken>> {
let mut opts = FtsIndexBuilder::default();
if let Some(base_tokenizer) = base_tokenizer {
opts = opts.base_tokenizer(base_tokenizer);
}
if let Some(language) = language {
opts = opts.language(&language).map_err(|_| {
napi::Error::from_reason(format!(
"LanceDB does not support the requested language: '{}'",
language
))
})?;
}
if let Some(max_token_length) = max_token_length {
opts = opts.max_token_length(Some(max_token_length as usize));
}
if let Some(lower_case) = lower_case {
opts = opts.lower_case(lower_case);
}
if let Some(stem) = stem {
opts = opts.stem(stem);
}
if let Some(remove_stop_words) = remove_stop_words {
opts = opts.remove_stop_words(remove_stop_words);
}
if let Some(ascii_folding) = ascii_folding {
opts = opts.ascii_folding(ascii_folding);
}
if let Some(ngram_min_length) = ngram_min_length {
opts = opts.ngram_min_length(ngram_min_length);
}
if let Some(ngram_max_length) = ngram_max_length {
opts = opts.ngram_max_length(ngram_max_length);
}
if let Some(prefix_only) = prefix_only {
opts = opts.ngram_prefix_only(prefix_only);
}
Ok(lancedb_tokenize(&query, &opts)
.default_error()?
.into_iter()
.map(FtsToken::from)
.collect())
}
#[napi]
impl Index {
#[napi(factory)]
@@ -205,13 +143,6 @@ impl Index {
}
}
#[napi(factory)]
pub fn fm() -> Self {
Self {
inner: Mutex::new(Some(LanceDbIndex::Fm(FmIndexBuilder::default()))),
}
}
#[napi(factory)]
#[allow(clippy::too_many_arguments)]
pub fn fts(
-6
View File
@@ -12,7 +12,6 @@ mod header;
mod index;
mod iterator;
pub mod merge;
pub mod otel;
pub mod permutation;
mod query;
pub mod remote;
@@ -66,11 +65,6 @@ 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)]
+6 -4
View File
@@ -3,7 +3,7 @@
use std::time::Duration;
use lancedb::{ipc::ipc_file_to_batches, table::merge::MergeInsertBuilder};
use lancedb::{arrow::IntoArrow, ipc::ipc_file_to_batches, table::merge::MergeInsertBuilder};
use napi::bindgen_prelude::*;
use napi_derive::napi;
@@ -66,9 +66,11 @@ impl NativeMergeInsertBuilder {
#[napi(catch_unwind)]
pub async fn execute(&self, buf: Buffer) -> napi::Result<MergeResult> {
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 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 this = self.clone();
-119
View File
@@ -1,119 +0,0 @@
// 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()
}
+1 -7
View File
@@ -16,7 +16,6 @@ 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>>,
}
@@ -126,15 +125,10 @@ 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,
clump_size,
},
SplitStrategy::Random { seed, sizes },
options.split_names.clone(),
)
})
+21 -56
View File
@@ -19,7 +19,6 @@ use lancedb::index::scalar::{
BooleanQuery, BoostQuery, FtsQuery, FullTextSearchQuery, MatchQuery, MultiMatchQuery, Occur,
Operator, PhraseQuery,
};
use lancedb::query::AnalyzePlanDistributedMetrics;
use lancedb::query::ExecutableQuery;
use lancedb::query::Query as LanceDbQuery;
use lancedb::query::QueryBase;
@@ -48,28 +47,6 @@ impl From<ColumnOrdering> for LanceDbColumnOrdering {
}
}
fn analyze_plan_options(
distributed_metrics: Option<String>,
) -> napi::Result<QueryExecutionOptions> {
let analyze_plan_distributed_metrics =
match distributed_metrics.as_deref().unwrap_or("aggregate") {
"aggregate" => AnalyzePlanDistributedMetrics::Aggregate,
"per_worker" => AnalyzePlanDistributedMetrics::PerWorker,
"full" => AnalyzePlanDistributedMetrics::Full,
mode => {
return Err(napi::Error::from_reason(format!(
"Invalid distributedMetrics value '{}'. Expected one of: \
'aggregate', 'per_worker', 'full'",
mode
)));
}
};
let mut options = QueryExecutionOptions::default();
options.analyze_plan_distributed_metrics = analyze_plan_distributed_metrics;
Ok(options)
}
fn bytes_to_arrow_array(data: Uint8Array, dtype: String) -> napi::Result<Arc<dyn Array>> {
let buf = arrow_buffer::Buffer::from(data.to_vec());
let num_bytes = buf.len();
@@ -223,17 +200,13 @@ impl Query {
}
#[napi(catch_unwind)]
pub async fn analyze_plan(&self, distributed_metrics: Option<String>) -> napi::Result<String> {
let options = analyze_plan_options(distributed_metrics)?;
self.inner
.analyze_plan_with_options(options)
.await
.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
pub async fn analyze_plan(&self) -> napi::Result<String> {
self.inner.analyze_plan().await.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
}
}
@@ -439,17 +412,13 @@ impl VectorQuery {
}
#[napi(catch_unwind)]
pub async fn analyze_plan(&self, distributed_metrics: Option<String>) -> napi::Result<String> {
let options = analyze_plan_options(distributed_metrics)?;
self.inner
.analyze_plan_with_options(options)
.await
.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
pub async fn analyze_plan(&self) -> napi::Result<String> {
self.inner.analyze_plan().await.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
}
}
@@ -522,17 +491,13 @@ impl TakeQuery {
}
#[napi(catch_unwind)]
pub async fn analyze_plan(&self, distributed_metrics: Option<String>) -> napi::Result<String> {
let options = analyze_plan_options(distributed_metrics)?;
self.inner
.analyze_plan_with_options(options)
.await
.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
pub async fn analyze_plan(&self) -> napi::Result<String> {
self.inner.analyze_plan().await.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to execute analyze plan: {}",
convert_error(&e)
))
})
}
}
-121
View File
@@ -3,7 +3,6 @@
use std::collections::HashMap;
use lancedb::error::Error;
use napi_derive::*;
/// Timeout configuration for remote HTTP client.
@@ -141,84 +140,6 @@ 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 {
@@ -235,45 +156,3 @@ 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>\")"));
}
}
+7 -235
View File
@@ -3,13 +3,11 @@
use std::collections::HashMap;
use chrono::{DateTime, Utc};
use lancedb::ipc::{ipc_file_to_batches, ipc_file_to_schema};
use lancedb::table::{
AddDataMode, ColumnAlteration as LanceColumnAlteration, Duration,
FieldMetadataUpdate as LanceFieldMetadataUpdate, FtsToken as LanceDbFtsToken,
NewColumnTransform, OptimizeAction, OptimizeOptions, Ref, Table as LanceDbTable,
FieldMetadataUpdate as LanceFieldMetadataUpdate, NewColumnTransform, OptimizeAction,
OptimizeOptions, Table as LanceDbTable,
};
use napi::bindgen_prelude::*;
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
@@ -165,7 +163,7 @@ impl Table {
if let Some(train) = train {
builder = builder.train(train);
}
builder.execute().await.default_error().map(|_| ())
builder.execute().await.default_error()
}
#[napi(catch_unwind)]
@@ -411,16 +409,6 @@ 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()
@@ -490,19 +478,6 @@ impl Table {
})
}
#[napi(catch_unwind)]
pub async fn branches(&self) -> napi::Result<Branches> {
Ok(Branches {
inner: self.inner_ref()?.clone(),
})
}
/// The branch this handle is scoped to, or `null` for the main branch.
#[napi]
pub fn current_branch(&self) -> napi::Result<Option<String>> {
Ok(self.inner_ref()?.current_branch())
}
#[napi(catch_unwind)]
pub async fn optimize(
&self,
@@ -574,27 +549,6 @@ impl Table {
.collect::<Vec<_>>())
}
#[napi(catch_unwind)]
pub async fn tokenize(
&self,
query: String,
column: Option<String>,
index_name: Option<String>,
) -> napi::Result<Vec<FtsToken>> {
let table = self.inner_ref()?;
let tokens = match (column.as_deref(), index_name.as_deref()) {
(Some(_), Some(_)) | (None, None) => {
return Err(napi::Error::from_reason(
"Specify exactly one of 'column' or 'indexName'",
));
}
(Some(column), None) => table.tokenize_with_column(&query, column).await,
(None, Some(index_name)) => table.tokenize(&query, index_name).await,
}
.default_error()?;
Ok(tokens.into_iter().map(FtsToken::from).collect())
}
#[napi(catch_unwind)]
pub async fn index_stats(&self, index_name: String) -> napi::Result<Option<IndexStatistics>> {
let tbl = self.inner_ref()?;
@@ -641,43 +595,6 @@ pub struct IndexConfig {
/// Currently this is always an array of size 1. In the future there may
/// be more columns to represent composite indices.
pub columns: Vec<String>,
/// The UUID of the first segment of the index.
///
/// `undefined` for remote tables, which do not yet surface this.
pub index_uuid: Option<String>,
/// The protobuf type URL, a precise type identifier for the index.
///
/// `undefined` for remote tables.
pub type_url: Option<String>,
/// When the index was created.
///
/// `undefined` for remote tables or indices created before timestamps were tracked.
pub created_at: Option<DateTime<Utc>>,
/// The number of rows indexed, across all segments.
///
/// `undefined` for remote tables.
pub num_indexed_rows: Option<i64>,
/// The number of rows not yet covered by this index.
///
/// `undefined` for remote tables.
pub num_unindexed_rows: Option<i64>,
/// The total size in bytes of all index files across all segments.
///
/// `undefined` for remote tables or indices without size tracking.
pub size_bytes: Option<i64>,
/// The number of segments that make up the index.
///
/// `undefined` for remote tables.
pub num_segments: Option<i32>,
/// The on-disk index format version.
///
/// `undefined` for remote tables.
pub index_version: Option<i32>,
/// Index-type-specific details parsed as a JavaScript object.
///
/// Falls back to a raw string if JSON parsing fails. `undefined` for
/// remote tables or when details are unavailable.
pub index_details: Option<serde_json::Value>,
}
impl From<lancedb::index::IndexConfig> for IndexConfig {
@@ -687,35 +604,6 @@ impl From<lancedb::index::IndexConfig> for IndexConfig {
index_type,
columns: value.columns,
name: value.name,
index_uuid: value.index_uuid,
type_url: value.type_url,
created_at: value.created_at,
num_indexed_rows: value.num_indexed_rows.map(|n| n as i64),
num_unindexed_rows: value.num_unindexed_rows.map(|n| n as i64),
size_bytes: value.size_bytes.map(|n| n as i64),
num_segments: value.num_segments.map(|n| n as i32),
index_version: value.index_version,
index_details: value
.index_details
.and_then(|s| serde_json::from_str(&s).ok()),
}
}
}
#[napi(object)]
/// A token produced by the tokenizer configured on a full-text search index.
pub struct FtsToken {
/// The token text after the index tokenizer has applied its filters.
pub text: String,
/// The token position used by full-text query matching.
pub position: u32,
}
impl From<LanceDbFtsToken> for FtsToken {
fn from(token: LanceDbFtsToken) -> Self {
Self {
text: token.text,
position: token.position,
}
}
}
@@ -777,47 +665,6 @@ 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)]
@@ -991,6 +838,9 @@ pub struct IndexStatistics {
pub distance_type: Option<String>,
/// The number of parts this index is split into.
pub num_indices: Option<u32>,
/// The KMeans loss value of the index,
/// it is only present for vector indices.
pub loss: Option<f64>,
}
impl From<lancedb::index::IndexStatistics> for IndexStatistics {
fn from(value: lancedb::index::IndexStatistics) -> Self {
@@ -1000,6 +850,7 @@ impl From<lancedb::index::IndexStatistics> for IndexStatistics {
index_type: value.index_type.to_string(),
distance_type: value.distance_type.map(|d| d.to_string()),
num_indices: value.num_indices,
loss: value.loss,
}
}
}
@@ -1209,13 +1060,6 @@ pub struct TagContents {
pub manifest_size: i64,
}
#[napi]
pub struct BranchContents {
pub parent_branch: Option<String>,
pub parent_version: i64,
pub manifest_size: i64,
}
#[napi]
pub struct Tags {
inner: LanceDbTable,
@@ -1284,75 +1128,3 @@ impl Tags {
.default_error()
}
}
#[napi]
pub struct Branches {
inner: LanceDbTable,
}
#[napi]
impl Branches {
#[napi]
pub async fn list(&self) -> napi::Result<HashMap<String, BranchContents>> {
let branches = self.inner.list_branches().await.default_error()?;
let result = branches
.into_iter()
.map(|(k, v)| {
(
k,
BranchContents {
parent_branch: v.parent_branch,
parent_version: v.parent_version as i64,
manifest_size: v.manifest_size as i64,
},
)
})
.collect();
Ok(result)
}
#[napi]
pub async fn create(
&self,
name: String,
from_ref: Option<String>,
from_version: Option<i64>,
) -> napi::Result<Table> {
let from_ref = from_ref.filter(|b| b != "main");
let from_version = from_version
.map(|v| {
u64::try_from(v).map_err(|_| {
napi::Error::from_reason("from_version must be a non-negative integer")
})
})
.transpose()?;
let from = Ref::Version(from_ref, from_version);
let table = self
.inner
.create_branch(&name, from)
.await
.default_error()?;
Ok(Table::new(table))
}
#[napi]
pub async fn checkout(&self, name: String, version: Option<i64>) -> napi::Result<Table> {
let version = version
.map(|v| {
u64::try_from(v)
.map_err(|_| napi::Error::from_reason("version must be a non-negative integer"))
})
.transpose()?;
let table = self
.inner
.checkout_branch(&name, version)
.await
.default_error()?;
Ok(Table::new(table))
}
#[napi]
pub async fn delete(&self, name: String) -> napi::Result<()> {
self.inner.delete_branch(&name).await.default_error()
}
}
+1 -3
View File
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "0.35.0-beta.2"
current_version = "0.33.1-beta.2"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
@@ -23,8 +23,6 @@ allow_dirty = true
commit = true
message = "Bump version: {current_version} → {new_version}"
commit_args = ""
# bump-my-version >=1.4.0 rejects pre_commit_hooks containing shell syntax unless opted in.
allow_shell_hooks = true
# Update Cargo.lock after version bump
pre_commit_hooks = [
+3 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "lancedb-python"
version = "0.35.0-beta.2"
version = "0.33.1-beta.2"
publish = false
edition.workspace = true
description = "Python bindings for LanceDB"
@@ -26,8 +26,7 @@ lance-namespace-impls.workspace = true
lance-io.workspace = true
env_logger.workspace = true
log.workspace = true
pyo3 = { version = "0.28", features = ["extension-module", "abi3-py39", "chrono"] }
chrono = { version = "0.4", default-features = false, features = ["clock"] }
pyo3 = { version = "0.28", features = ["extension-module", "abi3-py39"] }
pyo3-async-runtimes = { version = "0.28", features = [
"attributes",
"tokio-runtime",
@@ -47,6 +46,6 @@ pyo3-build-config = { version = "0.28", features = [
] }
[features]
default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface", "lancedb/cos", "lancedb/goosefs", "lancedb/metrics-otel"]
default = ["remote", "lancedb/aws", "lancedb/gcs", "lancedb/azure", "lancedb/dynamodb", "lancedb/oss", "lancedb/huggingface"]
fp16kernels = ["lancedb/fp16kernels"]
remote = ["lancedb/remote"]
@@ -1,135 +0,0 @@
#!/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()
+2 -8
View File
@@ -47,10 +47,6 @@ 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,12 +57,10 @@ tests = [
"duckdb>=0.9.0",
"pytz>=2023.3",
"polars>=0.19, <=1.3.0",
"pyarrow<25",
"pyarrow-stubs>=16.0",
"pylance==9.0.0rc1",
"pylance>=5.0.0b5",
"requests>=2.31.0",
"datafusion>=54,<55",
"opentelemetry-sdk>=1.30.0",
"datafusion>=52,<53",
]
dev = [
"ruff>=0.3.0",
+4 -82
View File
@@ -6,33 +6,19 @@ import importlib.metadata
import os
from concurrent.futures import ThreadPoolExecutor
from datetime import timedelta
from typing import Dict, Optional, Union, Any, List, Iterable
from typing import Dict, Optional, Union, Any, List
__version__ = importlib.metadata.version("lancedb")
from ._lancedb import connect as lancedb_connect
from ._lancedb import FtsToken
from ._lancedb import tokenize as _tokenize
from .common import URI, sanitize_uri
from urllib.parse import urlparse
from .db import AsyncConnection, DBConnection, LanceDBConnection
from .remote import ClientConfig
from .remote.db import RemoteDBConnection
from .expr import Expr, col, lit, func
from .udf import (
udf,
table_udf,
Udf,
Job,
JobFailedError,
MaterializedView,
AsyncJob,
AsyncMaterializedView,
)
from .lineage import Lineage, Node, Edge, FunctionRef
from .schema import blob, vector, BlobType
from .schema import vector
from .table import AsyncTable, Table
from .types import BaseTokenizerType
from ._lancedb import Session
from .namespace import (
connect_namespace,
@@ -103,8 +89,6 @@ 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
@@ -163,14 +147,8 @@ def connect(
For object storage, use a URI prefix:
>>> db = lancedb.connect( # doctest: +SKIP
... "s3://my-bucket/lancedb",
... storage_options={
... "aws_access_key_id": "***",
... "aws_secret_access_key": "***",
... "aws_region": "us-east-1",
... },
... )
>>> db = lancedb.connect("s3://my-bucket/lancedb",
... storage_options={"aws_access_key_id": "***"})
For tests and temporary data, use an in-memory database:
@@ -260,40 +238,6 @@ def connect(
)
def tokenize(
query: str,
*,
base_tokenizer: BaseTokenizerType = "simple",
language: str = "English",
max_token_length: Optional[int] = 40,
lower_case: bool = True,
stem: bool = True,
remove_stop_words: bool = True,
ascii_folding: bool = True,
ngram_min_length: int = 3,
ngram_max_length: int = 3,
prefix_only: bool = False,
) -> Iterable[FtsToken]:
"""Tokenize a full-text search query using an explicit tokenizer.
This does not require a table or FTS index. The tokenizer options match
:class:`lancedb.index.FTS`.
"""
return _tokenize(
query,
base_tokenizer=base_tokenizer,
language=language,
max_token_length=max_token_length,
lower_case=lower_case,
stem=stem,
remove_stop_words=remove_stop_words,
ascii_folding=ascii_folding,
ngram_min_length=ngram_min_length,
ngram_max_length=ngram_max_length,
prefix_only=prefix_only,
)
WORKER_PROPERTY_PREFIX = "_lancedb_worker_"
@@ -396,7 +340,6 @@ 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.
@@ -446,10 +389,6 @@ 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
--------
@@ -496,41 +435,24 @@ async def connect_async(
session,
manifest_enabled,
namespace_client_properties,
oauth_config,
)
)
__all__ = [
"udf",
"table_udf",
"Udf",
"Job",
"JobFailedError",
"MaterializedView",
"AsyncJob",
"AsyncMaterializedView",
"Lineage",
"Node",
"Edge",
"FunctionRef",
"connect",
"connect_async",
"tokenize",
"connect_namespace",
"connect_namespace_async",
"AsyncConnection",
"AsyncLanceNamespaceDBConnection",
"AsyncTable",
"FtsToken",
"col",
"Expr",
"func",
"lit",
"URI",
"sanitize_uri",
"blob",
"BlobType",
"vector",
"DBConnection",
"LanceDBConnection",
-420
View File
@@ -1,420 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Blob fetch API and v2 projection helpers."""
from __future__ import annotations
import io
from collections.abc import Awaitable, Callable, Iterable
from typing import TYPE_CHECKING, Optional, Union
import pyarrow as pa
from .expr import Expr
from .schema import blob_v2_column_paths
from .types import BlobMode, QueryProjection, QueryProjectionSpec
from .util import get_uri_scheme
if TYPE_CHECKING:
from _typeshed import WriteableBuffer
from .remote.table import RemoteTable
from .table import AsyncTable, Table
BLOB_MODE_TO_HANDLING = {
"lazy": "blobs_descriptions",
"bytes": "all_binary",
"descriptions": "blobs_descriptions",
}
ROW_ID_FIELD_NAME = "_lance_row_id"
FetchBlobsSync = Callable[[str, pa.Table], pa.Array | pa.ChunkedArray]
FetchBlobsAsync = Callable[[str, pa.Table], Awaitable[pa.Array | pa.ChunkedArray]]
class BlobFile(io.RawIOBase):
"""Seekable lazy handle from :meth:`~lancedb.table.Table.fetch_blob_files`.
Bytes load on ``read`` or ``read_range``, not when the handle is opened.
Use :meth:`aread` from async code.
"""
def __init__(self, inner) -> None:
self._inner = inner
async def aread(self) -> bytes:
return await self._inner.read()
def close(self) -> None:
self._inner.close()
@property
def closed(self) -> bool:
return self._inner.is_closed()
def readable(self) -> bool:
return True
def seekable(self) -> bool:
return True
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
if whence == io.SEEK_SET:
self._inner.seek(offset)
elif whence == io.SEEK_CUR:
self._inner.seek(self._inner.tell() + offset)
elif whence == io.SEEK_END:
self._inner.seek(self._inner.size() + offset)
else:
raise ValueError(f"invalid whence: {whence}")
return self._inner.tell()
def tell(self) -> int:
return self._inner.tell()
def size(self) -> int:
return self._inner.size()
def readall(self) -> bytes:
return self._inner.read_bytes()
def read(self, size: int = -1) -> bytes:
if size == -1:
return self._inner.read_bytes()
return super().read(size)
def read_range(self, offset: int, length: int) -> bytes:
return self._inner.read_range(offset, length)
def readinto(self, b: WriteableBuffer) -> int:
view = memoryview(b).cast("B")
chunk = self._inner.read_up_to(len(view))
view[: len(chunk)] = chunk
return len(chunk)
def __repr__(self) -> str:
return f"<BlobFile size={self.size()}>"
def validate_blob_mode(blob_mode: BlobMode) -> None:
if blob_mode not in BLOB_MODE_TO_HANDLING:
modes = ", ".join(repr(mode) for mode in BLOB_MODE_TO_HANDLING)
raise ValueError(f"blob_mode must be one of {modes}, got {blob_mode!r}")
def supports_blob_auto_row_id(table: Table | AsyncTable | RemoteTable) -> bool:
"""Blob auto row-id applies to native tables, not LanceDB Cloud."""
from .remote.table import RemoteTable
if isinstance(table, RemoteTable):
return False
inner = getattr(table, "_inner", None)
if inner is not None:
uri = inner.database().uri
if isinstance(uri, str) and get_uri_scheme(uri) == "db":
return False
return True
def projection_includes_blob_column(
projection: QueryProjection,
blob_columns: Iterable[str],
) -> bool:
columns = set(blob_columns)
if not columns:
return False
if projection is None:
return True
for output, source in _iter_projection_pairs(projection):
if output in columns or source in columns:
return True
return False
def blob_v2_projection_sources(
schema: pa.Schema,
projection: QueryProjection,
) -> dict[str, str]:
blob_columns = blob_v2_column_paths(schema)
if not blob_columns:
return {}
columns = set(blob_columns)
if projection is None:
return {column: column for column in blob_columns}
return {
output: source
for output, source in _iter_projection_pairs(projection)
if source in columns
}
def v2_projection_needs_row_id(
schema: pa.Schema,
projection: QueryProjection,
*,
with_row_id: bool,
) -> bool:
if with_row_id:
return False
return projection_includes_blob_column(projection, blob_v2_column_paths(schema))
def blob_auto_row_id_for_scan(
table: Table | AsyncTable | RemoteTable,
schema: pa.Schema,
projection: QueryProjection,
*,
with_row_id: bool | None,
) -> bool:
if with_row_id is not None:
return False
if not supports_blob_auto_row_id(table):
return False
return v2_projection_needs_row_id(schema, projection, with_row_id=False)
def finalize_blob_query_table(
tbl: pa.Table,
*,
user_requested_row_id: bool,
blob_auto_row_id: bool,
blob_paths: Iterable[str] = (),
) -> pa.Table:
if user_requested_row_id or not blob_auto_row_id:
return tbl
return stash_auto_row_ids(tbl, blob_paths)
async def replace_v2_blob_columns_with_bytes(
tbl: pa.Table,
blob_sources: dict[str, str],
fetch_blobs: FetchBlobsAsync,
) -> pa.Table:
for output_name, source_name in blob_sources.items():
if output_name not in tbl.column_names:
continue
blobs = await fetch_blobs(source_name, tbl)
tbl = _set_blob_column(tbl, output_name, blobs)
return tbl
def replace_v2_blob_columns_with_bytes_sync(
tbl: pa.Table,
blob_sources: dict[str, str],
fetch_blobs: FetchBlobsSync,
) -> pa.Table:
for output_name, source_name in blob_sources.items():
if output_name not in tbl.column_names:
continue
blobs = fetch_blobs(source_name, tbl)
tbl = _set_blob_column(tbl, output_name, blobs)
return tbl
def stash_auto_row_ids(tbl: pa.Table, blob_paths: Iterable[str]) -> pa.Table:
if "_rowid" not in tbl.column_names:
raise ValueError("query result has no '_rowid' column to hide")
present_paths = [p for p in blob_paths if p.split(".")[0] in tbl.column_names]
if not present_paths:
raise ValueError("query result has no blob v2 column to carry a row id")
row_ids = tbl["_rowid"]
if isinstance(row_ids, pa.ChunkedArray):
row_ids = row_ids.combine_chunks()
row_ids = row_ids.cast(pa.uint64())
for path in present_paths:
tbl = _embed_row_id_in_column(tbl, path, row_ids)
return tbl.drop_columns(["_rowid"])
def read_row_ids_from_hits(hits: pa.Table, blob_column: str) -> list[int]:
if "_rowid" in hits.column_names:
return hits["_rowid"].to_pylist()
try:
leaf = _leaf_struct_column(hits, blob_column)
if ROW_ID_FIELD_NAME in leaf.type.names:
return leaf.field(ROW_ID_FIELD_NAME).to_pylist()
except KeyError:
pass
# blob_column is the source name; aliased projections use the output name in hits.
row_ids = _find_row_id_in_any_column(hits)
if row_ids is not None:
return row_ids
raise ValueError(
f"query result has no '_rowid' column and no '{ROW_ID_FIELD_NAME}' "
f"field on blob column '{blob_column}'. Pass fresh blob query "
"results, call .with_row_id(True), or pass a list of row ids."
)
def _find_row_id_in_any_column(tbl: pa.Table) -> Optional[list[int]]:
for name in tbl.column_names:
column = tbl.column(name)
if isinstance(column, pa.ChunkedArray):
column = column.combine_chunks()
row_ids = _find_row_id_in_struct(column)
if row_ids is not None:
return row_ids
return None
def _find_row_id_in_struct(array: pa.Array) -> Optional[list[int]]:
if not pa.types.is_struct(array.type):
return None
if ROW_ID_FIELD_NAME in array.type.names:
return array.field(ROW_ID_FIELD_NAME).to_pylist()
for i in range(array.type.num_fields):
row_ids = _find_row_id_in_struct(array.field(i))
if row_ids is not None:
return row_ids
return None
def _iter_projection_pairs(
projection: QueryProjectionSpec,
) -> Iterable[tuple[str, str]]:
if isinstance(projection, dict):
for name, expr in projection.items():
if isinstance(expr, str):
yield name, expr
elif isinstance(expr, Expr):
yield name, expr.to_sql()
return
for column in projection:
if isinstance(column, str):
yield column, column
elif isinstance(column, tuple) and len(column) == 2:
name, expr = column
if isinstance(expr, str):
yield name, expr
elif isinstance(expr, Expr):
yield name, expr.to_sql()
def _set_blob_column(tbl: pa.Table, output_name: str, blobs: pa.Array) -> pa.Table:
index = tbl.schema.get_field_index(output_name)
return tbl.set_column(index, pa.field(output_name, blobs.type), [blobs])
def _embed_row_id_in_column(tbl: pa.Table, path: str, row_ids: pa.Array) -> pa.Table:
def add_row_id(children: list, child_fields: list) -> None:
children.append(row_ids)
child_fields.append(pa.field(ROW_ID_FIELD_NAME, pa.uint64(), nullable=False))
return _transform_struct_column(tbl, path, add_row_id)
def strip_auto_row_ids(tbl: pa.Table, blob_paths: Iterable[str]) -> pa.Table:
"""Remove any `_lance_row_id` field embedded in blob descriptor structs.
For read-only descriptor views (`blob_mode="descriptions"`) that never
fetch bytes, so have no use for the row id.
"""
def drop_row_id(children: list, child_fields: list) -> None:
for i, field in enumerate(child_fields):
if field.name == ROW_ID_FIELD_NAME:
del children[i], child_fields[i]
return
for path in blob_paths:
if path.split(".")[0] not in tbl.column_names:
continue
tbl = _transform_struct_column(tbl, path, drop_row_id)
return tbl
def _transform_struct_column(
tbl: pa.Table, path: str, leaf_transform: Callable[[list, list], None]
) -> pa.Table:
top_name, *rest = path.split(".")
top_index = tbl.schema.get_field_index(top_name)
top_field = tbl.schema.field(top_index)
top_array = tbl.column(top_name)
if isinstance(top_array, pa.ChunkedArray):
top_array = top_array.combine_chunks()
new_array, new_field = _rebuild_struct(top_array, top_field, rest, leaf_transform)
return tbl.set_column(top_index, new_field, new_array)
def _rebuild_struct(
struct_array: pa.StructArray,
struct_field: pa.Field,
remaining_path: list[str],
leaf_transform: Callable[[list, list], None],
) -> tuple[pa.StructArray, pa.Field]:
null_mask = struct_array.is_null()
if not remaining_path:
children = [struct_array.field(i) for i in range(struct_array.type.num_fields)]
child_fields = list(struct_array.type)
leaf_transform(children, child_fields)
new_array = pa.StructArray.from_arrays(
children, fields=child_fields, mask=null_mask
)
else:
child_name = remaining_path[0]
child_index = struct_array.type.get_field_index(child_name)
child_array = struct_array.field(child_index)
child_field = struct_array.type.field(child_index)
new_child_array, new_child_field = _rebuild_struct(
child_array, child_field, remaining_path[1:], leaf_transform
)
children = []
child_fields = []
for i in range(struct_array.type.num_fields):
field = struct_array.type.field(i)
if field.name == child_name:
children.append(new_child_array)
child_fields.append(new_child_field)
else:
children.append(struct_array.field(i))
child_fields.append(field)
new_array = pa.StructArray.from_arrays(
children, fields=child_fields, mask=null_mask
)
new_field = pa.field(
struct_field.name,
new_array.type,
nullable=struct_field.nullable,
metadata=struct_field.metadata,
)
return new_array, new_field
def _leaf_struct_column(tbl: pa.Table, path: str) -> pa.StructArray:
parts = path.split(".")
column = tbl.column(parts[0])
if isinstance(column, pa.ChunkedArray):
column = column.combine_chunks()
for part in parts[1:]:
column = column.field(part)
return column
def _normalize_blob_row_ids(
row_ids: Union[list[int], pa.Table], blob_column: str
) -> list[int]:
if isinstance(row_ids, pa.Table):
return read_row_ids_from_hits(row_ids, blob_column)
if isinstance(row_ids, (pa.Array, pa.ChunkedArray)):
raise ValueError(
"pass a query table with _rowid, not a column array "
"(use fetch_blobs('image', hits), not fetch_blobs('image', hits['image']))"
)
return list(row_ids)
def _wrap_blob_files(handles: Iterable[object]) -> list[Optional[BlobFile]]:
return [BlobFile(handle) if handle is not None else None for handle in handles]
+4 -130
View File
@@ -1,5 +1,4 @@
from datetime import date, datetime, timedelta
from decimal import Decimal
from datetime import timedelta
from typing import Dict, List, Optional, Tuple, Any, TypedDict, Union, Literal
import pyarrow as pa
@@ -11,7 +10,6 @@ from .index import (
IvfSq,
Bitmap,
LabelList,
Fm,
HnswPq,
HnswSq,
HnswFlat,
@@ -25,45 +23,10 @@ from lance_namespace import (
ListTablesResponse,
)
from .remote import ClientConfig
from .types import BaseTokenizerType
IvfHnswPq: type[HnswPq] = HnswPq
IvfHnswSq: type[HnswSq] = HnswSq
IvfHnswFlat: type[HnswFlat] = HnswFlat
AnalyzePlanDistributedMetrics = Literal["aggregate", "per_worker", "full"]
class MetricPoint:
name: str
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]: ...
def tokenize(
query: str,
*,
base_tokenizer: BaseTokenizerType = "simple",
language: str = "English",
max_token_length: Optional[int] = 40,
lower_case: bool = True,
stem: bool = True,
remove_stop_words: bool = True,
ascii_folding: bool = True,
ngram_min_length: int = 3,
ngram_max_length: int = 3,
prefix_only: bool = False,
) -> List["FtsToken"]: ...
class PyExpr:
"""A type-safe DataFusion expression node (Rust-side handle)."""
@@ -84,14 +47,11 @@ class PyExpr:
def lower(self) -> "PyExpr": ...
def upper(self) -> "PyExpr": ...
def contains(self, substr: "PyExpr") -> "PyExpr": ...
def isin(self, values: List["PyExpr"]) -> "PyExpr": ...
def cast(self, data_type: pa.DataType) -> "PyExpr": ...
def to_sql(self) -> str: ...
def expr_col(name: str) -> PyExpr: ...
def expr_lit(
value: Union[bool, int, float, str, bytes, date, datetime, Decimal],
) -> PyExpr: ...
def expr_lit(value: Union[bool, int, float, str, bytes]) -> PyExpr: ...
def expr_func(name: str, args: List[PyExpr]) -> PyExpr: ...
class Session:
@@ -197,17 +157,6 @@ class Connection(object):
self,
) -> Dict[str, Any]: ...
class BlobFile:
async def read(self) -> bytes: ...
def read_bytes(self) -> bytes: ...
def close(self) -> None: ...
def is_closed(self) -> bool: ...
def seek(self, position: int) -> None: ...
def tell(self) -> int: ...
def size(self) -> int: ...
def read_range(self, offset: int, length: int) -> bytes: ...
def read_up_to(self, length: int) -> bytes: ...
class Table:
def name(self) -> str: ...
def __repr__(self) -> str: ...
@@ -237,7 +186,6 @@ class Table:
BTree,
Bitmap,
LabelList,
Fm,
FTS,
],
replace: Optional[bool],
@@ -254,14 +202,7 @@ class Table:
async def prewarm_index(self, index_name: str) -> None: ...
async def prewarm_data(self, columns: Optional[List[str]] = None) -> None: ...
async def list_indices(self) -> list[IndexConfig]: ...
async def tokenize(
self,
query: str,
*,
column: Optional[str] = None,
index_name: Optional[str] = None,
) -> list[FtsToken]: ...
async def delete(self, filter: Union[str, PyExpr]) -> DeleteResult: ...
async def delete(self, filter: str) -> DeleteResult: ...
async def add_columns(self, columns: list[tuple[str, str]]) -> AddColumnsResult: ...
async def add_columns_with_schema(self, schema: pa.Schema) -> AddColumnsResult: ...
async def alter_columns(
@@ -282,23 +223,12 @@ 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: ...
@property
def branches(self) -> Branches: ...
def current_branch(self) -> Optional[str]: ...
def query(self) -> Query: ...
def take_offsets(self, offsets: list[int]) -> TakeQuery: ...
def take_row_ids(self, row_ids: list[int]) -> TakeQuery: ...
async def blob_columns(self) -> list[str]: ...
async def fetch_blobs(
self, column: str, row_ids: list[int]
) -> pa.LargeBinaryArray: ...
async def fetch_blob_files(
self, column: str, row_ids: list[int]
) -> list[Optional[BlobFile]]: ...
def vector_search(self) -> VectorQuery: ...
class Tags:
@@ -308,30 +238,10 @@ class Tags:
async def delete(self, tag: str): ...
async def update(self, tag: str, version: int): ...
class Branches:
async def list(self) -> Dict[str, Any]: ...
async def create(
self,
name: str,
from_ref: Optional[str] = None,
from_version: Optional[int] = None,
) -> Table: ...
async def checkout(self, name: str, version: Optional[int] = None) -> Table: ...
async def delete(self, name: str) -> None: ...
class IndexConfig:
name: str
index_type: str
columns: List[str]
index_uuid: Optional[str]
type_url: Optional[str]
created_at: Optional[datetime]
num_indexed_rows: Optional[int]
num_unindexed_rows: Optional[int]
size_bytes: Optional[int]
num_segments: Optional[int]
index_version: Optional[int]
index_details: Optional[Any]
async def connect(
uri: str,
@@ -344,24 +254,6 @@ 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:
@@ -394,9 +286,7 @@ class Query:
self, max_batch_length: Optional[int], timeout: Optional[timedelta]
) -> RecordBatchStream: ...
async def explain_plan(self, verbose: Optional[bool]) -> str: ...
async def analyze_plan(
self, distributed_metrics: Optional[AnalyzePlanDistributedMetrics] = None
) -> str: ...
async def analyze_plan(self) -> str: ...
def to_query_request(self) -> PyQueryRequest: ...
class TakeQuery:
@@ -404,10 +294,6 @@ class TakeQuery:
def with_row_id(self): ...
async def output_schema(self) -> pa.Schema: ...
async def execute(self) -> RecordBatchStream: ...
async def explain_plan(self, verbose: Optional[bool]) -> str: ...
async def analyze_plan(
self, distributed_metrics: Optional[AnalyzePlanDistributedMetrics] = None
) -> str: ...
def to_query_request(self) -> PyQueryRequest: ...
class FTSQuery:
@@ -428,10 +314,6 @@ class FTSQuery:
async def execute(
self, max_batch_length: Optional[int], timeout: Optional[timedelta]
) -> RecordBatchStream: ...
async def explain_plan(self, verbose: Optional[bool]) -> str: ...
async def analyze_plan(
self, distributed_metrics: Optional[AnalyzePlanDistributedMetrics] = None
) -> str: ...
def to_query_request(self) -> PyQueryRequest: ...
class VectorQuery:
@@ -454,10 +336,6 @@ class VectorQuery:
def bypass_vector_index(self): ...
def nearest_to_text(self, query: dict) -> HybridQuery: ...
def order_by(self, ordering: Optional[List[ColumnOrdering]]): ...
async def explain_plan(self, verbose: Optional[bool]) -> str: ...
async def analyze_plan(
self, distributed_metrics: Optional[AnalyzePlanDistributedMetrics] = None
) -> str: ...
def to_query_request(self) -> PyQueryRequest: ...
class HybridQuery:
@@ -548,10 +426,6 @@ class MergeResult:
num_attempts: int
num_rows: int
class FtsToken:
text: str
position: int
class LsmWriteSpec:
"""Specification selecting Lance's MemWAL LSM-style write path for
`merge_insert`."""
-25
View File
@@ -2,7 +2,6 @@
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
import asyncio
import concurrent.futures
import os
import threading
import warnings
@@ -38,24 +37,6 @@ class BackgroundEventLoop:
LOOP = BackgroundEventLoop()
def _new_embedding_executor() -> concurrent.futures.ThreadPoolExecutor:
return concurrent.futures.ThreadPoolExecutor(thread_name_prefix="lancedb-embedding")
# Embedding functions can block for a long time -- a heavy local model or an
# HTTP request to a remote embeddings API. Running them on asyncio's default
# executor lets them starve the unrelated blocking I/O that shares that pool,
# so they get a dedicated one. See
# https://github.com/lancedb/lancedb/issues/3310.
_EMBEDDING_EXECUTOR = _new_embedding_executor()
def embedding_executor() -> concurrent.futures.ThreadPoolExecutor:
"""Return the executor dedicated to running blocking embedding calls."""
return _EMBEDDING_EXECUTOR
_FORK_WARNED = False
@@ -66,12 +47,6 @@ def _reset_after_fork():
# the new state. The Rust-side tokio runtime is reset analogously by a
# pthread_atfork hook installed in the _lancedb extension.
LOOP._start()
# The embedding executor's worker threads are dead in the child as well.
# Replace it with a fresh pool (threads are spawned lazily, so this is
# cheap); we don't shut down the old one, since joining its dead workers
# could hang.
global _EMBEDDING_EXECUTOR
_EMBEDDING_EXECUTOR = _new_embedding_executor()
global _FORK_WARNED
if not _FORK_WARNED:
_FORK_WARNED = True
+11 -537
View File
@@ -65,7 +65,6 @@ if TYPE_CHECKING:
from .common import DATA, URI
from .embeddings import EmbeddingFunctionConfig
from ._lancedb import Session
from .udf import MaterializedView, AsyncMaterializedView
from .namespace_utils import (
_normalize_create_namespace_mode,
@@ -417,8 +416,6 @@ class DBConnection(EnforceOverrides):
namespace_path: Optional[List[str]] = None,
storage_options: Optional[Dict[str, str]] = None,
index_cache_size: Optional[int] = None,
branch: Optional[str] = None,
version: Optional[int] = None,
) -> Table:
"""Open a Lance Table in the database.
@@ -447,14 +444,6 @@ class DBConnection(EnforceOverrides):
connection will be inherited by the table, but can be overridden here.
See available options at
<https://docs.lancedb.com/storage/>
branch: str, optional
If provided, open a handle scoped to this branch instead of the
default branch. Reads and writes operate in the branch's context.
version: int, optional
If provided, open the table pinned to this version, producing a
read-only handle. Composes with ``branch``: when both are given,
opens that branch at the version; otherwise opens ``main`` at the
version. Call ``checkout_latest`` to return to a writable state.
Returns
-------
@@ -563,277 +552,6 @@ class DBConnection(EnforceOverrides):
"""
raise NotImplementedError("serialize is not supported for this connection type")
# -- Derived compute: functions, materialized views, jobs -------------
# Server-backed features (LanceDB Enterprise / Cloud); local
# connections raise NotImplementedError for now.
def create_function(
self,
name,
language: str = "python",
return_type: Optional[str] = None,
body: Optional[str] = None,
options: Optional[Dict[str, str]] = None,
*,
replace: bool = False,
):
"""Register a UDF (CREATE FUNCTION).
Pass a ``@udf`` / ``@table_udf``-decorated function (preferred):
db.create_function(embed)
or the explicit fields:
Parameters
----------
name: str or Udf
A decorated UDF object, or the function name.
language: str
Implementation language (currently "python").
return_type: str
SQL return type, e.g. "FLOAT", "FLOAT[1536]",
"STRUCT(a FLOAT, b VARCHAR)", "TABLE(chunk VARCHAR, idx INT)".
body: str
Function body: source text, or base64 cloudpickle bytes when
options["body_format"] == "cloudpickle".
options: dict, optional
input_columns, pip, num_gpus, batch_size, timeout,
error_policy, docker_image, body_format, ...
replace: bool
Drop an existing function of the same name first.
"""
from .udf import Udf
if isinstance(name, Udf):
req = name.create_request()
name, language, return_type, body, options = (
req["name"],
req["language"],
req["return_type"],
req["body"],
req["options"],
)
if replace:
try:
self.drop_function(name)
except Exception:
pass
LOOP.run(self._conn.create_function(name, language, return_type, body, options))
def list_functions(self):
"""List registered functions (SHOW FUNCTIONS)."""
return LOOP.run(self._conn.list_functions())
def drop_function(self, name: str):
"""Drop a registered function (DROP FUNCTION)."""
LOOP.run(self._conn.drop_function(name))
def create_materialized_view(
self,
name: str,
source=None,
select=None,
*,
query: Optional[str] = None,
where: Optional[str] = None,
auto_refresh: bool = False,
with_no_data: bool = False,
replace: bool = False,
partition_by: Optional[str] = None,
) -> "MaterializedView":
"""Create a materialized view (CREATE MATERIALIZED VIEW); returns a
`MaterializedView` handle (``.wait()`` blocks until it is populated).
Two ways to specify the view body:
- ergonomic: pass ``source`` (a table name or table) and ``select``
items -- column names, expression strings ("embed(body)"),
(alias, expression) tuples, or ``@udf`` / ``@table_udf`` objects.
The SELECT is assembled and parsed server-side (one parser, shared
with SQL).
- raw: pass ``query=`` with a full SELECT, e.g.
"SELECT id, embed(body) AS vec FROM articles WHERE id > 1".
`partition_by` partitions the view's (single) table function on a source
column. If that column has an IVF vector index the server partitions by
its index clusters (image-dedup style); otherwise it groups by distinct
value. (Geneva's `partition_by` and `partition_by_indexed_column` unify
here -- the engine picks the strategy from the column.)
"""
from .udf import build_view_query, MaterializedView
if query is None:
if source is None or select is None:
raise ValueError(
"create_materialized_view needs either query= or both "
"source and select"
)
query = build_view_query(source, select)
if where:
query += f" WHERE {where}"
if replace:
self._drop_view_if_exists(name)
job_id = LOOP.run(
self._conn.create_materialized_view(
name,
query=query,
auto_refresh=auto_refresh,
with_no_data=with_no_data,
partition_by=partition_by,
)
)
return MaterializedView(self, name, job_id=job_id)
def _drop_view_if_exists(self, name: str) -> None:
# `replace=True` is "drop if present"; only a not-found error is
# benign here. Anything else (perms, server fault) must surface rather
# than be masked by a later create failure.
try:
self.drop_materialized_view(name)
except Exception as e:
msg = str(e).lower()
if "not found" not in msg and "does not exist" not in msg:
raise
def job(self, job_id: str):
"""A `Job` for reconnecting to an inflight job by id -- e.g. an
id you stored, or one returned from the SQL / REST surface. Submit
methods (`refresh_column`, `MaterializedView.refresh`) already return a
handle directly, so you do not need this to wait on a fresh submission."""
from .udf import Job
return Job(self, job_id)
def lineage(
self,
table: str,
column: Optional[str] = None,
*,
direction: Optional[str] = None,
depth: Optional[int] = None,
):
"""Derived-compute lineage of a table/view, or one of its columns:
upstream sources, downstream dependents, and the function version +
location that produced each derived column (with a drift flag). Returns
a `Lineage`. `direction` is "upstream" | "downstream" | "both" (server
default both); `depth` limits column-hops (transitive when omitted)."""
# `self._conn` is the AsyncConnection; drive its async `lineage`
# (which parses the JSON) on the loop, mirroring create_materialized_view.
return LOOP.run(
self._conn.lineage(table, column, direction=direction, depth=depth)
)
def _refresh_materialized_view(
self,
name: str,
*,
full: bool = False,
src_version: Optional[int] = None,
num_workers: Optional[int] = None,
max_workers: Optional[int] = None,
) -> str:
"""Internal: submit a materialized-view refresh, return the job id.
The public surface is ``MaterializedView.refresh()`` (which returns a
`Job`); this stays private so refresh is only reached through the
handle.
``full=True`` forces a full rebuild (recompute and replace every row)
instead of the default incremental refresh.
"""
return LOOP.run(
self._conn._refresh_materialized_view(
name,
full=full,
src_version=src_version,
num_workers=num_workers,
max_workers=max_workers,
)
)
def explain_refresh_materialized_view(
self,
name: str,
*,
full: bool = False,
src_version: Optional[int] = None,
):
"""Plan a refresh without running it (EXPLAIN REFRESH). Returns a
plan with .has_work / .source_version / .last_refreshed_version /
.full_refresh / .rebuild / .units_total. `full=True` plans a full
rebuild (incremental planning needs stable row IDs on the source)."""
return LOOP.run(
self._conn.explain_refresh_materialized_view(
name, full=full, src_version=src_version
)
)
def alter_materialized_view(self, name: str, *, auto_refresh: bool):
"""Update a materialized view's options (ALTER MATERIALIZED VIEW)."""
LOOP.run(self._conn.alter_materialized_view(name, auto_refresh=auto_refresh))
def drop_materialized_view(self, name: str):
"""Drop a materialized view definition (DROP MATERIALIZED VIEW)."""
LOOP.run(self._conn.drop_materialized_view(name))
def list_materialized_views(self):
"""List registered materialized view definitions."""
return LOOP.run(self._conn.list_materialized_views())
def list_jobs(self):
"""List inflight server-side jobs across the database's tables."""
return LOOP.run(self._conn.list_jobs())
def get_job(self, job_id: str, table: "str | None" = None):
"""Look up one server-side job by id (the wait()/status poll path).
Passing ``table`` (the job's table) lets the server answer with an O(1)
single-node read instead of scanning the database's active jobs.
Returns the job's status, or None if it's unknown or no longer active.
"""
return LOOP.run(self._conn.get_job(job_id, table))
def cancel_job(self, job_id: str) -> bool:
"""Cancel an inflight server-side job by id (CANCEL JOB).
Returns True if a matching inflight job was found and flagged for
cancellation, False if none was inflight (already finished or
unknown id) -- cancellation is best-effort.
"""
return LOOP.run(self._conn.cancel_job(job_id))
def describe_platform_job(self, platform_job_id: str):
"""Describe a platform job (POST /v1/jobs/describe): registry-backed
lifecycle state plus the owner-written status payload. None when the
registry has no such job."""
return LOOP.run(self._conn.describe_platform_job(platform_job_id))
def resolve_platform_job_id(
self, manifest_job_id: str, table: "str | None" = None
):
"""Resolve a submission (manifest) job id to its platform job id.
None until the job has registered (dispatch is async)."""
return LOOP.run(self._conn.resolve_platform_job_id(manifest_job_id, table))
def cancel_platform_job(self, platform_job_id: str) -> None:
"""Cancel a platform job (POST /v1/jobs/cancel). Idempotent on
already-terminal jobs."""
return LOOP.run(self._conn.cancel_platform_job(platform_job_id))
def job_history(self, job_id: "str | None" = None):
"""Durable history of completed server-side jobs (SHOW JOB HISTORY).
Pass ``job_id`` to narrow to a single job. Unlike :meth:`list_jobs`
(live, inflight) these are the terminal records.
"""
return LOOP.run(self._conn.job_history(job_id))
def errors(self, job_id: "str | None" = None, table: "str | None" = None):
"""Per-row UDF errors recorded by ``error_policy=skip`` (SHOW ERRORS),
optionally filtered by ``job_id`` and/or ``table``.
"""
return LOOP.run(self._conn.errors(job_id, table))
class LanceDBConnection(DBConnection):
"""
@@ -1240,8 +958,6 @@ class LanceDBConnection(DBConnection):
namespace_path: Optional[List[str]] = None,
storage_options: Optional[Dict[str, str]] = None,
index_cache_size: Optional[int] = None,
branch: Optional[str] = None,
version: Optional[int] = None,
) -> LanceTable:
"""Open a table in the database.
@@ -1252,14 +968,6 @@ class LanceDBConnection(DBConnection):
namespace_path: List[str], optional
The namespace to open the table from. When non-empty, the
table is resolved through the directory namespace client.
branch: str, optional
If provided, open a handle scoped to this branch instead of the
default branch. Reads and writes operate in the branch's context.
version: int, optional
If provided, open the table pinned to this version, producing a
read-only handle. Composes with ``branch``: when both are given,
opens that branch at the version; otherwise opens ``main`` at the
version. Call ``checkout_latest`` to return to a writable state.
Returns
-------
@@ -1279,26 +987,20 @@ class LanceDBConnection(DBConnection):
)
if namespace_path:
tbl = self._namespace_conn().open_table(
name,
namespace_path=namespace_path,
storage_options=storage_options,
index_cache_size=index_cache_size,
)
else:
tbl = LanceTable.open(
self,
return self._namespace_conn().open_table(
name,
namespace_path=namespace_path,
storage_options=storage_options,
index_cache_size=index_cache_size,
)
if branch is not None:
tbl = tbl.branches.checkout(branch, version)
elif version is not None:
tbl.checkout(version)
return tbl
return LanceTable.open(
self,
name,
namespace_path=namespace_path,
storage_options=storage_options,
index_cache_size=index_cache_size,
)
def clone_table(
self,
@@ -1927,7 +1629,7 @@ class AsyncConnection(object):
namespace_client=namespace_client,
)
return AsyncTable(new_table, conn=self)
return AsyncTable(new_table)
async def open_table(
self,
@@ -1939,8 +1641,6 @@ class AsyncConnection(object):
location: Optional[str] = None,
namespace_client: Optional[Any] = None,
managed_versioning: Optional[bool] = None,
branch: Optional[str] = None,
version: Optional[int] = None,
) -> AsyncTable:
"""Open a Lance Table in the database.
@@ -1976,14 +1676,6 @@ class AsyncConnection(object):
managed_versioning: bool, optional
Whether managed versioning is enabled for this table. If provided,
avoids a redundant describe_table call when namespace_client is set.
branch: str, optional
If provided, open a handle scoped to this branch instead of the
default branch. Reads and writes operate in the branch's context.
version: int, optional
If provided, open the table pinned to this version, producing a
read-only handle. Composes with ``branch``: when both are given,
opens that branch at the version; otherwise opens ``main`` at the
version. Call ``checkout_latest`` to return to a writable state.
Returns
-------
@@ -2000,14 +1692,7 @@ class AsyncConnection(object):
namespace_client=namespace_client,
managed_versioning=managed_versioning,
)
tbl = AsyncTable(table, conn=self)
# "main" is the default branch, so treat it as no branch: remote rejects
# every branch checkout (even "main"), and the version still applies.
if branch is not None and branch != "main":
tbl = await tbl.branches.checkout(branch, version)
elif version is not None:
await tbl.checkout(version)
return tbl
return AsyncTable(table)
async def clone_table(
self,
@@ -2057,218 +1742,7 @@ class AsyncConnection(object):
source_tag=source_tag,
is_shallow=is_shallow,
)
return AsyncTable(table, conn=self)
# -- Derived compute: functions, materialized views, jobs -------------
# Server-backed features (LanceDB Enterprise / Cloud); local
# connections raise NotImplementedError for now.
async def create_function(
self,
name,
language: str = "python",
return_type: Optional[str] = None,
body: Optional[str] = None,
options: Optional[Dict[str, str]] = None,
*,
replace: bool = False,
):
"""Register a UDF (CREATE FUNCTION). Accepts a ``@udf``/``@table_udf``
object (preferred) or the explicit (name, language, return_type, body,
options)."""
from .udf import Udf
if isinstance(name, Udf):
req = name.create_request()
name, language, return_type, body, options = (
req["name"],
req["language"],
req["return_type"],
req["body"],
req["options"],
)
if replace:
try:
await self.drop_function(name)
except Exception:
pass
await self._inner.create_function(name, language, return_type, body, options)
async def list_functions(self):
"""List registered functions (SHOW FUNCTIONS)."""
return await self._inner.list_functions()
async def drop_function(self, name: str):
"""Drop a registered function (DROP FUNCTION)."""
await self._inner.drop_function(name)
async def create_materialized_view(
self,
name: str,
source=None,
select=None,
*,
query: Optional[str] = None,
where: Optional[str] = None,
auto_refresh: bool = False,
with_no_data: bool = False,
replace: bool = False,
partition_by: Optional[str] = None,
) -> "AsyncMaterializedView":
"""Create a materialized view; returns an `AsyncMaterializedView`
handle (``.wait()`` blocks until populated). Pass either ``query=`` (a
full SELECT) or ``source`` + ``select`` items; `partition_by`
partitions the view's table function on a source column (index-cluster
if the column is IVF-indexed, else distinct-value). See the sync
method for the select grammar."""
from .udf import build_view_query, AsyncMaterializedView
if query is None:
if source is None or select is None:
raise ValueError(
"create_materialized_view needs either query= or both "
"source and select"
)
query = build_view_query(source, select)
if where:
query += f" WHERE {where}"
if replace:
try:
await self.drop_materialized_view(name)
except Exception as e:
msg = str(e).lower()
if "not found" not in msg and "does not exist" not in msg:
raise
job_id = await self._inner.create_materialized_view(
name,
query,
auto_refresh=auto_refresh,
with_no_data=with_no_data,
partition_by=partition_by,
)
return AsyncMaterializedView(self, name, job_id=job_id)
def job(self, job_id: str):
"""An `AsyncJob` for reconnecting to an inflight job by id (a
stored id, or one from the SQL / REST surface). Submit methods already
return a handle, so this is only needed to re-attach to an existing
job."""
from .udf import AsyncJob
return AsyncJob(self, job_id)
async def lineage(
self,
table: str,
column: Optional[str] = None,
*,
direction: Optional[str] = None,
depth: Optional[int] = None,
):
"""Derived-compute lineage of a table/view (or column). See the sync
`Connection.lineage`. Returns a `Lineage`."""
from .lineage import Lineage
raw = await self._inner.table_lineage(table, column, direction, depth)
return Lineage.from_json(raw)
async def _refresh_materialized_view(
self,
name: str,
*,
full: bool = False,
src_version: Optional[int] = None,
num_workers: Optional[int] = None,
max_workers: Optional[int] = None,
) -> str:
"""Internal: submit a refresh, return the job id. The public surface is
``AsyncMaterializedView.refresh()`` (returns an `AsyncJob`).
``full=True`` forces a full rebuild (recompute and replace every row)
instead of the default incremental refresh.
"""
return await self._inner.refresh_materialized_view(
name,
full=full,
src_version=src_version,
num_workers=num_workers,
max_workers=max_workers,
)
async def explain_refresh_materialized_view(
self,
name: str,
*,
full: bool = False,
src_version: Optional[int] = None,
):
"""Plan a refresh without running it (EXPLAIN REFRESH)."""
return await self._inner.explain_refresh_materialized_view(
name, full=full, src_version=src_version
)
async def alter_materialized_view(self, name: str, *, auto_refresh: bool):
"""Update a materialized view's options."""
await self._inner.alter_materialized_view(name, auto_refresh)
async def drop_materialized_view(self, name: str):
"""Drop a materialized view definition."""
await self._inner.drop_materialized_view(name)
async def list_materialized_views(self):
"""List registered materialized view definitions."""
return await self._inner.list_materialized_views()
async def list_jobs(self):
"""List inflight server-side jobs across the database's tables."""
return await self._inner.list_jobs()
async def get_job(self, job_id: str, table: "str | None" = None):
"""Look up one server-side job by id (the wait()/status poll path).
``table`` (the job's table) enables an O(1) server-side lookup.
Returns the job's status, or None if unknown / no longer active."""
return await self._inner.get_job(job_id, table)
async def cancel_job(self, job_id: str) -> bool:
"""Cancel an inflight server-side job by id (CANCEL JOB).
Returns True if a matching inflight job was found and flagged for
cancellation, False otherwise (best-effort).
"""
return await self._inner.cancel_job(job_id)
async def describe_platform_job(self, platform_job_id: str):
"""Describe a platform job: registry-backed lifecycle state plus the
owner-written status payload. None when the registry has no such
job."""
return await self._inner.describe_platform_job(platform_job_id)
async def resolve_platform_job_id(
self, manifest_job_id: str, table: "str | None" = None
):
"""Resolve a submission (manifest) job id to its platform job id.
None until the job has registered (dispatch is async)."""
return await self._inner.resolve_platform_job_id(manifest_job_id, table)
async def cancel_platform_job(self, platform_job_id: str) -> None:
"""Cancel a platform job. Idempotent on already-terminal jobs."""
return await self._inner.cancel_platform_job(platform_job_id)
async def job_history(self, job_id: "str | None" = None):
"""Durable history of completed server-side jobs (SHOW JOB HISTORY).
Reads each table's durable job-history store. Pass ``job_id`` to narrow
to a single job. Unlike :meth:`list_jobs` (live, inflight) these are the
terminal records, with created/updated/completed timestamps.
"""
return await self._inner.job_history(job_id)
async def errors(self, job_id: "str | None" = None, table: "str | None" = None):
"""Per-row UDF errors recorded by ``error_policy=skip`` (SHOW ERRORS).
Optionally filtered by ``job_id`` and/or ``table``.
"""
return await self._inner.errors(job_id, table)
return AsyncTable(table)
async def rename_table(
self,
@@ -81,7 +81,6 @@ class ColPaliEmbeddings(EmbeddingFunction):
warnings.warn(
"use_token_pooling is deprecated, use pooling_strategy=None instead",
DeprecationWarning,
stacklevel=2,
)
self.pooling_strategy = None

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