mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-12 16:22:24 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
477a49cecb | ||
|
|
bd86cc5aa6 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tool.bumpversion]
|
[tool.bumpversion]
|
||||||
current_version = "0.39.0-beta.6"
|
current_version = "0.39.0-beta.1"
|
||||||
parse = """(?x)
|
parse = """(?x)
|
||||||
(?P<major>0|[1-9]\\d*)\\.
|
(?P<major>0|[1-9]\\d*)\\.
|
||||||
(?P<minor>0|[1-9]\\d*)\\.
|
(?P<minor>0|[1-9]\\d*)\\.
|
||||||
|
|||||||
@@ -232,10 +232,7 @@ jobs:
|
|||||||
ALL_FEATURES=`cargo metadata --format-version=1 --no-deps \
|
ALL_FEATURES=`cargo metadata --format-version=1 --no-deps \
|
||||||
| jq -r '.packages[] | .features | keys | .[]' \
|
| jq -r '.packages[] | .features | keys | .[]' \
|
||||||
| grep -v s3-test | sort | uniq | paste -s -d "," -`
|
| grep -v s3-test | sort | uniq | paste -s -d "," -`
|
||||||
# Run doctests before test binaries fill the runner disk. Examples are
|
cargo test --profile ci --features $ALL_FEATURES --locked
|
||||||
# already built by the Linux job, so avoid retaining them here.
|
|
||||||
cargo test --profile ci --features $ALL_FEATURES --locked --doc
|
|
||||||
cargo test --profile ci --features $ALL_FEATURES --locked --lib --tests
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
strategy:
|
strategy:
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
name: Typo checker
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run:
|
|
||||||
name: Spell Check with Typos
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check out code
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
|
|
||||||
- name: Check spelling of the entire repository
|
|
||||||
uses: crate-ci/typos@6802cc60d4e7f78b9d5454f6cf3935c042d5e1e3 # v1.26.0
|
|
||||||
@@ -10,10 +10,6 @@ repos:
|
|||||||
rev: v0.9.9
|
rev: v0.9.9
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- repo: https://github.com/crate-ci/typos
|
|
||||||
rev: v1.26.0
|
|
||||||
hooks:
|
|
||||||
- id: typos
|
|
||||||
# - repo: https://github.com/RobertCraigie/pyright-python
|
# - repo: https://github.com/RobertCraigie/pyright-python
|
||||||
# rev: v1.1.395
|
# rev: v1.1.395
|
||||||
# hooks:
|
# hooks:
|
||||||
|
|||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
[default]
|
|
||||||
extend-ignore-re = ["(?Rm)^.*(#|//)\\s*spellchecker:disable-line$"]
|
|
||||||
|
|
||||||
[default.extend-words]
|
|
||||||
# Azure Kubernetes Service, mentioned in rust/lancedb/src/remote/oauth.rs.
|
|
||||||
AKS = "AKS"
|
|
||||||
# RabitQ is the name of a vector quantization algorithm, not a typo of "Rabbit".
|
|
||||||
Rabit = "Rabit"
|
|
||||||
# `VarBuilder::from_mmaped_safetensors` is the real (if oddly-spelled) name of
|
|
||||||
# the candle-core API we call in rust/lancedb/src/embeddings/sentence_transformers.rs.
|
|
||||||
mmaped = "mmaped"
|
|
||||||
# `WriteableBuffer` is the real name of a type from Python's `_typeshed` stubs,
|
|
||||||
# used in python/python/lancedb/_blob.py.
|
|
||||||
Writeable = "Writeable"
|
|
||||||
|
|
||||||
[files]
|
|
||||||
extend-exclude = [
|
|
||||||
"*_THIRD_PARTY_LICENSES.*",
|
|
||||||
]
|
|
||||||
Generated
+251
-349
File diff suppressed because it is too large
Load Diff
+16
-18
@@ -13,20 +13,20 @@ categories = ["database-implementations"]
|
|||||||
rust-version = "1.91.0"
|
rust-version = "1.91.0"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
lance = { "version" = "=12.0.0-beta.17", default-features = false, "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance = { "version" = "=12.0.0-beta.11", default-features = false, "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-core = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-core = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-datagen = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-datagen = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-file = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-file = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-io = { "version" = "=12.0.0-beta.17", default-features = false, "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-io = { "version" = "=12.0.0-beta.11", default-features = false, "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-index = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-index = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-linalg = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-linalg = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-namespace = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-namespace = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-namespace-impls = { "version" = "=12.0.0-beta.17", default-features = false, "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-namespace-impls = { "version" = "=12.0.0-beta.11", default-features = false, "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-table = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-table = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-testing = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-testing = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-datafusion = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-datafusion = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-encoding = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-encoding = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-arrow = { "version" = "=12.0.0-beta.17", "tag" = "v12.0.0-beta.17", "git" = "https://github.com/lance-format/lance.git" }
|
lance-arrow = { "version" = "=12.0.0-beta.11", "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lancedb = { path = "rust/lancedb", default-features = false }
|
lancedb = { path = "rust/lancedb", default-features = false }
|
||||||
ahash = "0.8"
|
ahash = "0.8"
|
||||||
# Note that this one does not include pyarrow
|
# Note that this one does not include pyarrow
|
||||||
@@ -39,7 +39,6 @@ arrow-ord = "58.0.0"
|
|||||||
arrow-schema = "58.0.0"
|
arrow-schema = "58.0.0"
|
||||||
arrow-select = "58.0.0"
|
arrow-select = "58.0.0"
|
||||||
arrow-cast = "58.0.0"
|
arrow-cast = "58.0.0"
|
||||||
arrow-flight = { version = "58.0.0", features = ["flight-sql-experimental"] }
|
|
||||||
async-trait = "0"
|
async-trait = "0"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
datafusion = { version = "54.0.0", default-features = false }
|
datafusion = { version = "54.0.0", default-features = false }
|
||||||
@@ -60,7 +59,7 @@ log = "0.4"
|
|||||||
metrics = "0.24"
|
metrics = "0.24"
|
||||||
metrics-util = "0.19"
|
metrics-util = "0.19"
|
||||||
moka = { version = "0.12", features = ["future"] }
|
moka = { version = "0.12", features = ["future"] }
|
||||||
object_store = "0.14.1"
|
object_store = "0.13.2"
|
||||||
pin-project = "1.0.7"
|
pin-project = "1.0.7"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
snafu = "0.8"
|
snafu = "0.8"
|
||||||
@@ -72,8 +71,7 @@ serde = "1"
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tempfile = "3.5.0"
|
tempfile = "3.5.0"
|
||||||
tokio = { version = "1.23", features = ["rt-multi-thread", "sync"] }
|
tokio = { version = "1.23", features = ["rt-multi-thread", "sync"] }
|
||||||
tonic = { version = "0.14", features = ["tls-native-roots", "tls-ring"] }
|
uuid = { version = "1.7.0", features = ["v4"] }
|
||||||
uuid = { version = "1.7.0", features = ["v4", "v7"] }
|
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
|
|
||||||
[profile.ci]
|
[profile.ci]
|
||||||
|
|||||||
+1
-1
@@ -155,7 +155,7 @@ paths:
|
|||||||
vector:
|
vector:
|
||||||
type: FixedSizeList
|
type: FixedSizeList
|
||||||
description: |
|
description: |
|
||||||
The targeted vector to search for. Required.
|
The targetted vector to search for. Required.
|
||||||
vector_column:
|
vector_column:
|
||||||
type: string
|
type: string
|
||||||
description: |
|
description: |
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`:
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.lancedb</groupId>
|
<groupId>com.lancedb</groupId>
|
||||||
<artifactId>lancedb-core</artifactId>
|
<artifactId>lancedb-core</artifactId>
|
||||||
<version>0.39.0-beta.6</version>
|
<version>0.39.0-beta.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
[@lancedb/lancedb](../globals.md) / BlobFile
|
|
||||||
|
|
||||||
# Class: BlobFile
|
|
||||||
|
|
||||||
A lazy handle to blob bytes. Create one with [Table.fetchBlobFiles](Table.md#fetchblobfiles).
|
|
||||||
|
|
||||||
## Methods
|
|
||||||
|
|
||||||
### read()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
read(): Promise<Buffer>
|
|
||||||
```
|
|
||||||
|
|
||||||
Reads from the cursor to the end and advances the cursor.
|
|
||||||
|
|
||||||
A second call returns an empty buffer. [BlobFile.readRange](BlobFile.md#readrange) does
|
|
||||||
not move the cursor.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Promise`<`Buffer`>
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### readRange()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
readRange(start, end): Promise<Buffer>
|
|
||||||
```
|
|
||||||
|
|
||||||
Reads the half-open byte range `[start, end)`.
|
|
||||||
|
|
||||||
Fails when `end` is past the blob size. Does not move the cursor.
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
* **start**: `bigint`
|
|
||||||
|
|
||||||
* **end**: `bigint`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Promise`<`Buffer`>
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### size()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
size(): bigint
|
|
||||||
```
|
|
||||||
|
|
||||||
Returns the blob size in bytes.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`bigint`
|
|
||||||
@@ -448,6 +448,26 @@ on the returned job to know when cleanup has finished.
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
### getJob()
|
||||||
|
|
||||||
|
```ts
|
||||||
|
abstract getJob(jobId): Promise<null | JobDescription>
|
||||||
|
```
|
||||||
|
|
||||||
|
Describe a single server-side job by id.
|
||||||
|
|
||||||
|
Resolves to `null` when the server has no such job.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* **jobId**: `string`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
`Promise`<`null` \| [`JobDescription`](../interfaces/JobDescription.md)>
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
### isOpen()
|
### isOpen()
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -462,6 +482,48 @@ Return true if the connection has not been closed
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
### job()
|
||||||
|
|
||||||
|
```ts
|
||||||
|
abstract job(jobId): Job
|
||||||
|
```
|
||||||
|
|
||||||
|
A [Job](Job.md) handle for a server-side job by id.
|
||||||
|
|
||||||
|
The handle is constructed without a server round trip; an unknown id
|
||||||
|
surfaces when the handle is used. Dropping the handle has no effect on
|
||||||
|
the job itself.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* **jobId**: `string`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
[`Job`](Job.md)
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### jobHistory()
|
||||||
|
|
||||||
|
```ts
|
||||||
|
abstract jobHistory(jobId?): Promise<Table<any>>
|
||||||
|
```
|
||||||
|
|
||||||
|
The lifecycle event history of a server-side job, as an Arrow table.
|
||||||
|
|
||||||
|
Lists history across all jobs when `jobId` is omitted.
|
||||||
|
|
||||||
|
#### Parameters
|
||||||
|
|
||||||
|
* **jobId?**: `string`
|
||||||
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
`Promise`<`Table`<`any`>>
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
### listJobs()
|
### listJobs()
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -586,30 +648,6 @@ A page of table names and an
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### openJob()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
abstract openJob(jobId): Promise<Job>
|
|
||||||
```
|
|
||||||
|
|
||||||
Open a server-side job by id, returning a handle with its record already
|
|
||||||
populated. Rejects when the server has no such job, the way
|
|
||||||
[Connection.openTable](Connection.md#opentable) does for a missing table.
|
|
||||||
|
|
||||||
The returned [Job](Job.md) answers for its own state, specification,
|
|
||||||
result, failure and event history, so there is no separate
|
|
||||||
connection-level call for any of them.
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
* **jobId**: `string`
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Promise`<[`Job`](Job.md)>
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### openMaterializedView()
|
### openMaterializedView()
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
+16
-163
@@ -8,116 +8,28 @@
|
|||||||
|
|
||||||
A handle to an operation that may still be running.
|
A handle to an operation that may still be running.
|
||||||
|
|
||||||
The operation may already be complete when the handle is created.
|
## Constructors
|
||||||
|
|
||||||
The detail getters read what the handle last observed. Submitting an
|
### new Job()
|
||||||
operation returns only a job id, so populating them eagerly would cost an
|
|
||||||
extra round trip on every call:
|
|
||||||
|
|
||||||
- [Job.refresh](Job.md#refresh) and [Job.status](Job.md#status) fetch the whole record.
|
```ts
|
||||||
- [Job.wait](Job.md#wait) records the terminal state it establishes, but not the
|
new Job(): Job
|
||||||
rest of the record.
|
```
|
||||||
- Everything is null until one of those runs.
|
|
||||||
|
#### Returns
|
||||||
|
|
||||||
|
[`Job`](Job.md)
|
||||||
|
|
||||||
## Accessors
|
## Accessors
|
||||||
|
|
||||||
### creationMs
|
|
||||||
|
|
||||||
```ts
|
|
||||||
get creationMs(): null | number
|
|
||||||
```
|
|
||||||
|
|
||||||
When the job was created, in milliseconds since the epoch.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`null` \| `number`
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### failure
|
|
||||||
|
|
||||||
```ts
|
|
||||||
get failure(): null | JobFailureInfo
|
|
||||||
```
|
|
||||||
|
|
||||||
Why the job failed, when it failed and the server reports a reason.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`null` \| [`JobFailureInfo`](../interfaces/JobFailureInfo.md)
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### id
|
### id
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
get id(): null | string
|
get id(): null | string
|
||||||
```
|
```
|
||||||
|
|
||||||
Identifies the operation on the server that is running it.
|
Identifies the operation on the server that is running it. Operations
|
||||||
|
that run in this process have no server id. The value is opaque.
|
||||||
Operations that run in this process have no server id. The value is
|
|
||||||
opaque: parsing it or storing it to resume the job later is not supported.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`null` \| `string`
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### jobType
|
|
||||||
|
|
||||||
```ts
|
|
||||||
get jobType(): null | string
|
|
||||||
```
|
|
||||||
|
|
||||||
The job's type, as the server names it. Null for an in-process job, which
|
|
||||||
has no server-side record.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`null` \| `string`
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### result
|
|
||||||
|
|
||||||
```ts
|
|
||||||
get result(): any
|
|
||||||
```
|
|
||||||
|
|
||||||
The job-type-specific terminal result. Null until the job succeeds, so a
|
|
||||||
job that never terminates reports its progress through [Job.events](Job.md#events)
|
|
||||||
instead.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### spec
|
|
||||||
|
|
||||||
```ts
|
|
||||||
get spec(): any
|
|
||||||
```
|
|
||||||
|
|
||||||
The job-type-specific specification it was submitted with.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`any`
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### state
|
|
||||||
|
|
||||||
```ts
|
|
||||||
get state(): null | string
|
|
||||||
```
|
|
||||||
|
|
||||||
The last observed lifecycle state, without contacting the backend.
|
|
||||||
|
|
||||||
#### Returns
|
#### Returns
|
||||||
|
|
||||||
@@ -139,61 +51,18 @@ Request cancellation. Cancelling a finished operation is a no-op.
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### events()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
events(options?): Promise<Table<any>>
|
|
||||||
```
|
|
||||||
|
|
||||||
This job's recorded lifecycle events.
|
|
||||||
|
|
||||||
Where the getters above report a terminal result only once the job reaches
|
|
||||||
one, events are written as the job runs and outlive the workers that
|
|
||||||
produced them. A distributed job records a `claim`/`claim_complete` pair
|
|
||||||
per unit of work, each carrying `rows_processed`, so a job that never
|
|
||||||
finishes still accounts for what it did.
|
|
||||||
|
|
||||||
The server caps results at 1000 rows by default and 10,000 at most, and
|
|
||||||
truncates without saying so, so pass `limit` for a job that emits an event
|
|
||||||
per fragment. `filter` is a SQL-like expression over the `state`,
|
|
||||||
`updated_by`, `emitted_from`, `emitted_by`, and `claim_entity` columns.
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
* **options?**: [`JobEventsOptions`](../interfaces/JobEventsOptions.md)
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Promise`<`Table`<`any`>>
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### refresh()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
refresh(): Promise<void>
|
|
||||||
```
|
|
||||||
|
|
||||||
Ask the backend for this job's current state, and for a server-side job
|
|
||||||
its full record, then cache it for the getters above.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Promise`<`void`>
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### status()
|
### status()
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
status(): Promise<string>
|
status(): Promise<string>
|
||||||
```
|
```
|
||||||
|
|
||||||
The operation's current lifecycle state: "running", "finished", "failed",
|
The operation's current lifecycle state: "running", "finished",
|
||||||
or "cancelled".
|
"failed", or "cancelled".
|
||||||
|
|
||||||
A point snapshot; unlike [Job.wait](Job.md#wait) it does not block or reject on a
|
A point snapshot; unlike [Job.wait](Job.md#wait) it does not block or reject
|
||||||
terminal failure state. Also refreshes the getters above.
|
on a terminal failure state. States a newer server reports that this
|
||||||
|
client version does not know pass through as-is.
|
||||||
|
|
||||||
#### Returns
|
#### Returns
|
||||||
|
|
||||||
@@ -201,22 +70,6 @@ terminal failure state. Also refreshes the getters above.
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### toString()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
toString(): string
|
|
||||||
```
|
|
||||||
|
|
||||||
Every field the handle currently knows, one per line, with the JSON
|
|
||||||
payloads indented -- a refresh job's spec and result are the point of
|
|
||||||
printing it.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`string`
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### wait()
|
### wait()
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ Currently this causes multiple copies of the row to be created
|
|||||||
but that behavior is subject to change.
|
but that behavior is subject to change.
|
||||||
|
|
||||||
An optional condition may be specified. If it is, then only
|
An optional condition may be specified. If it is, then only
|
||||||
matched rows that satisfy the condition will be updated. Any
|
matched rows that satisfy the condtion will be updated. Any
|
||||||
rows that do not satisfy the condition will be left as they
|
rows that do not satisfy the condition will be left as they
|
||||||
are. Failing to satisfy the condition does not cause a
|
are. Failing to satisfy the condition does not cause a
|
||||||
"matched row" to become a "not matched" row.
|
"matched row" to become a "not matched" row.
|
||||||
|
|||||||
@@ -137,20 +137,6 @@ containing the new version number of the table after altering the columns.
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### blobColumns()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
abstract blobColumns(): Promise<string[]>
|
|
||||||
```
|
|
||||||
|
|
||||||
Blob v2 columns, including nested dotted paths.
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Promise`<`string`[]>
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### branches()
|
### branches()
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -513,54 +499,6 @@ Drop an index from the table.
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### fetchBlobFiles()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
abstract fetchBlobFiles(column, rowIds): Promise<(null | BlobFile)[]>
|
|
||||||
```
|
|
||||||
|
|
||||||
Opens lazy blob handles for `column` at the given row IDs using the
|
|
||||||
table's current checkout.
|
|
||||||
|
|
||||||
Preserves input order, duplicates, and nulls. Use this for large payloads.
|
|
||||||
See [Table.fetchBlobs](Table.md#fetchblobs) for row-ID validity across versions.
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
* **column**: `string`
|
|
||||||
|
|
||||||
* **rowIds**: readonly (`number` \| `bigint`)[]
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Promise`<(`null` \| [`BlobFile`](BlobFile.md))[]>
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### fetchBlobs()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
abstract fetchBlobs(column, rowIds): Promise<(null | Buffer)[]>
|
|
||||||
```
|
|
||||||
|
|
||||||
Bytes for `column` at row IDs from [Query.withRowId](Query.md#withrowid).
|
|
||||||
|
|
||||||
Reads the table's current checkout. IDs from another version can fail after
|
|
||||||
compaction unless stable row ids are enabled. Results keep input order and
|
|
||||||
duplicates. Null blobs are `null`. Empty blobs are empty buffers.
|
|
||||||
|
|
||||||
#### Parameters
|
|
||||||
|
|
||||||
* **column**: `string`
|
|
||||||
|
|
||||||
* **rowIds**: readonly (`number` \| `bigint`)[]
|
|
||||||
|
|
||||||
#### Returns
|
|
||||||
|
|
||||||
`Promise`<(`null` \| `Buffer`)[]>
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### flushLsm()
|
### flushLsm()
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
@@ -1328,7 +1266,7 @@ value is 0")
|
|||||||
Note: if your condition is something like "some_id_column == 7" and
|
Note: if your condition is something like "some_id_column == 7" and
|
||||||
you are updating many rows (with different ids) then you will get
|
you are updating many rows (with different ids) then you will get
|
||||||
better performance with a single [`merge_insert`] call instead of
|
better performance with a single [`merge_insert`] call instead of
|
||||||
repeatedly calling this method.
|
repeatedly calilng this method.
|
||||||
|
|
||||||
##### Parameters
|
##### Parameters
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
[@lancedb/lancedb](../globals.md) / blob
|
|
||||||
|
|
||||||
# Function: blob()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
function blob(name, options): Field
|
|
||||||
```
|
|
||||||
|
|
||||||
Declares a `lance.blob.v2` column.
|
|
||||||
|
|
||||||
Query results are descriptors, not payload bytes. Use [Table.fetchBlobs](../classes/Table.md#fetchblobs)
|
|
||||||
or [Table.fetchBlobFiles](../classes/Table.md#fetchblobfiles) to read bytes.
|
|
||||||
|
|
||||||
## Parameters
|
|
||||||
|
|
||||||
* **name**: `string`
|
|
||||||
|
|
||||||
* **options**: [`BlobOptions`](../type-aliases/BlobOptions.md) = `{}`
|
|
||||||
|
|
||||||
## Returns
|
|
||||||
|
|
||||||
`Field`
|
|
||||||
|
|
||||||
## Example
|
|
||||||
|
|
||||||
```ts
|
|
||||||
import { readFile } from "node:fs/promises";
|
|
||||||
import { Field, Int64, Schema } from "apache-arrow";
|
|
||||||
import { blob, connect } from "@lancedb/lancedb";
|
|
||||||
|
|
||||||
const db = await connect("./data");
|
|
||||||
const video = await readFile("clip.mp4");
|
|
||||||
const table = await db.createTable(
|
|
||||||
"videos",
|
|
||||||
[{ id: 1n, video }],
|
|
||||||
{
|
|
||||||
schema: new Schema([
|
|
||||||
new Field("id", new Int64()),
|
|
||||||
blob("video"),
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const rows = await table.query().select(["id"]).withRowId().toArray();
|
|
||||||
const rowIds = rows.map((row) => row._rowid as bigint);
|
|
||||||
const bytes = await table.fetchBlobs("video", rowIds);
|
|
||||||
|
|
||||||
const [handle] = await table.fetchBlobFiles("video", rowIds);
|
|
||||||
const size = handle!.size();
|
|
||||||
const header = await handle!.readRange(0n, size < 65536n ? size : 65536n);
|
|
||||||
```
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
[@lancedb/lancedb](../globals.md) / isBlobField
|
|
||||||
|
|
||||||
# Function: isBlobField()
|
|
||||||
|
|
||||||
```ts
|
|
||||||
function isBlobField(field): boolean
|
|
||||||
```
|
|
||||||
|
|
||||||
Checks for the `lance.blob.v2` extension marker. Does not validate the
|
|
||||||
field's storage type.
|
|
||||||
|
|
||||||
## Parameters
|
|
||||||
|
|
||||||
* **field**: `Field`<`any`>
|
|
||||||
|
|
||||||
## Returns
|
|
||||||
|
|
||||||
`boolean`
|
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
## Classes
|
## Classes
|
||||||
|
|
||||||
- [AutoQuery](classes/AutoQuery.md)
|
- [AutoQuery](classes/AutoQuery.md)
|
||||||
- [BlobFile](classes/BlobFile.md)
|
|
||||||
- [BooleanQuery](classes/BooleanQuery.md)
|
- [BooleanQuery](classes/BooleanQuery.md)
|
||||||
- [BoostQuery](classes/BoostQuery.md)
|
- [BoostQuery](classes/BoostQuery.md)
|
||||||
- [BranchContents](classes/BranchContents.md)
|
- [BranchContents](classes/BranchContents.md)
|
||||||
@@ -97,7 +96,7 @@
|
|||||||
- [IvfFlatOptions](interfaces/IvfFlatOptions.md)
|
- [IvfFlatOptions](interfaces/IvfFlatOptions.md)
|
||||||
- [IvfPqOptions](interfaces/IvfPqOptions.md)
|
- [IvfPqOptions](interfaces/IvfPqOptions.md)
|
||||||
- [IvfRqOptions](interfaces/IvfRqOptions.md)
|
- [IvfRqOptions](interfaces/IvfRqOptions.md)
|
||||||
- [JobEventsOptions](interfaces/JobEventsOptions.md)
|
- [JobDescription](interfaces/JobDescription.md)
|
||||||
- [JobFailureInfo](interfaces/JobFailureInfo.md)
|
- [JobFailureInfo](interfaces/JobFailureInfo.md)
|
||||||
- [JobInfo](interfaces/JobInfo.md)
|
- [JobInfo](interfaces/JobInfo.md)
|
||||||
- [ListNamespacesOptions](interfaces/ListNamespacesOptions.md)
|
- [ListNamespacesOptions](interfaces/ListNamespacesOptions.md)
|
||||||
@@ -144,7 +143,6 @@
|
|||||||
|
|
||||||
- [AnalyzePlanDistributedMetrics](type-aliases/AnalyzePlanDistributedMetrics.md)
|
- [AnalyzePlanDistributedMetrics](type-aliases/AnalyzePlanDistributedMetrics.md)
|
||||||
- [BaseTokenizer](type-aliases/BaseTokenizer.md)
|
- [BaseTokenizer](type-aliases/BaseTokenizer.md)
|
||||||
- [BlobOptions](type-aliases/BlobOptions.md)
|
|
||||||
- [Data](type-aliases/Data.md)
|
- [Data](type-aliases/Data.md)
|
||||||
- [DataLike](type-aliases/DataLike.md)
|
- [DataLike](type-aliases/DataLike.md)
|
||||||
- [FieldLike](type-aliases/FieldLike.md)
|
- [FieldLike](type-aliases/FieldLike.md)
|
||||||
@@ -160,11 +158,9 @@
|
|||||||
## Functions
|
## Functions
|
||||||
|
|
||||||
- [RecordBatchIterator](functions/RecordBatchIterator.md)
|
- [RecordBatchIterator](functions/RecordBatchIterator.md)
|
||||||
- [blob](functions/blob.md)
|
|
||||||
- [connect](functions/connect.md)
|
- [connect](functions/connect.md)
|
||||||
- [connectNamespace](functions/connectNamespace.md)
|
- [connectNamespace](functions/connectNamespace.md)
|
||||||
- [instrumentLanceDbMetrics](functions/instrumentLanceDbMetrics.md)
|
- [instrumentLanceDbMetrics](functions/instrumentLanceDbMetrics.md)
|
||||||
- [isBlobField](functions/isBlobField.md)
|
|
||||||
- [makeArrowTable](functions/makeArrowTable.md)
|
- [makeArrowTable](functions/makeArrowTable.md)
|
||||||
- [packBits](functions/packBits.md)
|
- [packBits](functions/packBits.md)
|
||||||
- [permutationBuilder](functions/permutationBuilder.md)
|
- [permutationBuilder](functions/permutationBuilder.md)
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ Number of sub-vectors of PQ.
|
|||||||
This value controls how much the vector is compressed during the quantization step.
|
This value controls how much the vector is compressed during the quantization step.
|
||||||
The more sub vectors there are the less the vector is compressed. The default is
|
The more sub vectors there are the less the vector is compressed. The default is
|
||||||
the dimension of the vector divided by 16. If the dimension is not evenly divisible
|
the dimension of the vector divided by 16. If the dimension is not evenly divisible
|
||||||
by 16 we use the dimension divided by 8.
|
by 16 we use the dimension divded by 8.
|
||||||
|
|
||||||
The above two cases are highly preferred. Having 8 or 16 values per subvector allows
|
The above two cases are highly preferred. Having 8 or 16 values per subvector allows
|
||||||
us to use efficient SIMD instructions.
|
us to use efficient SIMD instructions.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ optional config: Index;
|
|||||||
|
|
||||||
Advanced index configuration
|
Advanced index configuration
|
||||||
|
|
||||||
This option allows you to specify a specific index to create and also
|
This option allows you to specify a specfic index to create and also
|
||||||
allows you to pass in configuration for training the index.
|
allows you to pass in configuration for training the index.
|
||||||
|
|
||||||
See the static methods on Index for details on the various index types.
|
See the static methods on Index for details on the various index types.
|
||||||
@@ -50,7 +50,7 @@ If this is false, and another index already exists on the same columns
|
|||||||
and the same name, then an error will be returned. This is true even if
|
and the same name, then an error will be returned. This is true even if
|
||||||
that index is out of date.
|
that index is out of date.
|
||||||
|
|
||||||
The default is true
|
The default is false
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ Number of sub-vectors of PQ.
|
|||||||
This value controls how much the vector is compressed during the quantization step.
|
This value controls how much the vector is compressed during the quantization step.
|
||||||
The more sub vectors there are the less the vector is compressed. The default is
|
The more sub vectors there are the less the vector is compressed. The default is
|
||||||
the dimension of the vector divided by 16. If the dimension is not evenly divisible
|
the dimension of the vector divided by 16. If the dimension is not evenly divisible
|
||||||
by 16 we use the dimension divided by 8.
|
by 16 we use the dimension divded by 8.
|
||||||
|
|
||||||
The above two cases are highly preferred. Having 8 or 16 values per subvector allows
|
The above two cases are highly preferred. Having 8 or 16 values per subvector allows
|
||||||
us to use efficient SIMD instructions.
|
us to use efficient SIMD instructions.
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
[@lancedb/lancedb](../globals.md) / JobDescription
|
||||||
|
|
||||||
|
# Interface: JobDescription
|
||||||
|
|
||||||
|
A described job from `Connection.getJob`.
|
||||||
|
|
||||||
|
## Properties
|
||||||
|
|
||||||
|
### creationMs
|
||||||
|
|
||||||
|
```ts
|
||||||
|
creationMs: number;
|
||||||
|
```
|
||||||
|
|
||||||
|
When the job was created, in milliseconds since the epoch.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### failure?
|
||||||
|
|
||||||
|
```ts
|
||||||
|
optional failure: JobFailureInfo;
|
||||||
|
```
|
||||||
|
|
||||||
|
Why the job failed, when the job is failed and the server reports a
|
||||||
|
reason.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### jobId
|
||||||
|
|
||||||
|
```ts
|
||||||
|
jobId: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### jobType
|
||||||
|
|
||||||
|
```ts
|
||||||
|
jobType: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### specJson?
|
||||||
|
|
||||||
|
```ts
|
||||||
|
optional specJson: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
The job-type-specific specification as a JSON string, when present.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
|
### state
|
||||||
|
|
||||||
|
```ts
|
||||||
|
state: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
Lifecycle state: "running", "finished", "failed", or "cancelled".
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
[@lancedb/lancedb](../globals.md) / JobEventsOptions
|
|
||||||
|
|
||||||
# Interface: JobEventsOptions
|
|
||||||
|
|
||||||
Which of a job's events [Job.events](../classes/Job.md#events) returns.
|
|
||||||
|
|
||||||
## Properties
|
|
||||||
|
|
||||||
### filter?
|
|
||||||
|
|
||||||
```ts
|
|
||||||
optional filter: string;
|
|
||||||
```
|
|
||||||
|
|
||||||
SQL-like filter over the event columns.
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
### limit?
|
|
||||||
|
|
||||||
```ts
|
|
||||||
optional limit: number;
|
|
||||||
```
|
|
||||||
|
|
||||||
Maximum event rows to return, up to the server maximum of 10,000.
|
|
||||||
@@ -26,7 +26,7 @@ When the job was created, in milliseconds since the epoch.
|
|||||||
jobId: string;
|
jobId: string;
|
||||||
```
|
```
|
||||||
|
|
||||||
The job id -- what `Connection.openJob` and `Connection.cancelJob`
|
The job id -- what `Connection.getJob` and `Connection.cancelJob`
|
||||||
accept.
|
accept.
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
|
||||||
|
|
||||||
***
|
|
||||||
|
|
||||||
[@lancedb/lancedb](../globals.md) / BlobOptions
|
|
||||||
|
|
||||||
# Type Alias: BlobOptions
|
|
||||||
|
|
||||||
```ts
|
|
||||||
type BlobOptions: object;
|
|
||||||
```
|
|
||||||
|
|
||||||
## Type declaration
|
|
||||||
|
|
||||||
### dedicatedSizeThreshold?
|
|
||||||
|
|
||||||
```ts
|
|
||||||
optional dedicatedSizeThreshold: number;
|
|
||||||
```
|
|
||||||
|
|
||||||
Max payload bytes stored in a packed sidecar before a dedicated file. Must
|
|
||||||
be a positive safe integer.
|
|
||||||
|
|
||||||
### inlineSizeThreshold?
|
|
||||||
|
|
||||||
```ts
|
|
||||||
optional inlineSizeThreshold: number;
|
|
||||||
```
|
|
||||||
|
|
||||||
Max payload bytes kept inline in the data file. Zero is allowed. Must be a
|
|
||||||
safe integer.
|
|
||||||
|
|
||||||
### nullable?
|
|
||||||
|
|
||||||
```ts
|
|
||||||
optional nullable: boolean;
|
|
||||||
```
|
|
||||||
|
|
||||||
Defaults to true.
|
|
||||||
|
|
||||||
### packFileSizeThreshold?
|
|
||||||
|
|
||||||
```ts
|
|
||||||
optional packFileSizeThreshold: number;
|
|
||||||
```
|
|
||||||
|
|
||||||
Max bytes in one packed sidecar before starting another. Must be a positive
|
|
||||||
safe integer.
|
|
||||||
@@ -28,59 +28,6 @@ is also an [asynchronous API client](#connections-asynchronous).
|
|||||||
|
|
||||||
::: lancedb.Session
|
::: lancedb.Session
|
||||||
|
|
||||||
## Remote SQL
|
|
||||||
|
|
||||||
Submit SQL against a remote LanceDB database through the connection.
|
|
||||||
The connected database and `default_namespace_path=["public"]` are used for
|
|
||||||
unqualified tables. Fully qualified references can still query other databases
|
|
||||||
and namespaces available to the same deployment. `execute_query` returns a
|
|
||||||
reader as soon as its initial result stream is available. `execute_query_async`
|
|
||||||
returns a query handle immediately; use it to inspect progress, open a reader,
|
|
||||||
or cancel the query. The SQL client is initialized by the first query and
|
|
||||||
retained for the lifetime of the remote connection. Query ids are random,
|
|
||||||
connection-scoped references rather than encoded SQL or durable resume tokens:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import lancedb
|
|
||||||
|
|
||||||
db = lancedb.connect(
|
|
||||||
"db://analytics",
|
|
||||||
api_key="ldb_...",
|
|
||||||
host_override="https://api.example.com",
|
|
||||||
sql_host_override="grpc+tls://sql.example.com:10026",
|
|
||||||
)
|
|
||||||
reader = db.execute_query(
|
|
||||||
"""
|
|
||||||
SELECT events.id, accounts.name
|
|
||||||
FROM analytics.public.events AS events
|
|
||||||
JOIN users.public.accounts AS accounts ON events.user_id = accounts.id
|
|
||||||
""",
|
|
||||||
default_namespace_path=["public"],
|
|
||||||
)
|
|
||||||
for batch in reader:
|
|
||||||
print(batch.num_rows)
|
|
||||||
|
|
||||||
query = db.execute_query_async("SELECT * FROM events")
|
|
||||||
print(query.id)
|
|
||||||
print(query.describe().status)
|
|
||||||
for batch in query.reader():
|
|
||||||
print(batch.num_rows)
|
|
||||||
|
|
||||||
# The async connection exposes the same lifecycle without blocking:
|
|
||||||
# async_db = await lancedb.connect_async(
|
|
||||||
# "db://analytics",
|
|
||||||
# api_key="ldb_...",
|
|
||||||
# host_override="https://api.example.com",
|
|
||||||
# sql_host_override="grpc+tls://sql.example.com:10026",
|
|
||||||
# )
|
|
||||||
# reader = await async_db.execute_query("SELECT * FROM events")
|
|
||||||
# query = await async_db.execute_query_async("SELECT * FROM events")
|
|
||||||
# description = await async_db.describe_query(query.id)
|
|
||||||
# async for batch in await query.reader():
|
|
||||||
# print(batch.num_rows)
|
|
||||||
# await query.cancel()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Namespaces (Synchronous)
|
## Namespaces (Synchronous)
|
||||||
|
|
||||||
A namespace-backed connection resolves tables through a
|
A namespace-backed connection resolves tables through a
|
||||||
@@ -147,8 +94,6 @@ listing a storage directory.
|
|||||||
|
|
||||||
::: lancedb.functions.OutputMapping
|
::: lancedb.functions.OutputMapping
|
||||||
|
|
||||||
::: lancedb.functions.AssignmentMapping
|
|
||||||
|
|
||||||
::: lancedb.functions.FunctionBinding
|
::: lancedb.functions.FunctionBinding
|
||||||
|
|
||||||
::: lancedb.functions.RefreshColumnResult
|
::: lancedb.functions.RefreshColumnResult
|
||||||
@@ -157,18 +102,6 @@ listing a storage directory.
|
|||||||
|
|
||||||
::: lancedb.job.AsyncJob
|
::: lancedb.job.AsyncJob
|
||||||
|
|
||||||
::: lancedb.job.JobInfo
|
|
||||||
|
|
||||||
::: lancedb.job.JobDescription
|
|
||||||
|
|
||||||
::: lancedb.job.JobFailureInfo
|
|
||||||
|
|
||||||
::: lancedb.sql.Query
|
|
||||||
|
|
||||||
::: lancedb.sql.AsyncQuery
|
|
||||||
|
|
||||||
::: lancedb.sql.QueryDescription
|
|
||||||
|
|
||||||
## Materialized Views (Synchronous)
|
## Materialized Views (Synchronous)
|
||||||
|
|
||||||
::: lancedb.materialized_view.MaterializedView
|
::: lancedb.materialized_view.MaterializedView
|
||||||
@@ -316,12 +249,6 @@ still work. Queries return descriptors. Call
|
|||||||
|
|
||||||
::: lancedb.exceptions.MissingColumnError
|
::: lancedb.exceptions.MissingColumnError
|
||||||
|
|
||||||
::: lancedb.exceptions.JobNotFoundError
|
|
||||||
|
|
||||||
::: lancedb.exceptions.JobFailedError
|
|
||||||
|
|
||||||
::: lancedb.exceptions.JobCancelledError
|
|
||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
## Pydantic
|
## Pydantic
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.lancedb</groupId>
|
<groupId>com.lancedb</groupId>
|
||||||
<artifactId>lancedb-parent</artifactId>
|
<artifactId>lancedb-parent</artifactId>
|
||||||
<version>0.39.0-beta.6</version>
|
<version>0.39.0-beta.1</version>
|
||||||
<relativePath>../pom.xml</relativePath>
|
<relativePath>../pom.xml</relativePath>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>com.lancedb</groupId>
|
<groupId>com.lancedb</groupId>
|
||||||
<artifactId>lancedb-parent</artifactId>
|
<artifactId>lancedb-parent</artifactId>
|
||||||
<version>0.39.0-beta.6</version>
|
<version>0.39.0-beta.1</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<name>${project.artifactId}</name>
|
<name>${project.artifactId}</name>
|
||||||
<description>LanceDB Java SDK Parent POM</description>
|
<description>LanceDB Java SDK Parent POM</description>
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<arrow.version>15.0.0</arrow.version>
|
<arrow.version>15.0.0</arrow.version>
|
||||||
<lance-core.version>12.0.0-beta.17</lance-core.version>
|
<lance-core.version>12.0.0-beta.11</lance-core.version>
|
||||||
<spotless.skip>false</spotless.skip>
|
<spotless.skip>false</spotless.skip>
|
||||||
<spotless.version>2.30.0</spotless.version>
|
<spotless.version>2.30.0</spotless.version>
|
||||||
<spotless.java.googlejavaformat.version>1.7</spotless.java.googlejavaformat.version>
|
<spotless.java.googlejavaformat.version>1.7</spotless.java.googlejavaformat.version>
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lancedb-nodejs"
|
name = "lancedb-nodejs"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
version = "0.39.0-beta.6"
|
version = "0.39.0-beta.1"
|
||||||
publish = false
|
publish = false
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
||||||
|
|
||||||
import { Field, Int64, List, Schema, Struct, Utf8 } from "apache-arrow";
|
|
||||||
import { makeArrowTable } from "../lancedb/arrow";
|
|
||||||
import { BlobFile, blob, coerceBlobValue, isBlobField } from "../lancedb/blob";
|
|
||||||
|
|
||||||
describe("blob()", () => {
|
|
||||||
it("marks the field as lance.blob.v2", () => {
|
|
||||||
const field = blob("image", { nullable: false });
|
|
||||||
expect(field.nullable).toBe(false);
|
|
||||||
expect(isBlobField(field)).toBe(true);
|
|
||||||
expect(field.metadata.get("ARROW:extension:name")).toBe("lance.blob.v2");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("writes encoding thresholds as field metadata", () => {
|
|
||||||
const field = blob("video", {
|
|
||||||
inlineSizeThreshold: 1024,
|
|
||||||
dedicatedSizeThreshold: 2 * 1024 * 1024,
|
|
||||||
packFileSizeThreshold: 64 * 1024 * 1024,
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
field.metadata.get("lance-encoding:blob-inline-size-threshold"),
|
|
||||||
).toBe("1024");
|
|
||||||
expect(
|
|
||||||
field.metadata.get("lance-encoding:blob-dedicated-size-threshold"),
|
|
||||||
).toBe(String(2 * 1024 * 1024));
|
|
||||||
expect(
|
|
||||||
field.metadata.get("lance-encoding:blob-pack-file-size-threshold"),
|
|
||||||
).toBe(String(64 * 1024 * 1024));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects invalid thresholds", () => {
|
|
||||||
expect(() => blob("image", { inlineSizeThreshold: -1 })).toThrow(
|
|
||||||
/inlineSizeThreshold must be non-negative/,
|
|
||||||
);
|
|
||||||
expect(() => blob("image", { dedicatedSizeThreshold: 0 })).toThrow(
|
|
||||||
/dedicatedSizeThreshold must be positive/,
|
|
||||||
);
|
|
||||||
expect(() => blob("image", { packFileSizeThreshold: 1.5 })).toThrow(
|
|
||||||
/packFileSizeThreshold must be a safe integer/,
|
|
||||||
);
|
|
||||||
expect(() =>
|
|
||||||
blob("image", { dedicatedSizeThreshold: Number.MAX_SAFE_INTEGER + 1 }),
|
|
||||||
).toThrow(/dedicatedSizeThreshold must be a safe integer/);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("coerceBlobValue", () => {
|
|
||||||
it.each([
|
|
||||||
["Buffer", Buffer.from("x"), { data: Buffer.from("x"), uri: null }],
|
|
||||||
[
|
|
||||||
"Uint8Array",
|
|
||||||
new Uint8Array([120]),
|
|
||||||
{ data: new Uint8Array([120]), uri: null },
|
|
||||||
],
|
|
||||||
["URI string", "s3://bucket/key", { data: null, uri: "s3://bucket/key" }],
|
|
||||||
[
|
|
||||||
"data struct",
|
|
||||||
{ data: Buffer.from("y") },
|
|
||||||
{ data: Buffer.from("y"), uri: null },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"uri struct",
|
|
||||||
{ uri: "s3://bucket/key" },
|
|
||||||
{ data: null, uri: "s3://bucket/key" },
|
|
||||||
],
|
|
||||||
["null", null, null],
|
|
||||||
])("accepts %s", (_name, input, expected) => {
|
|
||||||
expect(coerceBlobValue(input)).toEqual(expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
["empty URI", "", /uri cannot be empty/],
|
|
||||||
["object without data or uri", { position: 0 }, /data' or 'uri/],
|
|
||||||
[
|
|
||||||
"Int16Array",
|
|
||||||
new Int16Array([1]),
|
|
||||||
/Blob data must be Buffer or Uint8Array/,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"both data and uri",
|
|
||||||
{ data: Buffer.from("y"), uri: "s3://bucket/key" },
|
|
||||||
/exactly one of 'data' or 'uri'/,
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"neither data nor uri",
|
|
||||||
{ data: null, uri: null },
|
|
||||||
/exactly one of 'data' or 'uri'/,
|
|
||||||
],
|
|
||||||
])("rejects %s", (_name, input, message) => {
|
|
||||||
expect(() => coerceBlobValue(input)).toThrow(message);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("BlobFile", () => {
|
|
||||||
it("rejects constructing BlobFile without a native handle", () => {
|
|
||||||
expect(() => new (BlobFile as unknown as { new (): BlobFile })()).toThrow(
|
|
||||||
/fetchBlobFiles/,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("makeArrowTable blob columns", () => {
|
|
||||||
it("coerces Buffer input onto a blob field", () => {
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
blob("image"),
|
|
||||||
]);
|
|
||||||
const table = makeArrowTable([{ id: 1n, image: Buffer.from("hello") }], {
|
|
||||||
schema,
|
|
||||||
});
|
|
||||||
expect(isBlobField(table.schema.fields[1])).toBe(true);
|
|
||||||
const image = table.getChild("image")!;
|
|
||||||
expect(image.nullCount).toBe(0);
|
|
||||||
expect(image.getChild("uri")!.get(0)).toBeNull();
|
|
||||||
expect(image.getChild("data")!.nullCount).toBe(0);
|
|
||||||
expect(Buffer.from(image.getChild("data")!.get(0)!).toString()).toBe(
|
|
||||||
"hello",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("coerces Buffer elements inside a list and keeps null slots", () => {
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
new Field("images", new List(blob("image")), true),
|
|
||||||
]);
|
|
||||||
const table = makeArrowTable(
|
|
||||||
[
|
|
||||||
{ id: 1n, images: [Buffer.from("a"), Buffer.from("bb")] },
|
|
||||||
{ id: 2n, images: null },
|
|
||||||
{ id: 3n, images: [Buffer.from("c"), null] },
|
|
||||||
{ id: 4n, images: [] },
|
|
||||||
],
|
|
||||||
{ schema },
|
|
||||||
);
|
|
||||||
const images = table.getChild("images")!;
|
|
||||||
expect(images.nullCount).toBe(1);
|
|
||||||
const rows = images.toArray();
|
|
||||||
expect(rows[1]).toBeNull();
|
|
||||||
expect(Array.from(rows[3] as Iterable<unknown>)).toHaveLength(0);
|
|
||||||
const first = Array.from(rows[0] as Iterable<{ data: Uint8Array | null }>);
|
|
||||||
expect(Buffer.from(first[0].data!).toString()).toBe("a");
|
|
||||||
expect(Buffer.from(first[1].data!).toString()).toBe("bb");
|
|
||||||
const third = Array.from(
|
|
||||||
rows[2] as Iterable<{ data: Uint8Array | null } | null>,
|
|
||||||
);
|
|
||||||
expect(Buffer.from(third[0]!.data!).toString()).toBe("c");
|
|
||||||
expect(third[1]).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("coerces Buffer fields inside list structs", () => {
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
new Field(
|
|
||||||
"items",
|
|
||||||
new List(
|
|
||||||
new Field(
|
|
||||||
"item",
|
|
||||||
new Struct([new Field("name", new Utf8(), true), blob("image")]),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
const table = makeArrowTable(
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: 1n,
|
|
||||||
items: [{ name: "one", image: Buffer.from("alpha") }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ schema },
|
|
||||||
);
|
|
||||||
const items = Array.from(
|
|
||||||
table.getChild("items")!.toArray()[0] as Iterable<{
|
|
||||||
name: string;
|
|
||||||
image: { data: Uint8Array | null };
|
|
||||||
}>,
|
|
||||||
);
|
|
||||||
expect(items[0].name).toBe("one");
|
|
||||||
expect(Buffer.from(items[0].image.data!).toString()).toBe("alpha");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -939,7 +939,6 @@ describe("remote connection jobs surface", () => {
|
|||||||
const { tableFromArrays, tableToIPC } = await import("apache-arrow");
|
const { tableFromArrays, tableToIPC } = await import("apache-arrow");
|
||||||
const eventsTable = tableFromArrays({ state: ["created", "succeeded"] });
|
const eventsTable = tableFromArrays({ state: ["created", "succeeded"] });
|
||||||
const eventsBody = Buffer.from(tableToIPC(eventsTable, "stream"));
|
const eventsBody = Buffer.from(tableToIPC(eventsTable, "stream"));
|
||||||
const queryEventsPayloads: Record<string, unknown>[] = [];
|
|
||||||
|
|
||||||
await withMockDatabase(
|
await withMockDatabase(
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
@@ -968,16 +967,6 @@ describe("remote connection jobs surface", () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else if (req.url === "/v1/jobs/describe") {
|
} else if (req.url === "/v1/jobs/describe") {
|
||||||
if (payload["job_id"] === "job-2") {
|
|
||||||
res
|
|
||||||
.writeHead(200, { "Content-Type": "application/json" })
|
|
||||||
.end(
|
|
||||||
'{"job_id": "job-2", "job_type": "refresh_column", ' +
|
|
||||||
'"job_state": "DONE", "creation_ms": 2000, ' +
|
|
||||||
'"result": {"rows_assigned": 1000000}}',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (payload["job_id"] !== "job-1") {
|
if (payload["job_id"] !== "job-1") {
|
||||||
res.writeHead(404).end("no such job");
|
res.writeHead(404).end("no such job");
|
||||||
return;
|
return;
|
||||||
@@ -999,7 +988,6 @@ describe("remote connection jobs surface", () => {
|
|||||||
.writeHead(200, { "Content-Type": "application/json" })
|
.writeHead(200, { "Content-Type": "application/json" })
|
||||||
.end('{"job_id": "job-1"}');
|
.end('{"job_id": "job-1"}');
|
||||||
} else if (req.url === "/v1/jobs/query_events") {
|
} else if (req.url === "/v1/jobs/query_events") {
|
||||||
queryEventsPayloads.push(payload);
|
|
||||||
res
|
res
|
||||||
.writeHead(200, {
|
.writeHead(200, {
|
||||||
"Content-Type": "application/vnd.apache.arrow.stream",
|
"Content-Type": "application/vnd.apache.arrow.stream",
|
||||||
@@ -1016,65 +1004,22 @@ describe("remote connection jobs surface", () => {
|
|||||||
expect(jobs[0].state).toEqual("running");
|
expect(jobs[0].state).toEqual("running");
|
||||||
expect(jobs[1].state).toEqual("finished");
|
expect(jobs[1].state).toEqual("finished");
|
||||||
|
|
||||||
|
const description = await db.getJob("job-1");
|
||||||
|
expect(description?.state).toEqual("failed");
|
||||||
|
expect(JSON.parse(description?.specJson ?? "")).toEqual({
|
||||||
|
column: "vec",
|
||||||
|
});
|
||||||
|
expect(description?.failure?.message).toEqual("worker died");
|
||||||
|
expect(await db.getJob("missing")).toBeNull();
|
||||||
|
|
||||||
expect(await db.cancelJob("job-1")).toBe(true);
|
expect(await db.cancelJob("job-1")).toBe(true);
|
||||||
expect(await db.cancelJob("missing")).toBe(false);
|
expect(await db.cancelJob("missing")).toBe(false);
|
||||||
|
|
||||||
// Opening a job hands back a populated handle; a missing one rejects.
|
const history = await db.jobHistory("job-1");
|
||||||
await expect(db.openJob("missing")).rejects.toThrow("not found");
|
expect(history.numRows).toEqual(2);
|
||||||
const finished = await db.openJob("job-2");
|
|
||||||
expect(finished.state).toEqual("finished");
|
|
||||||
expect(finished.result).toEqual({
|
|
||||||
// biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format
|
|
||||||
rows_assigned: 1000000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const job = await db.openJob("job-1");
|
const job = db.job("job-1");
|
||||||
expect(job.id).toEqual("job-1");
|
expect(job.id).toEqual("job-1");
|
||||||
|
|
||||||
// openJob already populated the handle; refresh() re-reads it.
|
|
||||||
expect(job.state).toEqual("failed");
|
|
||||||
await job.refresh();
|
|
||||||
expect(job.state).toEqual("failed");
|
|
||||||
expect(job.jobType).toEqual("create_index");
|
|
||||||
expect(job.creationMs).toEqual(1000);
|
|
||||||
expect(job.spec).toEqual({ column: "vec" });
|
|
||||||
expect(job.result).toBeNull();
|
|
||||||
expect(job.failure?.message).toEqual("worker died");
|
|
||||||
|
|
||||||
// The handle reaches its own events, supplying its job id.
|
|
||||||
const jobEvents = await job.events({
|
|
||||||
limit: 500,
|
|
||||||
filter: "state = 'claim_complete'",
|
|
||||||
});
|
|
||||||
expect(jobEvents.numRows).toEqual(2);
|
|
||||||
expect(queryEventsPayloads.pop()).toEqual({
|
|
||||||
// biome-ignore lint/style/useNamingConvention: snake_case mandated by the server wire format
|
|
||||||
job_id: "job-1",
|
|
||||||
limit: 500,
|
|
||||||
filter: "state = 'claim_complete'",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Printing lays every known field out on its own line, with the JSON
|
|
||||||
// payloads indented rather than crammed onto one line.
|
|
||||||
expect(`${job}`).toEqual(
|
|
||||||
[
|
|
||||||
"Job(",
|
|
||||||
' id="job-1",',
|
|
||||||
' state="failed",',
|
|
||||||
' jobType="create_index",',
|
|
||||||
" creationMs=1000,",
|
|
||||||
" spec={",
|
|
||||||
' "column": "vec"',
|
|
||||||
" },",
|
|
||||||
" failure={",
|
|
||||||
' "phase": "execute",',
|
|
||||||
' "message": "worker died",',
|
|
||||||
' "retryable": true',
|
|
||||||
" },",
|
|
||||||
")",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(await job.status()).toEqual("failed");
|
expect(await job.status()).toEqual("failed");
|
||||||
await expect(job.wait()).rejects.toThrow("worker died");
|
await expect(job.wait()).rejects.toThrow("worker died");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Table,
|
Table,
|
||||||
VectorQuery,
|
VectorQuery,
|
||||||
blob,
|
|
||||||
connect,
|
connect,
|
||||||
tokenize,
|
tokenize,
|
||||||
} from "../lancedb";
|
} from "../lancedb";
|
||||||
@@ -282,7 +281,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
|
|||||||
numIndices: 0,
|
numIndices: 0,
|
||||||
numRows: 3,
|
numRows: 3,
|
||||||
// Full on-disk size of the two data files, footers and metadata included.
|
// Full on-disk size of the two data files, footers and metadata included.
|
||||||
totalBytes: 550,
|
totalBytes: 684,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Index files count toward totalBytes too (only deletion files and
|
// Index files count toward totalBytes too (only deletion files and
|
||||||
@@ -290,7 +289,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
|
|||||||
await table.createIndex("id", { config: Index.btree() });
|
await table.createIndex("id", { config: Index.btree() });
|
||||||
const statsWithIndex = await table.stats();
|
const statsWithIndex = await table.stats();
|
||||||
expect(statsWithIndex.numIndices).toBe(1);
|
expect(statsWithIndex.numIndices).toBe(1);
|
||||||
expect(statsWithIndex.totalBytes).toBeGreaterThan(550);
|
expect(statsWithIndex.totalBytes).toBeGreaterThan(684);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should overwrite data if asked", async () => {
|
it("should overwrite data if asked", async () => {
|
||||||
@@ -1527,11 +1526,7 @@ describe("When creating an index", () => {
|
|||||||
|
|
||||||
it("should allow me to replace (or not) an existing index", async () => {
|
it("should allow me to replace (or not) an existing index", async () => {
|
||||||
await tbl.createIndex("id");
|
await tbl.createIndex("id");
|
||||||
// Default is replace=true
|
await expect(tbl.createIndex("id")).rejects.toThrow("already exists");
|
||||||
await tbl.createIndex("id");
|
|
||||||
await expect(tbl.createIndex("id", { replace: false })).rejects.toThrow(
|
|
||||||
"already exists",
|
|
||||||
);
|
|
||||||
await tbl.createIndex("id", { replace: true });
|
await tbl.createIndex("id", { replace: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2402,276 +2397,6 @@ describe("when dealing with versioning", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when dealing with blob columns", () => {
|
|
||||||
let tmpDir: tmp.DirResult;
|
|
||||||
beforeEach(() => {
|
|
||||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
tmpDir.removeCallback();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("discovers blob columns", async () => {
|
|
||||||
const { table } = await openBlobTable();
|
|
||||||
expect(await table.blobColumns()).toEqual(["image"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("preserves order, duplicates, and nulls", async () => {
|
|
||||||
const { table, rowIds } = await openBlobTable();
|
|
||||||
const [alphaId, betaId, nullId] = rowIds;
|
|
||||||
const bytes = await table.fetchBlobs("image", [
|
|
||||||
betaId,
|
|
||||||
alphaId,
|
|
||||||
betaId,
|
|
||||||
nullId,
|
|
||||||
]);
|
|
||||||
expect(bytes.map((b) => (b == null ? null : b.toString()))).toEqual([
|
|
||||||
"beta",
|
|
||||||
"alpha",
|
|
||||||
"beta",
|
|
||||||
null,
|
|
||||||
]);
|
|
||||||
const files = await table.fetchBlobFiles("image", [
|
|
||||||
betaId,
|
|
||||||
nullId,
|
|
||||||
alphaId,
|
|
||||||
]);
|
|
||||||
expect(files.map((f) => f == null)).toEqual([false, true, false]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reads full blob contents", async () => {
|
|
||||||
const { table, rowIds, alpha, beta } = await openBlobTable();
|
|
||||||
const bytes = await table.fetchBlobs("image", rowIds);
|
|
||||||
expect(bytes[0]!.equals(alpha)).toBe(true);
|
|
||||||
expect(bytes[1]!.equals(beta)).toBe(true);
|
|
||||||
const files = await table.fetchBlobFiles("image", rowIds);
|
|
||||||
expect(files[0]!.size()).toBe(BigInt(alpha.length));
|
|
||||||
expect(Buffer.from(await files[0]!.read()).toString()).toBe("alpha");
|
|
||||||
expect(Buffer.from(await files[1]!.read()).toString()).toBe("beta");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("reads a half-open range", async () => {
|
|
||||||
const { table, rowIds } = await openBlobTable();
|
|
||||||
const files = await table.fetchBlobFiles("image", rowIds);
|
|
||||||
expect(Buffer.from(await files[0]!.readRange(0n, 2n)).toString()).toBe(
|
|
||||||
"al",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("readRange does not move the cursor", async () => {
|
|
||||||
const { table, rowIds, alpha } = await openBlobTable();
|
|
||||||
const [handle] = await table.fetchBlobFiles("image", rowIds);
|
|
||||||
expect((await handle!.readRange(1n, 3n)).toString()).toBe("lp");
|
|
||||||
expect(await handle!.read()).toEqual(alpha);
|
|
||||||
expect(await handle!.read()).toEqual(Buffer.alloc(0));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fails when readRange end is past the blob size", async () => {
|
|
||||||
const { table, rowIds, alpha } = await openBlobTable();
|
|
||||||
const files = await table.fetchBlobFiles("image", rowIds);
|
|
||||||
await expect(
|
|
||||||
files[0]!.readRange(0n, BigInt(alpha.length + 1)),
|
|
||||||
).rejects.toThrow(/exceeds blob size/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects fetchBlobs on a non-blob column", async () => {
|
|
||||||
const { table, rowIds } = await openBlobTable();
|
|
||||||
await expect(table.fetchBlobs("id", rowIds)).rejects.toThrow(/blob/i);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("discovers and fetches nested blob columns", async () => {
|
|
||||||
const db = await connect(tmpDir.name);
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
new Field("info", new Struct([blob("image")]), true),
|
|
||||||
]);
|
|
||||||
const payload = Buffer.from("nested");
|
|
||||||
const table = await db.createTable(
|
|
||||||
"nested_blobs",
|
|
||||||
[{ id: 1n, info: { image: payload } }],
|
|
||||||
{ schema },
|
|
||||||
);
|
|
||||||
expect(await table.blobColumns()).toEqual(["info.image"]);
|
|
||||||
const rows = await table.query().withRowId().toArray();
|
|
||||||
const bytes = await table.fetchBlobs("info.image", [
|
|
||||||
rows[0]._rowid as bigint,
|
|
||||||
]);
|
|
||||||
expect(bytes[0]!.equals(payload)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates and adds list blob columns", async () => {
|
|
||||||
const db = await connect(tmpDir.name);
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
new Field("images", new List(blob("image")), true),
|
|
||||||
]);
|
|
||||||
const alpha = Buffer.from("alpha");
|
|
||||||
const beta = Buffer.from("beta");
|
|
||||||
const gamma = Buffer.from("gamma");
|
|
||||||
const table = await db.createTable(
|
|
||||||
"list_blobs",
|
|
||||||
[{ id: 1n, images: [alpha, beta] }],
|
|
||||||
{ schema },
|
|
||||||
);
|
|
||||||
await table.add([
|
|
||||||
{ id: 2n, images: null },
|
|
||||||
{ id: 3n, images: [gamma, null] },
|
|
||||||
{ id: 4n, images: [] },
|
|
||||||
]);
|
|
||||||
expect(await table.blobColumns()).toEqual(["images.image"]);
|
|
||||||
const rows = await table.query().toArray();
|
|
||||||
const byId = new Map(rows.map((row) => [Number(row.id), row]));
|
|
||||||
expect(descriptorSizes(byId.get(1)!.images)).toEqual([
|
|
||||||
alpha.length,
|
|
||||||
beta.length,
|
|
||||||
]);
|
|
||||||
expect(byId.get(2)!.images).toBeNull();
|
|
||||||
expect(descriptorSizes(byId.get(3)!.images)).toEqual([gamma.length, null]);
|
|
||||||
expect(Array.from(byId.get(4)!.images as Iterable<unknown>)).toHaveLength(
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates and adds list struct blob columns", async () => {
|
|
||||||
const db = await connect(tmpDir.name);
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
new Field(
|
|
||||||
"items",
|
|
||||||
new List(
|
|
||||||
new Field(
|
|
||||||
"item",
|
|
||||||
new Struct([new Field("name", new Utf8(), true), blob("image")]),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
const alpha = Buffer.from("nested-alpha");
|
|
||||||
const beta = Buffer.from("nested-beta");
|
|
||||||
const table = await db.createTable(
|
|
||||||
"list_struct_blobs",
|
|
||||||
[{ id: 1n, items: [{ name: "one", image: alpha }] }],
|
|
||||||
{ schema },
|
|
||||||
);
|
|
||||||
await table.add([
|
|
||||||
{
|
|
||||||
id: 2n,
|
|
||||||
items: [
|
|
||||||
{ name: "two", image: beta },
|
|
||||||
{ name: "three", image: null },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const rows = await table.query().toArray();
|
|
||||||
const byId = new Map(rows.map((row) => [Number(row.id), row]));
|
|
||||||
expect(
|
|
||||||
descriptorSizes(
|
|
||||||
Array.from(byId.get(1)!.items as Iterable<{ image: unknown }>).map(
|
|
||||||
(item) => item.image,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).toEqual([alpha.length]);
|
|
||||||
expect(
|
|
||||||
descriptorSizes(
|
|
||||||
Array.from(byId.get(2)!.items as Iterable<{ image: unknown }>).map(
|
|
||||||
(item) => item.image,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
).toEqual([beta.length, null]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects blob fields inside a fixed-size list", async () => {
|
|
||||||
const db = await connect(tmpDir.name);
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
new Field("frames", new FixedSizeList(2, blob("frame")), true),
|
|
||||||
]);
|
|
||||||
await expect(
|
|
||||||
db.createTable(
|
|
||||||
"fsl_blobs",
|
|
||||||
[{ id: 1n, frames: [Buffer.from("a"), Buffer.from("b")] }],
|
|
||||||
{ schema },
|
|
||||||
),
|
|
||||||
).rejects.toThrow(
|
|
||||||
"Blob fields inside FixedSizeList are not supported. Use List instead.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects blob fields inside a nested fixed-size list", async () => {
|
|
||||||
const db = await connect(tmpDir.name);
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
new Field(
|
|
||||||
"clip",
|
|
||||||
new Struct([
|
|
||||||
new Field("frames", new FixedSizeList(2, blob("frame")), true),
|
|
||||||
]),
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
await expect(
|
|
||||||
db.createTable(
|
|
||||||
"nested_fsl_blobs",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: 1n,
|
|
||||||
clip: { frames: [Buffer.from("a"), Buffer.from("b")] },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ schema },
|
|
||||||
),
|
|
||||||
).rejects.toThrow(
|
|
||||||
"Blob fields inside FixedSizeList are not supported. Use List instead.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects an Arrow table with blob fields inside a fixed-size list", async () => {
|
|
||||||
const db = await connect(tmpDir.name);
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
new Field("frames", new FixedSizeList(2, blob("frame")), true),
|
|
||||||
]);
|
|
||||||
await expect(
|
|
||||||
db.createTable("fsl_blobs_ipc", new ArrowTable(schema)),
|
|
||||||
).rejects.toThrow(
|
|
||||||
"Blob fields inside FixedSizeList are not supported. Use List instead.",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
function descriptorSizes(values: unknown): (number | null)[] {
|
|
||||||
return Array.from(
|
|
||||||
values as Iterable<{ size?: bigint | number } | null>,
|
|
||||||
).map((value) => (value == null ? null : Number(value.size)));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openBlobTable() {
|
|
||||||
const db = await connect(tmpDir.name);
|
|
||||||
const schema = new Schema([
|
|
||||||
new Field("id", new Int64(), true),
|
|
||||||
blob("image"),
|
|
||||||
]);
|
|
||||||
const alpha = Buffer.from("alpha");
|
|
||||||
const beta = Buffer.from("beta");
|
|
||||||
const table = await db.createTable(
|
|
||||||
"blobs",
|
|
||||||
[
|
|
||||||
{ id: 1n, image: alpha },
|
|
||||||
{ id: 2n, image: beta },
|
|
||||||
{ id: 3n, image: null },
|
|
||||||
],
|
|
||||||
{ schema },
|
|
||||||
);
|
|
||||||
const rows = await table.query().withRowId().toArray();
|
|
||||||
const rowIdById = new Map(
|
|
||||||
rows.map((r) => [Number(r.id), r._rowid as bigint]),
|
|
||||||
);
|
|
||||||
const rowIds = [1, 2, 3].map((id) => rowIdById.get(id)!);
|
|
||||||
return { table, rowIds, alpha, beta };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when dealing with tags", () => {
|
describe("when dealing with tags", () => {
|
||||||
let tmpDir: tmp.DirResult;
|
let tmpDir: tmp.DirResult;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -3523,7 +3248,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
|
|||||||
const db = await connect(tmpDir.name);
|
const db = await connect(tmpDir.name);
|
||||||
const data = [
|
const data = [
|
||||||
{ text: "fa", vector: [0.1, 0.2, 0.3] },
|
{ text: "fa", vector: [0.1, 0.2, 0.3] },
|
||||||
{ text: "fo", vector: [0.4, 0.5, 0.6] }, // spellchecker:disable-line
|
{ text: "fo", vector: [0.4, 0.5, 0.6] },
|
||||||
{ text: "fob", vector: [0.4, 0.5, 0.6] },
|
{ text: "fob", vector: [0.4, 0.5, 0.6] },
|
||||||
{ text: "focus", vector: [0.4, 0.5, 0.6] },
|
{ text: "focus", vector: [0.4, 0.5, 0.6] },
|
||||||
{ text: "foo", vector: [0.4, 0.5, 0.6] },
|
{ text: "foo", vector: [0.4, 0.5, 0.6] },
|
||||||
@@ -3548,7 +3273,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
|
|||||||
const resultSet = new Set(fuzzyResults.map((r) => r.text));
|
const resultSet = new Set(fuzzyResults.map((r) => r.text));
|
||||||
expect(resultSet.has("foo")).toBe(true);
|
expect(resultSet.has("foo")).toBe(true);
|
||||||
expect(resultSet.has("fob")).toBe(true);
|
expect(resultSet.has("fob")).toBe(true);
|
||||||
expect(resultSet.has("fo")).toBe(true); // spellchecker:disable-line
|
expect(resultSet.has("fo")).toBe(true);
|
||||||
expect(resultSet.has("food")).toBe(true);
|
expect(resultSet.has("food")).toBe(true);
|
||||||
|
|
||||||
const prefixResults = await table
|
const prefixResults = await table
|
||||||
|
|||||||
+3
-104
@@ -40,7 +40,6 @@ import {
|
|||||||
} from "apache-arrow";
|
} from "apache-arrow";
|
||||||
import { Buffers } from "apache-arrow/data";
|
import { Buffers } from "apache-arrow/data";
|
||||||
import { typedArrayToArrowType } from "./arrow_type";
|
import { typedArrayToArrowType } from "./arrow_type";
|
||||||
import { coerceBlobValue, isBlobField } from "./blob";
|
|
||||||
import { type EmbeddingFunction } from "./embedding/embedding_function";
|
import { type EmbeddingFunction } from "./embedding/embedding_function";
|
||||||
import {
|
import {
|
||||||
EmbeddingFunctionConfig,
|
EmbeddingFunctionConfig,
|
||||||
@@ -431,14 +430,12 @@ export function makeArrowTable(
|
|||||||
throw new Error("A schema must be provided if data is empty");
|
throw new Error("A schema must be provided if data is empty");
|
||||||
} else {
|
} else {
|
||||||
schema = new Schema(schema.fields, schemaMetadata);
|
schema = new Schema(schema.fields, schemaMetadata);
|
||||||
validateBlobSchema(schema);
|
|
||||||
return new ArrowTable(schema);
|
return new ArrowTable(schema);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let inferredSchema = inferSchema(data, schema, opt);
|
let inferredSchema = inferSchema(data, schema, opt);
|
||||||
inferredSchema = new Schema(inferredSchema.fields, schemaMetadata);
|
inferredSchema = new Schema(inferredSchema.fields, schemaMetadata);
|
||||||
validateBlobSchema(inferredSchema);
|
|
||||||
|
|
||||||
const finalColumns: Record<string, Vector> = {};
|
const finalColumns: Record<string, Vector> = {};
|
||||||
for (const field of inferredSchema.fields) {
|
for (const field of inferredSchema.fields) {
|
||||||
@@ -448,35 +445,6 @@ export function makeArrowTable(
|
|||||||
return new ArrowTable(inferredSchema, finalColumns);
|
return new ArrowTable(inferredSchema, finalColumns);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateBlobSchema(schema: Schema): void {
|
|
||||||
for (const field of schema.fields) {
|
|
||||||
validateBlobField(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateBlobField(field: Field): void {
|
|
||||||
if (
|
|
||||||
isFixedSizeList(field.type) &&
|
|
||||||
containsBlobField(field.type.children[0])
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
"Blob fields inside FixedSizeList are not supported. Use List instead.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for (const child of field.type.children ?? []) {
|
|
||||||
validateBlobField(child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function containsBlobField(field: Field): boolean {
|
|
||||||
if (isBlobField(field)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return (field.type.children ?? []).some((child: Field) =>
|
|
||||||
containsBlobField(child),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isObject(value: unknown): value is Record<string, unknown> {
|
function isObject(value: unknown): value is Record<string, unknown> {
|
||||||
return (
|
return (
|
||||||
typeof value === "object" &&
|
typeof value === "object" &&
|
||||||
@@ -512,32 +480,6 @@ function transposeData(
|
|||||||
path: string[] = [],
|
path: string[] = [],
|
||||||
): Vector {
|
): Vector {
|
||||||
const valuesPath = [...path, field.name];
|
const valuesPath = [...path, field.name];
|
||||||
if (isBlobField(field) && field.type instanceof Struct) {
|
|
||||||
const blobRows = data.map((datum) =>
|
|
||||||
coerceBlobValue(valueAtPath(datum, valuesPath)),
|
|
||||||
);
|
|
||||||
const childVectors = field.type.children.map((child) => {
|
|
||||||
const values = blobRows.map((row) =>
|
|
||||||
row == null ? null : (row[child.name as "data" | "uri"] ?? null),
|
|
||||||
);
|
|
||||||
return makeVector(values, child.type, undefined, child.nullable);
|
|
||||||
});
|
|
||||||
const nullCount = blobRows.filter((row) => row === null).length;
|
|
||||||
const structData = makeData({
|
|
||||||
type: field.type,
|
|
||||||
length: blobRows.length,
|
|
||||||
nullCount,
|
|
||||||
nullBitmap:
|
|
||||||
nullCount > 0
|
|
||||||
? arrowUtil.packBools(blobRows.map((row) => row !== null))
|
|
||||||
: undefined,
|
|
||||||
children: childVectors.map((v) => v.data[0]),
|
|
||||||
});
|
|
||||||
return arrowMakeVector(structData);
|
|
||||||
}
|
|
||||||
if (isList(field.type) && containsBlobField(field.type.children[0])) {
|
|
||||||
return transposeListData(data, field, valuesPath);
|
|
||||||
}
|
|
||||||
const values = data.map((datum) => valueAtPath(datum, valuesPath));
|
const values = data.map((datum) => valueAtPath(datum, valuesPath));
|
||||||
if (field.type instanceof Struct) {
|
if (field.type instanceof Struct) {
|
||||||
const childFields = field.type.children;
|
const childFields = field.type.children;
|
||||||
@@ -553,7 +495,7 @@ function transposeData(
|
|||||||
nullCount > 0
|
nullCount > 0
|
||||||
? arrowUtil.packBools(values.map((value) => value !== null))
|
? arrowUtil.packBools(values.map((value) => value !== null))
|
||||||
: undefined,
|
: undefined,
|
||||||
children: childVectors.map((v) => v.data[0]),
|
children: childVectors as unknown as ArrowData<DataType>[],
|
||||||
});
|
});
|
||||||
return arrowMakeVector(structData);
|
return arrowMakeVector(structData);
|
||||||
} else {
|
} else {
|
||||||
@@ -561,48 +503,6 @@ function transposeData(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function transposeListData(
|
|
||||||
data: Record<string, unknown>[],
|
|
||||||
field: Field,
|
|
||||||
valuesPath: string[],
|
|
||||||
): Vector {
|
|
||||||
const listType = field.type as List;
|
|
||||||
const childField = listType.children[0];
|
|
||||||
const lists = data.map((datum) => valueAtPath(datum, valuesPath));
|
|
||||||
const flattened: Record<string, unknown>[] = [];
|
|
||||||
const validity: boolean[] = [];
|
|
||||||
const offsets: number[] = [0];
|
|
||||||
|
|
||||||
for (const list of lists) {
|
|
||||||
if (list == null) {
|
|
||||||
validity.push(false);
|
|
||||||
offsets.push(flattened.length);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!Array.isArray(list)) {
|
|
||||||
throw new Error(`expected an array for list field '${field.name}'`);
|
|
||||||
}
|
|
||||||
validity.push(true);
|
|
||||||
for (const element of list) {
|
|
||||||
flattened.push({ [childField.name]: element });
|
|
||||||
}
|
|
||||||
offsets.push(flattened.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
const childVector = transposeData(flattened, childField, []);
|
|
||||||
const nullCount = validity.filter((valid) => !valid).length;
|
|
||||||
return arrowMakeVector(
|
|
||||||
makeData({
|
|
||||||
type: listType,
|
|
||||||
length: lists.length,
|
|
||||||
nullCount,
|
|
||||||
nullBitmap: nullCount > 0 ? arrowUtil.packBools(validity) : undefined,
|
|
||||||
valueOffsets: Int32Array.from(offsets),
|
|
||||||
child: childVector.data[0],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an empty Arrow table with the provided schema
|
* Create an empty Arrow table with the provided schema
|
||||||
*/
|
*/
|
||||||
@@ -700,7 +600,7 @@ function makeVector(
|
|||||||
}
|
}
|
||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
throw Error(
|
throw Error(
|
||||||
"makeVector requires at least one value or the type must be specified",
|
"makeVector requires at least one value or the type must be specfied",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const sampleValue = values.find((val) => val !== null && val !== undefined);
|
const sampleValue = values.find((val) => val !== null && val !== undefined);
|
||||||
@@ -958,7 +858,7 @@ async function applyEmbeddings<T>(
|
|||||||
* customized by the `embeddingDataType` property of the embedding function.
|
* customized by the `embeddingDataType` property of the embedding function.
|
||||||
*
|
*
|
||||||
* If a schema is provided in `makeTableOptions` then it should include the
|
* If a schema is provided in `makeTableOptions` then it should include the
|
||||||
* embedding columns. If no schema is provided then embedding columns will
|
* embedding columns. If no schema is provded then embedding columns will
|
||||||
* be placed at the end of the table, after all of the input columns.
|
* be placed at the end of the table, after all of the input columns.
|
||||||
*/
|
*/
|
||||||
export async function convertToTable(
|
export async function convertToTable(
|
||||||
@@ -1052,7 +952,6 @@ export async function fromTableToBuffer(
|
|||||||
schema = sanitizeSchema(schema);
|
schema = sanitizeSchema(schema);
|
||||||
}
|
}
|
||||||
const tableWithEmbeddings = await applyEmbeddings(table, embeddings, schema);
|
const tableWithEmbeddings = await applyEmbeddings(table, embeddings, schema);
|
||||||
validateBlobSchema(tableWithEmbeddings.schema);
|
|
||||||
const writer = RecordBatchFileWriter.writeAll(tableWithEmbeddings);
|
const writer = RecordBatchFileWriter.writeAll(tableWithEmbeddings);
|
||||||
return Buffer.from(await writer.toUint8Array());
|
return Buffer.from(await writer.toUint8Array());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,236 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
||||||
|
|
||||||
import { Field, LargeBinary, Struct, Utf8 } from "apache-arrow";
|
|
||||||
import { BlobFile as NativeBlobFile } from "./native";
|
|
||||||
|
|
||||||
const BLOB_V2_EXTENSION_NAME = "lance.blob.v2";
|
|
||||||
|
|
||||||
const INLINE_SIZE_THRESHOLD_KEY = "lance-encoding:blob-inline-size-threshold";
|
|
||||||
const DEDICATED_SIZE_THRESHOLD_KEY =
|
|
||||||
"lance-encoding:blob-dedicated-size-threshold";
|
|
||||||
const PACK_FILE_SIZE_THRESHOLD_KEY =
|
|
||||||
"lance-encoding:blob-pack-file-size-threshold";
|
|
||||||
|
|
||||||
export type BlobInput = {
|
|
||||||
data: Buffer | Uint8Array | null;
|
|
||||||
uri: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type BlobOptions = {
|
|
||||||
/** Defaults to true. */
|
|
||||||
nullable?: boolean;
|
|
||||||
/**
|
|
||||||
* Max payload bytes kept inline in the data file. Zero is allowed. Must be a
|
|
||||||
* safe integer.
|
|
||||||
*/
|
|
||||||
inlineSizeThreshold?: number;
|
|
||||||
/**
|
|
||||||
* Max payload bytes stored in a packed sidecar before a dedicated file. Must
|
|
||||||
* be a positive safe integer.
|
|
||||||
*/
|
|
||||||
dedicatedSizeThreshold?: number;
|
|
||||||
/**
|
|
||||||
* Max bytes in one packed sidecar before starting another. Must be a positive
|
|
||||||
* safe integer.
|
|
||||||
*/
|
|
||||||
packFileSizeThreshold?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Declares a `lance.blob.v2` column.
|
|
||||||
*
|
|
||||||
* Query results are descriptors, not payload bytes. Use {@link Table.fetchBlobs}
|
|
||||||
* or {@link Table.fetchBlobFiles} to read bytes.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* ```ts
|
|
||||||
* import { readFile } from "node:fs/promises";
|
|
||||||
* import { Field, Int64, Schema } from "apache-arrow";
|
|
||||||
* import { blob, connect } from "@lancedb/lancedb";
|
|
||||||
*
|
|
||||||
* const db = await connect("./data");
|
|
||||||
* const video = await readFile("clip.mp4");
|
|
||||||
* const table = await db.createTable(
|
|
||||||
* "videos",
|
|
||||||
* [{ id: 1n, video }],
|
|
||||||
* {
|
|
||||||
* schema: new Schema([
|
|
||||||
* new Field("id", new Int64()),
|
|
||||||
* blob("video"),
|
|
||||||
* ]),
|
|
||||||
* },
|
|
||||||
* );
|
|
||||||
*
|
|
||||||
* const rows = await table.query().select(["id"]).withRowId().toArray();
|
|
||||||
* const rowIds = rows.map((row) => row._rowid as bigint);
|
|
||||||
* const bytes = await table.fetchBlobs("video", rowIds);
|
|
||||||
*
|
|
||||||
* const [handle] = await table.fetchBlobFiles("video", rowIds);
|
|
||||||
* const size = handle!.size();
|
|
||||||
* const header = await handle!.readRange(0n, size < 65536n ? size : 65536n);
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
export function blob(name: string, options: BlobOptions = {}): Field {
|
|
||||||
const metadata = new Map<string, string>([
|
|
||||||
["ARROW:extension:name", BLOB_V2_EXTENSION_NAME],
|
|
||||||
]);
|
|
||||||
setThreshold(
|
|
||||||
metadata,
|
|
||||||
INLINE_SIZE_THRESHOLD_KEY,
|
|
||||||
"inlineSizeThreshold",
|
|
||||||
options.inlineSizeThreshold,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
setThreshold(
|
|
||||||
metadata,
|
|
||||||
DEDICATED_SIZE_THRESHOLD_KEY,
|
|
||||||
"dedicatedSizeThreshold",
|
|
||||||
options.dedicatedSizeThreshold,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
setThreshold(
|
|
||||||
metadata,
|
|
||||||
PACK_FILE_SIZE_THRESHOLD_KEY,
|
|
||||||
"packFileSizeThreshold",
|
|
||||||
options.packFileSizeThreshold,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
return new Field(
|
|
||||||
name,
|
|
||||||
new Struct([
|
|
||||||
new Field("data", new LargeBinary(), true),
|
|
||||||
new Field("uri", new Utf8(), true),
|
|
||||||
]),
|
|
||||||
options.nullable ?? true,
|
|
||||||
metadata,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for the `lance.blob.v2` extension marker. Does not validate the
|
|
||||||
* field's storage type.
|
|
||||||
*/
|
|
||||||
export function isBlobField(field: Field): boolean {
|
|
||||||
return field.metadata?.get("ARROW:extension:name") === BLOB_V2_EXTENSION_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A lazy handle to blob bytes. Create one with {@link Table.fetchBlobFiles}.
|
|
||||||
*
|
|
||||||
* @hideconstructor
|
|
||||||
*/
|
|
||||||
export class BlobFile {
|
|
||||||
private readonly inner: NativeBlobFile;
|
|
||||||
|
|
||||||
private constructor(inner: NativeBlobFile) {
|
|
||||||
if (!(inner instanceof NativeBlobFile)) {
|
|
||||||
throw new Error("BlobFile handles come from Table.fetchBlobFiles");
|
|
||||||
}
|
|
||||||
this.inner = inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @ignore */
|
|
||||||
static fromNative(inner: NativeBlobFile): BlobFile {
|
|
||||||
return new BlobFile(inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the blob size in bytes. */
|
|
||||||
size(): bigint {
|
|
||||||
return this.inner.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads from the cursor to the end and advances the cursor.
|
|
||||||
*
|
|
||||||
* A second call returns an empty buffer. {@link BlobFile.readRange} does
|
|
||||||
* not move the cursor.
|
|
||||||
*/
|
|
||||||
read(): Promise<Buffer> {
|
|
||||||
return this.inner.read();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the half-open byte range `[start, end)`.
|
|
||||||
*
|
|
||||||
* Fails when `end` is past the blob size. Does not move the cursor.
|
|
||||||
*/
|
|
||||||
readRange(start: bigint, end: bigint): Promise<Buffer> {
|
|
||||||
return this.inner.readRange(start, end);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function coerceBlobValue(value: unknown): BlobInput | null {
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (isBlobBytes(value)) {
|
|
||||||
return { data: value, uri: null };
|
|
||||||
}
|
|
||||||
if (ArrayBuffer.isView(value)) {
|
|
||||||
throw new Error("Blob data must be Buffer or Uint8Array");
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
if (value === "") {
|
|
||||||
throw new Error("Blob uri cannot be empty");
|
|
||||||
}
|
|
||||||
return { data: null, uri: value };
|
|
||||||
}
|
|
||||||
if (typeof value === "object") {
|
|
||||||
const record = value as Record<string, unknown>;
|
|
||||||
if (!("data" in record) && !("uri" in record)) {
|
|
||||||
throw new Error(
|
|
||||||
"Blob struct values must include a 'data' or 'uri' field",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const uri = record.uri;
|
|
||||||
if (uri === "") {
|
|
||||||
throw new Error("Blob uri cannot be empty");
|
|
||||||
}
|
|
||||||
if (uri != null && typeof uri !== "string") {
|
|
||||||
throw new Error(`Blob uri must be a string or null, got ${typeof uri}`);
|
|
||||||
}
|
|
||||||
const data = record.data;
|
|
||||||
if (data != null && !isBlobBytes(data)) {
|
|
||||||
throw new Error("Blob data must be Buffer, Uint8Array, or null");
|
|
||||||
}
|
|
||||||
const bytes = (data as Buffer | Uint8Array | null | undefined) ?? null;
|
|
||||||
const uriValue = uri ?? null;
|
|
||||||
if ((bytes == null) === (uriValue == null)) {
|
|
||||||
throw new Error(
|
|
||||||
"Blob struct values must set exactly one of 'data' or 'uri'",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return { data: bytes, uri: uriValue };
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
"Blob column values must be Buffer, Uint8Array, a URI string, null, or { data?, uri? }",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isBlobBytes(value: unknown): value is Buffer | Uint8Array {
|
|
||||||
return Buffer.isBuffer(value) || value instanceof Uint8Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setThreshold(
|
|
||||||
metadata: Map<string, string>,
|
|
||||||
key: string,
|
|
||||||
optionName: string,
|
|
||||||
value: number | undefined,
|
|
||||||
minimum: number,
|
|
||||||
): void {
|
|
||||||
if (value === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!Number.isSafeInteger(value)) {
|
|
||||||
throw new Error(`${optionName} must be a safe integer`);
|
|
||||||
}
|
|
||||||
if (value < minimum) {
|
|
||||||
throw new Error(
|
|
||||||
minimum <= 0
|
|
||||||
? `${optionName} must be non-negative`
|
|
||||||
: `${optionName} must be positive`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
metadata.set(key, String(value));
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||||
|
|
||||||
|
import { tableFromIPC } from "apache-arrow";
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
SchemaLike,
|
SchemaLike,
|
||||||
@@ -15,7 +16,6 @@ import {
|
|||||||
makeEmptyTable,
|
makeEmptyTable,
|
||||||
} from "./arrow";
|
} from "./arrow";
|
||||||
import { EmbeddingFunctionConfig, getRegistry } from "./embedding/registry";
|
import { EmbeddingFunctionConfig, getRegistry } from "./embedding/registry";
|
||||||
import { Job } from "./job";
|
|
||||||
import {
|
import {
|
||||||
MaterializedView,
|
MaterializedView,
|
||||||
MaterializedViewSelect,
|
MaterializedViewSelect,
|
||||||
@@ -27,6 +27,8 @@ import type {
|
|||||||
CreateNamespaceResponse,
|
CreateNamespaceResponse,
|
||||||
DescribeNamespaceResponse,
|
DescribeNamespaceResponse,
|
||||||
DropNamespaceResponse,
|
DropNamespaceResponse,
|
||||||
|
Job,
|
||||||
|
JobDescription,
|
||||||
JobInfo,
|
JobInfo,
|
||||||
ListNamespacesResponse,
|
ListNamespacesResponse,
|
||||||
ListTablesResponse,
|
ListTablesResponse,
|
||||||
@@ -555,19 +557,24 @@ export abstract class Connection {
|
|||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a server-side job by id, returning a handle with its record already
|
* A {@link Job} handle for a server-side job by id.
|
||||||
* populated. Rejects when the server has no such job, the way
|
|
||||||
* {@link Connection.openTable} does for a missing table.
|
|
||||||
*
|
*
|
||||||
* The returned {@link Job} answers for its own state, specification,
|
* The handle is constructed without a server round trip; an unknown id
|
||||||
* result, failure and event history, so there is no separate
|
* surfaces when the handle is used. Dropping the handle has no effect on
|
||||||
* connection-level call for any of them.
|
* the job itself.
|
||||||
*/
|
*/
|
||||||
abstract openJob(jobId: string): Promise<Job>;
|
abstract job(jobId: string): Job;
|
||||||
|
|
||||||
/** List server-side jobs across the database's tables. */
|
/** List server-side jobs across the database's tables. */
|
||||||
abstract listJobs(): Promise<JobInfo[]>;
|
abstract listJobs(): Promise<JobInfo[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describe a single server-side job by id.
|
||||||
|
*
|
||||||
|
* Resolves to `null` when the server has no such job.
|
||||||
|
*/
|
||||||
|
abstract getJob(jobId: string): Promise<JobDescription | null>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request cancellation of a server-side job by id.
|
* Request cancellation of a server-side job by id.
|
||||||
*
|
*
|
||||||
@@ -575,6 +582,13 @@ export abstract class Connection {
|
|||||||
* such job exists. Cancelling an already-terminal job is a no-op success.
|
* such job exists. Cancelling an already-terminal job is a no-op success.
|
||||||
*/
|
*/
|
||||||
abstract cancelJob(jobId: string): Promise<boolean>;
|
abstract cancelJob(jobId: string): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lifecycle event history of a server-side job, as an Arrow table.
|
||||||
|
*
|
||||||
|
* Lists history across all jobs when `jobId` is omitted.
|
||||||
|
*/
|
||||||
|
abstract jobHistory(jobId?: string): Promise<ArrowTable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @hideconstructor */
|
/** @hideconstructor */
|
||||||
@@ -855,7 +869,7 @@ export class LocalConnection extends Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async dropTableAsync(name: string, namespacePath?: string[]): Promise<Job> {
|
async dropTableAsync(name: string, namespacePath?: string[]): Promise<Job> {
|
||||||
return new Job(await this.inner.dropTableAsync(name, namespacePath ?? []));
|
return this.inner.dropTableAsync(name, namespacePath ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
async dropAllTables(namespacePath?: string[]): Promise<void> {
|
async dropAllTables(namespacePath?: string[]): Promise<void> {
|
||||||
@@ -914,17 +928,29 @@ export class LocalConnection extends Connection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async openJob(jobId: string): Promise<Job> {
|
job(jobId: string): Job {
|
||||||
return new Job(await this.inner.openJob(jobId));
|
return this.inner.job(jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listJobs(): Promise<JobInfo[]> {
|
async listJobs(): Promise<JobInfo[]> {
|
||||||
return this.inner.listJobs();
|
return this.inner.listJobs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getJob(jobId: string): Promise<JobDescription | null> {
|
||||||
|
return this.inner.getJob(jobId);
|
||||||
|
}
|
||||||
|
|
||||||
async cancelJob(jobId: string): Promise<boolean> {
|
async cancelJob(jobId: string): Promise<boolean> {
|
||||||
return this.inner.cancelJob(jobId);
|
return this.inner.cancelJob(jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async jobHistory(jobId?: string): Promise<ArrowTable> {
|
||||||
|
const buf = await this.inner.jobHistory(jobId);
|
||||||
|
if (buf.length === 0) {
|
||||||
|
return new ArrowTable();
|
||||||
|
}
|
||||||
|
return tableFromIPC(buf);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -77,9 +77,6 @@ export {
|
|||||||
VectorColumnOptions,
|
VectorColumnOptions,
|
||||||
} from "./arrow";
|
} from "./arrow";
|
||||||
|
|
||||||
export { blob, isBlobField, BlobFile } from "./blob";
|
|
||||||
export type { BlobOptions } from "./blob";
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Connection,
|
Connection,
|
||||||
CreateTableOptions,
|
CreateTableOptions,
|
||||||
@@ -97,9 +94,13 @@ export {
|
|||||||
RenameTableOptions,
|
RenameTableOptions,
|
||||||
} from "./connection";
|
} from "./connection";
|
||||||
|
|
||||||
export { JobFailureInfo, JobInfo, Session } from "./native.js";
|
export {
|
||||||
|
Job,
|
||||||
export { Job, JobEventsOptions } from "./job";
|
JobDescription,
|
||||||
|
JobFailureInfo,
|
||||||
|
JobInfo,
|
||||||
|
Session,
|
||||||
|
} from "./native.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
AutoQuery,
|
AutoQuery,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface IvfPqOptions {
|
|||||||
* This value controls how much the vector is compressed during the quantization step.
|
* This value controls how much the vector is compressed during the quantization step.
|
||||||
* The more sub vectors there are the less the vector is compressed. The default is
|
* The more sub vectors there are the less the vector is compressed. The default is
|
||||||
* the dimension of the vector divided by 16. If the dimension is not evenly divisible
|
* the dimension of the vector divided by 16. If the dimension is not evenly divisible
|
||||||
* by 16 we use the dimension divided by 8.
|
* by 16 we use the dimension divded by 8.
|
||||||
*
|
*
|
||||||
* The above two cases are highly preferred. Having 8 or 16 values per subvector allows
|
* The above two cases are highly preferred. Having 8 or 16 values per subvector allows
|
||||||
* us to use efficient SIMD instructions.
|
* us to use efficient SIMD instructions.
|
||||||
@@ -228,7 +228,7 @@ export interface HnswPqOptions {
|
|||||||
* This value controls how much the vector is compressed during the quantization step.
|
* This value controls how much the vector is compressed during the quantization step.
|
||||||
* The more sub vectors there are the less the vector is compressed. The default is
|
* The more sub vectors there are the less the vector is compressed. The default is
|
||||||
* the dimension of the vector divided by 16. If the dimension is not evenly divisible
|
* the dimension of the vector divided by 16. If the dimension is not evenly divisible
|
||||||
* by 16 we use the dimension divided by 8.
|
* by 16 we use the dimension divded by 8.
|
||||||
*
|
*
|
||||||
* The above two cases are highly preferred. Having 8 or 16 values per subvector allows
|
* The above two cases are highly preferred. Having 8 or 16 values per subvector allows
|
||||||
* us to use efficient SIMD instructions.
|
* us to use efficient SIMD instructions.
|
||||||
@@ -825,7 +825,7 @@ export interface IndexOptions {
|
|||||||
/**
|
/**
|
||||||
* Advanced index configuration
|
* Advanced index configuration
|
||||||
*
|
*
|
||||||
* This option allows you to specify a specific index to create and also
|
* This option allows you to specify a specfic index to create and also
|
||||||
* allows you to pass in configuration for training the index.
|
* allows you to pass in configuration for training the index.
|
||||||
*
|
*
|
||||||
* See the static methods on Index for details on the various index types.
|
* See the static methods on Index for details on the various index types.
|
||||||
@@ -841,7 +841,7 @@ export interface IndexOptions {
|
|||||||
* and the same name, then an error will be returned. This is true even if
|
* and the same name, then an error will be returned. This is true even if
|
||||||
* that index is out of date.
|
* that index is out of date.
|
||||||
*
|
*
|
||||||
* The default is true
|
* The default is false
|
||||||
*/
|
*/
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
||||||
|
|
||||||
import { Table as ArrowTable, tableFromIPC } from "apache-arrow";
|
|
||||||
import { JobFailureInfo, Job as NativeJob } from "./native";
|
|
||||||
|
|
||||||
/** Which of a job's events {@link Job.events} returns. */
|
|
||||||
export interface JobEventsOptions {
|
|
||||||
/** Maximum event rows to return, up to the server maximum of 10,000. */
|
|
||||||
limit?: number;
|
|
||||||
/** SQL-like filter over the event columns. */
|
|
||||||
filter?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A handle to an operation that may still be running.
|
|
||||||
*
|
|
||||||
* The operation may already be complete when the handle is created.
|
|
||||||
*
|
|
||||||
* The detail getters read what the handle last observed. Submitting an
|
|
||||||
* operation returns only a job id, so populating them eagerly would cost an
|
|
||||||
* extra round trip on every call:
|
|
||||||
*
|
|
||||||
* - {@link Job.refresh} and {@link Job.status} fetch the whole record.
|
|
||||||
* - {@link Job.wait} records the terminal state it establishes, but not the
|
|
||||||
* rest of the record.
|
|
||||||
* - Everything is null until one of those runs.
|
|
||||||
*
|
|
||||||
* @hideconstructor
|
|
||||||
*/
|
|
||||||
export class Job {
|
|
||||||
private readonly inner: NativeJob;
|
|
||||||
|
|
||||||
constructor(inner: NativeJob) {
|
|
||||||
this.inner = inner;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Identifies the operation on the server that is running it.
|
|
||||||
*
|
|
||||||
* Operations that run in this process have no server id. The value is
|
|
||||||
* opaque: parsing it or storing it to resume the job later is not supported.
|
|
||||||
*/
|
|
||||||
get id(): string | null {
|
|
||||||
return this.inner.id ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The last observed lifecycle state, without contacting the backend. */
|
|
||||||
get state(): string | null {
|
|
||||||
return this.inner.state ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The job's type, as the server names it. Null for an in-process job, which
|
|
||||||
* has no server-side record.
|
|
||||||
*/
|
|
||||||
get jobType(): string | null {
|
|
||||||
return this.inner.jobType ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** When the job was created, in milliseconds since the epoch. */
|
|
||||||
get creationMs(): number | null {
|
|
||||||
return this.inner.creationMs ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The job-type-specific specification it was submitted with. */
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: shape varies by job type
|
|
||||||
get spec(): any | null {
|
|
||||||
return parseJson(this.inner.specJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The job-type-specific terminal result. Null until the job succeeds, so a
|
|
||||||
* job that never terminates reports its progress through {@link Job.events}
|
|
||||||
* instead.
|
|
||||||
*/
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: shape varies by job type
|
|
||||||
get result(): any | null {
|
|
||||||
return parseJson(this.inner.resultJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Why the job failed, when it failed and the server reports a reason. */
|
|
||||||
get failure(): JobFailureInfo | null {
|
|
||||||
return this.inner.failure ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The operation's current lifecycle state: "running", "finished", "failed",
|
|
||||||
* or "cancelled".
|
|
||||||
*
|
|
||||||
* A point snapshot; unlike {@link Job.wait} it does not block or reject on a
|
|
||||||
* terminal failure state. Also refreshes the getters above.
|
|
||||||
*/
|
|
||||||
async status(): Promise<string> {
|
|
||||||
return this.inner.status();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Wait until the operation reaches a terminal state. */
|
|
||||||
async wait(): Promise<void> {
|
|
||||||
return this.inner.wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Request cancellation. Cancelling a finished operation is a no-op. */
|
|
||||||
async cancel(): Promise<void> {
|
|
||||||
return this.inner.cancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ask the backend for this job's current state, and for a server-side job
|
|
||||||
* its full record, then cache it for the getters above.
|
|
||||||
*/
|
|
||||||
async refresh(): Promise<void> {
|
|
||||||
return this.inner.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This job's recorded lifecycle events.
|
|
||||||
*
|
|
||||||
* Where the getters above report a terminal result only once the job reaches
|
|
||||||
* one, events are written as the job runs and outlive the workers that
|
|
||||||
* produced them. A distributed job records a `claim`/`claim_complete` pair
|
|
||||||
* per unit of work, each carrying `rows_processed`, so a job that never
|
|
||||||
* finishes still accounts for what it did.
|
|
||||||
*
|
|
||||||
* The server caps results at 1000 rows by default and 10,000 at most, and
|
|
||||||
* truncates without saying so, so pass `limit` for a job that emits an event
|
|
||||||
* per fragment. `filter` is a SQL-like expression over the `state`,
|
|
||||||
* `updated_by`, `emitted_from`, `emitted_by`, and `claim_entity` columns.
|
|
||||||
*/
|
|
||||||
async events(options?: JobEventsOptions): Promise<ArrowTable> {
|
|
||||||
const buf = await this.inner.events(options?.limit, options?.filter);
|
|
||||||
if (buf.length === 0) {
|
|
||||||
return new ArrowTable();
|
|
||||||
}
|
|
||||||
return tableFromIPC(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Every field the handle currently knows, one per line, with the JSON
|
|
||||||
* payloads indented -- a refresh job's spec and result are the point of
|
|
||||||
* printing it.
|
|
||||||
*/
|
|
||||||
toString(): string {
|
|
||||||
if (this.state === null) {
|
|
||||||
const known = this.id === null ? "" : `id=${JSON.stringify(this.id)}, `;
|
|
||||||
return `Job(${known}not refreshed)`;
|
|
||||||
}
|
|
||||||
const fields: string[] = [];
|
|
||||||
if (this.id !== null) {
|
|
||||||
fields.push(`id=${JSON.stringify(this.id)}`);
|
|
||||||
}
|
|
||||||
fields.push(`state=${JSON.stringify(this.state)}`);
|
|
||||||
if (this.jobType !== null) {
|
|
||||||
fields.push(`jobType=${JSON.stringify(this.jobType)}`);
|
|
||||||
}
|
|
||||||
if (this.creationMs !== null) {
|
|
||||||
fields.push(`creationMs=${this.creationMs}`);
|
|
||||||
}
|
|
||||||
for (const [name, value] of [
|
|
||||||
["spec", this.spec],
|
|
||||||
["result", this.result],
|
|
||||||
] as const) {
|
|
||||||
if (value !== null) {
|
|
||||||
fields.push(`${name}=${indentJson(value)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.failure !== null) {
|
|
||||||
fields.push(`failure=${indentJson(this.failure)}`);
|
|
||||||
}
|
|
||||||
return `Job(${fields.map((field) => `\n${REPR_INDENT}${field},`).join("")}\n)`;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Symbol.for("nodejs.util.inspect.custom")](): string {
|
|
||||||
return this.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const REPR_INDENT = " ";
|
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: shape varies by job type
|
|
||||||
function indentJson(value: any): string {
|
|
||||||
return JSON.stringify(value, null, 4).replace(/\n/g, `\n${REPR_INDENT}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: shape varies by job type
|
|
||||||
function parseJson(raw: string | null | undefined): any | null {
|
|
||||||
return raw === null || raw === undefined ? null : JSON.parse(raw);
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ export class MergeInsertBuilder {
|
|||||||
* but that behavior is subject to change.
|
* but that behavior is subject to change.
|
||||||
*
|
*
|
||||||
* An optional condition may be specified. If it is, then only
|
* An optional condition may be specified. If it is, then only
|
||||||
* matched rows that satisfy the condition will be updated. Any
|
* matched rows that satisfy the condtion will be updated. Any
|
||||||
* rows that do not satisfy the condition will be left as they
|
* rows that do not satisfy the condition will be left as they
|
||||||
* are. Failing to satisfy the condition does not cause a
|
* are. Failing to satisfy the condition does not cause a
|
||||||
* "matched row" to become a "not matched" row.
|
* "matched row" to become a "not matched" row.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
// The utilities in this file help sanitize data from the user's arrow
|
// The utilities in this file help sanitize data from the user's arrow
|
||||||
// library into the types expected by vectordb's arrow library. Node
|
// library into the types expected by vectordb's arrow library. Node
|
||||||
// generally allows for multiple versions of the same library (and sometimes
|
// generally allows for mulitple versions of the same library (and sometimes
|
||||||
// even multiple copies of the same version) to be installed at the same
|
// even multiple copies of the same version) to be installed at the same
|
||||||
// time. However, arrow-js uses instanceof which expected that the input
|
// time. However, arrow-js uses instanceof which expected that the input
|
||||||
// comes from the exact same library instance. This is not always the case
|
// comes from the exact same library instance. This is not always the case
|
||||||
|
|||||||
+26
-87
@@ -17,10 +17,8 @@ import {
|
|||||||
tableFromIPC,
|
tableFromIPC,
|
||||||
} from "./arrow";
|
} from "./arrow";
|
||||||
|
|
||||||
import { BlobFile } from "./blob";
|
|
||||||
import { EmbeddingFunctionConfig, getRegistry } from "./embedding/registry";
|
import { EmbeddingFunctionConfig, getRegistry } from "./embedding/registry";
|
||||||
import { IndexOptions } from "./indices";
|
import { IndexOptions } from "./indices";
|
||||||
import { Job } from "./job";
|
|
||||||
import { MergeInsertBuilder } from "./merge";
|
import { MergeInsertBuilder } from "./merge";
|
||||||
import {
|
import {
|
||||||
AddColumnsResult,
|
AddColumnsResult,
|
||||||
@@ -32,6 +30,7 @@ import {
|
|||||||
DropColumnsResult,
|
DropColumnsResult,
|
||||||
IndexConfig,
|
IndexConfig,
|
||||||
IndexStatistics,
|
IndexStatistics,
|
||||||
|
Job,
|
||||||
LsmStats,
|
LsmStats,
|
||||||
Branches as NativeBranches,
|
Branches as NativeBranches,
|
||||||
OptimizeStats,
|
OptimizeStats,
|
||||||
@@ -314,7 +313,7 @@ export abstract class Table {
|
|||||||
* Note: if your condition is something like "some_id_column == 7" and
|
* Note: if your condition is something like "some_id_column == 7" and
|
||||||
* you are updating many rows (with different ids) then you will get
|
* you are updating many rows (with different ids) then you will get
|
||||||
* better performance with a single [`merge_insert`] call instead of
|
* better performance with a single [`merge_insert`] call instead of
|
||||||
* repeatedly calling this method.
|
* repeatedly calilng this method.
|
||||||
* @param {Map<string, string> | Record<string, string>} updates - the
|
* @param {Map<string, string> | Record<string, string>} updates - the
|
||||||
* columns to update
|
* columns to update
|
||||||
* @returns {Promise<UpdateResult>} A promise that resolves to an object
|
* @returns {Promise<UpdateResult>} A promise that resolves to an object
|
||||||
@@ -511,35 +510,6 @@ export abstract class Table {
|
|||||||
*/
|
*/
|
||||||
abstract takeRowIds(rowIds: readonly (bigint | number)[]): TakeQuery;
|
abstract takeRowIds(rowIds: readonly (bigint | number)[]): TakeQuery;
|
||||||
|
|
||||||
/**
|
|
||||||
* Blob v2 columns, including nested dotted paths.
|
|
||||||
*/
|
|
||||||
abstract blobColumns(): Promise<string[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bytes for `column` at row IDs from {@link Query.withRowId}.
|
|
||||||
*
|
|
||||||
* Reads the table's current checkout. IDs from another version can fail after
|
|
||||||
* compaction unless stable row ids are enabled. Results keep input order and
|
|
||||||
* duplicates. Null blobs are `null`. Empty blobs are empty buffers.
|
|
||||||
*/
|
|
||||||
abstract fetchBlobs(
|
|
||||||
column: string,
|
|
||||||
rowIds: readonly (bigint | number)[],
|
|
||||||
): Promise<(Buffer | null)[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens lazy blob handles for `column` at the given row IDs using the
|
|
||||||
* table's current checkout.
|
|
||||||
*
|
|
||||||
* Preserves input order, duplicates, and nulls. Use this for large payloads.
|
|
||||||
* See {@link Table.fetchBlobs} for row-ID validity across versions.
|
|
||||||
*/
|
|
||||||
abstract fetchBlobFiles(
|
|
||||||
column: string,
|
|
||||||
rowIds: readonly (bigint | number)[],
|
|
||||||
): Promise<(BlobFile | null)[]>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a search query to find the nearest neighbors
|
* Create a search query to find the nearest neighbors
|
||||||
* of the given query
|
* of the given query
|
||||||
@@ -1154,15 +1124,13 @@ export class LocalTable extends Table {
|
|||||||
): Promise<Job> {
|
): Promise<Job> {
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: skip
|
// biome-ignore lint/suspicious/noExplicitAny: skip
|
||||||
const nativeIndex = (options?.config as any)?.inner;
|
const nativeIndex = (options?.config as any)?.inner;
|
||||||
return new Job(
|
return await this.inner.createIndexAsync(
|
||||||
await this.inner.createIndexAsync(
|
nativeIndex,
|
||||||
nativeIndex,
|
column,
|
||||||
column,
|
options?.replace,
|
||||||
options?.replace,
|
options?.waitTimeoutSeconds,
|
||||||
options?.waitTimeoutSeconds,
|
options?.name,
|
||||||
options?.name,
|
options?.train,
|
||||||
options?.train,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1190,34 +1158,23 @@ export class LocalTable extends Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
takeRowIds(rowIds: readonly (bigint | number)[]): TakeQuery {
|
takeRowIds(rowIds: readonly (bigint | number)[]): TakeQuery {
|
||||||
return new TakeQuery(this.inner.takeRowIds(rowIdsToBigInts(rowIds)));
|
const ids = rowIds.map((id) => {
|
||||||
}
|
if (typeof id === "bigint") {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
if (!Number.isInteger(id)) {
|
||||||
|
throw new Error("Row id must be an integer (or bigint)");
|
||||||
|
}
|
||||||
|
if (id < 0) {
|
||||||
|
throw new Error("Row id cannot be negative");
|
||||||
|
}
|
||||||
|
if (!Number.isSafeInteger(id)) {
|
||||||
|
throw new Error("Row id is too large for number; use bigint instead");
|
||||||
|
}
|
||||||
|
return BigInt(id);
|
||||||
|
});
|
||||||
|
|
||||||
blobColumns(): Promise<string[]> {
|
return new TakeQuery(this.inner.takeRowIds(ids));
|
||||||
return this.inner.blobColumns();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchBlobs(
|
|
||||||
column: string,
|
|
||||||
rowIds: readonly (bigint | number)[],
|
|
||||||
): Promise<(Buffer | null)[]> {
|
|
||||||
const values = await this.inner.fetchBlobs(column, rowIdsToBigInts(rowIds));
|
|
||||||
// N-API Option maps missing values to undefined. Collapse those to null.
|
|
||||||
return values.map((value) => value ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchBlobFiles(
|
|
||||||
column: string,
|
|
||||||
rowIds: readonly (bigint | number)[],
|
|
||||||
): Promise<(BlobFile | null)[]> {
|
|
||||||
const files = await this.inner.fetchBlobFiles(
|
|
||||||
column,
|
|
||||||
rowIdsToBigInts(rowIds),
|
|
||||||
);
|
|
||||||
// N-API Option maps missing values to undefined. Collapse those to null.
|
|
||||||
return files.map((file) =>
|
|
||||||
file == null ? null : BlobFile.fromNative(file),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
query(): Query {
|
query(): Query {
|
||||||
@@ -1356,7 +1313,7 @@ export class LocalTable extends Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshColumnAsync(column: string): Promise<Job> {
|
async refreshColumnAsync(column: string): Promise<Job> {
|
||||||
return new Job(await this.inner.refreshColumnAsync(column));
|
return await this.inner.refreshColumnAsync(column);
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshMaterializedView(
|
async refreshMaterializedView(
|
||||||
@@ -1774,21 +1731,3 @@ export class Branches {
|
|||||||
)) as unknown as CherryPickResult;
|
)) as unknown as CherryPickResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowIdsToBigInts(rowIds: readonly (bigint | number)[]): bigint[] {
|
|
||||||
return rowIds.map((id) => {
|
|
||||||
if (typeof id === "bigint") {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(id)) {
|
|
||||||
throw new Error("Row id must be an integer (or bigint)");
|
|
||||||
}
|
|
||||||
if (id < 0) {
|
|
||||||
throw new Error("Row id cannot be negative");
|
|
||||||
}
|
|
||||||
if (!Number.isSafeInteger(id)) {
|
|
||||||
throw new Error("Row id is too large for number; use bigint instead");
|
|
||||||
}
|
|
||||||
return BigInt(id);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-darwin-arm64",
|
"name": "@lancedb/lancedb-darwin-arm64",
|
||||||
"version": "0.39.0-beta.6",
|
"version": "0.39.0-beta.1",
|
||||||
"os": ["darwin"],
|
"os": ["darwin"],
|
||||||
"cpu": ["arm64"],
|
"cpu": ["arm64"],
|
||||||
"main": "lancedb.darwin-arm64.node",
|
"main": "lancedb.darwin-arm64.node",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-linux-arm64-gnu",
|
"name": "@lancedb/lancedb-linux-arm64-gnu",
|
||||||
"version": "0.39.0-beta.6",
|
"version": "0.39.0-beta.1",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["arm64"],
|
"cpu": ["arm64"],
|
||||||
"main": "lancedb.linux-arm64-gnu.node",
|
"main": "lancedb.linux-arm64-gnu.node",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-linux-arm64-musl",
|
"name": "@lancedb/lancedb-linux-arm64-musl",
|
||||||
"version": "0.39.0-beta.6",
|
"version": "0.39.0-beta.1",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["arm64"],
|
"cpu": ["arm64"],
|
||||||
"main": "lancedb.linux-arm64-musl.node",
|
"main": "lancedb.linux-arm64-musl.node",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-linux-x64-gnu",
|
"name": "@lancedb/lancedb-linux-x64-gnu",
|
||||||
"version": "0.39.0-beta.6",
|
"version": "0.39.0-beta.1",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["x64"],
|
"cpu": ["x64"],
|
||||||
"main": "lancedb.linux-x64-gnu.node",
|
"main": "lancedb.linux-x64-gnu.node",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-linux-x64-musl",
|
"name": "@lancedb/lancedb-linux-x64-musl",
|
||||||
"version": "0.39.0-beta.6",
|
"version": "0.39.0-beta.1",
|
||||||
"os": ["linux"],
|
"os": ["linux"],
|
||||||
"cpu": ["x64"],
|
"cpu": ["x64"],
|
||||||
"main": "lancedb.linux-x64-musl.node",
|
"main": "lancedb.linux-x64-musl.node",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-win32-arm64-msvc",
|
"name": "@lancedb/lancedb-win32-arm64-msvc",
|
||||||
"version": "0.39.0-beta.6",
|
"version": "0.39.0-beta.1",
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-win32-x64-msvc",
|
"name": "@lancedb/lancedb-win32-x64-msvc",
|
||||||
"version": "0.39.0-beta.6",
|
"version": "0.39.0-beta.1",
|
||||||
"os": ["win32"],
|
"os": ["win32"],
|
||||||
"cpu": ["x64"],
|
"cpu": ["x64"],
|
||||||
"main": "lancedb.win32-x64-msvc.node",
|
"main": "lancedb.win32-x64-msvc.node",
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"ann"
|
"ann"
|
||||||
],
|
],
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "0.39.0-beta.6",
|
"version": "0.39.0-beta.1",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
|
|||||||
Generated
+24
-24
@@ -41,7 +41,7 @@ importers:
|
|||||||
version: 3.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@types/node@22.7.4)
|
version: 3.7.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.11.3)(@types/node@22.7.4)
|
||||||
'@opentelemetry/sdk-metrics':
|
'@opentelemetry/sdk-metrics':
|
||||||
specifier: ^2.10.0
|
specifier: ^2.10.0
|
||||||
version: 2.11.0(@opentelemetry/api@1.9.1)
|
version: 2.10.0(@opentelemetry/api@1.9.1)
|
||||||
'@types/axios':
|
'@types/axios':
|
||||||
specifier: ^0.14.0
|
specifier: ^0.14.0
|
||||||
version: 0.14.4
|
version: 0.14.4
|
||||||
@@ -80,7 +80,7 @@ importers:
|
|||||||
version: 0.2.7
|
version: 0.2.7
|
||||||
ts-jest:
|
ts-jest:
|
||||||
specifier: ^29.1.2
|
specifier: ^29.1.2
|
||||||
version: 29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.7.4))(typescript@5.5.4)
|
version: 29.4.9(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.7.4))(typescript@5.5.4)
|
||||||
typedoc:
|
typedoc:
|
||||||
specifier: 0.26.4
|
specifier: 0.26.4
|
||||||
version: 0.26.4(typescript@5.5.4)
|
version: 0.26.4(typescript@5.5.4)
|
||||||
@@ -1394,20 +1394,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
|
resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
|
||||||
'@opentelemetry/core@2.11.0':
|
'@opentelemetry/core@2.10.0':
|
||||||
resolution: {integrity: sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==}
|
resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
'@opentelemetry/api': '>=1.0.0 <1.10.0'
|
||||||
|
|
||||||
'@opentelemetry/resources@2.11.0':
|
'@opentelemetry/resources@2.10.0':
|
||||||
resolution: {integrity: sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==}
|
resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
'@opentelemetry/api': '>=1.3.0 <1.10.0'
|
||||||
|
|
||||||
'@opentelemetry/sdk-metrics@2.11.0':
|
'@opentelemetry/sdk-metrics@2.10.0':
|
||||||
resolution: {integrity: sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==}
|
resolution: {integrity: sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==}
|
||||||
engines: {node: ^18.19.0 || >=20.6.0}
|
engines: {node: ^18.19.0 || >=20.6.0}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
'@opentelemetry/api': '>=1.9.0 <1.10.0'
|
||||||
@@ -1480,7 +1480,6 @@ packages:
|
|||||||
'@smithy/core@3.24.1':
|
'@smithy/core@3.24.1':
|
||||||
resolution: {integrity: sha512-3mT7o4qQyUWttYnVK3A0Z/u3Xha3E81tXn32Tz6vjZiUXhBrkEivpw1hBYfh84iFF9CSzkBU9Y1DJ3Q6RQ231g==}
|
resolution: {integrity: sha512-3mT7o4qQyUWttYnVK3A0Z/u3Xha3E81tXn32Tz6vjZiUXhBrkEivpw1hBYfh84iFF9CSzkBU9Y1DJ3Q6RQ231g==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
deprecated: Deprecated due to bug in browser bundling instructions https://github.com/smithy-lang/smithy-typescript/issues/2025
|
|
||||||
|
|
||||||
'@smithy/credential-provider-imds@4.3.1':
|
'@smithy/credential-provider-imds@4.3.1':
|
||||||
resolution: {integrity: sha512-0S/acwHnqX4WrjXzhdiDRxsG2s9SC0cpPIK9nZ1R6UOHd+j7uL28+4bHu22urbLk2TVw3fkp6na/+fkUt/pLNQ==}
|
resolution: {integrity: sha512-0S/acwHnqX4WrjXzhdiDRxsG2s9SC0cpPIK9nZ1R6UOHd+j7uL28+4bHu22urbLk2TVw3fkp6na/+fkUt/pLNQ==}
|
||||||
@@ -3239,8 +3238,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.2.0'
|
typescript: '>=4.2.0'
|
||||||
|
|
||||||
ts-jest@29.4.12:
|
ts-jest@29.4.9:
|
||||||
resolution: {integrity: sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==}
|
resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -5111,22 +5110,22 @@ snapshots:
|
|||||||
|
|
||||||
'@opentelemetry/api@1.9.1': {}
|
'@opentelemetry/api@1.9.1': {}
|
||||||
|
|
||||||
'@opentelemetry/core@2.11.0(@opentelemetry/api@1.9.1)':
|
'@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
'@opentelemetry/semantic-conventions': 1.43.0
|
'@opentelemetry/semantic-conventions': 1.43.0
|
||||||
|
|
||||||
'@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1)':
|
'@opentelemetry/resources@2.10.0(@opentelemetry/api@1.9.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
'@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1)
|
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
|
||||||
'@opentelemetry/semantic-conventions': 1.43.0
|
'@opentelemetry/semantic-conventions': 1.43.0
|
||||||
|
|
||||||
'@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1)':
|
'@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
'@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1)
|
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
|
||||||
'@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1)
|
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.43.0': {}
|
'@opentelemetry/semantic-conventions@1.43.0': {}
|
||||||
|
|
||||||
@@ -5575,7 +5574,7 @@ snapshots:
|
|||||||
globby: 11.1.0
|
globby: 11.1.0
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
minimatch: 9.0.9
|
minimatch: 9.0.9
|
||||||
semver: 7.8.5
|
semver: 7.8.0
|
||||||
ts-api-utils: 1.4.3(typescript@5.5.4)
|
ts-api-utils: 1.4.3(typescript@5.5.4)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
@@ -6427,7 +6426,7 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.3
|
'@babel/parser': 7.29.3
|
||||||
'@istanbuljs/schema': 0.1.6
|
'@istanbuljs/schema': 0.1.6
|
||||||
istanbul-lib-coverage: 3.2.2
|
istanbul-lib-coverage: 3.2.2
|
||||||
semver: 7.8.5
|
semver: 7.8.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6706,7 +6705,7 @@ snapshots:
|
|||||||
jest-util: 29.7.0
|
jest-util: 29.7.0
|
||||||
natural-compare: 1.4.0
|
natural-compare: 1.4.0
|
||||||
pretty-format: 29.7.0
|
pretty-format: 29.7.0
|
||||||
semver: 7.8.5
|
semver: 7.8.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6829,7 +6828,7 @@ snapshots:
|
|||||||
|
|
||||||
make-dir@4.0.0:
|
make-dir@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.8.5
|
semver: 7.8.0
|
||||||
|
|
||||||
make-error@1.3.6: {}
|
make-error@1.3.6: {}
|
||||||
|
|
||||||
@@ -7163,7 +7162,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@7.8.0: {}
|
semver@7.8.0: {}
|
||||||
|
|
||||||
semver@7.8.5: {}
|
semver@7.8.5:
|
||||||
|
optional: true
|
||||||
|
|
||||||
sharp@0.35.4(@types/node@22.7.4):
|
sharp@0.35.4(@types/node@22.7.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7327,7 +7327,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
|
|
||||||
ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.7.4))(typescript@5.5.4):
|
ts-jest@29.4.9(@babel/core@7.29.7)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.29.7))(jest-util@29.7.0)(jest@29.7.0(@types/node@22.7.4))(typescript@5.5.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
bs-logger: 0.2.6
|
bs-logger: 0.2.6
|
||||||
fast-json-stable-stringify: 2.1.0
|
fast-json-stable-stringify: 2.1.0
|
||||||
@@ -7336,7 +7336,7 @@ snapshots:
|
|||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
lodash.memoize: 4.1.2
|
lodash.memoize: 4.1.2
|
||||||
make-error: 1.3.6
|
make-error: 1.3.6
|
||||||
semver: 7.8.5
|
semver: 7.8.0
|
||||||
type-fest: 4.41.0
|
type-fest: 4.41.0
|
||||||
typescript: 5.5.4
|
typescript: 5.5.4
|
||||||
yargs-parser: 21.1.1
|
yargs-parser: 21.1.1
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
||||||
|
|
||||||
use std::ops::Range;
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use arrow_array::{Array, LargeBinaryArray};
|
|
||||||
use lancedb::blob::BlobFile as LanceBlobFile;
|
|
||||||
use napi::bindgen_prelude::*;
|
|
||||||
use napi_derive::napi;
|
|
||||||
|
|
||||||
use crate::error::convert_error;
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub struct BlobFile {
|
|
||||||
inner: Arc<LanceBlobFile>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BlobFile {
|
|
||||||
pub(crate) fn new(inner: LanceBlobFile) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Arc::new(inner),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
impl BlobFile {
|
|
||||||
#[napi]
|
|
||||||
pub fn size(&self) -> BigInt {
|
|
||||||
BigInt::from(self.inner.size())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub async fn read(&self) -> napi::Result<Buffer> {
|
|
||||||
let bytes = self.inner.read().await.map_err(|err| convert_error(&err))?;
|
|
||||||
Ok(Buffer::from(bytes.as_ref()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi]
|
|
||||||
pub async fn read_range(&self, start: BigInt, end: BigInt) -> napi::Result<Buffer> {
|
|
||||||
let range = bigint_range(start, end)?;
|
|
||||||
let bytes = self
|
|
||||||
.inner
|
|
||||||
.read_range(range)
|
|
||||||
.await
|
|
||||||
.map_err(|err| convert_error(&err))?;
|
|
||||||
Ok(Buffer::from(bytes.as_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bigint_range(start: BigInt, end: BigInt) -> napi::Result<Range<u64>> {
|
|
||||||
let start = parse_u64(start, "start")?;
|
|
||||||
let end = parse_u64(end, "end")?;
|
|
||||||
if start > end {
|
|
||||||
return Err(napi::Error::from_reason(format!(
|
|
||||||
"invalid blob range: start ({start}) > end ({end})"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Ok(start..end)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_u64(value: BigInt, name: &str) -> napi::Result<u64> {
|
|
||||||
let (negative, value, lossless) = value.get_u64();
|
|
||||||
if negative {
|
|
||||||
return Err(napi::Error::from_reason(format!(
|
|
||||||
"{name} cannot be negative"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
if !lossless {
|
|
||||||
return Err(napi::Error::from_reason(format!(
|
|
||||||
"{name} is too large to fit in u64"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn parse_row_ids(row_ids: Vec<BigInt>) -> napi::Result<Vec<u64>> {
|
|
||||||
row_ids
|
|
||||||
.into_iter()
|
|
||||||
.map(|id| parse_u64(id, "row id"))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn copy_blob_buffers(array: LargeBinaryArray) -> Vec<Option<Buffer>> {
|
|
||||||
(0..array.len())
|
|
||||||
.map(|i| {
|
|
||||||
if array.is_null(i) {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(Buffer::from(array.value(i).to_vec()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -442,15 +442,13 @@ impl Connection {
|
|||||||
self.get_inner()?.drop_all_tables(&ns).await.default_error()
|
self.get_inner()?.drop_all_tables(&ns).await.default_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a server-side job by id, returning a handle with its record
|
/// A `Job` handle for a server-side job by id.
|
||||||
/// already populated. Rejects when the server has no such job.
|
|
||||||
///
|
///
|
||||||
/// The returned handle answers for its own state, specification, result,
|
/// The handle is constructed without a server round trip; an unknown id
|
||||||
/// failure and event history, so there is no separate connection-level
|
/// surfaces when the handle is used.
|
||||||
/// call for any of them.
|
#[napi]
|
||||||
#[napi(catch_unwind)]
|
pub fn job(&self, job_id: String) -> napi::Result<crate::job::Job> {
|
||||||
pub async fn open_job(&self, job_id: String) -> napi::Result<crate::job::Job> {
|
let job = self.get_inner()?.job(job_id).default_error()?;
|
||||||
let job = self.get_inner()?.open_job(&job_id).await.default_error()?;
|
|
||||||
Ok(crate::job::Job::new(job))
|
Ok(crate::job::Job::new(job))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -461,6 +459,17 @@ impl Connection {
|
|||||||
Ok(jobs.into_iter().map(Into::into).collect())
|
Ok(jobs.into_iter().map(Into::into).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Describe a single server-side job by id. `null` when the server has
|
||||||
|
/// no such job.
|
||||||
|
#[napi(catch_unwind)]
|
||||||
|
pub async fn get_job(
|
||||||
|
&self,
|
||||||
|
job_id: String,
|
||||||
|
) -> napi::Result<Option<crate::job::JobDescription>> {
|
||||||
|
let description = self.get_inner()?.get_job(&job_id).await.default_error()?;
|
||||||
|
Ok(description.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
/// Request cancellation of a server-side job by id. Returns true if the
|
/// Request cancellation of a server-side job by id. Returns true if the
|
||||||
/// server accepted the cancellation, false if no such job exists.
|
/// server accepted the cancellation, false if no such job exists.
|
||||||
#[napi(catch_unwind)]
|
#[napi(catch_unwind)]
|
||||||
@@ -468,6 +477,34 @@ impl Connection {
|
|||||||
self.get_inner()?.cancel_job(&job_id).await.default_error()
|
self.get_inner()?.cancel_job(&job_id).await.default_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The lifecycle event history of a server-side job (all jobs when
|
||||||
|
/// `job_id` is null), as an Arrow IPC stream buffer. Empty when there is
|
||||||
|
/// no history.
|
||||||
|
#[napi(catch_unwind)]
|
||||||
|
pub async fn job_history(&self, job_id: Option<String>) -> napi::Result<Buffer> {
|
||||||
|
let batches = self
|
||||||
|
.get_inner()?
|
||||||
|
.job_history(job_id.as_deref())
|
||||||
|
.await
|
||||||
|
.default_error()?;
|
||||||
|
let Some(first) = batches.first() else {
|
||||||
|
return Ok(Buffer::from(Vec::<u8>::new()));
|
||||||
|
};
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut writer = arrow_ipc::writer::StreamWriter::try_new(&mut out, &first.schema())
|
||||||
|
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||||||
|
for batch in &batches {
|
||||||
|
writer
|
||||||
|
.write(batch)
|
||||||
|
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||||||
|
}
|
||||||
|
writer
|
||||||
|
.finish()
|
||||||
|
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
||||||
|
drop(writer);
|
||||||
|
Ok(Buffer::from(out))
|
||||||
|
}
|
||||||
|
|
||||||
#[napi(catch_unwind)]
|
#[napi(catch_unwind)]
|
||||||
/// Describe a namespace and return its properties.
|
/// Describe a namespace and return its properties.
|
||||||
pub async fn describe_namespace(
|
pub async fn describe_namespace(
|
||||||
|
|||||||
+34
-90
@@ -3,9 +3,6 @@
|
|||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use arrow_array::RecordBatch;
|
|
||||||
use lancedb::job::JobEventsRequest;
|
|
||||||
use napi::bindgen_prelude::Buffer;
|
|
||||||
use napi_derive::napi;
|
use napi_derive::napi;
|
||||||
|
|
||||||
use crate::error::NapiErrorExt;
|
use crate::error::NapiErrorExt;
|
||||||
@@ -58,98 +55,12 @@ impl Job {
|
|||||||
pub async fn cancel(&self) -> napi::Result<()> {
|
pub async fn cancel(&self) -> napi::Result<()> {
|
||||||
self.inner.cancel().await.default_error()
|
self.inner.cancel().await.default_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ask the backend for this job's current state, and for a server-side job
|
|
||||||
/// its full record, then cache it for the getters below.
|
|
||||||
///
|
|
||||||
/// They are all null until this runs, because submitting an operation
|
|
||||||
/// returns only a job id. {@link Job.status} fetches the whole record too;
|
|
||||||
/// {@link Job.wait} records only the terminal state it establishes.
|
|
||||||
#[napi(catch_unwind)]
|
|
||||||
pub async fn refresh(&self) -> napi::Result<()> {
|
|
||||||
self.inner.refresh().await.default_error()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The last observed lifecycle state, without contacting the backend.
|
|
||||||
#[napi(getter)]
|
|
||||||
pub fn state(&self) -> Option<String> {
|
|
||||||
self.inner.state()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The job's type, as the server names it. Null for an in-process job,
|
|
||||||
/// which has no server-side record.
|
|
||||||
#[napi(getter)]
|
|
||||||
pub fn job_type(&self) -> Option<String> {
|
|
||||||
self.inner.job_type()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When the job was created, in milliseconds since the epoch.
|
|
||||||
#[napi(getter)]
|
|
||||||
pub fn creation_ms(&self) -> Option<i64> {
|
|
||||||
self.inner.creation_ms()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The job-type-specific specification as a JSON string, when present.
|
|
||||||
#[napi(getter)]
|
|
||||||
pub fn spec_json(&self) -> Option<String> {
|
|
||||||
self.inner.spec().map(|spec| spec.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The job-type-specific terminal result as a JSON string. Null until the
|
|
||||||
/// job succeeds, so a job that never terminates reports its progress
|
|
||||||
/// through {@link Job.events} instead.
|
|
||||||
#[napi(getter)]
|
|
||||||
pub fn result_json(&self) -> Option<String> {
|
|
||||||
self.inner.result().map(|result| result.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Why the job failed, when it failed and the server reports a reason.
|
|
||||||
#[napi(getter)]
|
|
||||||
pub fn failure(&self) -> Option<JobFailureInfo> {
|
|
||||||
self.inner.failure().map(|failure| JobFailureInfo {
|
|
||||||
phase: failure.phase,
|
|
||||||
message: failure.message,
|
|
||||||
retryable: failure.retryable,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This job's recorded lifecycle events, as an Arrow IPC stream buffer.
|
|
||||||
/// The TypeScript wrapper turns it into an Arrow table.
|
|
||||||
#[napi(catch_unwind)]
|
|
||||||
pub async fn events(&self, limit: Option<u32>, filter: Option<String>) -> napi::Result<Buffer> {
|
|
||||||
let batches = self
|
|
||||||
.inner
|
|
||||||
.events(JobEventsRequest { limit, filter })
|
|
||||||
.await
|
|
||||||
.default_error()?;
|
|
||||||
batches_to_ipc_buffer(&batches)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialise Arrow batches as a single IPC stream for the TypeScript layer.
|
|
||||||
fn batches_to_ipc_buffer(batches: &[RecordBatch]) -> napi::Result<Buffer> {
|
|
||||||
let Some(first) = batches.first() else {
|
|
||||||
return Ok(Buffer::from(Vec::<u8>::new()));
|
|
||||||
};
|
|
||||||
let mut out = Vec::new();
|
|
||||||
let mut writer = arrow_ipc::writer::StreamWriter::try_new(&mut out, &first.schema())
|
|
||||||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
|
||||||
for batch in batches {
|
|
||||||
writer
|
|
||||||
.write(batch)
|
|
||||||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
|
||||||
}
|
|
||||||
writer
|
|
||||||
.finish()
|
|
||||||
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
|
|
||||||
drop(writer);
|
|
||||||
Ok(Buffer::from(out))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A row from `Connection.listJobs`: one server-side job.
|
/// A row from `Connection.listJobs`: one server-side job.
|
||||||
#[napi(object)]
|
#[napi(object)]
|
||||||
pub struct JobInfo {
|
pub struct JobInfo {
|
||||||
/// The job id -- what `Connection.openJob` and `Connection.cancelJob`
|
/// The job id -- what `Connection.getJob` and `Connection.cancelJob`
|
||||||
/// accept.
|
/// accept.
|
||||||
pub job_id: String,
|
pub job_id: String,
|
||||||
/// The table the job runs against, without URI or namespace.
|
/// The table the job runs against, without URI or namespace.
|
||||||
@@ -180,3 +91,36 @@ pub struct JobFailureInfo {
|
|||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
pub retryable: Option<bool>,
|
pub retryable: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A described job from `Connection.getJob`.
|
||||||
|
#[napi(object)]
|
||||||
|
pub struct JobDescription {
|
||||||
|
pub job_id: String,
|
||||||
|
pub job_type: String,
|
||||||
|
/// Lifecycle state: "running", "finished", "failed", or "cancelled".
|
||||||
|
pub state: String,
|
||||||
|
/// When the job was created, in milliseconds since the epoch.
|
||||||
|
pub creation_ms: i64,
|
||||||
|
/// The job-type-specific specification as a JSON string, when present.
|
||||||
|
pub spec_json: Option<String>,
|
||||||
|
/// Why the job failed, when the job is failed and the server reports a
|
||||||
|
/// reason.
|
||||||
|
pub failure: Option<JobFailureInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<lancedb::database::JobDescription> for JobDescription {
|
||||||
|
fn from(description: lancedb::database::JobDescription) -> Self {
|
||||||
|
Self {
|
||||||
|
job_id: description.job_id,
|
||||||
|
job_type: description.job_type,
|
||||||
|
state: description.state,
|
||||||
|
creation_ms: description.creation_ms,
|
||||||
|
spec_json: (!description.spec.is_null()).then(|| description.spec.to_string()),
|
||||||
|
failure: description.failure.map(|failure| JobFailureInfo {
|
||||||
|
phase: failure.phase,
|
||||||
|
message: failure.message,
|
||||||
|
retryable: failure.retryable,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ use std::collections::HashMap;
|
|||||||
use env_logger::Env;
|
use env_logger::Env;
|
||||||
use napi_derive::*;
|
use napi_derive::*;
|
||||||
|
|
||||||
mod blob;
|
|
||||||
mod connection;
|
mod connection;
|
||||||
mod error;
|
mod error;
|
||||||
mod header;
|
mod header;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ use napi::bindgen_prelude::*;
|
|||||||
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
|
||||||
use napi_derive::napi;
|
use napi_derive::napi;
|
||||||
|
|
||||||
use crate::blob::{BlobFile, copy_blob_buffers, parse_row_ids};
|
|
||||||
use crate::error::NapiErrorExt;
|
use crate::error::NapiErrorExt;
|
||||||
use crate::index::Index;
|
use crate::index::Index;
|
||||||
use crate::merge::NativeMergeInsertBuilder;
|
use crate::merge::NativeMergeInsertBuilder;
|
||||||
@@ -330,44 +329,6 @@ impl Table {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[napi(catch_unwind)]
|
|
||||||
pub async fn blob_columns(&self) -> napi::Result<Vec<String>> {
|
|
||||||
self.inner_ref()?.blob_columns().await.default_error()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(catch_unwind)]
|
|
||||||
pub async fn fetch_blobs(
|
|
||||||
&self,
|
|
||||||
column: String,
|
|
||||||
row_ids: Vec<BigInt>,
|
|
||||||
) -> napi::Result<Vec<Option<Buffer>>> {
|
|
||||||
let row_ids = parse_row_ids(row_ids)?;
|
|
||||||
let array = self
|
|
||||||
.inner_ref()?
|
|
||||||
.fetch_blobs(column.as_str(), &row_ids)
|
|
||||||
.await
|
|
||||||
.default_error()?;
|
|
||||||
Ok(copy_blob_buffers(array))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(catch_unwind)]
|
|
||||||
pub async fn fetch_blob_files(
|
|
||||||
&self,
|
|
||||||
column: String,
|
|
||||||
row_ids: Vec<BigInt>,
|
|
||||||
) -> napi::Result<Vec<Option<BlobFile>>> {
|
|
||||||
let row_ids = parse_row_ids(row_ids)?;
|
|
||||||
let files = self
|
|
||||||
.inner_ref()?
|
|
||||||
.fetch_blob_files(column.as_str(), &row_ids)
|
|
||||||
.await
|
|
||||||
.default_error()?;
|
|
||||||
Ok(files
|
|
||||||
.into_iter()
|
|
||||||
.map(|file| file.map(BlobFile::new))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[napi(catch_unwind)]
|
#[napi(catch_unwind)]
|
||||||
pub fn vector_search(&self, vector: Float32Array) -> napi::Result<VectorQuery> {
|
pub fn vector_search(&self, vector: Float32Array) -> napi::Result<VectorQuery> {
|
||||||
self.query()?.nearest_to(vector)
|
self.query()?.nearest_to(vector)
|
||||||
|
|||||||
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lancedb-python"
|
name = "lancedb-python"
|
||||||
version = "0.39.0-beta.6"
|
version = "0.39.0-beta.1"
|
||||||
publish = false
|
publish = false
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description = "Python bindings for LanceDB"
|
description = "Python bindings for LanceDB"
|
||||||
@@ -28,7 +28,7 @@ env_logger.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
# Maturin enables extension-module mode for Python builds. Keeping it out of
|
# Maturin enables extension-module mode for Python builds. Keeping it out of
|
||||||
# Cargo features lets Rust unit tests link against libpython.
|
# Cargo features lets Rust unit tests link against libpython.
|
||||||
pyo3 = { version = "0.28", features = ["abi3-py310", "chrono", "uuid"] }
|
pyo3 = { version = "0.28", features = ["abi3-py310", "chrono"] }
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
pyo3-async-runtimes = { version = "0.28", features = [
|
pyo3-async-runtimes = { version = "0.28", features = [
|
||||||
"attributes",
|
"attributes",
|
||||||
@@ -40,7 +40,6 @@ serde.workspace = true
|
|||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
snafu.workspace = true
|
snafu.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
uuid.workspace = true
|
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -139,7 +139,6 @@ include = [
|
|||||||
"python/lancedb/exceptions.py",
|
"python/lancedb/exceptions.py",
|
||||||
"python/lancedb/background_loop.py",
|
"python/lancedb/background_loop.py",
|
||||||
"python/lancedb/schema.py",
|
"python/lancedb/schema.py",
|
||||||
"python/lancedb/sql.py",
|
|
||||||
"python/lancedb/remote/__init__.py",
|
"python/lancedb/remote/__init__.py",
|
||||||
"python/lancedb/remote/errors.py",
|
"python/lancedb/remote/errors.py",
|
||||||
"python/lancedb/embeddings/__init__.py",
|
"python/lancedb/embeddings/__init__.py",
|
||||||
|
|||||||
@@ -22,11 +22,7 @@ from .remote.db import RemoteDBConnection
|
|||||||
from .expr import Expr, col, lit, func
|
from .expr import Expr, col, lit, func
|
||||||
from .schema import blob, vector
|
from .schema import blob, vector
|
||||||
from .job import AsyncJob, Job
|
from .job import AsyncJob, Job
|
||||||
from .sql import AsyncQuery as AsyncSqlQuery
|
|
||||||
from .sql import Query as SqlQuery
|
|
||||||
from .sql import QueryDescription
|
|
||||||
from .functions import (
|
from .functions import (
|
||||||
AssignmentMapping as AssignmentMapping,
|
|
||||||
FunctionArtifactRequest as FunctionArtifactRequest,
|
FunctionArtifactRequest as FunctionArtifactRequest,
|
||||||
FunctionApplication as FunctionApplication,
|
FunctionApplication as FunctionApplication,
|
||||||
FunctionBinding as FunctionBinding,
|
FunctionBinding as FunctionBinding,
|
||||||
@@ -105,7 +101,6 @@ def connect(
|
|||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
region: str = "us-east-1",
|
region: str = "us-east-1",
|
||||||
host_override: Optional[str] = None,
|
host_override: Optional[str] = None,
|
||||||
sql_host_override: Optional[str] = None,
|
|
||||||
read_consistency_interval: Optional[timedelta] = None,
|
read_consistency_interval: Optional[timedelta] = None,
|
||||||
request_thread_pool: Optional[Union[int, ThreadPoolExecutor]] = None,
|
request_thread_pool: Optional[Union[int, ThreadPoolExecutor]] = None,
|
||||||
client_config: Union[ClientConfig, Dict[str, Any], None] = None,
|
client_config: Union[ClientConfig, Dict[str, Any], None] = None,
|
||||||
@@ -134,9 +129,6 @@ def connect(
|
|||||||
The region to use for LanceDB Cloud.
|
The region to use for LanceDB Cloud.
|
||||||
host_override: str, optional
|
host_override: str, optional
|
||||||
The override url for LanceDB Cloud.
|
The override url for LanceDB Cloud.
|
||||||
sql_host_override: str, optional
|
|
||||||
The remote SQL service endpoint override. The client connects lazily when SQL
|
|
||||||
is first executed and retains that connection.
|
|
||||||
read_consistency_interval: timedelta, default None
|
read_consistency_interval: timedelta, default None
|
||||||
The interval at which to check for updates to the table from other
|
The interval at which to check for updates to the table from other
|
||||||
processes. If None, then consistency is not checked. For performance
|
processes. If None, then consistency is not checked. For performance
|
||||||
@@ -278,7 +270,6 @@ def connect(
|
|||||||
api_key,
|
api_key,
|
||||||
region,
|
region,
|
||||||
host_override,
|
host_override,
|
||||||
sql_host_override=sql_host_override,
|
|
||||||
# TODO: remove this (deprecation warning downstream)
|
# TODO: remove this (deprecation warning downstream)
|
||||||
request_thread_pool=request_thread_pool,
|
request_thread_pool=request_thread_pool,
|
||||||
client_config=client_config,
|
client_config=client_config,
|
||||||
@@ -421,7 +412,6 @@ def deserialize_conn(
|
|||||||
parsed["api_key"],
|
parsed["api_key"],
|
||||||
parsed.get("region", "us-east-1"),
|
parsed.get("region", "us-east-1"),
|
||||||
host_override=parsed.get("host_override"),
|
host_override=parsed.get("host_override"),
|
||||||
sql_host_override=parsed.get("sql_host_override"),
|
|
||||||
client_config=parsed.get("client_config"),
|
client_config=parsed.get("client_config"),
|
||||||
storage_options=storage_options,
|
storage_options=storage_options,
|
||||||
)
|
)
|
||||||
@@ -435,7 +425,6 @@ async def connect_async(
|
|||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
region: str = "us-east-1",
|
region: str = "us-east-1",
|
||||||
host_override: Optional[str] = None,
|
host_override: Optional[str] = None,
|
||||||
sql_host_override: Optional[str] = None,
|
|
||||||
read_consistency_interval: Optional[timedelta] = None,
|
read_consistency_interval: Optional[timedelta] = None,
|
||||||
client_config: Optional[Union[ClientConfig, Dict[str, Any]]] = None,
|
client_config: Optional[Union[ClientConfig, Dict[str, Any]]] = None,
|
||||||
storage_options: Optional[Dict[str, str]] = None,
|
storage_options: Optional[Dict[str, str]] = None,
|
||||||
@@ -458,9 +447,6 @@ async def connect_async(
|
|||||||
The region to use for LanceDB Cloud.
|
The region to use for LanceDB Cloud.
|
||||||
host_override: str, optional
|
host_override: str, optional
|
||||||
The override url for LanceDB Cloud.
|
The override url for LanceDB Cloud.
|
||||||
sql_host_override: str, optional
|
|
||||||
The remote SQL service endpoint override. The client connects lazily when SQL
|
|
||||||
is first executed and retains that connection.
|
|
||||||
read_consistency_interval: timedelta, default None
|
read_consistency_interval: timedelta, default None
|
||||||
The interval at which to check for updates to the table from other
|
The interval at which to check for updates to the table from other
|
||||||
processes. If None, then consistency is not checked. For performance
|
processes. If None, then consistency is not checked. For performance
|
||||||
@@ -548,7 +534,6 @@ async def connect_async(
|
|||||||
api_key,
|
api_key,
|
||||||
region,
|
region,
|
||||||
host_override,
|
host_override,
|
||||||
sql_host_override,
|
|
||||||
read_consistency_interval_secs,
|
read_consistency_interval_secs,
|
||||||
client_config,
|
client_config,
|
||||||
storage_options,
|
storage_options,
|
||||||
@@ -571,7 +556,6 @@ __all__ = [
|
|||||||
"connect_namespace_async",
|
"connect_namespace_async",
|
||||||
"AsyncConnection",
|
"AsyncConnection",
|
||||||
"AsyncJob",
|
"AsyncJob",
|
||||||
"AsyncSqlQuery",
|
|
||||||
"AsyncLanceNamespaceDBConnection",
|
"AsyncLanceNamespaceDBConnection",
|
||||||
"AsyncTable",
|
"AsyncTable",
|
||||||
"FtsToken",
|
"FtsToken",
|
||||||
@@ -586,8 +570,6 @@ __all__ = [
|
|||||||
"vector",
|
"vector",
|
||||||
"DBConnection",
|
"DBConnection",
|
||||||
"Job",
|
"Job",
|
||||||
"QueryDescription",
|
|
||||||
"SqlQuery",
|
|
||||||
"LanceDBConnection",
|
"LanceDBConnection",
|
||||||
"LanceNamespaceDBConnection",
|
"LanceNamespaceDBConnection",
|
||||||
"LsmWriteSpec",
|
"LsmWriteSpec",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Dict, List, Optional, Tuple, Any, TypedDict, Union, Literal
|
from typing import Dict, List, Optional, Tuple, Any, TypedDict, Union, Literal
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
|
|
||||||
@@ -148,20 +147,17 @@ class Connection(object):
|
|||||||
start_after: Optional[str],
|
start_after: Optional[str],
|
||||||
limit: Optional[int],
|
limit: Optional[int],
|
||||||
) -> list[str]: ... # Deprecated: Use list_tables instead
|
) -> list[str]: ... # Deprecated: Use list_tables instead
|
||||||
async def open_job(self, job_id: str) -> Job: ...
|
def job(self, job_id: str) -> Job: ...
|
||||||
async def create_function_async(self, request_json: str) -> Job: ...
|
async def create_function_async(self, request_json: str) -> Job: ...
|
||||||
async def get_function(self, name: str, version: str) -> str: ...
|
async def get_function(self, name: str, version: str) -> str: ...
|
||||||
async def list_functions(self) -> List[str]: ...
|
async def list_functions(self) -> List[str]: ...
|
||||||
async def drop_function(self, name: str, version: str) -> bool: ...
|
async def drop_function(self, name: str, version: str) -> bool: ...
|
||||||
async def list_jobs(self) -> List[JobInfo]: ...
|
async def list_jobs(self) -> List[JobInfo]: ...
|
||||||
|
async def get_job(self, job_id: str) -> Optional[JobDescription]: ...
|
||||||
async def cancel_job(self, job_id: str) -> bool: ...
|
async def cancel_job(self, job_id: str) -> bool: ...
|
||||||
async def execute_query_async(
|
async def job_history(
|
||||||
self,
|
self, job_id: Optional[str] = None
|
||||||
query: str,
|
) -> List[pa.RecordBatch]: ...
|
||||||
*,
|
|
||||||
default_namespace_path: Optional[List[str]] = None,
|
|
||||||
) -> SqlQuery: ...
|
|
||||||
async def describe_query(self, query_id: UUID) -> QueryDescription: ...
|
|
||||||
async def create_table(
|
async def create_table(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
@@ -240,20 +236,9 @@ class BlobFile:
|
|||||||
class Job:
|
class Job:
|
||||||
@property
|
@property
|
||||||
def id(self) -> Optional[str]: ...
|
def id(self) -> Optional[str]: ...
|
||||||
@property
|
|
||||||
def _state(self) -> Optional[str]: ...
|
|
||||||
@property
|
|
||||||
def _description(self) -> Optional[JobDescription]: ...
|
|
||||||
async def status(self) -> str: ...
|
async def status(self) -> str: ...
|
||||||
async def wait(self) -> Optional[str]: ...
|
async def wait(self) -> Optional[str]: ...
|
||||||
async def cancel(self) -> None: ...
|
async def cancel(self) -> None: ...
|
||||||
async def refresh(self) -> None: ...
|
|
||||||
async def events(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
limit: Optional[int] = None,
|
|
||||||
filter: Optional[str] = None,
|
|
||||||
) -> pa.Table: ...
|
|
||||||
|
|
||||||
class JobInfo:
|
class JobInfo:
|
||||||
@property
|
@property
|
||||||
@@ -285,33 +270,10 @@ class JobDescription:
|
|||||||
@property
|
@property
|
||||||
def creation_ms(self) -> int: ...
|
def creation_ms(self) -> int: ...
|
||||||
@property
|
@property
|
||||||
def _spec_json(self) -> Optional[str]: ...
|
def spec_json(self) -> Optional[str]: ...
|
||||||
@property
|
|
||||||
def _result_json(self) -> Optional[str]: ...
|
|
||||||
@property
|
|
||||||
def spec(self) -> Optional[Any]: ...
|
|
||||||
@property
|
|
||||||
def result(self) -> Optional[Any]: ...
|
|
||||||
@property
|
@property
|
||||||
def failure(self) -> Optional[JobFailureInfo]: ...
|
def failure(self) -> Optional[JobFailureInfo]: ...
|
||||||
|
|
||||||
class SqlQuery:
|
|
||||||
@property
|
|
||||||
def id(self) -> UUID: ...
|
|
||||||
async def describe(self) -> QueryDescription: ...
|
|
||||||
async def reader(self) -> RecordBatchStream: ...
|
|
||||||
async def cancel(self) -> None: ...
|
|
||||||
|
|
||||||
class QueryDescription:
|
|
||||||
@property
|
|
||||||
def id(self) -> UUID: ...
|
|
||||||
@property
|
|
||||||
def status(self) -> str: ...
|
|
||||||
@property
|
|
||||||
def progress(self) -> Optional[float]: ...
|
|
||||||
@property
|
|
||||||
def expires_at(self) -> Optional[datetime]: ...
|
|
||||||
|
|
||||||
class Table:
|
class Table:
|
||||||
def name(self) -> str: ...
|
def name(self) -> str: ...
|
||||||
def __repr__(self) -> str: ...
|
def __repr__(self) -> str: ...
|
||||||
@@ -490,7 +452,6 @@ async def connect(
|
|||||||
api_key: Optional[str],
|
api_key: Optional[str],
|
||||||
region: Optional[str],
|
region: Optional[str],
|
||||||
host_override: Optional[str],
|
host_override: Optional[str],
|
||||||
sql_host_override: Optional[str],
|
|
||||||
read_consistency_interval: Optional[float],
|
read_consistency_interval: Optional[float],
|
||||||
client_config: Optional[Union[ClientConfig, Dict[str, Any]]],
|
client_config: Optional[Union[ClientConfig, Dict[str, Any]]],
|
||||||
storage_options: Optional[Dict[str, str]],
|
storage_options: Optional[Dict[str, str]],
|
||||||
|
|||||||
+62
-93
@@ -19,7 +19,6 @@ from typing import (
|
|||||||
Optional,
|
Optional,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 12):
|
if sys.version_info >= (3, 12):
|
||||||
from typing import override
|
from typing import override
|
||||||
@@ -48,9 +47,6 @@ from . import __version__
|
|||||||
from ._lancedb import connect as lancedb_connect # type: ignore
|
from ._lancedb import connect as lancedb_connect # type: ignore
|
||||||
from .functions import FunctionVersion, UdfDefinition
|
from .functions import FunctionVersion, UdfDefinition
|
||||||
from .job import AsyncJob, Job, _typed_job
|
from .job import AsyncJob, Job, _typed_job
|
||||||
from .sql import AsyncQuery as AsyncSqlQuery
|
|
||||||
from .sql import Query as SqlQuery
|
|
||||||
from .sql import QueryDescription
|
|
||||||
from .materialized_view import (
|
from .materialized_view import (
|
||||||
AsyncMaterializedView,
|
AsyncMaterializedView,
|
||||||
MaterializedView,
|
MaterializedView,
|
||||||
@@ -72,11 +68,10 @@ import deprecation
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import pyarrow as pa
|
import pyarrow as pa
|
||||||
from .arrow import AsyncRecordBatchReader
|
|
||||||
from .pydantic import LanceModel
|
from .pydantic import LanceModel
|
||||||
|
|
||||||
from ._lancedb import Connection as LanceDbConnection
|
from ._lancedb import Connection as LanceDbConnection
|
||||||
from ._lancedb import JobInfo
|
from ._lancedb import JobDescription, JobInfo
|
||||||
from .common import DATA, URI
|
from .common import DATA, URI
|
||||||
from .embeddings import EmbeddingFunctionConfig
|
from .embeddings import EmbeddingFunctionConfig
|
||||||
from ._lancedb import Session
|
from ._lancedb import Session
|
||||||
@@ -745,23 +740,26 @@ class DBConnection(EnforceOverrides):
|
|||||||
"Function catalog operations are not supported for this connection type"
|
"Function catalog operations are not supported for this connection type"
|
||||||
)
|
)
|
||||||
|
|
||||||
def open_job(self, job_id: str) -> Job:
|
def job(self, job_id: str) -> Job:
|
||||||
"""Open a server-side job by id, returning a handle with its record
|
"""A [Job][lancedb.job.Job] handle for a server-side job by id.
|
||||||
already populated.
|
|
||||||
|
|
||||||
The returned [Job][lancedb.job.Job] answers for its own state,
|
The handle is constructed without a server round trip; an unknown id
|
||||||
specification, result, failure and event history, so there is no
|
surfaces when the handle is used. Dropping the handle has no effect
|
||||||
separate connection-level call for any of them.
|
on the job itself.
|
||||||
|
|
||||||
Raises `JobNotFoundError` when the server has no such job, the way
|
|
||||||
`open_table` does for a missing table.
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("open_job is not supported for this connection type")
|
raise NotImplementedError("job is not supported for this connection type")
|
||||||
|
|
||||||
def list_jobs(self) -> List[JobInfo]:
|
def list_jobs(self) -> List[JobInfo]:
|
||||||
"""List server-side jobs across the database's tables."""
|
"""List server-side jobs across the database's tables."""
|
||||||
raise NotImplementedError("list_jobs is not supported for this connection type")
|
raise NotImplementedError("list_jobs is not supported for this connection type")
|
||||||
|
|
||||||
|
def get_job(self, job_id: str) -> Optional[JobDescription]:
|
||||||
|
"""Describe a single server-side job by id.
|
||||||
|
|
||||||
|
Returns None when the server has no such job.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("get_job is not supported for this connection type")
|
||||||
|
|
||||||
def cancel_job(self, job_id: str) -> bool:
|
def cancel_job(self, job_id: str) -> bool:
|
||||||
"""Request cancellation of a server-side job by id.
|
"""Request cancellation of a server-side job by id.
|
||||||
|
|
||||||
@@ -773,38 +771,14 @@ class DBConnection(EnforceOverrides):
|
|||||||
"cancel_job is not supported for this connection type"
|
"cancel_job is not supported for this connection type"
|
||||||
)
|
)
|
||||||
|
|
||||||
def execute_query(
|
def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]:
|
||||||
self,
|
"""The lifecycle event history of a server-side job, as Arrow batches.
|
||||||
query: str,
|
|
||||||
*,
|
|
||||||
default_namespace_path: Optional[List[str]] = None,
|
|
||||||
) -> pa.RecordBatchReader:
|
|
||||||
"""Execute SQL and return a blocking Arrow reader.
|
|
||||||
|
|
||||||
This submits through :meth:`execute_query_async` and waits until the
|
Lists history across all jobs when `job_id` is None.
|
||||||
initial result stream is readable. It does not wait for the full query
|
|
||||||
to finish.
|
|
||||||
"""
|
"""
|
||||||
return self.execute_query_async(
|
raise NotImplementedError(
|
||||||
query,
|
"job_history is not supported for this connection type"
|
||||||
default_namespace_path=default_namespace_path,
|
)
|
||||||
).reader()
|
|
||||||
|
|
||||||
def execute_query_async(
|
|
||||||
self,
|
|
||||||
query: str,
|
|
||||||
*,
|
|
||||||
default_namespace_path: Optional[List[str]] = None,
|
|
||||||
) -> SqlQuery:
|
|
||||||
"""Start executing SQL and return its query handle.
|
|
||||||
|
|
||||||
Local connections do not support SQL.
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("SQL is not supported for this connection type")
|
|
||||||
|
|
||||||
def describe_query(self, query_id: UUID) -> QueryDescription:
|
|
||||||
"""Describe a submitted SQL query by its connection-scoped id."""
|
|
||||||
raise NotImplementedError("SQL is not supported for this connection type")
|
|
||||||
|
|
||||||
|
|
||||||
class LanceDBConnection(DBConnection):
|
class LanceDBConnection(DBConnection):
|
||||||
@@ -901,7 +875,6 @@ class LanceDBConnection(DBConnection):
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
read_consistency_interval_secs,
|
read_consistency_interval_secs,
|
||||||
None,
|
None,
|
||||||
storage_options,
|
storage_options,
|
||||||
@@ -1450,11 +1423,14 @@ class LanceDBConnection(DBConnection):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def open_job(self, job_id: str) -> Job:
|
def job(self, job_id: str) -> Job:
|
||||||
"""Open a server-side job by id. See
|
"""A [Job][lancedb.job.Job] handle for a server-side job by id.
|
||||||
[DBConnection.open_job][lancedb.db.DBConnection.open_job].
|
|
||||||
|
The handle is constructed without a server round trip; an unknown id
|
||||||
|
surfaces when the handle is used. Dropping the handle has no effect
|
||||||
|
on the job itself.
|
||||||
"""
|
"""
|
||||||
return Job(LOOP.run(self._conn.open_job(job_id)))
|
return Job(self._conn.job(job_id))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def create_function_async(self, definition: UdfDefinition) -> Job[FunctionVersion]:
|
def create_function_async(self, definition: UdfDefinition) -> Job[FunctionVersion]:
|
||||||
@@ -1478,6 +1454,14 @@ class LanceDBConnection(DBConnection):
|
|||||||
"""List server-side jobs across the database's tables."""
|
"""List server-side jobs across the database's tables."""
|
||||||
return LOOP.run(self._conn.list_jobs())
|
return LOOP.run(self._conn.list_jobs())
|
||||||
|
|
||||||
|
@override
|
||||||
|
def get_job(self, job_id: str) -> Optional[JobDescription]:
|
||||||
|
"""Describe a single server-side job by id.
|
||||||
|
|
||||||
|
Returns None when the server has no such job.
|
||||||
|
"""
|
||||||
|
return LOOP.run(self._conn.get_job(job_id))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def cancel_job(self, job_id: str) -> bool:
|
def cancel_job(self, job_id: str) -> bool:
|
||||||
"""Request cancellation of a server-side job by id.
|
"""Request cancellation of a server-side job by id.
|
||||||
@@ -1488,6 +1472,14 @@ class LanceDBConnection(DBConnection):
|
|||||||
"""
|
"""
|
||||||
return LOOP.run(self._conn.cancel_job(job_id))
|
return LOOP.run(self._conn.cancel_job(job_id))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]:
|
||||||
|
"""The lifecycle event history of a server-side job, as Arrow batches.
|
||||||
|
|
||||||
|
Lists history across all jobs when `job_id` is None.
|
||||||
|
"""
|
||||||
|
return LOOP.run(self._conn.job_history(job_id))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def namespace_client(self) -> LanceNamespace:
|
def namespace_client(self) -> LanceNamespace:
|
||||||
"""Get the equivalent namespace client for this connection.
|
"""Get the equivalent namespace client for this connection.
|
||||||
@@ -2258,11 +2250,15 @@ class AsyncConnection(object):
|
|||||||
namespace_path = []
|
namespace_path = []
|
||||||
await self._inner.drop_all_tables(namespace_path=namespace_path)
|
await self._inner.drop_all_tables(namespace_path=namespace_path)
|
||||||
|
|
||||||
async def open_job(self, job_id: str) -> AsyncJob:
|
def job(self, job_id: str) -> AsyncJob:
|
||||||
"""Open a server-side job by id. See
|
"""An [AsyncJob][lancedb.job.AsyncJob] handle for a server-side job
|
||||||
[DBConnection.open_job][lancedb.db.DBConnection.open_job].
|
by id.
|
||||||
|
|
||||||
|
The handle is constructed without a server round trip; an unknown id
|
||||||
|
surfaces when the handle is used. Dropping the handle has no effect
|
||||||
|
on the job itself.
|
||||||
"""
|
"""
|
||||||
return AsyncJob(await self._inner.open_job(job_id))
|
return AsyncJob(self._inner.job(job_id))
|
||||||
|
|
||||||
async def create_function_async(
|
async def create_function_async(
|
||||||
self, definition: UdfDefinition
|
self, definition: UdfDefinition
|
||||||
@@ -2302,6 +2298,13 @@ class AsyncConnection(object):
|
|||||||
"""List server-side jobs across the database's tables."""
|
"""List server-side jobs across the database's tables."""
|
||||||
return await self._inner.list_jobs()
|
return await self._inner.list_jobs()
|
||||||
|
|
||||||
|
async def get_job(self, job_id: str) -> Optional[JobDescription]:
|
||||||
|
"""Describe a single server-side job by id.
|
||||||
|
|
||||||
|
Returns None when the server has no such job.
|
||||||
|
"""
|
||||||
|
return await self._inner.get_job(job_id)
|
||||||
|
|
||||||
async def cancel_job(self, job_id: str) -> bool:
|
async def cancel_job(self, job_id: str) -> bool:
|
||||||
"""Request cancellation of a server-side job by id.
|
"""Request cancellation of a server-side job by id.
|
||||||
|
|
||||||
@@ -2311,46 +2314,12 @@ class AsyncConnection(object):
|
|||||||
"""
|
"""
|
||||||
return await self._inner.cancel_job(job_id)
|
return await self._inner.cancel_job(job_id)
|
||||||
|
|
||||||
async def execute_query(
|
async def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]:
|
||||||
self,
|
"""The lifecycle event history of a server-side job, as Arrow batches.
|
||||||
query: str,
|
|
||||||
*,
|
|
||||||
default_namespace_path: Optional[List[str]] = None,
|
|
||||||
) -> AsyncRecordBatchReader:
|
|
||||||
"""Execute SQL and return an asynchronous Arrow reader.
|
|
||||||
|
|
||||||
This submits through :meth:`execute_query_async` and waits until the
|
Lists history across all jobs when `job_id` is None.
|
||||||
initial result stream is readable. It does not wait for the full query
|
|
||||||
to finish.
|
|
||||||
"""
|
"""
|
||||||
submitted = await self.execute_query_async(
|
return await self._inner.job_history(job_id)
|
||||||
query,
|
|
||||||
default_namespace_path=default_namespace_path,
|
|
||||||
)
|
|
||||||
return await submitted.reader()
|
|
||||||
|
|
||||||
async def execute_query_async(
|
|
||||||
self,
|
|
||||||
query: str,
|
|
||||||
*,
|
|
||||||
default_namespace_path: Optional[List[str]] = None,
|
|
||||||
) -> AsyncSqlQuery:
|
|
||||||
"""Start executing SQL and return its query handle.
|
|
||||||
|
|
||||||
The database from ``connect_async`` is used for unqualified database
|
|
||||||
references. The namespace defaults to ``["public"]``. Local
|
|
||||||
connections raise ``NotImplementedError``.
|
|
||||||
"""
|
|
||||||
return AsyncSqlQuery(
|
|
||||||
await self._inner.execute_query_async(
|
|
||||||
query,
|
|
||||||
default_namespace_path=default_namespace_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
async def describe_query(self, query_id: UUID) -> QueryDescription:
|
|
||||||
"""Describe a submitted SQL query by its connection-scoped id."""
|
|
||||||
return await self._inner.describe_query(query_id)
|
|
||||||
|
|
||||||
async def namespace_client(self) -> LanceNamespace:
|
async def namespace_client(self) -> LanceNamespace:
|
||||||
"""Get the equivalent namespace client for this connection.
|
"""Get the equivalent namespace client for this connection.
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class GteEmbeddings(TextEmbeddingFunction):
|
|||||||
An embedding function that uses GTE-LARGE MLX format(for Apple silicon devices only)
|
An embedding function that uses GTE-LARGE MLX format(for Apple silicon devices only)
|
||||||
as well as the standard cpu/gpu version from: https://huggingface.co/thenlper/gte-large.
|
as well as the standard cpu/gpu version from: https://huggingface.co/thenlper/gte-large.
|
||||||
|
|
||||||
For Apple users, you will need the mlx package installed, which can be done with:
|
For Apple users, you will need the mlx package insalled, which can be done with:
|
||||||
pip install mlx
|
pip install mlx
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class InstructorEmbeddingFunction(TextEmbeddingFunction):
|
|||||||
|
|
||||||
import lancedb
|
import lancedb
|
||||||
from lancedb.pydantic import LanceModel, Vector
|
from lancedb.pydantic import LanceModel, Vector
|
||||||
from lancedb.embeddings import get_registry, InstructorEmbeddingFunction
|
from lancedb.embeddings import get_registry, InstuctorEmbeddingFunction
|
||||||
|
|
||||||
instructor = get_registry().get("instructor").create(
|
instructor = get_registry().get("instructor").create(
|
||||||
source_instruction="represent the document for retrieval",
|
source_instruction="represent the document for retrieval",
|
||||||
|
|||||||
@@ -35,9 +35,3 @@ class JobCancelledError(RuntimeError):
|
|||||||
"""Exception raised when an asynchronous job was cancelled."""
|
"""Exception raised when an asynchronous job was cancelled."""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class JobNotFoundError(ValueError):
|
|
||||||
"""Exception raised when opening a job the server does not have."""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -470,7 +470,11 @@ class InputBinding(_RemoteValue):
|
|||||||
|
|
||||||
|
|
||||||
class OutputMapping(_RemoteValue):
|
class OutputMapping(_RemoteValue):
|
||||||
"""One stable result-field mapping."""
|
"""One stable result-field mapping.
|
||||||
|
|
||||||
|
Assignment state is outside the Slice 1 client contract. During the NULL
|
||||||
|
transition Lance exposes no public cell-flag identifier to persist here.
|
||||||
|
"""
|
||||||
|
|
||||||
result_field: str
|
result_field: str
|
||||||
output_name: str
|
output_name: str
|
||||||
@@ -480,13 +484,6 @@ class OutputMapping(_RemoteValue):
|
|||||||
nullable: bool
|
nullable: bool
|
||||||
|
|
||||||
|
|
||||||
class AssignmentMapping(_RemoteValue):
|
|
||||||
"""Internal physical column preserving flattened struct validity."""
|
|
||||||
|
|
||||||
output_name: str
|
|
||||||
output_field_id: _Int32
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionBinding(_RemoteValue):
|
class FunctionBinding(_RemoteValue):
|
||||||
"""Immutable Function binding persisted by the Enterprise table service."""
|
"""Immutable Function binding persisted by the Enterprise table service."""
|
||||||
|
|
||||||
@@ -494,7 +491,6 @@ class FunctionBinding(_RemoteValue):
|
|||||||
function: FunctionVersionRef
|
function: FunctionVersionRef
|
||||||
inputs: tuple[InputBinding, ...]
|
inputs: tuple[InputBinding, ...]
|
||||||
outputs: tuple[OutputMapping, ...]
|
outputs: tuple[OutputMapping, ...]
|
||||||
assignment: Optional[AssignmentMapping] = None
|
|
||||||
input_schema: Optional[Mapping[str, Any]] = None
|
input_schema: Optional[Mapping[str, Any]] = None
|
||||||
output_schema: Optional[Mapping[str, Any]] = None
|
output_schema: Optional[Mapping[str, Any]] = None
|
||||||
|
|
||||||
@@ -915,6 +911,8 @@ def _function_output(output: pa.DataType | pa.Field | pa.Schema) -> FunctionOutp
|
|||||||
|
|
||||||
if not fields:
|
if not fields:
|
||||||
raise ValueError("named-struct Function output must contain at least one field")
|
raise ValueError("named-struct Function output must contain at least one field")
|
||||||
|
if any(field.nullable for field in fields):
|
||||||
|
raise ValueError("Function output fields must be non-nullable")
|
||||||
for field in fields:
|
for field in fields:
|
||||||
_validate_exact_arrow_field(field)
|
_validate_exact_arrow_field(field)
|
||||||
names = [field.name for field in fields]
|
names = [field.name for field in fields]
|
||||||
@@ -926,7 +924,7 @@ def _function_output(output: pa.DataType | pa.Field | pa.Schema) -> FunctionOutp
|
|||||||
FunctionResultField(
|
FunctionResultField(
|
||||||
name=field.name,
|
name=field.name,
|
||||||
arrow_type=_canonical_arrow_field(field),
|
arrow_type=_canonical_arrow_field(field),
|
||||||
nullable=field.nullable,
|
nullable=False,
|
||||||
)
|
)
|
||||||
for field in fields
|
for field in fields
|
||||||
),
|
),
|
||||||
@@ -1309,9 +1307,8 @@ def udf(
|
|||||||
|
|
||||||
Input and output signatures are inferred from supported annotations. For
|
Input and output signatures are inferred from supported annotations. For
|
||||||
Arrow types annotations cannot express precisely, pass ``input_schema``
|
Arrow types annotations cannot express precisely, pass ``input_schema``
|
||||||
and ``output_schema`` together. Scalar outputs must be non-nullable. Every
|
and ``output_schema`` together. Nullable outputs are rejected because V1
|
||||||
named-struct field may be nullable; Enterprise preserves the struct's
|
uses physical NULL to represent unassigned computed-column rows.
|
||||||
validity when the result is expanded into sibling columns.
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -1323,8 +1320,8 @@ def udf(
|
|||||||
Explicit input fields in the exact order of the callable parameters.
|
Explicit input fields in the exact order of the callable parameters.
|
||||||
Must be provided together with ``output_schema``.
|
Must be provided together with ``output_schema``.
|
||||||
output_schema : pyarrow.DataType, pyarrow.Field, or pyarrow.Schema, optional
|
output_schema : pyarrow.DataType, pyarrow.Field, or pyarrow.Schema, optional
|
||||||
Explicit scalar or named-struct output. Scalar outputs must be
|
Explicit scalar or named-struct output. Must be non-nullable and be
|
||||||
non-nullable. Must be provided together with ``input_schema``.
|
provided together with ``input_schema``.
|
||||||
pip : sequence of str, optional
|
pip : sequence of str, optional
|
||||||
Pip requirements for the remote environment.
|
Pip requirements for the remote environment.
|
||||||
conda : sequence of str, optional
|
conda : sequence of str, optional
|
||||||
@@ -1390,7 +1387,6 @@ def udf(
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AssignmentMapping",
|
|
||||||
"ApplicationInput",
|
"ApplicationInput",
|
||||||
"FunctionApplication",
|
"FunctionApplication",
|
||||||
"FunctionArtifact",
|
"FunctionArtifact",
|
||||||
|
|||||||
@@ -751,7 +751,7 @@ class IvfPq:
|
|||||||
This value controls how much the vector is compressed during the
|
This value controls how much the vector is compressed during the
|
||||||
quantization step. The more sub vectors there are the less the vector is
|
quantization step. The more sub vectors there are the less the vector is
|
||||||
compressed. The default is the dimension of the vector divided by 16. If
|
compressed. The default is the dimension of the vector divided by 16. If
|
||||||
the dimension is not evenly divisible by 16 we use the dimension divided by
|
the dimension is not evenly divisible by 16 we use the dimension divded by
|
||||||
8.
|
8.
|
||||||
|
|
||||||
The above two cases are highly preferred. Having 8 or 16 values per
|
The above two cases are highly preferred. Having 8 or 16 values per
|
||||||
|
|||||||
@@ -4,27 +4,15 @@
|
|||||||
"""Handles to operations a server may run asynchronously."""
|
"""Handles to operations a server may run asynchronously."""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import Any, Callable, Generic, Optional, TypeVar, cast
|
from typing import Any, Callable, Generic, Optional, TypeVar, cast
|
||||||
|
|
||||||
import pyarrow as pa
|
|
||||||
|
|
||||||
from lancedb.background_loop import LOOP
|
from lancedb.background_loop import LOOP
|
||||||
|
|
||||||
from . import _lancedb
|
from . import _lancedb
|
||||||
from ._lancedb import JobDescription, JobFailureInfo, JobInfo
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
"AsyncJob",
|
|
||||||
"Job",
|
|
||||||
"JobDescription",
|
|
||||||
"JobFailureInfo",
|
|
||||||
"JobInfo",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncJob(Generic[T]):
|
class AsyncJob(Generic[T]):
|
||||||
"""A handle to an operation that may still be running.
|
"""A handle to an operation that may still be running.
|
||||||
@@ -90,149 +78,6 @@ class AsyncJob(Generic[T]):
|
|||||||
return
|
return
|
||||||
await self._inner.cancel()
|
await self._inner.cancel()
|
||||||
|
|
||||||
async def refresh(self) -> None:
|
|
||||||
"""Ask the backend for this job's current state, and for a server-side
|
|
||||||
job its full record, then cache it for the properties below.
|
|
||||||
|
|
||||||
The properties are all `None` until this runs, because submitting an
|
|
||||||
operation returns only a job id. `status` fetches the whole record too;
|
|
||||||
`wait` records only the terminal state it establishes.
|
|
||||||
"""
|
|
||||||
if self._inner is None:
|
|
||||||
return
|
|
||||||
await self._inner.refresh()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> Optional[str]:
|
|
||||||
"""The last observed lifecycle state, without contacting the backend.
|
|
||||||
|
|
||||||
`None` until the handle has talked to it. See :meth:`AsyncJob.refresh`.
|
|
||||||
"""
|
|
||||||
if self._inner is None:
|
|
||||||
return "finished"
|
|
||||||
return self._inner._state
|
|
||||||
|
|
||||||
@property
|
|
||||||
def job_type(self) -> Optional[str]:
|
|
||||||
"""The job's type, as the server names it.
|
|
||||||
|
|
||||||
`None` for an in-process job, which has no server-side record.
|
|
||||||
"""
|
|
||||||
return self._field("job_type")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def creation_ms(self) -> Optional[int]:
|
|
||||||
"""When the job was created, in milliseconds since the epoch."""
|
|
||||||
return self._field("creation_ms")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def spec(self) -> Optional[Any]:
|
|
||||||
"""The job-type-specific specification it was submitted with."""
|
|
||||||
return self._field("spec")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def result(self) -> Optional[Any]:
|
|
||||||
"""The job-type-specific terminal result, as reported data rather than
|
|
||||||
the typed model :meth:`AsyncJob.wait` returns.
|
|
||||||
|
|
||||||
`None` until the job succeeds, so a job that never terminates reports
|
|
||||||
its progress through :meth:`AsyncJob.events` instead.
|
|
||||||
"""
|
|
||||||
return self._field("result")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def failure(self) -> Optional[JobFailureInfo]:
|
|
||||||
"""Why the job failed, when it failed and the server reports a reason."""
|
|
||||||
return self._field("failure")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _spec_json(self) -> Optional[str]:
|
|
||||||
return self._field("_spec_json")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _result_json(self) -> Optional[str]:
|
|
||||||
return self._field("_result_json")
|
|
||||||
|
|
||||||
def _field(self, name: str) -> Optional[Any]:
|
|
||||||
description = self._inner._description if self._inner is not None else None
|
|
||||||
return getattr(description, name) if description is not None else None
|
|
||||||
|
|
||||||
async def events(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
limit: Optional[int] = None,
|
|
||||||
filter: Optional[str] = None,
|
|
||||||
) -> "pa.Table":
|
|
||||||
"""This job's recorded lifecycle events.
|
|
||||||
|
|
||||||
Where the properties above report a terminal result only once the job
|
|
||||||
reaches one, events are written as the job runs and outlive the workers
|
|
||||||
that produced them. A distributed job records a `claim`/`claim_complete`
|
|
||||||
pair per unit of work, each carrying `rows_processed`, so a job that
|
|
||||||
never finishes still accounts for what it did.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
limit: int, optional
|
|
||||||
Maximum event rows to return. The server caps results at 1000 by
|
|
||||||
default and 10,000 at most, and truncates without saying so, so
|
|
||||||
pass this for a job that emits an event per fragment.
|
|
||||||
filter: str, optional
|
|
||||||
SQL-like expression over the `state`, `updated_by`, `emitted_from`,
|
|
||||||
`emitted_by`, and `claim_entity` columns, such as
|
|
||||||
``state = 'claim_complete'``.
|
|
||||||
"""
|
|
||||||
if self._inner is None:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"job event history is only available for server-side jobs"
|
|
||||||
)
|
|
||||||
return await self._inner.events(limit=limit, filter=filter)
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return _job_repr("AsyncJob", self)
|
|
||||||
|
|
||||||
|
|
||||||
_REPR_INDENT = " " * 4
|
|
||||||
|
|
||||||
|
|
||||||
def _repr_payload(value: Any) -> str:
|
|
||||||
"""Render a job payload as indented JSON, aligned under its field."""
|
|
||||||
try:
|
|
||||||
rendered = json.dumps(value, indent=4)
|
|
||||||
except TypeError:
|
|
||||||
return repr(value)
|
|
||||||
return rendered.replace("\n", "\n" + _REPR_INDENT)
|
|
||||||
|
|
||||||
|
|
||||||
def _job_repr(kind: str, job: Any) -> str:
|
|
||||||
"""Render every field the handle currently knows, omitting the rest.
|
|
||||||
|
|
||||||
One field per line, with the JSON payloads indented, because a refresh
|
|
||||||
job's spec and result are the point of printing it.
|
|
||||||
"""
|
|
||||||
state = job.state
|
|
||||||
if state is None:
|
|
||||||
# Nothing has been fetched yet, so there is nothing to lay out.
|
|
||||||
known = f"id={job.id!r}, " if job.id is not None else ""
|
|
||||||
return f"{kind}({known}not refreshed)"
|
|
||||||
|
|
||||||
fields = []
|
|
||||||
if job.id is not None:
|
|
||||||
fields.append(f"id={job.id!r}")
|
|
||||||
fields.append(f"state={state!r}")
|
|
||||||
for name in ("job_type", "creation_ms"):
|
|
||||||
value = getattr(job, name)
|
|
||||||
if value is not None:
|
|
||||||
fields.append(f"{name}={value!r}")
|
|
||||||
for name in ("spec", "result"):
|
|
||||||
value = getattr(job, name)
|
|
||||||
if value is not None:
|
|
||||||
fields.append(f"{name}={_repr_payload(value)}")
|
|
||||||
if job.failure is not None:
|
|
||||||
fields.append(f"failure={job.failure!r}")
|
|
||||||
body = "".join(f"\n{_REPR_INDENT}{field}," for field in fields)
|
|
||||||
return f"{kind}({body}\n)"
|
|
||||||
|
|
||||||
|
|
||||||
class Job(Generic[T]):
|
class Job(Generic[T]):
|
||||||
"""Synchronous counterpart of `AsyncJob` with the same result type."""
|
"""Synchronous counterpart of `AsyncJob` with the same result type."""
|
||||||
@@ -277,75 +122,6 @@ class Job(Generic[T]):
|
|||||||
return
|
return
|
||||||
LOOP.run(self._inner.cancel())
|
LOOP.run(self._inner.cancel())
|
||||||
|
|
||||||
def refresh(self) -> None:
|
|
||||||
"""Ask the backend for this job's current state and record.
|
|
||||||
|
|
||||||
See :meth:`AsyncJob.refresh`.
|
|
||||||
"""
|
|
||||||
if self._inner is None:
|
|
||||||
return
|
|
||||||
LOOP.run(self._inner.refresh())
|
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> Optional[str]:
|
|
||||||
"""The last observed lifecycle state. See :attr:`AsyncJob.state`."""
|
|
||||||
return self._inner.state if self._inner is not None else "finished"
|
|
||||||
|
|
||||||
@property
|
|
||||||
def job_type(self) -> Optional[str]:
|
|
||||||
"""The job's type. See :attr:`AsyncJob.job_type`."""
|
|
||||||
return self._field("job_type")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def creation_ms(self) -> Optional[int]:
|
|
||||||
"""When the job was created. See :attr:`AsyncJob.creation_ms`."""
|
|
||||||
return self._field("creation_ms")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def spec(self) -> Optional[Any]:
|
|
||||||
"""The job's specification. See :attr:`AsyncJob.spec`."""
|
|
||||||
return self._field("spec")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def result(self) -> Optional[Any]:
|
|
||||||
"""The job's terminal result. See :attr:`AsyncJob.result`."""
|
|
||||||
return self._field("result")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def failure(self) -> Optional[JobFailureInfo]:
|
|
||||||
"""Why the job failed. See :attr:`AsyncJob.failure`."""
|
|
||||||
return self._field("failure")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _spec_json(self) -> Optional[str]:
|
|
||||||
return self._field("_spec_json")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _result_json(self) -> Optional[str]:
|
|
||||||
return self._field("_result_json")
|
|
||||||
|
|
||||||
def _field(self, name: str) -> Optional[Any]:
|
|
||||||
return getattr(self._inner, name) if self._inner is not None else None
|
|
||||||
|
|
||||||
def events(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
limit: Optional[int] = None,
|
|
||||||
filter: Optional[str] = None,
|
|
||||||
) -> "pa.Table":
|
|
||||||
"""This job's recorded lifecycle events.
|
|
||||||
|
|
||||||
See :meth:`AsyncJob.events`.
|
|
||||||
"""
|
|
||||||
if self._inner is None:
|
|
||||||
raise NotImplementedError(
|
|
||||||
"job event history is only available for server-side jobs"
|
|
||||||
)
|
|
||||||
return LOOP.run(self._inner.events(limit=limit, filter=filter))
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return _job_repr("Job", self)
|
|
||||||
|
|
||||||
|
|
||||||
def _typed_job(
|
def _typed_job(
|
||||||
inner: "_lancedb.Job", result_decoder: Callable[[str], T]
|
inner: "_lancedb.Job", result_decoder: Callable[[str], T]
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
|
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
if sys.version_info >= (3, 12):
|
if sys.version_info >= (3, 12):
|
||||||
from typing import override
|
from typing import override
|
||||||
@@ -49,11 +48,8 @@ from lancedb._lancedb import (
|
|||||||
connect_namespace_client as _connect_namespace_client,
|
connect_namespace_client as _connect_namespace_client,
|
||||||
)
|
)
|
||||||
from lancedb.background_loop import LOOP
|
from lancedb.background_loop import LOOP
|
||||||
from lancedb.arrow import AsyncRecordBatchReader
|
|
||||||
from lancedb.db import AsyncConnection, DBConnection
|
from lancedb.db import AsyncConnection, DBConnection
|
||||||
from lancedb.job import AsyncJob, Job
|
from lancedb.job import AsyncJob, Job
|
||||||
from lancedb.sql import AsyncQuery as AsyncSqlQuery
|
|
||||||
from lancedb.sql import QueryDescription
|
|
||||||
from lance_namespace import (
|
from lance_namespace import (
|
||||||
LanceNamespace,
|
LanceNamespace,
|
||||||
connect as namespace_connect,
|
connect as namespace_connect,
|
||||||
@@ -1451,37 +1447,6 @@ class AsyncLanceNamespaceDBConnection:
|
|||||||
namespace_path=namespace_path, page_token=page_token, limit=limit
|
namespace_path=namespace_path, page_token=page_token, limit=limit
|
||||||
)
|
)
|
||||||
|
|
||||||
async def execute_query(
|
|
||||||
self,
|
|
||||||
query: str,
|
|
||||||
*,
|
|
||||||
default_namespace_path: Optional[List[str]] = None,
|
|
||||||
) -> AsyncRecordBatchReader:
|
|
||||||
"""Execute SQL when supported by the underlying connection."""
|
|
||||||
return await self._inner.execute_query(
|
|
||||||
query,
|
|
||||||
default_namespace_path=default_namespace_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def execute_query_async(
|
|
||||||
self,
|
|
||||||
query: str,
|
|
||||||
*,
|
|
||||||
default_namespace_path: Optional[List[str]] = None,
|
|
||||||
) -> AsyncSqlQuery:
|
|
||||||
"""Start executing SQL when supported by the underlying connection.
|
|
||||||
|
|
||||||
Namespace-backed local connections do not support SQL.
|
|
||||||
"""
|
|
||||||
return await self._inner.execute_query_async(
|
|
||||||
query,
|
|
||||||
default_namespace_path=default_namespace_path,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def describe_query(self, query_id: UUID) -> QueryDescription:
|
|
||||||
"""Describe a submitted SQL query when supported."""
|
|
||||||
return await self._inner.describe_query(query_id)
|
|
||||||
|
|
||||||
async def namespace_client(self) -> LanceNamespace:
|
async def namespace_client(self) -> LanceNamespace:
|
||||||
"""Get the namespace client for this connection.
|
"""Get the namespace client for this connection.
|
||||||
|
|
||||||
|
|||||||
@@ -78,10 +78,6 @@ if TYPE_CHECKING:
|
|||||||
T = TypeVar("T", bound="LanceModel")
|
T = TypeVar("T", bound="LanceModel")
|
||||||
AnalyzePlanDistributedMetrics = Literal["aggregate", "per_worker", "full"]
|
AnalyzePlanDistributedMetrics = Literal["aggregate", "per_worker", "full"]
|
||||||
|
|
||||||
# Number of rows a hybrid query returns when no limit was set on it. This
|
|
||||||
# mirrors the default the Rust query builder applies to its sub-queries.
|
|
||||||
DEFAULT_HYBRID_LIMIT = 10
|
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class _LanceScanner(Protocol):
|
class _LanceScanner(Protocol):
|
||||||
@@ -863,7 +859,7 @@ class Query(pydantic.BaseModel):
|
|||||||
return query
|
return query
|
||||||
|
|
||||||
# This tells pydantic to allow custom types (needed for the `vector` query since
|
# This tells pydantic to allow custom types (needed for the `vector` query since
|
||||||
# pa.Array wouldn't be allowed otherwise)
|
# pa.Array wouln't be allowed otherwise)
|
||||||
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -3897,54 +3893,14 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
|
|||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _create_child_queries(
|
|
||||||
self,
|
|
||||||
) -> Tuple["AsyncFTSQuery", "AsyncVectorQuery", int, int]:
|
|
||||||
"""Build the sub-queries that make up this hybrid query.
|
|
||||||
|
|
||||||
Execution, `explain_plan` and `analyze_plan` all go through here so that
|
|
||||||
the plans that are reported are the plans that actually run.
|
|
||||||
|
|
||||||
Returns the two sub-queries along with the effective limit and offset of
|
|
||||||
the hybrid query itself.
|
|
||||||
"""
|
|
||||||
fts_query = AsyncFTSQuery(self._inner.to_fts_query(), self._table)
|
|
||||||
vec_query = AsyncVectorQuery(self._inner.to_vector_query(), self._table)
|
|
||||||
|
|
||||||
fts_req = fts_query._inner.to_query_request()
|
|
||||||
vec_req = vec_query._inner.to_query_request()
|
|
||||||
|
|
||||||
# Only one of the two sub-queries carries the limit when it was never
|
|
||||||
# set explicitly: nearest_to()/nearest_to_text() build the sibling query
|
|
||||||
# from scratch, and that is where the default gets filled in. Which one
|
|
||||||
# that is depends on the order the hybrid query was built in, so look at
|
|
||||||
# both rather than at a single side.
|
|
||||||
limit = fts_req.limit if fts_req.limit is not None else vec_req.limit
|
|
||||||
if limit is None:
|
|
||||||
limit = DEFAULT_HYBRID_LIMIT
|
|
||||||
offset = fts_req.offset or vec_req.offset or 0
|
|
||||||
|
|
||||||
fts_query.with_row_id()
|
|
||||||
vec_query.with_row_id()
|
|
||||||
|
|
||||||
# offset() pushes the offset down into both sub-queries, which would make
|
|
||||||
# each of them skip its own first `offset` rows. The window has to be
|
|
||||||
# taken out of the combined, reranked results instead, so fetch the
|
|
||||||
# skipped prefix here too and slice it off afterwards.
|
|
||||||
fts_query.limit(limit + offset)
|
|
||||||
vec_query.limit(limit + offset)
|
|
||||||
fts_query.offset(0)
|
|
||||||
vec_query.offset(0)
|
|
||||||
|
|
||||||
return fts_query, vec_query, limit, offset
|
|
||||||
|
|
||||||
async def to_batches(
|
async def to_batches(
|
||||||
self,
|
self,
|
||||||
*,
|
*,
|
||||||
max_batch_length: Optional[int] = None,
|
max_batch_length: Optional[int] = None,
|
||||||
timeout: Optional[timedelta] = None,
|
timeout: Optional[timedelta] = None,
|
||||||
) -> AsyncRecordBatchReader:
|
) -> AsyncRecordBatchReader:
|
||||||
fts_query, vec_query, limit, offset = self._create_child_queries()
|
fts_query = AsyncFTSQuery(self._inner.to_fts_query(), self._table)
|
||||||
|
vec_query = AsyncVectorQuery(self._inner.to_vector_query(), self._table)
|
||||||
|
|
||||||
req = fts_query._inner.to_query_request()
|
req = fts_query._inner.to_query_request()
|
||||||
blob_auto_row_id = False
|
blob_auto_row_id = False
|
||||||
@@ -3964,6 +3920,9 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
|
|||||||
self._blob_auto_row_id = blob_auto_row_id
|
self._blob_auto_row_id = blob_auto_row_id
|
||||||
self._blob_paths = blob_paths
|
self._blob_paths = blob_paths
|
||||||
|
|
||||||
|
fts_query.with_row_id()
|
||||||
|
vec_query.with_row_id()
|
||||||
|
|
||||||
fts_results, vector_results = await asyncio.gather(
|
fts_results, vector_results = await asyncio.gather(
|
||||||
fts_query.to_arrow(timeout=timeout),
|
fts_query.to_arrow(timeout=timeout),
|
||||||
vec_query.to_arrow(timeout=timeout),
|
vec_query.to_arrow(timeout=timeout),
|
||||||
@@ -3975,9 +3934,8 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
|
|||||||
norm=self._norm,
|
norm=self._norm,
|
||||||
fts_query=fts_query.get_query(),
|
fts_query=fts_query.get_query(),
|
||||||
reranker=self._reranker,
|
reranker=self._reranker,
|
||||||
limit=limit,
|
limit=self._inner.get_limit(),
|
||||||
with_row_ids=True,
|
with_row_ids=True,
|
||||||
offset=offset,
|
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not self._user_requested_row_id()
|
not self._user_requested_row_id()
|
||||||
@@ -4006,14 +3964,14 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
|
|||||||
... print(plan)
|
... print(plan)
|
||||||
>>> asyncio.run(doctest_example()) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
>>> asyncio.run(doctest_example()) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
|
||||||
RRFReranker(K=60)
|
RRFReranker(K=60)
|
||||||
ProjectionExec: expr=[vector@0 as vector, text@3 as text, _distance@2 as _distance, _rowid@1 as _rowid]
|
ProjectionExec: expr=[vector@0 as vector, text@3 as text, _distance@2 as _distance]
|
||||||
LanceRead: uri=..., projection=[text], source=stream(_rowid)
|
LanceRead: uri=..., projection=[text], source=stream(_rowid)
|
||||||
GlobalLimitExec: skip=0, fetch=10
|
GlobalLimitExec: skip=0, fetch=10
|
||||||
FilterExec: _distance@2 IS NOT NULL
|
FilterExec: _distance@2 IS NOT NULL
|
||||||
SortExec: TopK(fetch=10), expr=[_distance@2 ASC NULLS LAST, _rowid@1 ASC NULLS LAST], preserve_partitioning=[false]
|
SortExec: TopK(fetch=10), expr=[_distance@2 ASC NULLS LAST, _rowid@1 ASC NULLS LAST], preserve_partitioning=[false]
|
||||||
KNNVectorDistance: metric=l2
|
KNNVectorDistance: metric=l2
|
||||||
LanceRead: uri=..., projection=[vector], ...
|
LanceRead: uri=..., projection=[vector], ...
|
||||||
ProjectionExec: expr=[vector@2 as vector, text@3 as text, _score@1 as _score, _rowid@0 as _rowid]
|
ProjectionExec: expr=[vector@2 as vector, text@3 as text, _score@1 as _score]
|
||||||
LanceRead: uri=..., projection=[vector, text], source=stream(_rowid)
|
LanceRead: uri=..., projection=[vector, text], source=stream(_rowid)
|
||||||
GlobalLimitExec: skip=0, fetch=10
|
GlobalLimitExec: skip=0, fetch=10
|
||||||
MatchQuery: column=text, query=[hello]
|
MatchQuery: column=text, query=[hello]
|
||||||
@@ -4028,9 +3986,8 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
|
|||||||
plan : str
|
plan : str
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
|
|
||||||
fts_query, vec_query, _, _ = self._create_child_queries()
|
vector_plan = await self._inner.to_vector_query().explain_plan(verbose)
|
||||||
vector_plan = await vec_query.explain_plan(verbose)
|
fts_plan = await self._inner.to_fts_query().explain_plan(verbose)
|
||||||
fts_plan = await fts_query.explain_plan(verbose)
|
|
||||||
# Indent sub-plans under the reranker
|
# Indent sub-plans under the reranker
|
||||||
indented_vector = "\n".join(" " + line for line in vector_plan.splitlines())
|
indented_vector = "\n".join(" " + line for line in vector_plan.splitlines())
|
||||||
indented_fts = "\n".join(" " + line for line in fts_plan.splitlines())
|
indented_fts = "\n".join(" " + line for line in fts_plan.splitlines())
|
||||||
@@ -4057,12 +4014,14 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
|
|||||||
-------
|
-------
|
||||||
plan : str
|
plan : str
|
||||||
"""
|
"""
|
||||||
fts_query, vec_query, _, _ = self._create_child_queries()
|
|
||||||
|
|
||||||
results = ["Vector Search Query:"]
|
results = ["Vector Search Query:"]
|
||||||
results.append(await vec_query.analyze_plan(distributed_metrics))
|
results.append(
|
||||||
|
await self._inner.to_vector_query().analyze_plan(distributed_metrics)
|
||||||
|
)
|
||||||
results.append("FTS Search Query:")
|
results.append("FTS Search Query:")
|
||||||
results.append(await fts_query.analyze_plan(distributed_metrics))
|
results.append(
|
||||||
|
await self._inner.to_fts_query().analyze_plan(distributed_metrics)
|
||||||
|
)
|
||||||
|
|
||||||
return "\n".join(results)
|
return "\n".join(results)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
|
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from uuid import UUID
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
if sys.version_info >= (3, 12):
|
if sys.version_info >= (3, 12):
|
||||||
@@ -26,12 +25,10 @@ from ..common import DATA
|
|||||||
from ..db import DBConnection, LOOP
|
from ..db import DBConnection, LOOP
|
||||||
from ..functions import FunctionVersion, UdfDefinition
|
from ..functions import FunctionVersion, UdfDefinition
|
||||||
from ..job import AsyncJob, Job
|
from ..job import AsyncJob, Job
|
||||||
from ..sql import Query as SqlQuery
|
|
||||||
from ..sql import QueryDescription
|
|
||||||
from ..materialized_view import MaterializedView, SelectArg
|
from ..materialized_view import MaterializedView, SelectArg
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .._lancedb import JobInfo
|
from .._lancedb import JobDescription, JobInfo
|
||||||
from ..embeddings import EmbeddingFunctionConfig
|
from ..embeddings import EmbeddingFunctionConfig
|
||||||
from lance_namespace import (
|
from lance_namespace import (
|
||||||
LanceNamespace,
|
LanceNamespace,
|
||||||
@@ -119,7 +116,6 @@ class RemoteDBConnection(DBConnection):
|
|||||||
read_timeout: Optional[float] = None,
|
read_timeout: Optional[float] = None,
|
||||||
storage_options: Optional[Dict[str, str]] = None,
|
storage_options: Optional[Dict[str, str]] = None,
|
||||||
read_consistency_interval: Optional[timedelta] = None,
|
read_consistency_interval: Optional[timedelta] = None,
|
||||||
sql_host_override: Optional[str] = None,
|
|
||||||
):
|
):
|
||||||
"""Connect to a remote LanceDB database."""
|
"""Connect to a remote LanceDB database."""
|
||||||
if isinstance(client_config, dict):
|
if isinstance(client_config, dict):
|
||||||
@@ -165,7 +161,6 @@ class RemoteDBConnection(DBConnection):
|
|||||||
self.api_key = api_key
|
self.api_key = api_key
|
||||||
self.region = region
|
self.region = region
|
||||||
self.host_override = host_override
|
self.host_override = host_override
|
||||||
self.sql_host_override = sql_host_override
|
|
||||||
self.storage_options = storage_options
|
self.storage_options = storage_options
|
||||||
self.db_name = parsed.netloc
|
self.db_name = parsed.netloc
|
||||||
|
|
||||||
@@ -180,7 +175,6 @@ class RemoteDBConnection(DBConnection):
|
|||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
region=region,
|
region=region,
|
||||||
host_override=host_override,
|
host_override=host_override,
|
||||||
sql_host_override=sql_host_override,
|
|
||||||
client_config=client_config,
|
client_config=client_config,
|
||||||
storage_options=storage_options,
|
storage_options=storage_options,
|
||||||
read_consistency_interval=read_consistency_interval,
|
read_consistency_interval=read_consistency_interval,
|
||||||
@@ -199,7 +193,6 @@ class RemoteDBConnection(DBConnection):
|
|||||||
"api_key": self.api_key,
|
"api_key": self.api_key,
|
||||||
"region": self.region,
|
"region": self.region,
|
||||||
"host_override": self.host_override,
|
"host_override": self.host_override,
|
||||||
"sql_host_override": self.sql_host_override,
|
|
||||||
"client_config": _client_config_to_dict(self.client_config),
|
"client_config": _client_config_to_dict(self.client_config),
|
||||||
"storage_options": self.storage_options,
|
"storage_options": self.storage_options,
|
||||||
}
|
}
|
||||||
@@ -739,11 +732,14 @@ class RemoteDBConnection(DBConnection):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def open_job(self, job_id: str) -> Job:
|
def job(self, job_id: str) -> Job:
|
||||||
"""Open a server-side job by id. See
|
"""A [Job][lancedb.job.Job] handle for a server-side job by id.
|
||||||
[DBConnection.open_job][lancedb.db.DBConnection.open_job].
|
|
||||||
|
The handle is constructed without a server round trip; an unknown id
|
||||||
|
surfaces when the handle is used. Dropping the handle has no effect
|
||||||
|
on the job itself.
|
||||||
"""
|
"""
|
||||||
return Job(LOOP.run(self._conn.open_job(job_id)))
|
return Job(self._conn.job(job_id))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def create_function_async(self, definition: UdfDefinition) -> Job[FunctionVersion]:
|
def create_function_async(self, definition: UdfDefinition) -> Job[FunctionVersion]:
|
||||||
@@ -766,6 +762,14 @@ class RemoteDBConnection(DBConnection):
|
|||||||
"""List server-side jobs across the database's tables."""
|
"""List server-side jobs across the database's tables."""
|
||||||
return LOOP.run(self._conn.list_jobs())
|
return LOOP.run(self._conn.list_jobs())
|
||||||
|
|
||||||
|
@override
|
||||||
|
def get_job(self, job_id: str) -> Optional["JobDescription"]:
|
||||||
|
"""Describe a single server-side job by id.
|
||||||
|
|
||||||
|
Returns None when the server has no such job.
|
||||||
|
"""
|
||||||
|
return LOOP.run(self._conn.get_job(job_id))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def cancel_job(self, job_id: str) -> bool:
|
def cancel_job(self, job_id: str) -> bool:
|
||||||
"""Request cancellation of a server-side job by id.
|
"""Request cancellation of a server-side job by id.
|
||||||
@@ -777,35 +781,12 @@ class RemoteDBConnection(DBConnection):
|
|||||||
return LOOP.run(self._conn.cancel_job(job_id))
|
return LOOP.run(self._conn.cancel_job(job_id))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def execute_query_async(
|
def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]:
|
||||||
self,
|
"""The lifecycle event history of a server-side job, as Arrow batches.
|
||||||
query: str,
|
|
||||||
*,
|
|
||||||
default_namespace_path: Optional[List[str]] = None,
|
|
||||||
) -> SqlQuery:
|
|
||||||
"""Start executing SQL through this remote connection.
|
|
||||||
|
|
||||||
Unqualified tables use this connection's database and the
|
Lists history across all jobs when `job_id` is None.
|
||||||
``["public"]`` namespace by default. Fully qualified table names may
|
|
||||||
reference other databases available to the same deployment.
|
|
||||||
"""
|
"""
|
||||||
return SqlQuery(
|
return LOOP.run(self._conn.job_history(job_id))
|
||||||
LOOP.run(
|
|
||||||
self._conn.execute_query_async(
|
|
||||||
query,
|
|
||||||
default_namespace_path=default_namespace_path,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@override
|
|
||||||
def describe_query(self, query_id: UUID) -> QueryDescription:
|
|
||||||
"""Describe a submitted SQL query by its connection-scoped id."""
|
|
||||||
return LOOP.run(
|
|
||||||
self._conn.describe_query(
|
|
||||||
query_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def namespace_client(self) -> LanceNamespace:
|
def namespace_client(self) -> LanceNamespace:
|
||||||
|
|||||||
@@ -177,7 +177,4 @@ class OAuthProvider(HeaderProvider):
|
|||||||
if not self._current_token:
|
if not self._current_token:
|
||||||
raise RuntimeError("Failed to obtain OAuth token")
|
raise RuntimeError("Failed to obtain OAuth token")
|
||||||
|
|
||||||
return {
|
return {"Authorization": f"Bearer {self._current_token}"}
|
||||||
"Authorization": f"Bearer {self._current_token}",
|
|
||||||
"x-lancedb-credential-type": "oidc",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -402,6 +402,7 @@ class RemoteTable(Table):
|
|||||||
/,
|
/,
|
||||||
*,
|
*,
|
||||||
config: IndexConfigType,
|
config: IndexConfigType,
|
||||||
|
replace: bool = ...,
|
||||||
wait_timeout: Optional[timedelta] = ...,
|
wait_timeout: Optional[timedelta] = ...,
|
||||||
name: Optional[str] = ...,
|
name: Optional[str] = ...,
|
||||||
train: bool = ...,
|
train: bool = ...,
|
||||||
@@ -416,7 +417,7 @@ class RemoteTable(Table):
|
|||||||
index_cache_size: Optional[int] = ...,
|
index_cache_size: Optional[int] = ...,
|
||||||
num_partitions: Optional[int] = ...,
|
num_partitions: Optional[int] = ...,
|
||||||
num_sub_vectors: Optional[int] = ...,
|
num_sub_vectors: Optional[int] = ...,
|
||||||
replace: Optional[bool] = ...,
|
replace: bool = ...,
|
||||||
accelerator: Optional[str] = ...,
|
accelerator: Optional[str] = ...,
|
||||||
index_type: Literal[
|
index_type: Literal[
|
||||||
"VECTOR", "IVF_FLAT", "IVF_SQ", "IVF_PQ", "IVF_HNSW_SQ", "IVF_HNSW_PQ"
|
"VECTOR", "IVF_FLAT", "IVF_SQ", "IVF_PQ", "IVF_HNSW_SQ", "IVF_HNSW_PQ"
|
||||||
@@ -435,7 +436,7 @@ class RemoteTable(Table):
|
|||||||
index_cache_size: Optional[int] = None,
|
index_cache_size: Optional[int] = None,
|
||||||
num_partitions: Optional[int] = None,
|
num_partitions: Optional[int] = None,
|
||||||
num_sub_vectors: Optional[int] = None,
|
num_sub_vectors: Optional[int] = None,
|
||||||
replace: Optional[bool] = None,
|
replace: bool = False,
|
||||||
accelerator: Optional[str] = None,
|
accelerator: Optional[str] = None,
|
||||||
index_type="vector",
|
index_type="vector",
|
||||||
wait_timeout: Optional[timedelta] = None,
|
wait_timeout: Optional[timedelta] = None,
|
||||||
@@ -479,7 +480,6 @@ class RemoteTable(Table):
|
|||||||
vector_column_name,
|
vector_column_name,
|
||||||
accelerator,
|
accelerator,
|
||||||
index_cache_size,
|
index_cache_size,
|
||||||
replace,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_legacy:
|
if is_legacy:
|
||||||
@@ -503,12 +503,6 @@ class RemoteTable(Table):
|
|||||||
"If you have 100M+ vectors to index,"
|
"If you have 100M+ vectors to index,"
|
||||||
"please contact us at contact@lancedb.com"
|
"please contact us at contact@lancedb.com"
|
||||||
)
|
)
|
||||||
if replace is not None:
|
|
||||||
logging.warning(
|
|
||||||
"replace is not supported on LanceDB cloud."
|
|
||||||
"Existing indexes will always be replaced."
|
|
||||||
)
|
|
||||||
|
|
||||||
idx_type = index_type.upper()
|
idx_type = index_type.upper()
|
||||||
if idx_type == "VECTOR" or idx_type == "IVF_PQ":
|
if idx_type == "VECTOR" or idx_type == "IVF_PQ":
|
||||||
config = IvfPq(
|
config = IvfPq(
|
||||||
@@ -561,7 +555,7 @@ class RemoteTable(Table):
|
|||||||
column: str,
|
column: str,
|
||||||
*,
|
*,
|
||||||
config: IndexConfigType,
|
config: IndexConfigType,
|
||||||
replace: Optional[bool] = None,
|
replace: bool = False,
|
||||||
wait_timeout: Optional[timedelta] = None,
|
wait_timeout: Optional[timedelta] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
train: bool = True,
|
train: bool = True,
|
||||||
@@ -593,7 +587,6 @@ class RemoteTable(Table):
|
|||||||
vector_column_name: str,
|
vector_column_name: str,
|
||||||
accelerator: Optional[str],
|
accelerator: Optional[str],
|
||||||
index_cache_size: Optional[int],
|
index_cache_size: Optional[int],
|
||||||
replace: Optional[bool],
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Detect if this is a legacy create_index call."""
|
"""Detect if this is a legacy create_index call."""
|
||||||
if config is not None:
|
if config is not None:
|
||||||
@@ -605,7 +598,6 @@ class RemoteTable(Table):
|
|||||||
num_sub_vectors,
|
num_sub_vectors,
|
||||||
accelerator,
|
accelerator,
|
||||||
index_cache_size,
|
index_cache_size,
|
||||||
replace,
|
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
return True
|
return True
|
||||||
@@ -720,7 +712,7 @@ class RemoteTable(Table):
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
query: list/np.ndarray/str/PIL.Image.Image, default None
|
query: list/np.ndarray/str/PIL.Image.Image, default None
|
||||||
The targeted vector to search for.
|
The targetted vector to search for.
|
||||||
|
|
||||||
- *default None*.
|
- *default None*.
|
||||||
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class Reranker(ABC):
|
|||||||
if the results haven't been executed yet or the results in arrow format.
|
if the results haven't been executed yet or the results in arrow format.
|
||||||
query : str or None,
|
query : str or None,
|
||||||
The input query. Some rerankers might not need the query to rerank.
|
The input query. Some rerankers might not need the query to rerank.
|
||||||
In that case, it can be set to None explicitly. This is intended to
|
In that case, it can be set to None explicitly. This is inteded to
|
||||||
be handled by the reranker implementations.
|
be handled by the reranker implementations.
|
||||||
deduplicate : bool, optional
|
deduplicate : bool, optional
|
||||||
Whether to deduplicate the results based on the `_rowid` column,
|
Whether to deduplicate the results based on the `_rowid` column,
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
||||||
|
|
||||||
"""Handles to SQL queries running on a remote database."""
|
|
||||||
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
import pyarrow as pa
|
|
||||||
|
|
||||||
from lancedb.background_loop import LOOP
|
|
||||||
|
|
||||||
from . import _lancedb
|
|
||||||
from .arrow import AsyncRecordBatchReader
|
|
||||||
|
|
||||||
QueryDescription = _lancedb.QueryDescription
|
|
||||||
|
|
||||||
|
|
||||||
class AsyncQuery:
|
|
||||||
"""A handle to a submitted SQL query on an asynchronous connection."""
|
|
||||||
|
|
||||||
def __init__(self, inner: "_lancedb.SqlQuery"):
|
|
||||||
self._inner = inner
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self) -> UUID:
|
|
||||||
"""The stable identifier scoped to the connection that submitted it."""
|
|
||||||
return self._inner.id
|
|
||||||
|
|
||||||
async def describe(self) -> QueryDescription:
|
|
||||||
"""Get a point-in-time description of the query."""
|
|
||||||
return await self._inner.describe()
|
|
||||||
|
|
||||||
async def reader(self) -> AsyncRecordBatchReader:
|
|
||||||
"""Wait for the initial result stream and return its Arrow reader.
|
|
||||||
|
|
||||||
Results are single-consumer. Calling this method more than once on the
|
|
||||||
same query raises an error. Later batches are streamed as they become
|
|
||||||
available without waiting for the full query to finish.
|
|
||||||
"""
|
|
||||||
return AsyncRecordBatchReader(await self._inner.reader())
|
|
||||||
|
|
||||||
async def cancel(self) -> None:
|
|
||||||
"""Request cancellation of the query."""
|
|
||||||
await self._inner.cancel()
|
|
||||||
|
|
||||||
|
|
||||||
class Query:
|
|
||||||
"""Synchronous counterpart of :class:`AsyncQuery`."""
|
|
||||||
|
|
||||||
def __init__(self, inner: AsyncQuery):
|
|
||||||
self._inner = inner
|
|
||||||
|
|
||||||
@property
|
|
||||||
def id(self) -> UUID:
|
|
||||||
"""The stable identifier scoped to the connection that submitted it."""
|
|
||||||
return self._inner.id
|
|
||||||
|
|
||||||
def describe(self) -> QueryDescription:
|
|
||||||
"""Get a point-in-time description of the query."""
|
|
||||||
return LOOP.run(self._inner.describe())
|
|
||||||
|
|
||||||
def reader(self) -> pa.RecordBatchReader:
|
|
||||||
"""Wait for the initial result stream and return a blocking reader.
|
|
||||||
|
|
||||||
Results are single-consumer. Calling this method more than once on the
|
|
||||||
same query raises an error. Later batches block only until they become
|
|
||||||
available, without waiting for the full query to finish.
|
|
||||||
"""
|
|
||||||
reader = LOOP.run(self._inner.reader())
|
|
||||||
|
|
||||||
def next_batch():
|
|
||||||
try:
|
|
||||||
return LOOP.run(reader.__anext__())
|
|
||||||
except StopAsyncIteration:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def batches():
|
|
||||||
while (batch := next_batch()) is not None:
|
|
||||||
yield batch
|
|
||||||
|
|
||||||
return pa.RecordBatchReader.from_batches(reader.schema, batches())
|
|
||||||
|
|
||||||
def cancel(self) -> None:
|
|
||||||
"""Request cancellation of the query."""
|
|
||||||
LOOP.run(self._inner.cancel())
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["AsyncQuery", "Query", "QueryDescription"]
|
|
||||||
@@ -1132,7 +1132,7 @@ class Table(ABC):
|
|||||||
num_partitions: Optional[int] = None,
|
num_partitions: Optional[int] = None,
|
||||||
num_sub_vectors: Optional[int] = None,
|
num_sub_vectors: Optional[int] = None,
|
||||||
vector_column_name: str = VECTOR_COLUMN_NAME,
|
vector_column_name: str = VECTOR_COLUMN_NAME,
|
||||||
replace: bool = True,
|
replace: bool = False,
|
||||||
accelerator: Optional[str] = None,
|
accelerator: Optional[str] = None,
|
||||||
index_cache_size: Optional[int] = None,
|
index_cache_size: Optional[int] = None,
|
||||||
*,
|
*,
|
||||||
@@ -1166,7 +1166,7 @@ class Table(ABC):
|
|||||||
The index configuration object. If provided, uses the new unified API.
|
The index configuration object. If provided, uses the new unified API.
|
||||||
Can be one of: IvfFlat, IvfPq, IvfSq, IvfRq, HnswPq, HnswSq,
|
Can be one of: IvfFlat, IvfPq, IvfSq, IvfRq, HnswPq, HnswSq,
|
||||||
BTree, Bitmap, LabelList, Fm, FTS.
|
BTree, Bitmap, LabelList, Fm, FTS.
|
||||||
replace : bool, default True
|
replace : bool, default False
|
||||||
Whether to replace an existing index on this column.
|
Whether to replace an existing index on this column.
|
||||||
wait_timeout : timedelta, optional
|
wait_timeout : timedelta, optional
|
||||||
Timeout to wait for async indexing to complete.
|
Timeout to wait for async indexing to complete.
|
||||||
@@ -1198,7 +1198,7 @@ class Table(ABC):
|
|||||||
column: str,
|
column: str,
|
||||||
*,
|
*,
|
||||||
config: IndexConfigType,
|
config: IndexConfigType,
|
||||||
replace: Optional[bool] = None,
|
replace: bool = False,
|
||||||
wait_timeout: Optional[timedelta] = None,
|
wait_timeout: Optional[timedelta] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
train: bool = True,
|
train: bool = True,
|
||||||
@@ -1260,7 +1260,7 @@ class Table(ABC):
|
|||||||
self,
|
self,
|
||||||
column: str,
|
column: str,
|
||||||
*,
|
*,
|
||||||
replace: bool = True,
|
replace: bool = False,
|
||||||
index_type: ScalarIndexType = "BTREE",
|
index_type: ScalarIndexType = "BTREE",
|
||||||
wait_timeout: Optional[timedelta] = None,
|
wait_timeout: Optional[timedelta] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
@@ -1272,7 +1272,7 @@ class Table(ABC):
|
|||||||
column : str
|
column : str
|
||||||
The column to be indexed. Must be a boolean, integer, float,
|
The column to be indexed. Must be a boolean, integer, float,
|
||||||
or string column.
|
or string column.
|
||||||
replace : bool, default True
|
replace : bool, default False
|
||||||
Replace the existing index if it exists.
|
Replace the existing index if it exists.
|
||||||
index_type: Literal["BTREE", "BITMAP", "LABEL_LIST"], default "BTREE"
|
index_type: Literal["BTREE", "BITMAP", "LABEL_LIST"], default "BTREE"
|
||||||
The type of index to create.
|
The type of index to create.
|
||||||
@@ -1619,7 +1619,7 @@ class Table(ABC):
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
query: list/np.ndarray/str/PIL.Image.Image, default None
|
query: list/np.ndarray/str/PIL.Image.Image, default None
|
||||||
The targeted vector to search for.
|
The targetted vector to search for.
|
||||||
|
|
||||||
- *default None*.
|
- *default None*.
|
||||||
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
||||||
@@ -1793,9 +1793,6 @@ class Table(ABC):
|
|||||||
The result has the same length and order as ``row_ids``. Null blobs
|
The result has the same length and order as ``row_ids``. Null blobs
|
||||||
produce null slots; valid empty blobs produce ``b""``.
|
produce null slots; valid empty blobs produce ``b""``.
|
||||||
|
|
||||||
``_rowid`` values stay valid after compaction when the table has stable
|
|
||||||
row ids.
|
|
||||||
|
|
||||||
Convenience for small payloads. For large values use
|
Convenience for small payloads. For large values use
|
||||||
:meth:`fetch_blob_files`.
|
:meth:`fetch_blob_files`.
|
||||||
"""
|
"""
|
||||||
@@ -1813,9 +1810,6 @@ class Table(ABC):
|
|||||||
The result has the same length and order as ``requests``; null blobs
|
The result has the same length and order as ``requests``; null blobs
|
||||||
produce null slots and empty ranges on non-null blobs produce ``b""``.
|
produce null slots and empty ranges on non-null blobs produce ``b""``.
|
||||||
|
|
||||||
``_rowid`` values stay valid after compaction when the table has stable
|
|
||||||
row ids.
|
|
||||||
|
|
||||||
Row IDs can be obtained from a query with ``with_row_id(True)``. This
|
Row IDs can be obtained from a query with ``with_row_id(True)``. This
|
||||||
API is currently supported only by local tables.
|
API is currently supported only by local tables.
|
||||||
"""
|
"""
|
||||||
@@ -1831,9 +1825,6 @@ class Table(ABC):
|
|||||||
``_rowid`` or a ``_lance_row_id`` field on the blob descriptor. Null
|
``_rowid`` or a ``_lance_row_id`` field on the blob descriptor. Null
|
||||||
rows are ``None``. Remote tables require LanceDB Cloud server 0.5.0 or
|
rows are ``None``. Remote tables require LanceDB Cloud server 0.5.0 or
|
||||||
newer.
|
newer.
|
||||||
|
|
||||||
``_rowid`` values stay valid after compaction when the table has stable
|
|
||||||
row ids.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -2997,7 +2988,7 @@ class LanceTable(Table):
|
|||||||
num_partitions: Optional[int] = None,
|
num_partitions: Optional[int] = None,
|
||||||
num_sub_vectors: Optional[int] = None,
|
num_sub_vectors: Optional[int] = None,
|
||||||
vector_column_name: str = VECTOR_COLUMN_NAME,
|
vector_column_name: str = VECTOR_COLUMN_NAME,
|
||||||
replace: bool = True,
|
replace: bool = False,
|
||||||
accelerator: Optional[str] = None,
|
accelerator: Optional[str] = None,
|
||||||
index_cache_size: Optional[int] = None,
|
index_cache_size: Optional[int] = None,
|
||||||
num_bits: int = 8,
|
num_bits: int = 8,
|
||||||
@@ -3039,7 +3030,7 @@ class LanceTable(Table):
|
|||||||
The index configuration object. If provided, uses the new unified API.
|
The index configuration object. If provided, uses the new unified API.
|
||||||
Can be one of: IvfFlat, IvfPq, IvfSq, IvfRq, HnswPq, HnswSq,
|
Can be one of: IvfFlat, IvfPq, IvfSq, IvfRq, HnswPq, HnswSq,
|
||||||
BTree, Bitmap, LabelList, Fm, FTS.
|
BTree, Bitmap, LabelList, Fm, FTS.
|
||||||
replace : bool, default True
|
replace : bool, default False
|
||||||
Whether to replace an existing index on this column.
|
Whether to replace an existing index on this column.
|
||||||
wait_timeout : timedelta, optional
|
wait_timeout : timedelta, optional
|
||||||
Timeout to wait for async indexing to complete.
|
Timeout to wait for async indexing to complete.
|
||||||
@@ -3178,7 +3169,7 @@ class LanceTable(Table):
|
|||||||
column: str,
|
column: str,
|
||||||
*,
|
*,
|
||||||
config: IndexConfigType,
|
config: IndexConfigType,
|
||||||
replace: Optional[bool] = None,
|
replace: bool = False,
|
||||||
wait_timeout: Optional[timedelta] = None,
|
wait_timeout: Optional[timedelta] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
train: bool = True,
|
train: bool = True,
|
||||||
@@ -3433,7 +3424,7 @@ class LanceTable(Table):
|
|||||||
self,
|
self,
|
||||||
column: str,
|
column: str,
|
||||||
*,
|
*,
|
||||||
replace: bool = True,
|
replace: bool = False,
|
||||||
index_type: ScalarIndexType = "BTREE",
|
index_type: ScalarIndexType = "BTREE",
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
):
|
):
|
||||||
@@ -3841,7 +3832,7 @@ class LanceTable(Table):
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
query: list/np.ndarray/str/PIL.Image.Image, default None
|
query: list/np.ndarray/str/PIL.Image.Image, default None
|
||||||
The targeted vector to search for.
|
The targetted vector to search for.
|
||||||
|
|
||||||
- *default None*.
|
- *default None*.
|
||||||
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
||||||
@@ -5322,7 +5313,7 @@ class AsyncTable:
|
|||||||
self,
|
self,
|
||||||
column: str,
|
column: str,
|
||||||
*,
|
*,
|
||||||
replace: Optional[bool] = None,
|
replace: bool = False,
|
||||||
config: Optional[
|
config: Optional[
|
||||||
Union[
|
Union[
|
||||||
IvfFlat,
|
IvfFlat,
|
||||||
@@ -5353,14 +5344,14 @@ class AsyncTable:
|
|||||||
----------
|
----------
|
||||||
column: str
|
column: str
|
||||||
The column to index.
|
The column to index.
|
||||||
replace: bool, default True
|
replace: bool, default False
|
||||||
Whether to replace the existing index
|
Whether to replace the existing index
|
||||||
|
|
||||||
If this is false, and another index already exists on the same columns
|
If this is false, and another index already exists on the same columns
|
||||||
and the same name, then an error will be returned. This is true even if
|
and the same name, then an error will be returned. This is true even if
|
||||||
that index is out of date.
|
that index is out of date.
|
||||||
|
|
||||||
The default is True
|
The default is False
|
||||||
config: default None
|
config: default None
|
||||||
For advanced configuration you can specify the type of index you would
|
For advanced configuration you can specify the type of index you would
|
||||||
like to create. You can also specify index-specific parameters when
|
like to create. You can also specify index-specific parameters when
|
||||||
@@ -5418,7 +5409,7 @@ class AsyncTable:
|
|||||||
self,
|
self,
|
||||||
column: str,
|
column: str,
|
||||||
*,
|
*,
|
||||||
replace: Optional[bool] = None,
|
replace: bool = False,
|
||||||
config: Optional[
|
config: Optional[
|
||||||
Union[
|
Union[
|
||||||
IvfFlat,
|
IvfFlat,
|
||||||
@@ -5638,7 +5629,7 @@ class AsyncTable:
|
|||||||
if fill_value is None:
|
if fill_value is None:
|
||||||
fill_value = 0.0
|
fill_value = 0.0
|
||||||
|
|
||||||
# _sanitize_data is an old code path, but we will use it until the
|
# _santitize_data is an old code path, but we will use it until the
|
||||||
# new code path is ready.
|
# new code path is ready.
|
||||||
if mode == "overwrite":
|
if mode == "overwrite":
|
||||||
# For overwrite, apply the same preprocessing as create_table
|
# For overwrite, apply the same preprocessing as create_table
|
||||||
@@ -5814,7 +5805,7 @@ class AsyncTable:
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
query: list/np.ndarray/str/PIL.Image.Image, default None
|
query: list/np.ndarray/str/PIL.Image.Image, default None
|
||||||
The targeted vector to search for.
|
The targetted vector to search for.
|
||||||
|
|
||||||
- *default None*.
|
- *default None*.
|
||||||
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
Acceptable types are: list, np.ndarray, PIL.Image.Image
|
||||||
|
|||||||
@@ -66,25 +66,6 @@ def _row_ids_by_id(table):
|
|||||||
return dict(zip(hits["id"].to_pylist(), hits["_rowid"].to_pylist()))
|
return dict(zip(hits["id"].to_pylist(), hits["_rowid"].to_pylist()))
|
||||||
|
|
||||||
|
|
||||||
def _assert_missing_blob_row_ids(exc_info):
|
|
||||||
message = str(exc_info.value)
|
|
||||||
assert "row ids" in message
|
|
||||||
assert "rowaddr" not in message
|
|
||||||
assert "fragment" not in message
|
|
||||||
|
|
||||||
|
|
||||||
def _assert_fetch_apis_reject_missing_row_ids(table, row_ids):
|
|
||||||
with pytest.raises(ValueError) as exc_info:
|
|
||||||
table.fetch_blobs("image", row_ids)
|
|
||||||
_assert_missing_blob_row_ids(exc_info)
|
|
||||||
with pytest.raises(ValueError) as exc_info:
|
|
||||||
table.fetch_blob_files("image", row_ids)
|
|
||||||
_assert_missing_blob_row_ids(exc_info)
|
|
||||||
with pytest.raises(ValueError) as exc_info:
|
|
||||||
table.fetch_blob_ranges("image", [(row_id, 0, 1) for row_id in row_ids])
|
|
||||||
_assert_missing_blob_row_ids(exc_info)
|
|
||||||
|
|
||||||
|
|
||||||
def test_blob_factory_declares_v2_field():
|
def test_blob_factory_declares_v2_field():
|
||||||
field = lancedb.blob("image")
|
field = lancedb.blob("image")
|
||||||
assert isinstance(field.type, pa.ExtensionType)
|
assert isinstance(field.type, pa.ExtensionType)
|
||||||
@@ -297,10 +278,7 @@ def test_blob_v2_projection_sources_use_typed_column_name():
|
|||||||
|
|
||||||
|
|
||||||
def _legacy_v1_table(name):
|
def _legacy_v1_table(name):
|
||||||
# Legacy v1 blob columns are only writable at file version <= 2.1.
|
db = lancedb.connect("memory:///")
|
||||||
db = lancedb.connect(
|
|
||||||
"memory:///", storage_options={"new_table_data_storage_version": "2.1"}
|
|
||||||
)
|
|
||||||
schema = pa.schema(
|
schema = pa.schema(
|
||||||
[
|
[
|
||||||
pa.field("id", pa.int64()),
|
pa.field("id", pa.int64()),
|
||||||
@@ -713,25 +691,6 @@ def test_fetch_blobs_accepts_query_result():
|
|||||||
assert {blobs[i].as_py() for i in range(len(blobs))} == {b"gamma"}
|
assert {blobs[i].as_py() for i in range(len(blobs))} == {b"gamma"}
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_blobs_after_compact_with_stable_row_ids(tmp_path):
|
|
||||||
db = lancedb.connect(
|
|
||||||
tmp_path, storage_options={"new_table_enable_stable_row_ids": "true"}
|
|
||||||
)
|
|
||||||
schema = pa.schema([pa.field("id", pa.int64()), lancedb.blob("image")])
|
|
||||||
table = db.create_table("t", schema=schema)
|
|
||||||
table.add([{"id": 1, "image": b"frag-one"}])
|
|
||||||
table.add([{"id": 2, "image": b"frag-two"}])
|
|
||||||
by_id = _row_ids_by_id(table)
|
|
||||||
ids = [by_id[1], by_id[2]]
|
|
||||||
|
|
||||||
table.optimize()
|
|
||||||
|
|
||||||
blobs = table.fetch_blobs("image", ids)
|
|
||||||
assert blobs.to_pylist() == [b"frag-one", b"frag-two"]
|
|
||||||
ranges = table.fetch_blob_ranges("image", [(ids[0], 5, 3), (ids[1], 5, 3)])
|
|
||||||
assert ranges.to_pylist() == [b"one", b"two"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_blobs_preserves_null_and_empty_values():
|
def test_fetch_blobs_preserves_null_and_empty_values():
|
||||||
table = _blob_table(
|
table = _blob_table(
|
||||||
"nulls",
|
"nulls",
|
||||||
@@ -780,25 +739,8 @@ def test_fetch_blob_ranges_validates_requests():
|
|||||||
with pytest.raises(ValueError, match="offset \\+ length overflowed"):
|
with pytest.raises(ValueError, match="offset \\+ length overflowed"):
|
||||||
table.fetch_blob_ranges("image", [(row_id, 2**64 - 1, 1)])
|
table.fetch_blob_ranges("image", [(row_id, 2**64 - 1, 1)])
|
||||||
|
|
||||||
with pytest.raises(ValueError) as exc_info:
|
with pytest.raises(ValueError, match="row IDs"):
|
||||||
table.fetch_blob_ranges("image", [(2**64 - 1, 0, 1)])
|
table.fetch_blob_ranges("image", [(2**64 - 1, 0, 1)])
|
||||||
_assert_missing_blob_row_ids(exc_info)
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_blob_apis_reject_missing_fragment_row_addr():
|
|
||||||
table = _blob_table("missing_frag", [{"id": 1, "image": b"x"}])
|
|
||||||
live = _row_ids_by_id(table)[1]
|
|
||||||
_assert_fetch_apis_reject_missing_row_ids(table, [1 << 32, live])
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_blob_apis_reject_deleted_row_ids():
|
|
||||||
table = _blob_table(
|
|
||||||
"deleted_rows",
|
|
||||||
[{"id": 1, "image": b"one"}, {"id": 2, "image": b"two"}],
|
|
||||||
)
|
|
||||||
by_id = _row_ids_by_id(table)
|
|
||||||
table.delete("id = 2")
|
|
||||||
_assert_fetch_apis_reject_missing_row_ids(table, [by_id[2], by_id[1]])
|
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_blob_ranges_empty_requests_returns_empty_array():
|
def test_fetch_blob_ranges_empty_requests_returns_empty_array():
|
||||||
|
|||||||
@@ -327,8 +327,8 @@ def test_embedding_function_with_pandas(tmp_path):
|
|||||||
) -> List[np.array]:
|
) -> List[np.array]:
|
||||||
return [np.random.randn(self.ndims()).tolist() for _ in range(len(texts))]
|
return [np.random.randn(self.ndims()).tolist() for _ in range(len(texts))]
|
||||||
|
|
||||||
registry = get_registry()
|
registery = get_registry()
|
||||||
func = registry.get("mock-embedding").create()
|
func = registery.get("mock-embedding").create()
|
||||||
|
|
||||||
class TestSchema(LanceModel):
|
class TestSchema(LanceModel):
|
||||||
text: str = func.SourceField()
|
text: str = func.SourceField()
|
||||||
@@ -394,9 +394,9 @@ def test_multiple_embeddings_for_pandas(tmp_path):
|
|||||||
) -> List[np.array]:
|
) -> List[np.array]:
|
||||||
return [np.random.randn(self.ndims()).tolist() for _ in range(len(texts))]
|
return [np.random.randn(self.ndims()).tolist() for _ in range(len(texts))]
|
||||||
|
|
||||||
registry = get_registry()
|
registery = get_registry()
|
||||||
func1 = registry.get("mock-embedding").create()
|
func1 = registery.get("mock-embedding").create()
|
||||||
func2 = registry.get("mock-embedding2").create()
|
func2 = registery.get("mock-embedding2").create()
|
||||||
|
|
||||||
class TestSchema(LanceModel):
|
class TestSchema(LanceModel):
|
||||||
text: str = func1.SourceField()
|
text: str = func1.SourceField()
|
||||||
|
|||||||
@@ -850,43 +850,6 @@ def test_named_struct_function_can_include_a_blob_result_field():
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_named_struct_function_preserves_nullable_result_fields():
|
|
||||||
@udf(
|
|
||||||
input_schema=pa.schema([pa.field("value", pa.int64(), nullable=False)]),
|
|
||||||
output_schema=pa.schema(
|
|
||||||
[
|
|
||||||
pa.field("result", pa.int64(), nullable=True),
|
|
||||||
pa.field("failure_code", pa.int32(), nullable=False),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def nullable_result(value):
|
|
||||||
return {"result": value, "failure_code": 0}
|
|
||||||
|
|
||||||
output = nullable_result.registration_request.signature.output
|
|
||||||
assert [(field.name, field.nullable) for field in output.fields] == [
|
|
||||||
("result", True),
|
|
||||||
("failure_code", False),
|
|
||||||
]
|
|
||||||
|
|
||||||
@udf(
|
|
||||||
input_schema=pa.schema([pa.field("value", pa.int64(), nullable=False)]),
|
|
||||||
output_schema=pa.schema(
|
|
||||||
[
|
|
||||||
pa.field("result", pa.int64(), nullable=True),
|
|
||||||
pa.field("failure_code", pa.int32(), nullable=True),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
def all_nullable(value):
|
|
||||||
return {"result": value, "failure_code": None}
|
|
||||||
|
|
||||||
assert all(
|
|
||||||
field.nullable
|
|
||||||
for field in all_nullable.registration_request.signature.output.fields
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_metadata_marked_blob_field_uses_the_semantic_type():
|
def test_metadata_marked_blob_field_uses_the_semantic_type():
|
||||||
extension = lancedb.blob("image", nullable=False).type
|
extension = lancedb.blob("image", nullable=False).type
|
||||||
storage = (
|
storage = (
|
||||||
|
|||||||
@@ -1011,13 +1011,8 @@ def test_fts_ngram(mem_db: DBConnection):
|
|||||||
assert set(r["text"] for r in results) == {"lance database", "lance is cool"}
|
assert set(r["text"] for r in results) == {"lance database", "lance is cool"}
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
table.search(
|
table.search("nce", query_type="fts").limit(10).to_list()
|
||||||
"nce", # spellchecker:disable-line
|
) # spellchecker:disable-line
|
||||||
query_type="fts",
|
|
||||||
)
|
|
||||||
.limit(10)
|
|
||||||
.to_list()
|
|
||||||
)
|
|
||||||
assert len(results) == 2
|
assert len(results) == 2
|
||||||
assert set(r["text"] for r in results) == {"lance database", "lance is cool"}
|
assert set(r["text"] for r in results) == {"lance database", "lance is cool"}
|
||||||
|
|
||||||
@@ -1039,13 +1034,8 @@ def test_fts_ngram(mem_db: DBConnection):
|
|||||||
assert set(r["text"] for r in results) == {"lance database", "lance is cool"}
|
assert set(r["text"] for r in results) == {"lance database", "lance is cool"}
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
table.search(
|
table.search("nce", query_type="fts").limit(10).to_list()
|
||||||
"nce", # spellchecker:disable-line
|
) # spellchecker:disable-line
|
||||||
query_type="fts",
|
|
||||||
)
|
|
||||||
.limit(10)
|
|
||||||
.to_list()
|
|
||||||
)
|
|
||||||
assert len(results) == 0
|
assert len(results) == 0
|
||||||
|
|
||||||
results = table.search("la", query_type="fts").limit(10).to_list()
|
results = table.search("la", query_type="fts").limit(10).to_list()
|
||||||
|
|||||||
@@ -54,10 +54,7 @@ class TestOAuthProvider:
|
|||||||
provider = OAuthProvider(fetcher)
|
provider = OAuthProvider(fetcher)
|
||||||
headers = provider.get_headers()
|
headers = provider.get_headers()
|
||||||
|
|
||||||
assert headers == {
|
assert headers == {"Authorization": "Bearer token123"}
|
||||||
"Authorization": "Bearer token123",
|
|
||||||
"x-lancedb-credential-type": "oidc",
|
|
||||||
}
|
|
||||||
assert provider._current_token == "token123"
|
assert provider._current_token == "token123"
|
||||||
assert provider._token_expires_at is not None
|
assert provider._token_expires_at is not None
|
||||||
|
|
||||||
@@ -76,20 +73,14 @@ class TestOAuthProvider:
|
|||||||
|
|
||||||
# First call
|
# First call
|
||||||
headers1 = provider.get_headers()
|
headers1 = provider.get_headers()
|
||||||
assert headers1 == {
|
assert headers1 == {"Authorization": "Bearer token1"}
|
||||||
"Authorization": "Bearer token1",
|
|
||||||
"x-lancedb-credential-type": "oidc",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Wait for token to expire
|
# Wait for token to expire
|
||||||
time.sleep(1.1)
|
time.sleep(1.1)
|
||||||
|
|
||||||
# Second call should refresh
|
# Second call should refresh
|
||||||
headers2 = provider.get_headers()
|
headers2 = provider.get_headers()
|
||||||
assert headers2 == {
|
assert headers2 == {"Authorization": "Bearer token2"}
|
||||||
"Authorization": "Bearer token2",
|
|
||||||
"x-lancedb-credential-type": "oidc",
|
|
||||||
}
|
|
||||||
assert call_count == 2
|
assert call_count == 2
|
||||||
|
|
||||||
def test_no_expiry_info(self):
|
def test_no_expiry_info(self):
|
||||||
@@ -101,18 +92,12 @@ class TestOAuthProvider:
|
|||||||
provider = OAuthProvider(fetcher)
|
provider = OAuthProvider(fetcher)
|
||||||
headers = provider.get_headers()
|
headers = provider.get_headers()
|
||||||
|
|
||||||
assert headers == {
|
assert headers == {"Authorization": "Bearer permanent_token"}
|
||||||
"Authorization": "Bearer permanent_token",
|
|
||||||
"x-lancedb-credential-type": "oidc",
|
|
||||||
}
|
|
||||||
assert provider._token_expires_at is None
|
assert provider._token_expires_at is None
|
||||||
|
|
||||||
# Should not refresh on second call
|
# Should not refresh on second call
|
||||||
headers2 = provider.get_headers()
|
headers2 = provider.get_headers()
|
||||||
assert headers2 == {
|
assert headers2 == {"Authorization": "Bearer permanent_token"}
|
||||||
"Authorization": "Bearer permanent_token",
|
|
||||||
"x-lancedb-credential-type": "oidc",
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_missing_access_token(self):
|
def test_missing_access_token(self):
|
||||||
"""Test error handling when access_token is missing."""
|
"""Test error handling when access_token is missing."""
|
||||||
@@ -136,10 +121,7 @@ class TestOAuthProvider:
|
|||||||
provider = OAuthProvider(fetcher)
|
provider = OAuthProvider(fetcher)
|
||||||
headers = provider.get_headers()
|
headers = provider.get_headers()
|
||||||
|
|
||||||
assert headers == {
|
assert headers == {"Authorization": "Bearer sync_token"}
|
||||||
"Authorization": "Bearer sync_token",
|
|
||||||
"x-lancedb-credential-type": "oidc",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TestClientConfigIntegration:
|
class TestClientConfigIntegration:
|
||||||
|
|||||||
@@ -203,93 +203,6 @@ async def test_async_hybrid_query_default_limit(table: AsyncTable):
|
|||||||
assert texts.count("a") == 1
|
assert texts.count("a") == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_async_hybrid_query_offset(table: AsyncTable):
|
|
||||||
# The offset window of a hybrid query must be a suffix of the same query
|
|
||||||
# run without an offset. Skipping the first rows of each sub-query instead
|
|
||||||
# of the first rows of the fused result silently changes which rows land in
|
|
||||||
# the window.
|
|
||||||
full = await (
|
|
||||||
table.query()
|
|
||||||
.nearest_to([0.0, 0.4])
|
|
||||||
.nearest_to_text("dog")
|
|
||||||
.limit(4)
|
|
||||||
.with_row_id()
|
|
||||||
.to_arrow()
|
|
||||||
)
|
|
||||||
assert len(full) == 4
|
|
||||||
|
|
||||||
second_page = await (
|
|
||||||
table.query()
|
|
||||||
.nearest_to([0.0, 0.4])
|
|
||||||
.nearest_to_text("dog")
|
|
||||||
.offset(2)
|
|
||||||
.limit(2)
|
|
||||||
.with_row_id()
|
|
||||||
.to_arrow()
|
|
||||||
)
|
|
||||||
assert second_page["_rowid"].to_pylist() == full["_rowid"].to_pylist()[2:]
|
|
||||||
|
|
||||||
first_page = await (
|
|
||||||
table.query()
|
|
||||||
.nearest_to([0.0, 0.4])
|
|
||||||
.nearest_to_text("dog")
|
|
||||||
.limit(2)
|
|
||||||
.with_row_id()
|
|
||||||
.to_arrow()
|
|
||||||
)
|
|
||||||
# Paging through the result must visit every row exactly once: no row
|
|
||||||
# repeated from the previous page and none dropped between the two.
|
|
||||||
paged = first_page["_rowid"].to_pylist() + second_page["_rowid"].to_pylist()
|
|
||||||
assert sorted(paged) == sorted(full["_rowid"].to_pylist())
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_async_hybrid_query_fts_first_default_limit(table: AsyncTable):
|
|
||||||
# nearest_to() and nearest_to_text() build their new sibling sub-query from
|
|
||||||
# scratch, and that is the sub-query the default limit ends up on. So the
|
|
||||||
# side that carries the limit depends on the order the hybrid query was
|
|
||||||
# built in, and looking at only one side loses the limit for half the ways
|
|
||||||
# a hybrid query can be written. Without a limit the combined results are
|
|
||||||
# not truncated at all and the whole union of both candidate lists is
|
|
||||||
# returned.
|
|
||||||
await table.add([{"text": "dog", "vector": [50.0 + i, 50.0]} for i in range(10)])
|
|
||||||
|
|
||||||
result = await (
|
|
||||||
table.query().nearest_to_text("dog").nearest_to([0.1, 0.1]).to_arrow()
|
|
||||||
)
|
|
||||||
assert len(result) == 10
|
|
||||||
|
|
||||||
offset_result = await (
|
|
||||||
table.query().nearest_to_text("dog").nearest_to([0.1, 0.1]).offset(2).to_arrow()
|
|
||||||
)
|
|
||||||
assert len(offset_result) == 10
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_async_hybrid_query_explain_plan_matches_execution(table: AsyncTable):
|
|
||||||
# Paging rewrites the sub-queries: each one fetches limit + offset rows with
|
|
||||||
# no offset of its own, and the window is sliced out after fusion. The plans
|
|
||||||
# have to be built from those rewritten sub-queries, otherwise explain_plan
|
|
||||||
# and analyze_plan describe a query that is never run.
|
|
||||||
query = (
|
|
||||||
table.query().nearest_to([0.0, 0.4]).nearest_to_text("dog").offset(2).limit(2)
|
|
||||||
)
|
|
||||||
await query.to_arrow()
|
|
||||||
|
|
||||||
plan = await query.explain_plan()
|
|
||||||
assert [
|
|
||||||
line.strip() for line in plan.splitlines() if "GlobalLimitExec" in line
|
|
||||||
] == [
|
|
||||||
"GlobalLimitExec: skip=0, fetch=4",
|
|
||||||
"GlobalLimitExec: skip=0, fetch=4",
|
|
||||||
]
|
|
||||||
|
|
||||||
analyzed = await query.analyze_plan()
|
|
||||||
assert analyzed.count("skip=0, fetch=4") == 2
|
|
||||||
assert "skip=2" not in analyzed
|
|
||||||
|
|
||||||
|
|
||||||
def test_hybrid_query_offset(sync_table: Table):
|
def test_hybrid_query_offset(sync_table: Table):
|
||||||
# The offset window of a hybrid query must be a suffix of the same query
|
# The offset window of a hybrid query must be a suffix of the same query
|
||||||
# run without an offset -- it must not be silently ignored.
|
# run without an offset -- it must not be silently ignored.
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ async def test_create_index_async_returns_done_job(some_table: AsyncTable):
|
|||||||
async def test_create_scalar_index(some_table: AsyncTable):
|
async def test_create_scalar_index(some_table: AsyncTable):
|
||||||
# Can create
|
# Can create
|
||||||
await some_table.create_index("id")
|
await some_table.create_index("id")
|
||||||
|
# Can't recreate by default
|
||||||
|
with pytest.raises(RuntimeError, match="already exists"):
|
||||||
|
await some_table.create_index("id")
|
||||||
# Can recreate if replace=True
|
# Can recreate if replace=True
|
||||||
await some_table.create_index("id", replace=True)
|
await some_table.create_index("id", replace=True)
|
||||||
indices = await some_table.list_indices()
|
indices = await some_table.list_indices()
|
||||||
@@ -110,7 +113,7 @@ async def test_create_scalar_index(some_table: AsyncTable):
|
|||||||
with pytest.raises(RuntimeError, match="already exists"):
|
with pytest.raises(RuntimeError, match="already exists"):
|
||||||
await some_table.create_index("id", replace=False)
|
await some_table.create_index("id", replace=False)
|
||||||
# can also specify index type
|
# can also specify index type
|
||||||
await some_table.create_index("id", config=BTree())
|
await some_table.create_index("id", config=BTree(), replace=True)
|
||||||
|
|
||||||
await some_table.drop_index("id_idx")
|
await some_table.drop_index("id_idx")
|
||||||
indices = await some_table.list_indices()
|
indices = await some_table.list_indices()
|
||||||
@@ -351,13 +354,18 @@ async def test_full_text_search_index(some_table: AsyncTable):
|
|||||||
async def test_create_vector_index(some_table: AsyncTable):
|
async def test_create_vector_index(some_table: AsyncTable):
|
||||||
# Can create
|
# Can create
|
||||||
await some_table.create_index("vector")
|
await some_table.create_index("vector")
|
||||||
|
# Can't recreate by default
|
||||||
|
with pytest.raises(RuntimeError, match="already exists"):
|
||||||
|
await some_table.create_index("vector")
|
||||||
# Can recreate if replace=True
|
# Can recreate if replace=True
|
||||||
await some_table.create_index("vector", replace=True)
|
await some_table.create_index("vector", replace=True)
|
||||||
# Can't recreate if replace=False
|
# Can't recreate if replace=False
|
||||||
with pytest.raises(RuntimeError, match="already exists"):
|
with pytest.raises(RuntimeError, match="already exists"):
|
||||||
await some_table.create_index("vector", replace=False)
|
await some_table.create_index("vector", replace=False)
|
||||||
# Can also specify index type
|
# Can also specify index type
|
||||||
await some_table.create_index("vector", config=IvfPq(num_partitions=100))
|
await some_table.create_index(
|
||||||
|
"vector", config=IvfPq(num_partitions=100), replace=True
|
||||||
|
)
|
||||||
indices = await some_table.list_indices()
|
indices = await some_table.list_indices()
|
||||||
assert len(indices) == 1
|
assert len(indices) == 1
|
||||||
assert indices[0].index_type == "IvfPq"
|
assert indices[0].index_type == "IvfPq"
|
||||||
|
|||||||
@@ -193,13 +193,7 @@ class TestNamespaceConnection:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Legacy v1 blob columns are only writable at file version <= 2.1.
|
table = db.create_table("blob_table", data, namespace_path=["test_ns"])
|
||||||
table = db.create_table(
|
|
||||||
"blob_table",
|
|
||||||
data,
|
|
||||||
namespace_path=["test_ns"],
|
|
||||||
storage_options={"new_table_data_storage_version": "2.1"},
|
|
||||||
)
|
|
||||||
df = table.to_pandas(blob_mode="lazy").sort_values("id")
|
df = table.to_pandas(blob_mode="lazy").sort_values("id")
|
||||||
|
|
||||||
blob = df["blob"].iloc[0]
|
blob = df["blob"].iloc[0]
|
||||||
|
|||||||
@@ -40,10 +40,6 @@ from utils import exception_output
|
|||||||
from importlib.util import find_spec
|
from importlib.util import find_spec
|
||||||
|
|
||||||
|
|
||||||
# Legacy v1 blob columns are only writable at file version <= 2.1.
|
|
||||||
LEGACY_BLOB_STORAGE_OPTIONS = {"new_table_data_storage_version": "2.1"}
|
|
||||||
|
|
||||||
|
|
||||||
def _blob_query_data():
|
def _blob_query_data():
|
||||||
return pa.table(
|
return pa.table(
|
||||||
{
|
{
|
||||||
@@ -123,17 +119,13 @@ def _assert_blob_bytes_projection(df):
|
|||||||
|
|
||||||
def _blob_query_table(db, name, blob_schema):
|
def _blob_query_table(db, name, blob_schema):
|
||||||
if blob_schema == "v1":
|
if blob_schema == "v1":
|
||||||
return db.create_table(
|
return db.create_table(name, _blob_query_data())
|
||||||
name, _blob_query_data(), storage_options=LEGACY_BLOB_STORAGE_OPTIONS
|
|
||||||
)
|
|
||||||
return _create_blob_v2_query_table(db, name)
|
return _create_blob_v2_query_table(db, name)
|
||||||
|
|
||||||
|
|
||||||
async def _blob_query_table_async(db, name, blob_schema):
|
async def _blob_query_table_async(db, name, blob_schema):
|
||||||
if blob_schema == "v1":
|
if blob_schema == "v1":
|
||||||
return await db.create_table(
|
return await db.create_table(name, _blob_query_data())
|
||||||
name, _blob_query_data(), storage_options=LEGACY_BLOB_STORAGE_OPTIONS
|
|
||||||
)
|
|
||||||
return await _create_blob_v2_query_table_async(db, name)
|
return await _create_blob_v2_query_table_async(db, name)
|
||||||
|
|
||||||
|
|
||||||
@@ -283,9 +275,7 @@ async def test_query_to_pandas_kwargs(table, table_async):
|
|||||||
def test_plain_scan_query_to_pandas_blob_modes(tmp_db, blob_mode):
|
def test_plain_scan_query_to_pandas_blob_modes(tmp_db, blob_mode):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
table = tmp_db.create_table(
|
table = tmp_db.create_table(
|
||||||
f"test_query_to_pandas_blob_{blob_mode}",
|
f"test_query_to_pandas_blob_{blob_mode}", _blob_query_data()
|
||||||
_blob_query_data(),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
df = (
|
df = (
|
||||||
@@ -332,9 +322,7 @@ def test_plain_scan_query_to_pandas_blob_mode_does_not_collect_arrow(
|
|||||||
):
|
):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
table = tmp_db.create_table(
|
table = tmp_db.create_table(
|
||||||
"test_query_to_pandas_blob_no_arrow_collect",
|
"test_query_to_pandas_blob_no_arrow_collect", _blob_query_data()
|
||||||
_blob_query_data(),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
)
|
||||||
query = table.search().where("id = 1").select(["id", "blob"])
|
query = table.search().where("id = 1").select(["id", "blob"])
|
||||||
|
|
||||||
@@ -359,9 +347,7 @@ def test_plain_scan_query_to_pandas_blob_descriptions_flatten_uses_scanner(
|
|||||||
):
|
):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
table = tmp_db.create_table(
|
table = tmp_db.create_table(
|
||||||
"test_query_to_pandas_blob_desc_flatten",
|
"test_query_to_pandas_blob_desc_flatten", _blob_query_data()
|
||||||
_blob_query_data(),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
)
|
||||||
query = table.search().where("id = 1").select(["id", "blob"])
|
query = table.search().where("id = 1").select(["id", "blob"])
|
||||||
|
|
||||||
@@ -379,11 +365,7 @@ def test_plain_scan_query_to_pandas_blob_descriptions_flatten_uses_scanner(
|
|||||||
def test_plain_scan_query_to_pandas_scanner_state(tmp_db):
|
def test_plain_scan_query_to_pandas_scanner_state(tmp_db):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
data = _blob_query_data()
|
data = _blob_query_data()
|
||||||
table = tmp_db.create_table(
|
table = tmp_db.create_table("test_query_to_pandas_scanner_state", data.slice(0, 2))
|
||||||
"test_query_to_pandas_scanner_state",
|
|
||||||
data.slice(0, 2),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
|
||||||
table.add(data.slice(2, 2))
|
table.add(data.slice(2, 2))
|
||||||
|
|
||||||
fragments = table.to_lance().get_fragments()
|
fragments = table.to_lance().get_fragments()
|
||||||
@@ -418,9 +400,7 @@ def test_plain_scan_query_to_pandas_scanner_state(tmp_db):
|
|||||||
async def test_async_plain_scan_query_to_pandas_blob_projection(tmp_db_async):
|
async def test_async_plain_scan_query_to_pandas_blob_projection(tmp_db_async):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
table = await tmp_db_async.create_table(
|
table = await tmp_db_async.create_table(
|
||||||
"test_async_query_to_pandas_blob_projection",
|
"test_async_query_to_pandas_blob_projection", _blob_query_data()
|
||||||
_blob_query_data(),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy_df = await (
|
lazy_df = await (
|
||||||
@@ -472,9 +452,7 @@ async def test_async_plain_scan_query_to_pandas_blob_mode_does_not_collect_arrow
|
|||||||
):
|
):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
table = await tmp_db_async.create_table(
|
table = await tmp_db_async.create_table(
|
||||||
"test_async_query_to_pandas_blob_no_arrow_collect",
|
"test_async_query_to_pandas_blob_no_arrow_collect", _blob_query_data()
|
||||||
_blob_query_data(),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
)
|
||||||
query = table.query().where("id = 1").select(["id", "blob"])
|
query = table.query().where("id = 1").select(["id", "blob"])
|
||||||
|
|
||||||
@@ -496,11 +474,7 @@ async def test_async_plain_scan_query_to_pandas_blob_mode_does_not_collect_arrow
|
|||||||
|
|
||||||
def test_vector_query_to_pandas_blob_mode_requires_native_path(tmp_db):
|
def test_vector_query_to_pandas_blob_mode_requires_native_path(tmp_db):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
table = tmp_db.create_table(
|
table = tmp_db.create_table("test_vector_query_blob_mode", _blob_query_data())
|
||||||
"test_vector_query_blob_mode",
|
|
||||||
_blob_query_data(),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="Lance native pandas conversion"):
|
with pytest.raises(RuntimeError, match="Lance native pandas conversion"):
|
||||||
table.search([1.0, 0.0]).select(["blob", "vector"]).limit(1).to_pandas(
|
table.search([1.0, 0.0]).select(["blob", "vector"]).limit(1).to_pandas(
|
||||||
@@ -511,9 +485,7 @@ def test_vector_query_to_pandas_blob_mode_requires_native_path(tmp_db):
|
|||||||
def test_vector_query_to_pandas_blob_descriptions_requires_plain_scan(tmp_db):
|
def test_vector_query_to_pandas_blob_descriptions_requires_plain_scan(tmp_db):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
table = tmp_db.create_table(
|
table = tmp_db.create_table(
|
||||||
"test_vector_query_blob_descriptions",
|
"test_vector_query_blob_descriptions", _blob_query_data()
|
||||||
_blob_query_data(),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(RuntimeError, match="plain scan query"):
|
with pytest.raises(RuntimeError, match="plain scan query"):
|
||||||
|
|||||||
@@ -834,7 +834,7 @@ def test_table_create_indices():
|
|||||||
vector_req = received_requests[2]
|
vector_req = received_requests[2]
|
||||||
assert "name" in vector_req
|
assert "name" in vector_req
|
||||||
assert vector_req["name"] == "custom_vector_idx"
|
assert vector_req["name"] == "custom_vector_idx"
|
||||||
assert "replace" not in vector_req
|
assert vector_req["replace"] is False
|
||||||
|
|
||||||
table.wait_for_index(["custom_scalar_idx"], timedelta(seconds=2))
|
table.wait_for_index(["custom_scalar_idx"], timedelta(seconds=2))
|
||||||
table.wait_for_index(
|
table.wait_for_index(
|
||||||
@@ -2467,7 +2467,7 @@ def test_remote_blob_byte_apis_not_supported_on_old_server():
|
|||||||
|
|
||||||
|
|
||||||
def test_remote_connection_jobs_surface():
|
def test_remote_connection_jobs_surface():
|
||||||
from lancedb.exceptions import JobFailedError, JobNotFoundError
|
from lancedb.exceptions import JobFailedError
|
||||||
|
|
||||||
schema = pa.schema([("state", pa.string())])
|
schema = pa.schema([("state", pa.string())])
|
||||||
batch = pa.record_batch([pa.array(["created", "done"])], schema=schema)
|
batch = pa.record_batch([pa.array(["created", "done"])], schema=schema)
|
||||||
@@ -2475,7 +2475,6 @@ def test_remote_connection_jobs_surface():
|
|||||||
with pa.ipc.new_stream(sink, schema) as writer:
|
with pa.ipc.new_stream(sink, schema) as writer:
|
||||||
writer.write_batch(batch)
|
writer.write_batch(batch)
|
||||||
events_body = sink.getvalue().to_pybytes()
|
events_body = sink.getvalue().to_pybytes()
|
||||||
query_events_payloads = []
|
|
||||||
|
|
||||||
def handler(request):
|
def handler(request):
|
||||||
content_len = int(request.headers.get("Content-Length", 0))
|
content_len = int(request.headers.get("Content-Length", 0))
|
||||||
@@ -2513,22 +2512,6 @@ def test_remote_connection_jobs_surface():
|
|||||||
request.end_headers()
|
request.end_headers()
|
||||||
request.wfile.write(json.dumps(rsp).encode())
|
request.wfile.write(json.dumps(rsp).encode())
|
||||||
elif request.path == "/v1/jobs/describe":
|
elif request.path == "/v1/jobs/describe":
|
||||||
if payload["job_id"] == "job-2":
|
|
||||||
request.send_response(200)
|
|
||||||
request.send_header("Content-Type", "application/json")
|
|
||||||
request.end_headers()
|
|
||||||
request.wfile.write(
|
|
||||||
json.dumps(
|
|
||||||
dict(
|
|
||||||
job_id="job-2",
|
|
||||||
job_type="refresh_column",
|
|
||||||
job_state="DONE",
|
|
||||||
creation_ms=2000,
|
|
||||||
result=dict(rows_assigned=1000000, rows_failed=0),
|
|
||||||
)
|
|
||||||
).encode()
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if payload["job_id"] != "job-1":
|
if payload["job_id"] != "job-1":
|
||||||
request.send_response(404)
|
request.send_response(404)
|
||||||
request.end_headers()
|
request.end_headers()
|
||||||
@@ -2560,7 +2543,7 @@ def test_remote_connection_jobs_surface():
|
|||||||
request.end_headers()
|
request.end_headers()
|
||||||
request.wfile.write(b'{"job_id": "job-1"}')
|
request.wfile.write(b'{"job_id": "job-1"}')
|
||||||
elif request.path == "/v1/jobs/query_events":
|
elif request.path == "/v1/jobs/query_events":
|
||||||
query_events_payloads.append(payload)
|
assert payload["job_id"] == "job-1"
|
||||||
request.send_response(200)
|
request.send_response(200)
|
||||||
request.send_header("Content-Type", "application/vnd.apache.arrow.stream")
|
request.send_header("Content-Type", "application/vnd.apache.arrow.stream")
|
||||||
request.end_headers()
|
request.end_headers()
|
||||||
@@ -2576,109 +2559,24 @@ def test_remote_connection_jobs_surface():
|
|||||||
assert jobs[0].table == "t1"
|
assert jobs[0].table == "t1"
|
||||||
assert jobs[1].state == "finished"
|
assert jobs[1].state == "finished"
|
||||||
|
|
||||||
|
description = db.get_job("job-1")
|
||||||
|
assert description.job_type == "create_index"
|
||||||
|
assert description.state == "failed"
|
||||||
|
assert json.loads(description.spec_json) == {"column": "vec"}
|
||||||
|
assert description.failure.message == "worker died"
|
||||||
|
assert description.failure.retryable is True
|
||||||
|
assert db.get_job("missing") is None
|
||||||
|
|
||||||
assert db.cancel_job("job-1") is True
|
assert db.cancel_job("job-1") is True
|
||||||
assert db.cancel_job("missing") is False
|
assert db.cancel_job("missing") is False
|
||||||
|
|
||||||
# Opening a job hands back a populated handle; a missing one fails.
|
batches = db.job_history("job-1")
|
||||||
with pytest.raises(JobNotFoundError, match="missing"):
|
assert len(batches) == 1
|
||||||
db.open_job("missing")
|
assert batches[0].num_rows == 2
|
||||||
finished = db.open_job("job-2")
|
assert batches[0].column("state").to_pylist() == ["created", "done"]
|
||||||
assert finished.state == "finished"
|
|
||||||
assert finished.result == {"rows_assigned": 1000000, "rows_failed": 0}
|
|
||||||
|
|
||||||
job = db.open_job("job-1")
|
job = db.job("job-1")
|
||||||
assert job.id == "job-1"
|
assert job.id == "job-1"
|
||||||
# Opening already populated the handle.
|
|
||||||
assert job.state == "failed"
|
|
||||||
assert job.spec == {"column": "vec"}
|
|
||||||
assert job.failure.message == "worker died"
|
|
||||||
assert job.status() == "failed"
|
assert job.status() == "failed"
|
||||||
with pytest.raises(JobFailedError, match="worker died"):
|
with pytest.raises(JobFailedError, match="worker died"):
|
||||||
job.wait(timeout=timedelta(seconds=5))
|
job.wait(timeout=timedelta(seconds=5))
|
||||||
|
|
||||||
|
|
||||||
def test_remote_job_handle_reports_its_own_detail():
|
|
||||||
schema = pa.schema([("state", pa.string())])
|
|
||||||
batch = pa.record_batch([pa.array(["claim_complete"])], schema=schema)
|
|
||||||
sink = pa.BufferOutputStream()
|
|
||||||
with pa.ipc.new_stream(sink, schema) as writer:
|
|
||||||
writer.write_batch(batch)
|
|
||||||
events_body = sink.getvalue().to_pybytes()
|
|
||||||
event_payloads = []
|
|
||||||
|
|
||||||
def handler(request):
|
|
||||||
content_len = int(request.headers.get("Content-Length", 0))
|
|
||||||
body = request.rfile.read(content_len) if content_len > 0 else b""
|
|
||||||
payload = json.loads(body) if body else {}
|
|
||||||
if request.path == "/v1/jobs/describe":
|
|
||||||
request.send_response(200)
|
|
||||||
request.send_header("Content-Type", "application/json")
|
|
||||||
request.end_headers()
|
|
||||||
request.wfile.write(
|
|
||||||
json.dumps(
|
|
||||||
dict(
|
|
||||||
job_id="job-1",
|
|
||||||
job_type="refresh_column",
|
|
||||||
job_state="DONE",
|
|
||||||
creation_ms=2000,
|
|
||||||
spec=dict(column="vec"),
|
|
||||||
result=dict(rows_assigned=1000000),
|
|
||||||
)
|
|
||||||
).encode()
|
|
||||||
)
|
|
||||||
elif request.path == "/v1/jobs/query_events":
|
|
||||||
event_payloads.append(payload)
|
|
||||||
request.send_response(200)
|
|
||||||
request.send_header("Content-Type", "application/vnd.apache.arrow.stream")
|
|
||||||
request.end_headers()
|
|
||||||
request.wfile.write(events_body)
|
|
||||||
else:
|
|
||||||
request.send_response(404)
|
|
||||||
request.end_headers()
|
|
||||||
|
|
||||||
with mock_lancedb_connection(handler) as db:
|
|
||||||
job = db.open_job("job-1")
|
|
||||||
|
|
||||||
# Opening populates the handle in the same round trip.
|
|
||||||
assert job.state == "finished"
|
|
||||||
job.refresh()
|
|
||||||
assert job.job_type == "refresh_column"
|
|
||||||
assert job.creation_ms == 2000
|
|
||||||
assert job.spec == {"column": "vec"}
|
|
||||||
assert job.result == {"rows_assigned": 1000000}
|
|
||||||
assert job.failure is None
|
|
||||||
# The JSON payloads stay reachable, but as internal APIs.
|
|
||||||
assert json.loads(job._spec_json) == {"column": "vec"}
|
|
||||||
assert json.loads(job._result_json) == {"rows_assigned": 1000000}
|
|
||||||
|
|
||||||
# print() shows everything the handle knows and nothing it does not.
|
|
||||||
# print() lays every known field out on its own line, with the JSON
|
|
||||||
# payloads indented rather than crammed onto one line.
|
|
||||||
assert repr(job) == "\n".join(
|
|
||||||
[
|
|
||||||
"Job(",
|
|
||||||
" id='job-1',",
|
|
||||||
" state='finished',",
|
|
||||||
" job_type='refresh_column',",
|
|
||||||
" creation_ms=2000,",
|
|
||||||
" spec={",
|
|
||||||
' "column": "vec"',
|
|
||||||
" },",
|
|
||||||
" result={",
|
|
||||||
' "rows_assigned": 1000000',
|
|
||||||
" },",
|
|
||||||
")",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
# Nothing it does not know shows up.
|
|
||||||
assert "failure" not in repr(job)
|
|
||||||
|
|
||||||
events = job.events(filter="state = 'claim_complete'", limit=500)
|
|
||||||
assert isinstance(events, pa.Table)
|
|
||||||
assert events.column("state").to_pylist() == ["claim_complete"]
|
|
||||||
# The handle supplies job_id; the caller only narrows the query.
|
|
||||||
assert event_payloads[-1] == {
|
|
||||||
"job_id": "job-1",
|
|
||||||
"limit": 500,
|
|
||||||
"filter": "state = 'claim_complete'",
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ def get_test_table(tmp_path):
|
|||||||
"but his son was mortal",
|
"but his son was mortal",
|
||||||
"there hasn't been a good battlefield game since 2142",
|
"there hasn't been a good battlefield game since 2142",
|
||||||
"I wish they would make another one",
|
"I wish they would make another one",
|
||||||
"campaigns are not as good as they used to be",
|
"campains are not as good as they used to be",
|
||||||
"Multiplayer and open world games have destroyed the single player experience",
|
"Multiplayer and open world games have destroyed the single player experience",
|
||||||
"Maybe the future is console games",
|
"Maybe the future is console games",
|
||||||
"I don't know",
|
"I don't know",
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
||||||
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
import pyarrow as pa
|
|
||||||
|
|
||||||
import lancedb
|
|
||||||
from lancedb import _lancedb
|
|
||||||
from lancedb.arrow import AsyncRecordBatchReader
|
|
||||||
from lancedb.db import AsyncConnection
|
|
||||||
from lancedb.remote.db import RemoteDBConnection
|
|
||||||
from lancedb.sql import AsyncQuery, Query
|
|
||||||
|
|
||||||
NIL_QUERY_ID = UUID(int=0)
|
|
||||||
|
|
||||||
|
|
||||||
class FakeNativeQuery:
|
|
||||||
id = UUID("0198f1b2-c3d4-7e5f-8123-456789abcdef")
|
|
||||||
|
|
||||||
async def reader(self):
|
|
||||||
return pa.table({"value": [1, 2]})
|
|
||||||
|
|
||||||
|
|
||||||
class FakeNativeConnection:
|
|
||||||
async def execute_query_async(self, query, *, default_namespace_path=None):
|
|
||||||
return FakeNativeQuery()
|
|
||||||
|
|
||||||
|
|
||||||
class FakeAsyncConnection:
|
|
||||||
async def execute_query_async(self, query, *, default_namespace_path=None):
|
|
||||||
return AsyncQuery(FakeNativeQuery())
|
|
||||||
|
|
||||||
|
|
||||||
def remote_connection(sql_host_override=None):
|
|
||||||
return lancedb.connect(
|
|
||||||
"db://analytics",
|
|
||||||
api_key="test-key",
|
|
||||||
host_override="http://localhost:10024",
|
|
||||||
sql_host_override=sql_host_override,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_sql_is_connection_scoped():
|
|
||||||
assert hasattr(lancedb, "sql")
|
|
||||||
assert not callable(lancedb.sql)
|
|
||||||
assert not hasattr(_lancedb, "sql")
|
|
||||||
assert not hasattr(remote_connection(), "sql")
|
|
||||||
assert hasattr(remote_connection(), "execute_query")
|
|
||||||
assert hasattr(remote_connection(), "execute_query_async")
|
|
||||||
assert hasattr(remote_connection(), "describe_query")
|
|
||||||
|
|
||||||
|
|
||||||
def test_query_id_is_uuid():
|
|
||||||
query = AsyncQuery(FakeNativeQuery())
|
|
||||||
assert isinstance(query.id, UUID)
|
|
||||||
assert Query(query).id == query.id
|
|
||||||
|
|
||||||
|
|
||||||
def test_connection_serializes_sql_host_override():
|
|
||||||
endpoint = "grpc+tls://sql.example.com:10026"
|
|
||||||
restored = lancedb.deserialize_conn(
|
|
||||||
remote_connection(sql_host_override=endpoint).serialize()
|
|
||||||
)
|
|
||||||
assert restored.sql_host_override == endpoint
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_async_sql_reader_is_record_batch_stream():
|
|
||||||
reader = await AsyncQuery(FakeNativeQuery()).reader()
|
|
||||||
assert isinstance(reader, AsyncRecordBatchReader)
|
|
||||||
assert (await reader.read_all())[0].column(0).to_pylist() == [1, 2]
|
|
||||||
|
|
||||||
|
|
||||||
def test_sync_sql_reader_is_record_batch_reader():
|
|
||||||
reader = Query(AsyncQuery(FakeNativeQuery())).reader()
|
|
||||||
assert isinstance(reader, pa.RecordBatchReader)
|
|
||||||
assert reader.read_all().column(0).to_pylist() == [1, 2]
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_query_returns_blocking_reader():
|
|
||||||
connection = RemoteDBConnection.__new__(RemoteDBConnection)
|
|
||||||
connection._conn = FakeAsyncConnection()
|
|
||||||
reader = connection.execute_query("SELECT 1")
|
|
||||||
assert isinstance(reader, pa.RecordBatchReader)
|
|
||||||
assert reader.read_all().column(0).to_pylist() == [1, 2]
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_async_execute_query_returns_async_reader():
|
|
||||||
connection = AsyncConnection(FakeNativeConnection())
|
|
||||||
reader = await connection.execute_query("SELECT 1")
|
|
||||||
assert isinstance(reader, AsyncRecordBatchReader)
|
|
||||||
assert (await reader.read_all())[0].column(0).to_pylist() == [1, 2]
|
|
||||||
|
|
||||||
|
|
||||||
def test_local_connection_rejects_sql(tmp_path):
|
|
||||||
connection = lancedb.connect(tmp_path)
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
connection.execute_query("SELECT 1")
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
connection.execute_query_async("SELECT 1")
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
connection.describe_query(NIL_QUERY_ID)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_local_async_connection_rejects_sql(tmp_path):
|
|
||||||
connection = await lancedb.connect_async(tmp_path)
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
await connection.execute_query("SELECT 1")
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
await connection.execute_query_async("SELECT 1")
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
await connection.describe_query(NIL_QUERY_ID)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_async_namespace_connection_rejects_sql(tmp_path):
|
|
||||||
connection = lancedb.connect_namespace_async("dir", {"root": str(tmp_path)})
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
await connection.execute_query("SELECT 1")
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
await connection.execute_query_async("SELECT 1")
|
|
||||||
with pytest.raises(NotImplementedError, match="SQL"):
|
|
||||||
await connection.describe_query(NIL_QUERY_ID)
|
|
||||||
|
|
||||||
|
|
||||||
def test_describe_query_requires_uuid():
|
|
||||||
with pytest.raises(TypeError, match="UUID"):
|
|
||||||
remote_connection().describe_query(str(NIL_QUERY_ID))
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"default_namespace_path",
|
|
||||||
["public", ("public",), [1]],
|
|
||||||
)
|
|
||||||
def test_execute_query_async_requires_namespace_path_list(default_namespace_path):
|
|
||||||
with pytest.raises(ValueError, match="default_namespace_path"):
|
|
||||||
remote_connection().execute_query_async(
|
|
||||||
"SELECT 1", default_namespace_path=default_namespace_path
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_query_async_rejects_invalid_endpoint():
|
|
||||||
connection = remote_connection(sql_host_override="invalid://localhost")
|
|
||||||
with pytest.raises(ValueError, match="sql_host_override"):
|
|
||||||
connection.execute_query_async("SELECT 1")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"default_namespace_path",
|
|
||||||
[[""], ["café"], ["pub\tlic"], ["events$raw"]],
|
|
||||||
)
|
|
||||||
def test_execute_query_async_rejects_invalid_namespace_components(
|
|
||||||
default_namespace_path,
|
|
||||||
):
|
|
||||||
with pytest.raises(ValueError, match="default_namespace_path"):
|
|
||||||
remote_connection().execute_query_async(
|
|
||||||
"SELECT 1", default_namespace_path=default_namespace_path
|
|
||||||
)
|
|
||||||
@@ -64,23 +64,15 @@ async def _blob_v2_table_async(db: AsyncConnection, name: str):
|
|||||||
return table
|
return table
|
||||||
|
|
||||||
|
|
||||||
# Legacy v1 blob columns are only writable at file version <= 2.1.
|
|
||||||
LEGACY_BLOB_STORAGE_OPTIONS = {"new_table_data_storage_version": "2.1"}
|
|
||||||
|
|
||||||
|
|
||||||
def _blob_table(db: DBConnection, name: str, blob_schema: str):
|
def _blob_table(db: DBConnection, name: str, blob_schema: str):
|
||||||
if blob_schema == "v1":
|
if blob_schema == "v1":
|
||||||
return db.create_table(
|
return db.create_table(name, data=_blob_test_data())
|
||||||
name, data=_blob_test_data(), storage_options=LEGACY_BLOB_STORAGE_OPTIONS
|
|
||||||
)
|
|
||||||
return _blob_v2_table(db, name)
|
return _blob_v2_table(db, name)
|
||||||
|
|
||||||
|
|
||||||
async def _blob_table_async(db: AsyncConnection, name: str, blob_schema: str):
|
async def _blob_table_async(db: AsyncConnection, name: str, blob_schema: str):
|
||||||
if blob_schema == "v1":
|
if blob_schema == "v1":
|
||||||
return await db.create_table(
|
return await db.create_table(name, data=_blob_test_data())
|
||||||
name, data=_blob_test_data(), storage_options=LEGACY_BLOB_STORAGE_OPTIONS
|
|
||||||
)
|
|
||||||
return await _blob_v2_table_async(db, name)
|
return await _blob_v2_table_async(db, name)
|
||||||
|
|
||||||
|
|
||||||
@@ -155,11 +147,7 @@ def test_table_to_pandas_invalid_blob_mode_non_blob_table(tmp_db: DBConnection):
|
|||||||
@pytest.mark.parametrize("blob_mode", ["lazy", "bytes", "descriptions"])
|
@pytest.mark.parametrize("blob_mode", ["lazy", "bytes", "descriptions"])
|
||||||
def test_table_to_pandas_blob_modes(tmp_db: DBConnection, blob_mode):
|
def test_table_to_pandas_blob_modes(tmp_db: DBConnection, blob_mode):
|
||||||
pytest.importorskip("lance")
|
pytest.importorskip("lance")
|
||||||
table = tmp_db.create_table(
|
table = tmp_db.create_table(f"test_to_pandas_blob_{blob_mode}", _blob_test_data())
|
||||||
f"test_to_pandas_blob_{blob_mode}",
|
|
||||||
_blob_test_data(),
|
|
||||||
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
|
||||||
)
|
|
||||||
|
|
||||||
df = table.to_pandas(blob_mode=blob_mode)
|
df = table.to_pandas(blob_mode=blob_mode)
|
||||||
|
|
||||||
@@ -1612,7 +1600,7 @@ def test_create_index_method(mock_create_index, mem_db: DBConnection):
|
|||||||
)
|
)
|
||||||
mock_create_index.assert_called_with(
|
mock_create_index.assert_called_with(
|
||||||
"my_vector",
|
"my_vector",
|
||||||
replace=True,
|
replace=False,
|
||||||
config=expected_config,
|
config=expected_config,
|
||||||
wait_timeout=None,
|
wait_timeout=None,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -1632,7 +1620,7 @@ def test_create_index_method(mock_create_index, mem_db: DBConnection):
|
|||||||
)
|
)
|
||||||
mock_create_index.assert_called_with(
|
mock_create_index.assert_called_with(
|
||||||
"my_vector",
|
"my_vector",
|
||||||
replace=True,
|
replace=False,
|
||||||
config=expected_config,
|
config=expected_config,
|
||||||
wait_timeout=None,
|
wait_timeout=None,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -1658,7 +1646,7 @@ def test_create_index_name_and_train_parameters(
|
|||||||
expected_config = IvfPq() # Default config
|
expected_config = IvfPq() # Default config
|
||||||
mock_create_index.assert_called_with(
|
mock_create_index.assert_called_with(
|
||||||
"vector",
|
"vector",
|
||||||
replace=True,
|
replace=False,
|
||||||
config=expected_config,
|
config=expected_config,
|
||||||
wait_timeout=None,
|
wait_timeout=None,
|
||||||
name="my_custom_index",
|
name="my_custom_index",
|
||||||
@@ -1669,7 +1657,7 @@ def test_create_index_name_and_train_parameters(
|
|||||||
table.create_index(vector_column_name="vector", train=False)
|
table.create_index(vector_column_name="vector", train=False)
|
||||||
mock_create_index.assert_called_with(
|
mock_create_index.assert_called_with(
|
||||||
"vector",
|
"vector",
|
||||||
replace=True,
|
replace=False,
|
||||||
config=expected_config,
|
config=expected_config,
|
||||||
wait_timeout=None,
|
wait_timeout=None,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -1680,7 +1668,7 @@ def test_create_index_name_and_train_parameters(
|
|||||||
table.create_index(vector_column_name="vector", name="my_index_name", train=True)
|
table.create_index(vector_column_name="vector", name="my_index_name", train=True)
|
||||||
mock_create_index.assert_called_with(
|
mock_create_index.assert_called_with(
|
||||||
"vector",
|
"vector",
|
||||||
replace=True,
|
replace=False,
|
||||||
config=expected_config,
|
config=expected_config,
|
||||||
wait_timeout=None,
|
wait_timeout=None,
|
||||||
name="my_index_name",
|
name="my_index_name",
|
||||||
@@ -1717,7 +1705,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
|
|||||||
table.create_index("vector", config=IvfPq(distance_type="l2"))
|
table.create_index("vector", config=IvfPq(distance_type="l2"))
|
||||||
mock_create_index.assert_called_with(
|
mock_create_index.assert_called_with(
|
||||||
"vector",
|
"vector",
|
||||||
replace=True,
|
replace=False,
|
||||||
config=IvfPq(distance_type="l2"),
|
config=IvfPq(distance_type="l2"),
|
||||||
wait_timeout=None,
|
wait_timeout=None,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -1728,7 +1716,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
|
|||||||
table.create_index("category", config=BTree())
|
table.create_index("category", config=BTree())
|
||||||
mock_create_index.assert_called_with(
|
mock_create_index.assert_called_with(
|
||||||
"category",
|
"category",
|
||||||
replace=True,
|
replace=False,
|
||||||
config=BTree(),
|
config=BTree(),
|
||||||
wait_timeout=None,
|
wait_timeout=None,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -1739,7 +1727,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
|
|||||||
table.create_index("text", config=FTS(with_position=True))
|
table.create_index("text", config=FTS(with_position=True))
|
||||||
mock_create_index.assert_called_with(
|
mock_create_index.assert_called_with(
|
||||||
"text",
|
"text",
|
||||||
replace=True,
|
replace=False,
|
||||||
config=FTS(with_position=True),
|
config=FTS(with_position=True),
|
||||||
wait_timeout=None,
|
wait_timeout=None,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -3354,7 +3342,7 @@ def test_empty_query(mem_db: DBConnection):
|
|||||||
# None is the same as default
|
# None is the same as default
|
||||||
df = table.search().select(["id"]).limit(None).to_arrow()
|
df = table.search().select(["id"]).limit(None).to_arrow()
|
||||||
assert df.num_rows == 100
|
assert df.num_rows == 100
|
||||||
# invalid limist is the same as None, which is the same as default
|
# invalid limist is the same as None, wihch is the same as default
|
||||||
df = table.search().select(["id"]).limit(-1).to_arrow()
|
df = table.search().select(["id"]).limit(-1).to_arrow()
|
||||||
assert df.num_rows == 100
|
assert df.num_rows == 100
|
||||||
# valid limit should work
|
# valid limit should work
|
||||||
@@ -3971,7 +3959,7 @@ def test_stats(mem_db: DBConnection):
|
|||||||
print(f"{stats=}")
|
print(f"{stats=}")
|
||||||
assert stats == {
|
assert stats == {
|
||||||
# Full on-disk size of the data file, footer and metadata included.
|
# Full on-disk size of the data file, footer and metadata included.
|
||||||
"total_bytes": 637,
|
"total_bytes": 633,
|
||||||
"num_rows": 2,
|
"num_rows": 2,
|
||||||
"num_indices": 0,
|
"num_indices": 0,
|
||||||
"fragment_stats": {
|
"fragment_stats": {
|
||||||
|
|||||||
+36
-68
@@ -13,7 +13,11 @@ use crate::{
|
|||||||
runtime::future_into_py,
|
runtime::future_into_py,
|
||||||
table::Table,
|
table::Table,
|
||||||
};
|
};
|
||||||
use arrow::{datatypes::Schema, ffi_stream::ArrowArrayStreamReader, pyarrow::FromPyArrow};
|
use arrow::{
|
||||||
|
datatypes::Schema,
|
||||||
|
ffi_stream::ArrowArrayStreamReader,
|
||||||
|
pyarrow::{FromPyArrow, ToPyArrow},
|
||||||
|
};
|
||||||
use lancedb::{
|
use lancedb::{
|
||||||
connection::Connection as LanceConnection,
|
connection::Connection as LanceConnection,
|
||||||
connection::NamespaceClientPushdownOperation,
|
connection::NamespaceClientPushdownOperation,
|
||||||
@@ -24,7 +28,7 @@ use pyo3::{
|
|||||||
Bound, FromPyObject, Py, PyAny, PyRef, PyResult, Python,
|
Bound, FromPyObject, Py, PyAny, PyRef, PyResult, Python,
|
||||||
exceptions::{PyRuntimeError, PyValueError},
|
exceptions::{PyRuntimeError, PyValueError},
|
||||||
pyclass, pyfunction, pymethods,
|
pyclass, pyfunction, pymethods,
|
||||||
types::{PyAnyMethods, PyDict, PyDictMethods, PyList},
|
types::{PyDict, PyDictMethods, PyList, PyListMethods},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[pyclass]
|
#[pyclass]
|
||||||
@@ -82,24 +86,6 @@ impl Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_default_namespace_path(path: Option<Bound<'_, PyAny>>) -> PyResult<Vec<String>> {
|
|
||||||
match path {
|
|
||||||
Some(path) => {
|
|
||||||
if !path.is_instance_of::<PyList>() {
|
|
||||||
return Err(PyValueError::new_err(
|
|
||||||
"Connection.execute_query_async default_namespace_path must be a list",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
path.extract::<Vec<String>>().map_err(|_| {
|
|
||||||
PyValueError::new_err(
|
|
||||||
"Connection.execute_query_async default_namespace_path components must be strings",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => Ok(vec!["public".to_string()]),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pymethods]
|
#[pymethods]
|
||||||
impl Connection {
|
impl Connection {
|
||||||
fn __repr__(&self) -> String {
|
fn __repr__(&self) -> String {
|
||||||
@@ -122,40 +108,6 @@ impl Connection {
|
|||||||
self.get_inner().map(|inner| inner.uri().to_string())
|
self.get_inner().map(|inner| inner.uri().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyo3(signature = (query, *, default_namespace_path=None))]
|
|
||||||
pub fn execute_query_async<'a>(
|
|
||||||
self_: PyRef<'a, Self>,
|
|
||||||
query: String,
|
|
||||||
default_namespace_path: Option<Bound<'_, PyAny>>,
|
|
||||||
) -> PyResult<Bound<'a, PyAny>> {
|
|
||||||
let inner = self_.get_inner()?.clone();
|
|
||||||
let default_namespace_path = parse_default_namespace_path(default_namespace_path)?;
|
|
||||||
future_into_py(self_.py(), async move {
|
|
||||||
let operation = inner
|
|
||||||
.execute_query_async(query)
|
|
||||||
.default_namespace_path(default_namespace_path);
|
|
||||||
operation
|
|
||||||
.execute()
|
|
||||||
.await
|
|
||||||
.map(crate::sql::Query::new)
|
|
||||||
.infer_error()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn describe_query<'a>(
|
|
||||||
self_: PyRef<'a, Self>,
|
|
||||||
query_id: uuid::Uuid,
|
|
||||||
) -> PyResult<Bound<'a, PyAny>> {
|
|
||||||
let inner = self_.get_inner()?.clone();
|
|
||||||
future_into_py(self_.py(), async move {
|
|
||||||
inner
|
|
||||||
.describe_query(query_id)
|
|
||||||
.await
|
|
||||||
.map(crate::sql::QueryDescription::from)
|
|
||||||
.infer_error()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pyo3(signature = ())]
|
#[pyo3(signature = ())]
|
||||||
pub fn get_read_consistency_interval(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
pub fn get_read_consistency_interval(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
||||||
let inner = self_.get_inner()?.clone();
|
let inner = self_.get_inner()?.clone();
|
||||||
@@ -640,12 +592,9 @@ impl Connection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_job(self_: PyRef<'_, Self>, job_id: String) -> PyResult<Bound<'_, PyAny>> {
|
pub fn job(&self, job_id: String) -> PyResult<crate::job::Job> {
|
||||||
let inner = self_.get_inner()?.clone();
|
let inner = self.get_inner()?.clone();
|
||||||
future_into_py(self_.py(), async move {
|
Ok(crate::job::Job::new(inner.job(job_id).infer_error()?))
|
||||||
let job = inner.open_job(&job_id).await.infer_error()?;
|
|
||||||
Ok(crate::job::Job::new(job))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_function_async(
|
pub fn create_function_async(
|
||||||
@@ -715,16 +664,42 @@ impl Connection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_job(self_: PyRef<'_, Self>, job_id: String) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
let description = inner.get_job(&job_id).await.infer_error()?;
|
||||||
|
Ok(description.map(crate::job::JobDescription::from))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cancel_job(self_: PyRef<'_, Self>, job_id: String) -> PyResult<Bound<'_, PyAny>> {
|
pub fn cancel_job(self_: PyRef<'_, Self>, job_id: String) -> PyResult<Bound<'_, PyAny>> {
|
||||||
let inner = self_.get_inner()?.clone();
|
let inner = self_.get_inner()?.clone();
|
||||||
future_into_py(self_.py(), async move {
|
future_into_py(self_.py(), async move {
|
||||||
inner.cancel_job(&job_id).await.infer_error()
|
inner.cancel_job(&job_id).await.infer_error()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[pyo3(signature = (job_id=None))]
|
||||||
|
pub fn job_history(
|
||||||
|
self_: PyRef<'_, Self>,
|
||||||
|
job_id: Option<String>,
|
||||||
|
) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
let batches = inner.job_history(job_id.as_deref()).await.infer_error()?;
|
||||||
|
Python::attach(|py| {
|
||||||
|
let list = PyList::empty(py);
|
||||||
|
for batch in batches {
|
||||||
|
list.append(batch.to_pyarrow(py)?)?;
|
||||||
|
}
|
||||||
|
Ok(list.unbind())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
#[pyo3(signature = (uri, api_key=None, region=None, host_override=None, sql_host_override=None, read_consistency_interval=None, client_config=None, storage_options=None, session=None, manifest_enabled=false, namespace_client_properties=None, oauth_config=None))]
|
#[pyo3(signature = (uri, api_key=None, region=None, host_override=None, read_consistency_interval=None, client_config=None, storage_options=None, session=None, manifest_enabled=false, namespace_client_properties=None, oauth_config=None))]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn connect(
|
pub fn connect(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
@@ -732,7 +707,6 @@ pub fn connect(
|
|||||||
api_key: Option<String>,
|
api_key: Option<String>,
|
||||||
region: Option<String>,
|
region: Option<String>,
|
||||||
host_override: Option<String>,
|
host_override: Option<String>,
|
||||||
sql_host_override: Option<String>,
|
|
||||||
read_consistency_interval: Option<f64>,
|
read_consistency_interval: Option<f64>,
|
||||||
client_config: Option<PyClientConfig>,
|
client_config: Option<PyClientConfig>,
|
||||||
storage_options: Option<HashMap<String, String>>,
|
storage_options: Option<HashMap<String, String>>,
|
||||||
@@ -752,12 +726,6 @@ pub fn connect(
|
|||||||
if let Some(host_override) = host_override {
|
if let Some(host_override) = host_override {
|
||||||
builder = builder.host_override(&host_override);
|
builder = builder.host_override(&host_override);
|
||||||
}
|
}
|
||||||
#[cfg(feature = "remote")]
|
|
||||||
if let Some(sql_host_override) = sql_host_override {
|
|
||||||
builder = builder.sql_host_override(&sql_host_override);
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "remote"))]
|
|
||||||
let _ = sql_host_override;
|
|
||||||
if let Some(read_consistency_interval) = read_consistency_interval {
|
if let Some(read_consistency_interval) = read_consistency_interval {
|
||||||
let read_consistency_interval = Duration::from_secs_f64(read_consistency_interval);
|
let read_consistency_interval = Duration::from_secs_f64(read_consistency_interval);
|
||||||
builder = builder.read_consistency_interval(read_consistency_interval);
|
builder = builder.read_consistency_interval(read_consistency_interval);
|
||||||
|
|||||||
@@ -114,12 +114,6 @@ impl<T> PythonErrorExt<T> for std::result::Result<T, LanceError> {
|
|||||||
.getattr(intern!(py, "JobCancelledError"))?;
|
.getattr(intern!(py, "JobCancelledError"))?;
|
||||||
Err(PyErr::from_value(cls.call1((err.to_string(),))?))
|
Err(PyErr::from_value(cls.call1((err.to_string(),))?))
|
||||||
}),
|
}),
|
||||||
LanceError::JobNotFound { .. } => Python::attach(|py| {
|
|
||||||
let cls = py
|
|
||||||
.import(intern!(py, "lancedb.exceptions"))?
|
|
||||||
.getattr(intern!(py, "JobNotFoundError"))?;
|
|
||||||
Err(PyErr::from_value(cls.call1((err.to_string(),))?))
|
|
||||||
}),
|
|
||||||
_ => self.runtime_error(),
|
_ => self.runtime_error(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-126
@@ -4,50 +4,11 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::runtime::future_into_py;
|
use crate::runtime::future_into_py;
|
||||||
use arrow::{
|
use pyo3::{Bound, PyAny, PyRef, PyResult, pyclass, pymethods};
|
||||||
datatypes::Schema,
|
|
||||||
pyarrow::{IntoPyArrow, Table as PyArrowTable},
|
|
||||||
};
|
|
||||||
use lancedb::job::JobEventsRequest;
|
|
||||||
use pyo3::{
|
|
||||||
Bound, PyAny, PyRef, PyResult, Python,
|
|
||||||
exceptions::PyValueError,
|
|
||||||
pyclass, pymethods,
|
|
||||||
types::{PyAnyMethods, PyDict, PyDictMethods},
|
|
||||||
};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::error::PythonErrorExt;
|
use crate::error::PythonErrorExt;
|
||||||
|
|
||||||
const REPR_INDENT: &str = " ";
|
|
||||||
|
|
||||||
/// Parse a stored JSON payload into Python data. The bindings carry these as
|
|
||||||
/// strings because that is what crosses the boundary cheaply; the public
|
|
||||||
/// Python surface is the parsed form.
|
|
||||||
fn parse_json_payload<'py>(
|
|
||||||
py: Python<'py>,
|
|
||||||
raw: Option<&str>,
|
|
||||||
) -> PyResult<Option<Bound<'py, PyAny>>> {
|
|
||||||
match raw {
|
|
||||||
None => Ok(None),
|
|
||||||
Some(raw) => Ok(Some(py.import("json")?.call_method1("loads", (raw,))?)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A payload rendered as indented JSON, aligned under the field that holds it.
|
|
||||||
fn pretty_json_payload(py: Python<'_>, raw: Option<&str>) -> PyResult<Option<String>> {
|
|
||||||
let Some(parsed) = parse_json_payload(py, raw)? else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let kwargs = PyDict::new(py);
|
|
||||||
kwargs.set_item("indent", 4)?;
|
|
||||||
let rendered: String = py
|
|
||||||
.import("json")?
|
|
||||||
.call_method("dumps", (parsed,), Some(&kwargs))?
|
|
||||||
.extract()?;
|
|
||||||
Ok(Some(rendered.replace('\n', &format!("\n{REPR_INDENT}"))))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pyclass]
|
#[pyclass]
|
||||||
pub struct Job {
|
pub struct Job {
|
||||||
inner: Arc<lancedb::Job<std::result::Result<Option<String>, String>>>,
|
inner: Arc<lancedb::Job<std::result::Result<Option<String>, String>>>,
|
||||||
@@ -106,48 +67,6 @@ impl Job {
|
|||||||
Ok(())
|
Ok(())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn refresh(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
|
||||||
let inner = self_.inner.clone();
|
|
||||||
future_into_py(self_.py(), async move {
|
|
||||||
inner.refresh().await.infer_error()?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The last observed lifecycle state, without contacting the backend.
|
|
||||||
#[getter]
|
|
||||||
pub fn _state(&self) -> Option<String> {
|
|
||||||
self.inner.state()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The last observed server-side record. `None` for an in-process job.
|
|
||||||
#[getter]
|
|
||||||
pub fn _description(&self) -> Option<JobDescription> {
|
|
||||||
self.inner.description().map(JobDescription::from)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pyo3(signature = (*, limit=None, filter=None))]
|
|
||||||
pub fn events(
|
|
||||||
self_: PyRef<'_, Self>,
|
|
||||||
limit: Option<u32>,
|
|
||||||
filter: Option<String>,
|
|
||||||
) -> PyResult<Bound<'_, PyAny>> {
|
|
||||||
let inner = self_.inner.clone();
|
|
||||||
let request = JobEventsRequest { limit, filter };
|
|
||||||
future_into_py(self_.py(), async move {
|
|
||||||
let batches = inner.events(request).await.infer_error()?;
|
|
||||||
Python::attach(|py| {
|
|
||||||
let schema = batches
|
|
||||||
.first()
|
|
||||||
.map(|batch| batch.schema())
|
|
||||||
.unwrap_or_else(|| Arc::new(Schema::empty()));
|
|
||||||
let table = PyArrowTable::try_new(batches, schema)
|
|
||||||
.map_err(|err| PyValueError::new_err(err.to_string()))?;
|
|
||||||
table.into_pyarrow(py).map(|table| table.unbind())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A row from `Connection.list_jobs`: one server-side job.
|
/// A row from `Connection.list_jobs`: one server-side job.
|
||||||
@@ -202,7 +121,7 @@ impl JobFailureInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The server-side record behind a `Job` handle.
|
/// A described job from `Connection.get_job`.
|
||||||
#[pyclass(get_all, skip_from_py_object)]
|
#[pyclass(get_all, skip_from_py_object)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct JobDescription {
|
pub struct JobDescription {
|
||||||
@@ -210,49 +129,17 @@ pub struct JobDescription {
|
|||||||
job_type: String,
|
job_type: String,
|
||||||
state: String,
|
state: String,
|
||||||
creation_ms: i64,
|
creation_ms: i64,
|
||||||
/// Internal: the wire form behind the `spec` property.
|
spec_json: Option<String>,
|
||||||
_spec_json: Option<String>,
|
|
||||||
/// Internal: the wire form behind the `result` property.
|
|
||||||
_result_json: Option<String>,
|
|
||||||
failure: Option<JobFailureInfo>,
|
failure: Option<JobFailureInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pymethods]
|
#[pymethods]
|
||||||
impl JobDescription {
|
impl JobDescription {
|
||||||
/// The job-type-specific specification it was submitted with.
|
fn __repr__(&self) -> String {
|
||||||
#[getter]
|
format!(
|
||||||
fn spec<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyAny>>> {
|
"JobDescription(job_id={:?}, job_type={:?}, state={:?}, creation_ms={})",
|
||||||
parse_json_payload(py, self._spec_json.as_deref())
|
self.job_id, self.job_type, self.state, self.creation_ms
|
||||||
}
|
)
|
||||||
|
|
||||||
/// The job-type-specific terminal result. `None` until the job succeeds.
|
|
||||||
#[getter]
|
|
||||||
fn result<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyAny>>> {
|
|
||||||
parse_json_payload(py, self._result_json.as_deref())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn __repr__(&self, py: Python<'_>) -> PyResult<String> {
|
|
||||||
let mut fields = vec![
|
|
||||||
format!("job_id={:?}", self.job_id),
|
|
||||||
format!("job_type={:?}", self.job_type),
|
|
||||||
format!("state={:?}", self.state),
|
|
||||||
format!("creation_ms={}", self.creation_ms),
|
|
||||||
];
|
|
||||||
// Lay the payloads out as indented JSON, the same way the `Job` repr
|
|
||||||
// does, so the two agree on how the same data looks.
|
|
||||||
for (name, payload) in [("spec", &self._spec_json), ("result", &self._result_json)] {
|
|
||||||
if let Some(rendered) = pretty_json_payload(py, payload.as_deref())? {
|
|
||||||
fields.push(format!("{name}={rendered}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(failure) = &self.failure {
|
|
||||||
fields.push(format!("failure={}", failure.__repr__()));
|
|
||||||
}
|
|
||||||
let body = fields
|
|
||||||
.iter()
|
|
||||||
.map(|field| format!("\n{REPR_INDENT}{field},"))
|
|
||||||
.collect::<String>();
|
|
||||||
Ok(format!("JobDescription({body}\n)"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,11 +150,7 @@ impl From<lancedb::database::JobDescription> for JobDescription {
|
|||||||
job_type: description.job_type,
|
job_type: description.job_type,
|
||||||
state: description.state,
|
state: description.state,
|
||||||
creation_ms: description.creation_ms,
|
creation_ms: description.creation_ms,
|
||||||
_spec_json: (!description.spec.is_null()).then(|| description.spec.to_string()),
|
spec_json: (!description.spec.is_null()).then(|| description.spec.to_string()),
|
||||||
_result_json: description
|
|
||||||
.result
|
|
||||||
.filter(|result| !result.is_null())
|
|
||||||
.map(|result| result.to_string()),
|
|
||||||
failure: description.failure.map(|failure| JobFailureInfo {
|
failure: description.failure.map(|failure| JobFailureInfo {
|
||||||
phase: failure.phase,
|
phase: failure.phase,
|
||||||
message: failure.message,
|
message: failure.message,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ pub mod permutation;
|
|||||||
pub mod query;
|
pub mod query;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod sql;
|
|
||||||
pub mod table;
|
pub mod table;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
||||||
@@ -51,8 +50,6 @@ pub fn _lancedb(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
|
|||||||
m.add_class::<crate::job::JobInfo>()?;
|
m.add_class::<crate::job::JobInfo>()?;
|
||||||
m.add_class::<crate::job::JobDescription>()?;
|
m.add_class::<crate::job::JobDescription>()?;
|
||||||
m.add_class::<crate::job::JobFailureInfo>()?;
|
m.add_class::<crate::job::JobFailureInfo>()?;
|
||||||
m.add_class::<crate::sql::Query>()?;
|
|
||||||
m.add_class::<crate::sql::QueryDescription>()?;
|
|
||||||
m.add_class::<PyBlobFile>()?;
|
m.add_class::<PyBlobFile>()?;
|
||||||
m.add_class::<IndexConfig>()?;
|
m.add_class::<IndexConfig>()?;
|
||||||
m.add_class::<Query>()?;
|
m.add_class::<Query>()?;
|
||||||
|
|||||||
+1
-1
@@ -334,7 +334,7 @@ pub struct PyQueryRequest {
|
|||||||
pub column: Option<String>,
|
pub column: Option<String>,
|
||||||
pub query_vector: Option<PyQueryVectors>,
|
pub query_vector: Option<PyQueryVectors>,
|
||||||
pub minimum_nprobes: Option<usize>,
|
pub minimum_nprobes: Option<usize>,
|
||||||
// None means user did not set it and default should be used (currently 20)
|
// None means user did not set it and default shoud be used (currenty 20)
|
||||||
// Some(0) means user set it to None and there is no limit
|
// Some(0) means user set it to None and there is no limit
|
||||||
pub maximum_nprobes: Option<usize>,
|
pub maximum_nprobes: Option<usize>,
|
||||||
pub lower_bound: Option<f32>,
|
pub lower_bound: Option<f32>,
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use pyo3::{Bound, PyAny, PyRef, PyResult, pyclass, pymethods};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::arrow::RecordBatchStream;
|
|
||||||
use crate::error::PythonErrorExt;
|
|
||||||
use crate::runtime::future_into_py;
|
|
||||||
|
|
||||||
#[pyclass(name = "SqlQuery")]
|
|
||||||
pub struct Query {
|
|
||||||
inner: Arc<lancedb::sql::Query>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Query {
|
|
||||||
pub(crate) fn new(inner: lancedb::sql::Query) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Arc::new(inner),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pymethods]
|
|
||||||
impl Query {
|
|
||||||
#[getter]
|
|
||||||
pub fn id(&self) -> Uuid {
|
|
||||||
self.inner.id()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn describe(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
|
||||||
let inner = self_.inner.clone();
|
|
||||||
future_into_py(self_.py(), async move {
|
|
||||||
inner
|
|
||||||
.describe()
|
|
||||||
.await
|
|
||||||
.map(QueryDescription::from)
|
|
||||||
.infer_error()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn reader(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
|
||||||
let inner = self_.inner.clone();
|
|
||||||
future_into_py(self_.py(), async move {
|
|
||||||
let stream = inner.reader().await.infer_error()?;
|
|
||||||
Ok(RecordBatchStream::new(stream))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cancel(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
|
||||||
let inner = self_.inner.clone();
|
|
||||||
future_into_py(self_.py(), async move {
|
|
||||||
inner.cancel().await.infer_error()?;
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pyclass(get_all, skip_from_py_object)]
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct QueryDescription {
|
|
||||||
id: Uuid,
|
|
||||||
status: String,
|
|
||||||
progress: Option<f64>,
|
|
||||||
expires_at: Option<DateTime<Utc>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pymethods]
|
|
||||||
impl QueryDescription {
|
|
||||||
fn __repr__(&self) -> String {
|
|
||||||
format!(
|
|
||||||
"QueryDescription(id={:?}, status={:?}, progress={:?}, expires_at={:?})",
|
|
||||||
self.id, self.status, self.progress, self.expires_at
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<lancedb::sql::QueryDescription> for QueryDescription {
|
|
||||||
fn from(description: lancedb::sql::QueryDescription) -> Self {
|
|
||||||
Self {
|
|
||||||
id: description.id,
|
|
||||||
status: description.status.to_string(),
|
|
||||||
progress: description.progress,
|
|
||||||
expires_at: description.expires_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lancedb"
|
name = "lancedb"
|
||||||
version = "0.39.0-beta.6"
|
version = "0.39.0-beta.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
description = "LanceDB: A serverless, low-latency vector database for AI applications"
|
description = "LanceDB: A serverless, low-latency vector database for AI applications"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -21,8 +21,6 @@ arrow-select = { workspace = true }
|
|||||||
arrow-ord = { workspace = true }
|
arrow-ord = { workspace = true }
|
||||||
arrow-cast = { workspace = true }
|
arrow-cast = { workspace = true }
|
||||||
arrow-ipc.workspace = true
|
arrow-ipc.workspace = true
|
||||||
arrow-flight = { workspace = true, optional = true }
|
|
||||||
prost = { version = "0.14", optional = true }
|
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
datafusion-catalog.workspace = true
|
datafusion-catalog.workspace = true
|
||||||
datafusion-common.workspace = true
|
datafusion-common.workspace = true
|
||||||
@@ -79,7 +77,6 @@ reqwest = { version = "0.12.0", default-features = false, features = [
|
|||||||
"rustls-tls-native-roots",
|
"rustls-tls-native-roots",
|
||||||
"stream",
|
"stream",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
tonic = { workspace = true, optional = true }
|
|
||||||
http = { version = "1", optional = true } # Matching what is in reqwest
|
http = { version = "1", optional = true } # Matching what is in reqwest
|
||||||
urlencoding = { version = "2", optional = true }
|
urlencoding = { version = "2", optional = true }
|
||||||
uuid = { workspace = true, features = ["v5"] }
|
uuid = { workspace = true, features = ["v5"] }
|
||||||
@@ -148,11 +145,8 @@ huggingface = [
|
|||||||
]
|
]
|
||||||
dynamodb = ["lance/dynamodb", "aws"]
|
dynamodb = ["lance/dynamodb", "aws"]
|
||||||
remote = [
|
remote = [
|
||||||
"dep:arrow-flight",
|
|
||||||
"dep:prost",
|
|
||||||
"dep:reqwest",
|
"dep:reqwest",
|
||||||
"dep:http",
|
"dep:http",
|
||||||
"dep:tonic",
|
|
||||||
"dep:urlencoding",
|
"dep:urlencoding",
|
||||||
"lance-namespace-impls/rest",
|
"lance-namespace-impls/rest",
|
||||||
"lance-namespace-impls/rest-adapter",
|
"lance-namespace-impls/rest-adapter",
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ pub struct PolarsDataFrameRecordBatchReader {
|
|||||||
impl PolarsDataFrameRecordBatchReader {
|
impl PolarsDataFrameRecordBatchReader {
|
||||||
/// Creates a new `PolarsDataFrameRecordBatchReader` from a given Polars DataFrame.
|
/// Creates a new `PolarsDataFrameRecordBatchReader` from a given Polars DataFrame.
|
||||||
/// If the input dataframe does not have aligned chunks, this function undergoes
|
/// If the input dataframe does not have aligned chunks, this function undergoes
|
||||||
/// the costly operation of reallocating each series as a single contiguous chunk.
|
/// the costly operation of reallocating each series as a single contigous chunk.
|
||||||
pub fn new(mut df: DataFrame) -> Result<Self> {
|
pub fn new(mut df: DataFrame) -> Result<Self> {
|
||||||
df.align_chunks();
|
df.align_chunks();
|
||||||
let arrow_schema =
|
let arrow_schema =
|
||||||
|
|||||||
@@ -7,9 +7,7 @@
|
|||||||
//! raw `Binary` / `LargeBinary` into the blob struct layout. Queries return
|
//! raw `Binary` / `LargeBinary` into the blob struct layout. Queries return
|
||||||
//! small descriptors, not bytes.
|
//! small descriptors, not bytes.
|
||||||
//!
|
//!
|
||||||
//! Blob tables require Lance file format >= 2.2. `_rowid` values stay valid
|
//! Blob tables require Lance file format >= 2.2 and stable row ids at create.
|
||||||
//! after compaction when the table has stable row ids. Overwrite is a new
|
|
||||||
//! create and does not keep the previous table's stable row id setting.
|
|
||||||
|
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -326,7 +324,6 @@ pub(crate) fn blob_column_names(schema: &Schema) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Bumps storage format to at least [`LanceFileVersion::V2_2`] for blob schemas.
|
/// Bumps storage format to at least [`LanceFileVersion::V2_2`] for blob schemas.
|
||||||
/// Leaves `enable_stable_row_ids` unchanged.
|
|
||||||
pub(crate) fn ensure_blob_storage_version(schema: &Schema, params: &mut WriteParams) {
|
pub(crate) fn ensure_blob_storage_version(schema: &Schema, params: &mut WriteParams) {
|
||||||
if !has_blob_columns(schema) {
|
if !has_blob_columns(schema) {
|
||||||
return;
|
return;
|
||||||
@@ -388,30 +385,6 @@ fn ensure_all_row_ids_resolved(column: &str, requested: usize, resolved: usize)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lance take reports a missing physical row address as NotSupported or InvalidInput.
|
|
||||||
fn map_blob_take_error(column: &str, requested: usize, err: lance::Error) -> Error {
|
|
||||||
let missing_row_addr = match &err {
|
|
||||||
lance::Error::NotSupported { source, .. } => {
|
|
||||||
source.to_string().contains("must not target deleted rows")
|
|
||||||
}
|
|
||||||
lance::Error::InvalidInput { source, .. } => source
|
|
||||||
.to_string()
|
|
||||||
.contains("belongs to non-existent fragment"),
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
if missing_row_addr {
|
|
||||||
Error::InvalidInput {
|
|
||||||
message: format!(
|
|
||||||
"blob read for column '{column}' requested {requested} row ids but some \
|
|
||||||
do not exist in the table; pass row ids collected from this table"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err.into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Materialize blob-local ranges (same length and order as `requests`, nulls preserved).
|
/// Materialize blob-local ranges (same length and order as `requests`, nulls preserved).
|
||||||
pub(crate) async fn take_blob_ranges_aligned(
|
pub(crate) async fn take_blob_ranges_aligned(
|
||||||
dataset: &Arc<Dataset>,
|
dataset: &Arc<Dataset>,
|
||||||
@@ -432,8 +405,7 @@ pub(crate) async fn take_blob_ranges_aligned(
|
|||||||
.with_row_ids(lance_requests)
|
.with_row_ids(lance_requests)
|
||||||
.preserve_order(true)
|
.preserve_order(true)
|
||||||
.execute()
|
.execute()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|err| map_blob_take_error(column, requests.len(), err))?;
|
|
||||||
ensure_all_row_ids_resolved(column, requests.len(), payloads.len())?;
|
ensure_all_row_ids_resolved(column, requests.len(), payloads.len())?;
|
||||||
|
|
||||||
let mut builder = LargeBinaryBuilder::new();
|
let mut builder = LargeBinaryBuilder::new();
|
||||||
@@ -462,8 +434,7 @@ pub(crate) async fn take_blobs_aligned(
|
|||||||
.with_row_ids(row_ids.to_vec())
|
.with_row_ids(row_ids.to_vec())
|
||||||
.preserve_order(true)
|
.preserve_order(true)
|
||||||
.execute()
|
.execute()
|
||||||
.await
|
.await?;
|
||||||
.map_err(|err| map_blob_take_error(column, row_ids.len(), err))?;
|
|
||||||
ensure_all_row_ids_resolved(column, row_ids.len(), payloads.len())?;
|
ensure_all_row_ids_resolved(column, row_ids.len(), payloads.len())?;
|
||||||
|
|
||||||
let mut builder = LargeBinaryBuilder::new();
|
let mut builder = LargeBinaryBuilder::new();
|
||||||
@@ -487,10 +458,7 @@ pub(crate) async fn take_blob_files_aligned(
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let handles = dataset
|
let handles = dataset.take_blobs(row_ids, column).await?;
|
||||||
.take_blobs(row_ids, column)
|
|
||||||
.await
|
|
||||||
.map_err(|err| map_blob_take_error(column, row_ids.len(), err))?;
|
|
||||||
ensure_all_row_ids_resolved(column, row_ids.len(), handles.len())?;
|
ensure_all_row_ids_resolved(column, row_ids.len(), handles.len())?;
|
||||||
Ok(handles
|
Ok(handles
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -532,27 +500,10 @@ mod tests {
|
|||||||
fn storage_version_bumps_to_v2_2() {
|
fn storage_version_bumps_to_v2_2() {
|
||||||
let mut params = WriteParams::default();
|
let mut params = WriteParams::default();
|
||||||
ensure_blob_storage_version(&blob_schema(), &mut params);
|
ensure_blob_storage_version(&blob_schema(), &mut params);
|
||||||
let resolved = params
|
assert_eq!(
|
||||||
.data_storage_version
|
params.data_storage_version.unwrap().resolve(),
|
||||||
.unwrap_or(LanceFileVersion::Stable)
|
ConcreteFileVersion::V2_2
|
||||||
.resolve();
|
);
|
||||||
assert_eq!(resolved, ConcreteFileVersion::V2_2);
|
|
||||||
assert!(!params.enable_stable_row_ids);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn storage_version_leaves_stable_row_ids_enabled() {
|
|
||||||
let mut params = WriteParams {
|
|
||||||
enable_stable_row_ids: true,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
ensure_blob_storage_version(&blob_schema(), &mut params);
|
|
||||||
assert!(params.enable_stable_row_ids);
|
|
||||||
let resolved = params
|
|
||||||
.data_storage_version
|
|
||||||
.unwrap_or(LanceFileVersion::Stable)
|
|
||||||
.resolve();
|
|
||||||
assert_eq!(resolved, ConcreteFileVersion::V2_2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -625,6 +576,5 @@ mod tests {
|
|||||||
let mut params = WriteParams::default();
|
let mut params = WriteParams::default();
|
||||||
ensure_blob_storage_version(&schema, &mut params);
|
ensure_blob_storage_version(&schema, &mut params);
|
||||||
assert!(params.data_storage_version.is_none());
|
assert!(params.data_storage_version.is_none());
|
||||||
assert!(!params.enable_stable_row_ids);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-152
@@ -23,18 +23,15 @@ use crate::connection::create_table::CreateTableBuilder;
|
|||||||
use crate::data::scannable::Scannable;
|
use crate::data::scannable::Scannable;
|
||||||
use crate::database::listing::ListingDatabase;
|
use crate::database::listing::ListingDatabase;
|
||||||
use crate::database::{
|
use crate::database::{
|
||||||
CloneTableRequest, Database, DatabaseOptions, JobInfo, OpenTableRequest, ReadConsistency,
|
CloneTableRequest, Database, DatabaseOptions, JobDescription, JobInfo, OpenTableRequest,
|
||||||
TableNamesRequest,
|
ReadConsistency, TableNamesRequest,
|
||||||
};
|
};
|
||||||
use crate::embeddings::{EmbeddingRegistry, MemoryRegistry};
|
use crate::embeddings::{EmbeddingRegistry, MemoryRegistry};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
#[cfg(feature = "remote")]
|
#[cfg(feature = "remote")]
|
||||||
use crate::remote::{
|
use crate::remote::{
|
||||||
client::ClientConfig,
|
client::ClientConfig,
|
||||||
db::{
|
db::{OPT_REMOTE_API_KEY, OPT_REMOTE_HOST_OVERRIDE, OPT_REMOTE_REGION},
|
||||||
OPT_REMOTE_API_KEY, OPT_REMOTE_HOST_OVERRIDE, OPT_REMOTE_REGION,
|
|
||||||
OPT_REMOTE_SQL_HOST_OVERRIDE,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use lance::io::ObjectStoreParams;
|
use lance::io::ObjectStoreParams;
|
||||||
pub use lance_file::version::LanceFileVersion;
|
pub use lance_file::version::LanceFileVersion;
|
||||||
@@ -325,43 +322,6 @@ pub struct CloneTableBuilder {
|
|||||||
request: CloneTableRequest,
|
request: CloneTableRequest,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builder for asynchronously executing a SQL statement on a remote database.
|
|
||||||
pub struct ExecuteQueryAsyncBuilder {
|
|
||||||
parent: Arc<dyn Database>,
|
|
||||||
query: String,
|
|
||||||
default_namespace_path: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExecuteQueryAsyncBuilder {
|
|
||||||
fn new(parent: Arc<dyn Database>, query: String) -> Self {
|
|
||||||
Self {
|
|
||||||
parent,
|
|
||||||
query,
|
|
||||||
default_namespace_path: vec!["public".to_string()],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the namespace used for unqualified table names.
|
|
||||||
///
|
|
||||||
/// An empty path is treated as `public`, which is the SQL name for the
|
|
||||||
/// root Lance namespace.
|
|
||||||
pub fn default_namespace_path<I, S>(mut self, path: I) -> Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = S>,
|
|
||||||
S: Into<String>,
|
|
||||||
{
|
|
||||||
self.default_namespace_path = path.into_iter().map(Into::into).collect();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start the statement and return its asynchronous query handle.
|
|
||||||
pub async fn execute(self) -> Result<crate::sql::Query> {
|
|
||||||
self.parent
|
|
||||||
.execute_query_async(&self.query, &self.default_namespace_path)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CloneTableBuilder {
|
impl CloneTableBuilder {
|
||||||
fn new(parent: Arc<dyn Database>, target_table_name: String, source_uri: String) -> Self {
|
fn new(parent: Arc<dyn Database>, target_table_name: String, source_uri: String) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@@ -445,51 +405,6 @@ impl Connection {
|
|||||||
&self.internal
|
&self.internal
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start executing SQL on a remote LanceDB database.
|
|
||||||
///
|
|
||||||
/// The query can reference tables in other databases with SQL dot notation.
|
|
||||||
/// Use [`ExecuteQueryAsyncBuilder::default_namespace_path`] to avoid qualifying
|
|
||||||
/// tables in the default namespace. Local connections return
|
|
||||||
/// [`Error::NotSupported`].
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// # async fn query(db: &lancedb::Connection) -> lancedb::Result<()> {
|
|
||||||
/// use futures::TryStreamExt;
|
|
||||||
///
|
|
||||||
/// let query = db
|
|
||||||
/// .execute_query_async("SELECT * FROM events LIMIT 10")
|
|
||||||
/// .default_namespace_path(["public"])
|
|
||||||
/// .execute()
|
|
||||||
/// .await?;
|
|
||||||
/// println!("query id: {}", query.id());
|
|
||||||
/// let mut batches = query.reader().await?;
|
|
||||||
/// while let Some(batch) = batches.try_next().await? {
|
|
||||||
/// println!("received {} rows", batch.num_rows());
|
|
||||||
/// }
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn execute_query_async(&self, query: impl Into<String>) -> ExecuteQueryAsyncBuilder {
|
|
||||||
ExecuteQueryAsyncBuilder::new(self.internal.clone(), query.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describe a submitted SQL query by its connection-scoped id.
|
|
||||||
///
|
|
||||||
/// This performs one bounded status poll using state retained by this
|
|
||||||
/// connection. Running state with a live query handle is not evicted;
|
|
||||||
/// abandoned state has bounded retention, and server expiration is
|
|
||||||
/// honored. Terminal state is retained briefly.
|
|
||||||
/// Query ids are not portable to another connection. Local connections
|
|
||||||
/// return [`Error::NotSupported`].
|
|
||||||
pub async fn describe_query(
|
|
||||||
&self,
|
|
||||||
query_id: uuid::Uuid,
|
|
||||||
) -> Result<crate::sql::QueryDescription> {
|
|
||||||
self.internal.describe_query(query_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the names of all tables in the database
|
/// Get the names of all tables in the database
|
||||||
///
|
///
|
||||||
/// The names will be returned in lexicographical order (ascending)
|
/// The names will be returned in lexicographical order (ascending)
|
||||||
@@ -670,34 +585,14 @@ impl Connection {
|
|||||||
self.internal.read_consistency().await
|
self.internal.read_consistency().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open a server-side job by id, returning a handle with its record
|
/// A [`crate::job::Job`] handle for a server-side job by id, suitable for
|
||||||
/// already populated. Fails with [`crate::Error::JobNotFound`] when the
|
/// waiting on or cancelling the job.
|
||||||
/// server has no such job, the way [`Connection::open_table`] does for a
|
|
||||||
/// missing table.
|
|
||||||
///
|
///
|
||||||
/// This is the one way in: the returned [`crate::job::Job`] answers for
|
/// The handle is constructed without a server round trip; an unknown id
|
||||||
/// its own state, specification, result, failure and event history, so
|
/// surfaces when the handle is used. Only server-backed databases support
|
||||||
/// there is no separate connection-level call for any of them.
|
/// job handles by id.
|
||||||
///
|
pub fn job(&self, job_id: impl AsRef<str>) -> Result<crate::job::Job> {
|
||||||
/// # Example
|
self.internal.job(job_id.as_ref())
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// # use lancedb::job::JobEventsRequest;
|
|
||||||
/// # async fn open_job(
|
|
||||||
/// # connection: &lancedb::Connection,
|
|
||||||
/// # job_id: &str,
|
|
||||||
/// # ) -> Result<(), Box<dyn std::error::Error>> {
|
|
||||||
/// let job = connection.open_job(job_id).await?;
|
|
||||||
/// println!("{:?} {:?}", job.state(), job.result());
|
|
||||||
/// let done = job
|
|
||||||
/// .events(JobEventsRequest::default().filter("state = 'claim_complete'"))
|
|
||||||
/// .await?;
|
|
||||||
/// println!("{} completions", done.iter().map(|b| b.num_rows()).sum::<usize>());
|
|
||||||
/// # Ok(())
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub async fn open_job(&self, job_id: impl AsRef<str>) -> Result<crate::job::Job> {
|
|
||||||
self.internal.open_job(job_id.as_ref()).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List server-side jobs across the database's tables.
|
/// List server-side jobs across the database's tables.
|
||||||
@@ -705,12 +600,24 @@ impl Connection {
|
|||||||
self.internal.list_jobs().await
|
self.internal.list_jobs().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Describe a single server-side job by id. `None` when the server has no
|
||||||
|
/// such job.
|
||||||
|
pub async fn get_job(&self, job_id: impl AsRef<str>) -> Result<Option<JobDescription>> {
|
||||||
|
self.internal.get_job(job_id.as_ref()).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Request cancellation of a server-side job by id. Returns true if the
|
/// Request cancellation of a server-side job by id. Returns true if the
|
||||||
/// server accepted the cancellation, false if no such job exists.
|
/// server accepted the cancellation, false if no such job exists.
|
||||||
pub async fn cancel_job(&self, job_id: impl AsRef<str>) -> Result<bool> {
|
pub async fn cancel_job(&self, job_id: impl AsRef<str>) -> Result<bool> {
|
||||||
self.internal.cancel_job(job_id.as_ref()).await
|
self.internal.cancel_job(job_id.as_ref()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The lifecycle event history of a server-side job (all jobs when
|
||||||
|
/// `job_id` is `None`), as recorded Arrow batches.
|
||||||
|
pub async fn job_history(&self, job_id: Option<&str>) -> Result<Vec<RecordBatch>> {
|
||||||
|
self.internal.job_history(job_id).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Drop a table in the database.
|
/// Drop a table in the database.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
@@ -827,7 +734,7 @@ impl Connection {
|
|||||||
pub struct ConnectRequest {
|
pub struct ConnectRequest {
|
||||||
/// Database URI
|
/// Database URI
|
||||||
///
|
///
|
||||||
/// ### Accepted URI formats
|
/// ### Accpeted URI formats
|
||||||
///
|
///
|
||||||
/// - `/path/to/database` - local database on file system.
|
/// - `/path/to/database` - local database on file system.
|
||||||
/// - `s3://bucket/path/to/database` or `gs://bucket/path/to/database` - database on cloud object store
|
/// - `s3://bucket/path/to/database` or `gs://bucket/path/to/database` - database on cloud object store
|
||||||
@@ -957,19 +864,6 @@ impl ConnectBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the SQL service host override for a remote connection.
|
|
||||||
///
|
|
||||||
/// The SQL client is initialized lazily when the connection first executes
|
|
||||||
/// SQL and is retained for the connection's lifetime.
|
|
||||||
#[cfg(feature = "remote")]
|
|
||||||
pub fn sql_host_override(mut self, sql_host_override: &str) -> Self {
|
|
||||||
self.request.options.insert(
|
|
||||||
OPT_REMOTE_SQL_HOST_OVERRIDE.to_string(),
|
|
||||||
sql_host_override.to_string(),
|
|
||||||
);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the database specific options
|
/// Set the database specific options
|
||||||
///
|
///
|
||||||
/// See [crate::database::listing::ListingDatabaseOptions] for the options available for
|
/// See [crate::database::listing::ListingDatabaseOptions] for the options available for
|
||||||
@@ -1159,7 +1053,6 @@ impl ConnectBuilder {
|
|||||||
|
|
||||||
let mut merged_options = self.request.options.clone();
|
let mut merged_options = self.request.options.clone();
|
||||||
Self::apply_env_defaults(&ENV_VARS_TO_STORAGE_OPTS, &mut merged_options);
|
Self::apply_env_defaults(&ENV_VARS_TO_STORAGE_OPTS, &mut merged_options);
|
||||||
let sql_host_override = merged_options.get(OPT_REMOTE_SQL_HOST_OVERRIDE).cloned();
|
|
||||||
let options = RemoteDatabaseOptions::parse_from_map(&merged_options)?;
|
let options = RemoteDatabaseOptions::parse_from_map(&merged_options)?;
|
||||||
|
|
||||||
let region = options.region.ok_or_else(|| Error::InvalidInput {
|
let region = options.region.ok_or_else(|| Error::InvalidInput {
|
||||||
@@ -1201,15 +1094,11 @@ impl ConnectBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let storage_options = StorageOptions(options.storage_options.clone());
|
let storage_options = StorageOptions(options.storage_options.clone());
|
||||||
let host_overrides = crate::remote::db::RemoteHostOverrides {
|
|
||||||
rest: options.host_override,
|
|
||||||
sql: sql_host_override,
|
|
||||||
};
|
|
||||||
let internal = Arc::new(crate::remote::db::RemoteDatabase::try_new(
|
let internal = Arc::new(crate::remote::db::RemoteDatabase::try_new(
|
||||||
&self.request.uri,
|
&self.request.uri,
|
||||||
&api_key,
|
&api_key,
|
||||||
®ion,
|
®ion,
|
||||||
host_overrides,
|
options.host_override,
|
||||||
client_config,
|
client_config,
|
||||||
storage_options.into(),
|
storage_options.into(),
|
||||||
self.request.read_consistency_interval,
|
self.request.read_consistency_interval,
|
||||||
@@ -1503,23 +1392,6 @@ mod tests {
|
|||||||
assert_eq!(tc.connection.uri(), tc.uri);
|
assert_eq!(tc.connection.uri(), tc.uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_local_connection_rejects_sql_queries() {
|
|
||||||
let directory = tempdir().unwrap();
|
|
||||||
let connection = connect(directory.path().to_str().unwrap())
|
|
||||||
.execute()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(matches!(
|
|
||||||
connection.execute_query_async("SELECT 1").execute().await,
|
|
||||||
Err(Error::NotSupported { .. })
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
connection.describe_query(uuid::Uuid::nil()).await,
|
|
||||||
Err(Error::NotSupported { .. })
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "remote")]
|
#[cfg(feature = "remote")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_apply_env_defaults() {
|
fn test_apply_env_defaults() {
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ use std::collections::HashMap;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use arrow_array::RecordBatch;
|
||||||
|
|
||||||
use lance::dataset::ReadParams;
|
use lance::dataset::ReadParams;
|
||||||
use lance_namespace::LanceNamespace;
|
use lance_namespace::LanceNamespace;
|
||||||
use lance_namespace::models::{
|
use lance_namespace::models::{
|
||||||
@@ -204,8 +206,8 @@ pub enum ReadConsistency {
|
|||||||
/// compaction, column refresh, ...).
|
/// compaction, column refresh, ...).
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct JobInfo {
|
pub struct JobInfo {
|
||||||
/// The job id -- what [`Database::open_job`] and
|
/// The job id -- what [`Database::get_job`] and [`Database::cancel_job`]
|
||||||
/// [`Database::cancel_job`] accept.
|
/// accept.
|
||||||
pub job_id: String,
|
pub job_id: String,
|
||||||
/// The table the job runs against, without URI or namespace.
|
/// The table the job runs against, without URI or namespace.
|
||||||
pub table: String,
|
pub table: String,
|
||||||
@@ -216,8 +218,8 @@ pub struct JobInfo {
|
|||||||
pub created_at_millis: i64,
|
pub created_at_millis: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The server-side record behind a [`crate::job::Job`] handle: lifecycle
|
/// A described job from [`Database::get_job`]: lifecycle state plus the
|
||||||
/// state plus the job-type-specific specification and result.
|
/// job-type-specific specification.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct JobDescription {
|
pub struct JobDescription {
|
||||||
pub job_id: String,
|
pub job_id: String,
|
||||||
@@ -228,10 +230,6 @@ pub struct JobDescription {
|
|||||||
pub creation_ms: i64,
|
pub creation_ms: i64,
|
||||||
/// The job-type-specific specification. Null when the server omits it.
|
/// The job-type-specific specification. Null when the server omits it.
|
||||||
pub spec: serde_json::Value,
|
pub spec: serde_json::Value,
|
||||||
/// The job-type-specific terminal result, for job types that define one.
|
|
||||||
/// `None` until the job succeeds, so a job that never terminates reports
|
|
||||||
/// its progress through [`crate::job::Job::events`] instead.
|
|
||||||
pub result: Option<serde_json::Value>,
|
|
||||||
/// Why the job failed, when the job is failed and the server reports a
|
/// Why the job failed, when the job is failed and the server reports a
|
||||||
/// reason.
|
/// reason.
|
||||||
pub failure: Option<crate::error::JobFailure>,
|
pub failure: Option<crate::error::JobFailure>,
|
||||||
@@ -317,37 +315,30 @@ pub trait Database:
|
|||||||
async fn drop_function(&self, _name: &str, _version: &str) -> Result<bool> {
|
async fn drop_function(&self, _name: &str, _version: &str) -> Result<bool> {
|
||||||
function_catalog_not_supported()
|
function_catalog_not_supported()
|
||||||
}
|
}
|
||||||
/// Open a job by id, returning a handle with its record already
|
/// A [`crate::job::Job`] handle for a server-side job by id, suitable for
|
||||||
/// populated. Fails with [`crate::Error::JobNotFound`] when the server has
|
/// waiting on or cancelling the job. The handle is constructed without a
|
||||||
/// no such job.
|
/// server round trip; an unknown id surfaces when the handle is used.
|
||||||
async fn open_job(&self, _job_id: &str) -> Result<crate::job::Job> {
|
fn job(&self, _job_id: &str) -> Result<crate::job::Job> {
|
||||||
job_op_not_supported("open_job")
|
job_op_not_supported("job")
|
||||||
}
|
}
|
||||||
/// List server-side jobs across the database's tables.
|
/// List server-side jobs across the database's tables.
|
||||||
async fn list_jobs(&self) -> Result<Vec<JobInfo>> {
|
async fn list_jobs(&self) -> Result<Vec<JobInfo>> {
|
||||||
job_op_not_supported("list_jobs")
|
job_op_not_supported("list_jobs")
|
||||||
}
|
}
|
||||||
|
/// Describe a single job by id. `None` when the server has no such job.
|
||||||
|
async fn get_job(&self, _job_id: &str) -> Result<Option<JobDescription>> {
|
||||||
|
job_op_not_supported("get_job")
|
||||||
|
}
|
||||||
/// Request cancellation of a job by id. Returns true if the server
|
/// Request cancellation of a job by id. Returns true if the server
|
||||||
/// accepted the cancellation, false if no such job exists. Cancelling an
|
/// accepted the cancellation, false if no such job exists. Cancelling an
|
||||||
/// already-terminal job is a no-op success.
|
/// already-terminal job is a no-op success.
|
||||||
async fn cancel_job(&self, _job_id: &str) -> Result<bool> {
|
async fn cancel_job(&self, _job_id: &str) -> Result<bool> {
|
||||||
job_op_not_supported("cancel_job")
|
job_op_not_supported("cancel_job")
|
||||||
}
|
}
|
||||||
/// Start executing a SQL statement on a remote database.
|
/// The lifecycle event history of a job (all jobs when `job_id` is
|
||||||
async fn execute_query_async(
|
/// `None`), as recorded Arrow batches.
|
||||||
&self,
|
async fn job_history(&self, _job_id: Option<&str>) -> Result<Vec<RecordBatch>> {
|
||||||
_query: &str,
|
job_op_not_supported("job_history")
|
||||||
_default_namespace_path: &[String],
|
|
||||||
) -> Result<crate::sql::Query> {
|
|
||||||
Err(crate::error::Error::NotSupported {
|
|
||||||
message: "SQL is not supported by this database".to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/// Describe a submitted SQL query by its connection-scoped id.
|
|
||||||
async fn describe_query(&self, _query_id: uuid::Uuid) -> Result<crate::sql::QueryDescription> {
|
|
||||||
Err(crate::error::Error::NotSupported {
|
|
||||||
message: "SQL is not supported by this database".to_string(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
/// Open a table in the database
|
/// Open a table in the database
|
||||||
async fn open_table(&self, request: OpenTableRequest) -> Result<Arc<dyn BaseTable>>;
|
async fn open_table(&self, request: OpenTableRequest) -> Result<Arc<dyn BaseTable>>;
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ use lance_table::io::commit::commit_handler_from_url;
|
|||||||
use object_store::local::LocalFileSystem;
|
use object_store::local::LocalFileSystem;
|
||||||
use snafu::ResultExt;
|
use snafu::ResultExt;
|
||||||
|
|
||||||
use crate::blob::ensure_blob_storage_version;
|
use crate::blob::{ensure_blob_storage_version, has_blob_columns};
|
||||||
use crate::connection::ConnectRequest;
|
use crate::connection::ConnectRequest;
|
||||||
use crate::database::ReadConsistency;
|
use crate::database::ReadConsistency;
|
||||||
use crate::database::namespace::LanceNamespaceDatabase;
|
use crate::database::namespace::LanceNamespaceDatabase;
|
||||||
@@ -512,7 +512,7 @@ impl ListingDatabase {
|
|||||||
// iter thru the query params and extract the commit store param
|
// iter thru the query params and extract the commit store param
|
||||||
let mut engine = None;
|
let mut engine = None;
|
||||||
let mut mirrored_store = None;
|
let mut mirrored_store = None;
|
||||||
let mut filtered_queries = vec![];
|
let mut filtered_querys = vec![];
|
||||||
|
|
||||||
// WARNING: specifying engine is NOT a publicly supported feature in lancedb yet
|
// WARNING: specifying engine is NOT a publicly supported feature in lancedb yet
|
||||||
// THE API WILL CHANGE
|
// THE API WILL CHANGE
|
||||||
@@ -528,13 +528,13 @@ impl ListingDatabase {
|
|||||||
mirrored_store = Some(value.to_string());
|
mirrored_store = Some(value.to_string());
|
||||||
} else {
|
} else {
|
||||||
// to owned so we can modify the url
|
// to owned so we can modify the url
|
||||||
filtered_queries.push((key.to_string(), value.to_string()));
|
filtered_querys.push((key.to_string(), value.to_string()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out the commit store query param -- it's a lancedb param
|
// Filter out the commit store query param -- it's a lancedb param
|
||||||
url.query_pairs_mut().clear();
|
url.query_pairs_mut().clear();
|
||||||
url.query_pairs_mut().extend_pairs(filtered_queries);
|
url.query_pairs_mut().extend_pairs(filtered_querys);
|
||||||
// Take a copy of the query string so we can propagate it to lance.
|
// Take a copy of the query string so we can propagate it to lance.
|
||||||
// `query_pairs_mut()` leaves the URL with `Some("")` even when no
|
// `query_pairs_mut()` leaves the URL with `Some("")` even when no
|
||||||
// pairs survive (or none existed in the first place), so an empty
|
// pairs survive (or none existed in the first place), so an empty
|
||||||
@@ -827,6 +827,7 @@ impl ListingDatabase {
|
|||||||
if let Some(enable_stable_row_ids) = overrides
|
if let Some(enable_stable_row_ids) = overrides
|
||||||
.enable_stable_row_ids
|
.enable_stable_row_ids
|
||||||
.or(self.new_table_config.enable_stable_row_ids)
|
.or(self.new_table_config.enable_stable_row_ids)
|
||||||
|
.or(has_blob_columns(&data_schema).then_some(true))
|
||||||
{
|
{
|
||||||
write_params.enable_stable_row_ids = enable_stable_row_ids;
|
write_params.enable_stable_row_ids = enable_stable_row_ids;
|
||||||
}
|
}
|
||||||
@@ -896,11 +897,11 @@ impl Database for ListingDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn read_consistency(&self) -> Result<ReadConsistency> {
|
async fn read_consistency(&self) -> Result<ReadConsistency> {
|
||||||
if let Some(interval) = self.read_consistency_interval {
|
if let Some(read_consistency_inverval) = self.read_consistency_interval {
|
||||||
if interval.is_zero() {
|
if read_consistency_inverval.is_zero() {
|
||||||
Ok(ReadConsistency::Strong)
|
Ok(ReadConsistency::Strong)
|
||||||
} else {
|
} else {
|
||||||
Ok(ReadConsistency::Eventual(interval))
|
Ok(ReadConsistency::Eventual(read_consistency_inverval))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(ReadConsistency::Manual)
|
Ok(ReadConsistency::Manual)
|
||||||
@@ -3043,15 +3044,15 @@ mod tests {
|
|||||||
/// across platforms — see the `file://` test below).
|
/// across platforms — see the `file://` test below).
|
||||||
fn capture_query_like_connect(input_uri: &str) -> Option<String> {
|
fn capture_query_like_connect(input_uri: &str) -> Option<String> {
|
||||||
let mut url = url::Url::parse(input_uri).unwrap();
|
let mut url = url::Url::parse(input_uri).unwrap();
|
||||||
let mut filtered_queries = Vec::new();
|
let mut filtered_querys = Vec::new();
|
||||||
for (key, value) in url.query_pairs() {
|
for (key, value) in url.query_pairs() {
|
||||||
if key == ENGINE || key == MIRRORED_STORE {
|
if key == ENGINE || key == MIRRORED_STORE {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
filtered_queries.push((key.to_string(), value.to_string()));
|
filtered_querys.push((key.to_string(), value.to_string()));
|
||||||
}
|
}
|
||||||
url.query_pairs_mut().clear();
|
url.query_pairs_mut().clear();
|
||||||
url.query_pairs_mut().extend_pairs(filtered_queries);
|
url.query_pairs_mut().extend_pairs(filtered_querys);
|
||||||
url.query().filter(|q| !q.is_empty()).map(|s| s.to_string())
|
url.query().filter(|q| !q.is_empty()).map(|s| s.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
//! Namespace-based database implementation that delegates table management to lance-namespace
|
//! Namespace-based database implementation that delegates table management to lance-namespace
|
||||||
|
|
||||||
use lance_datafusion::utils::StreamingWriteSource;
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ use lance_namespace_impls::ConnectBuilder;
|
|||||||
use lance_table::io::commit::CommitHandler;
|
use lance_table::io::commit::CommitHandler;
|
||||||
use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler;
|
use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler;
|
||||||
|
|
||||||
use crate::blob::ensure_blob_storage_version;
|
use crate::blob::{ensure_blob_storage_version, has_blob_columns};
|
||||||
use crate::connection::NamespaceClientPushdownOperation;
|
use crate::connection::NamespaceClientPushdownOperation;
|
||||||
use crate::database::ReadConsistency;
|
use crate::database::ReadConsistency;
|
||||||
use crate::database::listing::{NewTableConfig, take_request_creation_overrides};
|
use crate::database::listing::{NewTableConfig, take_request_creation_overrides};
|
||||||
@@ -218,6 +217,7 @@ impl LanceNamespaceDatabase {
|
|||||||
if let Some(enable_stable_row_ids) = overrides
|
if let Some(enable_stable_row_ids) = overrides
|
||||||
.enable_stable_row_ids
|
.enable_stable_row_ids
|
||||||
.or(self.new_table_config.enable_stable_row_ids)
|
.or(self.new_table_config.enable_stable_row_ids)
|
||||||
|
.or(has_blob_columns(data_schema.as_ref()).then_some(true))
|
||||||
{
|
{
|
||||||
params.enable_stable_row_ids = enable_stable_row_ids;
|
params.enable_stable_row_ids = enable_stable_row_ids;
|
||||||
}
|
}
|
||||||
@@ -251,11 +251,11 @@ impl Database for LanceNamespaceDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn read_consistency(&self) -> Result<ReadConsistency> {
|
async fn read_consistency(&self) -> Result<ReadConsistency> {
|
||||||
if let Some(interval) = self.read_consistency_interval {
|
if let Some(read_consistency_inverval) = self.read_consistency_interval {
|
||||||
if interval.is_zero() {
|
if read_consistency_inverval.is_zero() {
|
||||||
Ok(ReadConsistency::Strong)
|
Ok(ReadConsistency::Strong)
|
||||||
} else {
|
} else {
|
||||||
Ok(ReadConsistency::Eventual(interval))
|
Ok(ReadConsistency::Eventual(read_consistency_inverval))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(ReadConsistency::Manual)
|
Ok(ReadConsistency::Manual)
|
||||||
@@ -305,10 +305,6 @@ impl Database for LanceNamespaceDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn create_table(&self, request: DbCreateTableRequest) -> Result<Arc<dyn BaseTable>> {
|
async fn create_table(&self, request: DbCreateTableRequest) -> Result<Arc<dyn BaseTable>> {
|
||||||
// Refuse a bad declaration before the namespace records a table.
|
|
||||||
crate::table::computed_columns::ensure_declarations_are_planned(
|
|
||||||
&request.data.arrow_schema(),
|
|
||||||
)?;
|
|
||||||
let mut table_id = request.namespace_path.clone();
|
let mut table_id = request.namespace_path.clone();
|
||||||
table_id.push(request.name.clone());
|
table_id.push(request.name.clone());
|
||||||
let mut existing_table = None;
|
let mut existing_table = None;
|
||||||
|
|||||||
@@ -102,8 +102,6 @@ pub enum Error {
|
|||||||
},
|
},
|
||||||
#[snafu(display("Job{} was cancelled", job_id.as_ref().map(|id| format!(" {id}")).unwrap_or_default()))]
|
#[snafu(display("Job{} was cancelled", job_id.as_ref().map(|id| format!(" {id}")).unwrap_or_default()))]
|
||||||
JobCancelled { job_id: Option<String> },
|
JobCancelled { job_id: Option<String> },
|
||||||
#[snafu(display("Job '{job_id}' was not found"))]
|
|
||||||
JobNotFound { job_id: String },
|
|
||||||
|
|
||||||
// 3rd party / external errors
|
// 3rd party / external errors
|
||||||
#[snafu(display("object_store error: {source}"))]
|
#[snafu(display("object_store error: {source}"))]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user