mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-12 16:22:24 +00:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6702e3fec1 | ||
|
|
e0bd4b5fa1 | ||
|
|
13f9dd630b | ||
|
|
bc4497b21a | ||
|
|
1da5876870 | ||
|
|
577fb48376 | ||
|
|
c7b051aff7 | ||
|
|
2e205ac9bb | ||
|
|
3e3878b223 | ||
|
|
19fb665c76 | ||
|
|
1f95398c34 | ||
|
|
0111a72dc3 | ||
|
|
a487d4033e | ||
|
|
02ea0dda9f | ||
|
|
c7980dbc40 | ||
|
|
1b0f9329ea | ||
|
|
e5cc7a4d66 | ||
|
|
21f11b4463 | ||
|
|
8c9c5c5a5f | ||
|
|
aab23eb39e | ||
|
|
e639b1b650 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tool.bumpversion]
|
[tool.bumpversion]
|
||||||
current_version = "0.39.0-beta.1"
|
current_version = "0.39.0-beta.6"
|
||||||
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,7 +232,10 @@ 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 "," -`
|
||||||
cargo test --profile ci --features $ALL_FEATURES --locked
|
# Run doctests before test binaries fill the runner disk. Examples are
|
||||||
|
# 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:
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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,6 +10,10 @@ 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
@@ -0,0 +1,19 @@
|
|||||||
|
[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
+349
-251
File diff suppressed because it is too large
Load Diff
+18
-16
@@ -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.11", default-features = false, "tag" = "v12.0.0-beta.11", "git" = "https://github.com/lance-format/lance.git" }
|
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-core = { "version" = "=12.0.0-beta.11", "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-datagen = { "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-file = { "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-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-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-index = { "version" = "=12.0.0-beta.11", "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-linalg = { "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-namespace = { "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-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-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-table = { "version" = "=12.0.0-beta.11", "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-testing = { "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-datafusion = { "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-encoding = { "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-arrow = { "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" }
|
||||||
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,6 +39,7 @@ 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 }
|
||||||
@@ -59,7 +60,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.13.2"
|
object_store = "0.14.1"
|
||||||
pin-project = "1.0.7"
|
pin-project = "1.0.7"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
snafu = "0.8"
|
snafu = "0.8"
|
||||||
@@ -71,7 +72,8 @@ 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"] }
|
||||||
uuid = { version = "1.7.0", features = ["v4"] }
|
tonic = { version = "0.14", features = ["tls-native-roots", "tls-ring"] }
|
||||||
|
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 targetted vector to search for. Required.
|
The targeted 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.1</version>
|
<version>0.39.0-beta.6</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
[**@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,26 +448,6 @@ 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
|
||||||
@@ -482,48 +462,6 @@ 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
|
||||||
@@ -648,6 +586,30 @@ 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
|
||||||
|
|||||||
+159
-12
@@ -8,19 +8,46 @@
|
|||||||
|
|
||||||
A handle to an operation that may still be running.
|
A handle to an operation that may still be running.
|
||||||
|
|
||||||
## Constructors
|
The operation may already be complete when the handle is created.
|
||||||
|
|
||||||
### new Job()
|
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:
|
||||||
|
|
||||||
|
- [Job.refresh](Job.md#refresh) and [Job.status](Job.md#status) fetch the whole record.
|
||||||
|
- [Job.wait](Job.md#wait) records the terminal state it establishes, but not the
|
||||||
|
rest of the record.
|
||||||
|
- Everything is null until one of those runs.
|
||||||
|
|
||||||
|
## Accessors
|
||||||
|
|
||||||
|
### creationMs
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
new Job(): Job
|
get creationMs(): null | number
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When the job was created, in milliseconds since the epoch.
|
||||||
|
|
||||||
#### Returns
|
#### Returns
|
||||||
|
|
||||||
[`Job`](Job.md)
|
`null` \| `number`
|
||||||
|
|
||||||
## Accessors
|
***
|
||||||
|
|
||||||
|
### 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
|
||||||
|
|
||||||
@@ -28,8 +55,69 @@ new Job(): Job
|
|||||||
get id(): null | string
|
get id(): null | string
|
||||||
```
|
```
|
||||||
|
|
||||||
Identifies the operation on the server that is running it. Operations
|
Identifies the operation on the server that is running it.
|
||||||
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
|
||||||
|
|
||||||
@@ -51,18 +139,61 @@ 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",
|
The operation's current lifecycle state: "running", "finished", "failed",
|
||||||
"failed", or "cancelled".
|
or "cancelled".
|
||||||
|
|
||||||
A point snapshot; unlike [Job.wait](Job.md#wait) it does not block or reject
|
A point snapshot; unlike [Job.wait](Job.md#wait) it does not block or reject on a
|
||||||
on a terminal failure state. States a newer server reports that this
|
terminal failure state. Also refreshes the getters above.
|
||||||
client version does not know pass through as-is.
|
|
||||||
|
|
||||||
#### Returns
|
#### Returns
|
||||||
|
|
||||||
@@ -70,6 +201,22 @@ client version does not know pass through as-is.
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
### 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 condtion will be updated. Any
|
matched rows that satisfy the condition 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,6 +137,20 @@ 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
|
||||||
@@ -499,6 +513,54 @@ 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
|
||||||
@@ -1266,7 +1328,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 calilng this method.
|
repeatedly calling this method.
|
||||||
|
|
||||||
##### Parameters
|
##### Parameters
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
[**@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);
|
||||||
|
```
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[**@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,6 +19,7 @@
|
|||||||
## 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)
|
||||||
@@ -96,7 +97,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)
|
||||||
- [JobDescription](interfaces/JobDescription.md)
|
- [JobEventsOptions](interfaces/JobEventsOptions.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)
|
||||||
@@ -143,6 +144,7 @@
|
|||||||
|
|
||||||
- [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)
|
||||||
@@ -158,9 +160,11 @@
|
|||||||
## 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 divded by 8.
|
by 16 we use the dimension divided 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 specfic index to create and also
|
This option allows you to specify a specific 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.
|
||||||
|
|||||||
@@ -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 divded by 8.
|
by 16 we use the dimension divided 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.
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
[**@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".
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
[**@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.getJob` and `Connection.cancelJob`
|
The job id -- what `Connection.openJob` and `Connection.cancelJob`
|
||||||
accept.
|
accept.
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
[**@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,6 +28,59 @@ 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
|
||||||
@@ -94,6 +147,8 @@ 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
|
||||||
@@ -102,6 +157,18 @@ 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
|
||||||
@@ -249,6 +316,12 @@ 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.1</version>
|
<version>0.39.0-beta.6</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.1</version>
|
<version>0.39.0-beta.6</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.11</lance-core.version>
|
<lance-core.version>12.0.0-beta.17</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.1"
|
version = "0.39.0-beta.6"
|
||||||
publish = false
|
publish = false
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
// 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,6 +939,7 @@ 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) => {
|
||||||
@@ -967,6 +968,16 @@ 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;
|
||||||
@@ -988,6 +999,7 @@ 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",
|
||||||
@@ -1004,22 +1016,65 @@ 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);
|
||||||
|
|
||||||
const history = await db.jobHistory("job-1");
|
// Opening a job hands back a populated handle; a missing one rejects.
|
||||||
expect(history.numRows).toEqual(2);
|
await expect(db.openJob("missing")).rejects.toThrow("not found");
|
||||||
|
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 = db.job("job-1");
|
const job = await db.openJob("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,6 +18,7 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Table,
|
Table,
|
||||||
VectorQuery,
|
VectorQuery,
|
||||||
|
blob,
|
||||||
connect,
|
connect,
|
||||||
tokenize,
|
tokenize,
|
||||||
} from "../lancedb";
|
} from "../lancedb";
|
||||||
@@ -281,7 +282,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: 684,
|
totalBytes: 550,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Index files count toward totalBytes too (only deletion files and
|
// Index files count toward totalBytes too (only deletion files and
|
||||||
@@ -289,7 +290,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(684);
|
expect(statsWithIndex.totalBytes).toBeGreaterThan(550);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should overwrite data if asked", async () => {
|
it("should overwrite data if asked", async () => {
|
||||||
@@ -2401,6 +2402,276 @@ 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(() => {
|
||||||
@@ -3252,7 +3523,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] },
|
{ text: "fo", vector: [0.4, 0.5, 0.6] }, // spellchecker:disable-line
|
||||||
{ 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] },
|
||||||
@@ -3277,7 +3548,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);
|
expect(resultSet.has("fo")).toBe(true); // spellchecker:disable-line
|
||||||
expect(resultSet.has("food")).toBe(true);
|
expect(resultSet.has("food")).toBe(true);
|
||||||
|
|
||||||
const prefixResults = await table
|
const prefixResults = await table
|
||||||
|
|||||||
+104
-3
@@ -40,6 +40,7 @@ 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,
|
||||||
@@ -430,12 +431,14 @@ 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) {
|
||||||
@@ -445,6 +448,35 @@ 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" &&
|
||||||
@@ -480,6 +512,32 @@ 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;
|
||||||
@@ -495,7 +553,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 as unknown as ArrowData<DataType>[],
|
children: childVectors.map((v) => v.data[0]),
|
||||||
});
|
});
|
||||||
return arrowMakeVector(structData);
|
return arrowMakeVector(structData);
|
||||||
} else {
|
} else {
|
||||||
@@ -503,6 +561,48 @@ 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
|
||||||
*/
|
*/
|
||||||
@@ -600,7 +700,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 specfied",
|
"makeVector requires at least one value or the type must be specified",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const sampleValue = values.find((val) => val !== null && val !== undefined);
|
const sampleValue = values.find((val) => val !== null && val !== undefined);
|
||||||
@@ -858,7 +958,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 provded then embedding columns will
|
* embedding columns. If no schema is provided 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(
|
||||||
@@ -952,6 +1052,7 @@ 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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
// 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,7 +1,6 @@
|
|||||||
// 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,
|
||||||
@@ -16,6 +15,7 @@ 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,8 +27,6 @@ import type {
|
|||||||
CreateNamespaceResponse,
|
CreateNamespaceResponse,
|
||||||
DescribeNamespaceResponse,
|
DescribeNamespaceResponse,
|
||||||
DropNamespaceResponse,
|
DropNamespaceResponse,
|
||||||
Job,
|
|
||||||
JobDescription,
|
|
||||||
JobInfo,
|
JobInfo,
|
||||||
ListNamespacesResponse,
|
ListNamespacesResponse,
|
||||||
ListTablesResponse,
|
ListTablesResponse,
|
||||||
@@ -557,24 +555,19 @@ export abstract class Connection {
|
|||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link Job} handle for a server-side job by id.
|
* 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
|
||||||
|
* {@link Connection.openTable} does for a missing table.
|
||||||
*
|
*
|
||||||
* The handle is constructed without a server round trip; an unknown id
|
* The returned {@link Job} answers for its own state, specification,
|
||||||
* surfaces when the handle is used. Dropping the handle has no effect on
|
* result, failure and event history, so there is no separate
|
||||||
* the job itself.
|
* connection-level call for any of them.
|
||||||
*/
|
*/
|
||||||
abstract job(jobId: string): Job;
|
abstract openJob(jobId: string): Promise<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.
|
||||||
*
|
*
|
||||||
@@ -582,13 +575,6 @@ 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 */
|
||||||
@@ -869,7 +855,7 @@ export class LocalConnection extends Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async dropTableAsync(name: string, namespacePath?: string[]): Promise<Job> {
|
async dropTableAsync(name: string, namespacePath?: string[]): Promise<Job> {
|
||||||
return this.inner.dropTableAsync(name, namespacePath ?? []);
|
return new Job(await this.inner.dropTableAsync(name, namespacePath ?? []));
|
||||||
}
|
}
|
||||||
|
|
||||||
async dropAllTables(namespacePath?: string[]): Promise<void> {
|
async dropAllTables(namespacePath?: string[]): Promise<void> {
|
||||||
@@ -928,29 +914,17 @@ export class LocalConnection extends Connection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
job(jobId: string): Job {
|
async openJob(jobId: string): Promise<Job> {
|
||||||
return this.inner.job(jobId);
|
return new Job(await this.inner.openJob(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,6 +77,9 @@ export {
|
|||||||
VectorColumnOptions,
|
VectorColumnOptions,
|
||||||
} from "./arrow";
|
} from "./arrow";
|
||||||
|
|
||||||
|
export { blob, isBlobField, BlobFile } from "./blob";
|
||||||
|
export type { BlobOptions } from "./blob";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Connection,
|
Connection,
|
||||||
CreateTableOptions,
|
CreateTableOptions,
|
||||||
@@ -94,13 +97,9 @@ export {
|
|||||||
RenameTableOptions,
|
RenameTableOptions,
|
||||||
} from "./connection";
|
} from "./connection";
|
||||||
|
|
||||||
export {
|
export { JobFailureInfo, JobInfo, Session } from "./native.js";
|
||||||
Job,
|
|
||||||
JobDescription,
|
export { Job, JobEventsOptions } from "./job";
|
||||||
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 divded by 8.
|
* by 16 we use the dimension divided 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 divded by 8.
|
* by 16 we use the dimension divided 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 specfic index to create and also
|
* This option allows you to specify a specific 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.
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
// 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 condtion will be updated. Any
|
* matched rows that satisfy the condition 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 mulitple versions of the same library (and sometimes
|
// generally allows for multiple 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
|
||||||
|
|||||||
+87
-26
@@ -17,8 +17,10 @@ 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,
|
||||||
@@ -30,7 +32,6 @@ import {
|
|||||||
DropColumnsResult,
|
DropColumnsResult,
|
||||||
IndexConfig,
|
IndexConfig,
|
||||||
IndexStatistics,
|
IndexStatistics,
|
||||||
Job,
|
|
||||||
LsmStats,
|
LsmStats,
|
||||||
Branches as NativeBranches,
|
Branches as NativeBranches,
|
||||||
OptimizeStats,
|
OptimizeStats,
|
||||||
@@ -313,7 +314,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 calilng this method.
|
* repeatedly calling 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
|
||||||
@@ -510,6 +511,35 @@ 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
|
||||||
@@ -1124,13 +1154,15 @@ 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 await this.inner.createIndexAsync(
|
return new Job(
|
||||||
nativeIndex,
|
await this.inner.createIndexAsync(
|
||||||
column,
|
nativeIndex,
|
||||||
options?.replace,
|
column,
|
||||||
options?.waitTimeoutSeconds,
|
options?.replace,
|
||||||
options?.name,
|
options?.waitTimeoutSeconds,
|
||||||
options?.train,
|
options?.name,
|
||||||
|
options?.train,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1158,23 +1190,34 @@ export class LocalTable extends Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
takeRowIds(rowIds: readonly (bigint | number)[]): TakeQuery {
|
takeRowIds(rowIds: readonly (bigint | number)[]): TakeQuery {
|
||||||
const ids = rowIds.map((id) => {
|
return new TakeQuery(this.inner.takeRowIds(rowIdsToBigInts(rowIds)));
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
return new TakeQuery(this.inner.takeRowIds(ids));
|
blobColumns(): Promise<string[]> {
|
||||||
|
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 {
|
||||||
@@ -1313,7 +1356,7 @@ export class LocalTable extends Table {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refreshColumnAsync(column: string): Promise<Job> {
|
async refreshColumnAsync(column: string): Promise<Job> {
|
||||||
return await this.inner.refreshColumnAsync(column);
|
return new Job(await this.inner.refreshColumnAsync(column));
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshMaterializedView(
|
async refreshMaterializedView(
|
||||||
@@ -1731,3 +1774,21 @@ 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.1",
|
"version": "0.39.0-beta.6",
|
||||||
"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.1",
|
"version": "0.39.0-beta.6",
|
||||||
"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.1",
|
"version": "0.39.0-beta.6",
|
||||||
"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.1",
|
"version": "0.39.0-beta.6",
|
||||||
"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.1",
|
"version": "0.39.0-beta.6",
|
||||||
"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.1",
|
"version": "0.39.0-beta.6",
|
||||||
"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.1",
|
"version": "0.39.0-beta.6",
|
||||||
"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.1",
|
"version": "0.39.0-beta.6",
|
||||||
"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.10.0(@opentelemetry/api@1.9.1)
|
version: 2.11.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.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)
|
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)
|
||||||
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.10.0':
|
'@opentelemetry/core@2.11.0':
|
||||||
resolution: {integrity: sha512-/wNZ8twnEQQA4HoHu22+vcsdru6pWPWxW+7w+FlxT6Id7PE/WIbZmVKkte+PF72e0F2dnImFeHD2syyE1Mw6MQ==}
|
resolution: {integrity: sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==}
|
||||||
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.10.0':
|
'@opentelemetry/resources@2.11.0':
|
||||||
resolution: {integrity: sha512-q6MMm2zhggzsHVNbabYwut+a6nbuQQe3URUoxaojM/8K1IBfwwPzvxIjNi2/lI1TFe+fMHMW9MWhrtDLEXEnkA==}
|
resolution: {integrity: sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==}
|
||||||
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.10.0':
|
'@opentelemetry/sdk-metrics@2.11.0':
|
||||||
resolution: {integrity: sha512-t6r1VSvXNtSDnPXU1FbZeetJb7yyovHmgu0wRSoftxtE0g2rSNhQZQUy69sRUCL+iioJpX8SN/S6wq6ZtvLySQ==}
|
resolution: {integrity: sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A==}
|
||||||
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,6 +1480,7 @@ 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==}
|
||||||
@@ -3238,8 +3239,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: '>=4.2.0'
|
typescript: '>=4.2.0'
|
||||||
|
|
||||||
ts-jest@29.4.9:
|
ts-jest@29.4.12:
|
||||||
resolution: {integrity: sha512-LTb9496gYPMCqjeDLdPrKuXtncudeV1yRZnF4Wo5l3SFi0RYEnYRNgMrFIdg+FHvfzjCyQk1cLncWVqiSX+EvQ==}
|
resolution: {integrity: sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==}
|
||||||
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:
|
||||||
@@ -5110,22 +5111,22 @@ snapshots:
|
|||||||
|
|
||||||
'@opentelemetry/api@1.9.1': {}
|
'@opentelemetry/api@1.9.1': {}
|
||||||
|
|
||||||
'@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1)':
|
'@opentelemetry/core@2.11.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.10.0(@opentelemetry/api@1.9.1)':
|
'@opentelemetry/resources@2.11.0(@opentelemetry/api@1.9.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
|
'@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1)
|
||||||
'@opentelemetry/semantic-conventions': 1.43.0
|
'@opentelemetry/semantic-conventions': 1.43.0
|
||||||
|
|
||||||
'@opentelemetry/sdk-metrics@2.10.0(@opentelemetry/api@1.9.1)':
|
'@opentelemetry/sdk-metrics@2.11.0(@opentelemetry/api@1.9.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
'@opentelemetry/core': 2.10.0(@opentelemetry/api@1.9.1)
|
'@opentelemetry/core': 2.11.0(@opentelemetry/api@1.9.1)
|
||||||
'@opentelemetry/resources': 2.10.0(@opentelemetry/api@1.9.1)
|
'@opentelemetry/resources': 2.11.0(@opentelemetry/api@1.9.1)
|
||||||
|
|
||||||
'@opentelemetry/semantic-conventions@1.43.0': {}
|
'@opentelemetry/semantic-conventions@1.43.0': {}
|
||||||
|
|
||||||
@@ -5574,7 +5575,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.0
|
semver: 7.8.5
|
||||||
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
|
||||||
@@ -6426,7 +6427,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.0
|
semver: 7.8.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6705,7 +6706,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.0
|
semver: 7.8.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -6828,7 +6829,7 @@ snapshots:
|
|||||||
|
|
||||||
make-dir@4.0.0:
|
make-dir@4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.8.0
|
semver: 7.8.5
|
||||||
|
|
||||||
make-error@1.3.6: {}
|
make-error@1.3.6: {}
|
||||||
|
|
||||||
@@ -7162,8 +7163,7 @@ 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.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):
|
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):
|
||||||
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.0
|
semver: 7.8.5
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// 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,13 +442,15 @@ impl Connection {
|
|||||||
self.get_inner()?.drop_all_tables(&ns).await.default_error()
|
self.get_inner()?.drop_all_tables(&ns).await.default_error()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A `Job` handle for a server-side job by id.
|
/// Open a server-side job by id, returning a handle with its record
|
||||||
|
/// already populated. Rejects when the server has no such job.
|
||||||
///
|
///
|
||||||
/// The handle is constructed without a server round trip; an unknown id
|
/// The returned handle answers for its own state, specification, result,
|
||||||
/// surfaces when the handle is used.
|
/// failure and event history, so there is no separate connection-level
|
||||||
#[napi]
|
/// call for any of them.
|
||||||
pub fn job(&self, job_id: String) -> napi::Result<crate::job::Job> {
|
#[napi(catch_unwind)]
|
||||||
let job = self.get_inner()?.job(job_id).default_error()?;
|
pub async fn open_job(&self, job_id: String) -> napi::Result<crate::job::Job> {
|
||||||
|
let job = self.get_inner()?.open_job(&job_id).await.default_error()?;
|
||||||
Ok(crate::job::Job::new(job))
|
Ok(crate::job::Job::new(job))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,17 +461,6 @@ 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)]
|
||||||
@@ -477,34 +468,6 @@ 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(
|
||||||
|
|||||||
+90
-34
@@ -3,6 +3,9 @@
|
|||||||
|
|
||||||
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;
|
||||||
@@ -55,12 +58,98 @@ 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.getJob` and `Connection.cancelJob`
|
/// The job id -- what `Connection.openJob` 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.
|
||||||
@@ -91,36 +180,3 @@ 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,6 +10,7 @@ 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,6 +15,7 @@ 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;
|
||||||
@@ -329,6 +330,44 @@ 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)
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lancedb-python"
|
name = "lancedb-python"
|
||||||
version = "0.39.0-beta.1"
|
version = "0.39.0-beta.6"
|
||||||
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"] }
|
pyo3 = { version = "0.28", features = ["abi3-py310", "chrono", "uuid"] }
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
pyo3-async-runtimes = { version = "0.28", features = [
|
pyo3-async-runtimes = { version = "0.28", features = [
|
||||||
"attributes",
|
"attributes",
|
||||||
@@ -40,6 +40,7 @@ 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,6 +139,7 @@ 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,7 +22,11 @@ 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,
|
||||||
@@ -101,6 +105,7 @@ 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,
|
||||||
@@ -129,6 +134,9 @@ 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
|
||||||
@@ -270,6 +278,7 @@ 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,
|
||||||
@@ -412,6 +421,7 @@ 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,
|
||||||
)
|
)
|
||||||
@@ -425,6 +435,7 @@ 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,
|
||||||
@@ -447,6 +458,9 @@ 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
|
||||||
@@ -534,6 +548,7 @@ 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,
|
||||||
@@ -556,6 +571,7 @@ __all__ = [
|
|||||||
"connect_namespace_async",
|
"connect_namespace_async",
|
||||||
"AsyncConnection",
|
"AsyncConnection",
|
||||||
"AsyncJob",
|
"AsyncJob",
|
||||||
|
"AsyncSqlQuery",
|
||||||
"AsyncLanceNamespaceDBConnection",
|
"AsyncLanceNamespaceDBConnection",
|
||||||
"AsyncTable",
|
"AsyncTable",
|
||||||
"FtsToken",
|
"FtsToken",
|
||||||
@@ -570,6 +586,8 @@ __all__ = [
|
|||||||
"vector",
|
"vector",
|
||||||
"DBConnection",
|
"DBConnection",
|
||||||
"Job",
|
"Job",
|
||||||
|
"QueryDescription",
|
||||||
|
"SqlQuery",
|
||||||
"LanceDBConnection",
|
"LanceDBConnection",
|
||||||
"LanceNamespaceDBConnection",
|
"LanceNamespaceDBConnection",
|
||||||
"LsmWriteSpec",
|
"LsmWriteSpec",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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
|
||||||
|
|
||||||
@@ -147,17 +148,20 @@ 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
|
||||||
def job(self, job_id: str) -> Job: ...
|
async def open_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 job_history(
|
async def execute_query_async(
|
||||||
self, job_id: Optional[str] = None
|
self,
|
||||||
) -> List[pa.RecordBatch]: ...
|
query: str,
|
||||||
|
*,
|
||||||
|
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,
|
||||||
@@ -236,9 +240,20 @@ 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
|
||||||
@@ -270,10 +285,33 @@ 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: ...
|
||||||
@@ -452,6 +490,7 @@ 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]],
|
||||||
|
|||||||
+93
-62
@@ -19,6 +19,7 @@ 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
|
||||||
@@ -47,6 +48,9 @@ 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,
|
||||||
@@ -68,10 +72,11 @@ 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 JobDescription, JobInfo
|
from ._lancedb import 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
|
||||||
@@ -740,26 +745,23 @@ class DBConnection(EnforceOverrides):
|
|||||||
"Function catalog operations are not supported for this connection type"
|
"Function catalog operations are not supported for this connection type"
|
||||||
)
|
)
|
||||||
|
|
||||||
def job(self, job_id: str) -> Job:
|
def open_job(self, job_id: str) -> Job:
|
||||||
"""A [Job][lancedb.job.Job] handle for a server-side job by id.
|
"""Open a server-side job by id, returning a handle with its record
|
||||||
|
already populated.
|
||||||
|
|
||||||
The handle is constructed without a server round trip; an unknown id
|
The returned [Job][lancedb.job.Job] answers for its own state,
|
||||||
surfaces when the handle is used. Dropping the handle has no effect
|
specification, result, failure and event history, so there is no
|
||||||
on the job itself.
|
separate connection-level call for any of them.
|
||||||
|
|
||||||
|
Raises `JobNotFoundError` when the server has no such job, the way
|
||||||
|
`open_table` does for a missing table.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("job is not supported for this connection type")
|
raise NotImplementedError("open_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.
|
||||||
|
|
||||||
@@ -771,14 +773,38 @@ class DBConnection(EnforceOverrides):
|
|||||||
"cancel_job is not supported for this connection type"
|
"cancel_job is not supported for this connection type"
|
||||||
)
|
)
|
||||||
|
|
||||||
def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]:
|
def execute_query(
|
||||||
"""The lifecycle event history of a server-side job, as Arrow batches.
|
self,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
default_namespace_path: Optional[List[str]] = None,
|
||||||
|
) -> pa.RecordBatchReader:
|
||||||
|
"""Execute SQL and return a blocking Arrow reader.
|
||||||
|
|
||||||
Lists history across all jobs when `job_id` is None.
|
This submits through :meth:`execute_query_async` and waits until the
|
||||||
|
initial result stream is readable. It does not wait for the full query
|
||||||
|
to finish.
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError(
|
return self.execute_query_async(
|
||||||
"job_history is not supported for this connection type"
|
query,
|
||||||
)
|
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):
|
||||||
@@ -875,6 +901,7 @@ class LanceDBConnection(DBConnection):
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
read_consistency_interval_secs,
|
read_consistency_interval_secs,
|
||||||
None,
|
None,
|
||||||
storage_options,
|
storage_options,
|
||||||
@@ -1423,14 +1450,11 @@ class LanceDBConnection(DBConnection):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def job(self, job_id: str) -> Job:
|
def open_job(self, job_id: str) -> Job:
|
||||||
"""A [Job][lancedb.job.Job] handle for a server-side job by id.
|
"""Open a server-side job by id. See
|
||||||
|
[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(self._conn.job(job_id))
|
return Job(LOOP.run(self._conn.open_job(job_id)))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def create_function_async(self, definition: UdfDefinition) -> Job[FunctionVersion]:
|
def create_function_async(self, definition: UdfDefinition) -> Job[FunctionVersion]:
|
||||||
@@ -1454,14 +1478,6 @@ 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.
|
||||||
@@ -1472,14 +1488,6 @@ 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.
|
||||||
@@ -2250,15 +2258,11 @@ 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)
|
||||||
|
|
||||||
def job(self, job_id: str) -> AsyncJob:
|
async def open_job(self, job_id: str) -> AsyncJob:
|
||||||
"""An [AsyncJob][lancedb.job.AsyncJob] handle for a server-side job
|
"""Open a server-side job by id. See
|
||||||
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 AsyncJob(self._inner.job(job_id))
|
return AsyncJob(await self._inner.open_job(job_id))
|
||||||
|
|
||||||
async def create_function_async(
|
async def create_function_async(
|
||||||
self, definition: UdfDefinition
|
self, definition: UdfDefinition
|
||||||
@@ -2298,13 +2302,6 @@ 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.
|
||||||
|
|
||||||
@@ -2314,12 +2311,46 @@ class AsyncConnection(object):
|
|||||||
"""
|
"""
|
||||||
return await self._inner.cancel_job(job_id)
|
return await self._inner.cancel_job(job_id)
|
||||||
|
|
||||||
async def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]:
|
async def execute_query(
|
||||||
"""The lifecycle event history of a server-side job, as Arrow batches.
|
self,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
default_namespace_path: Optional[List[str]] = None,
|
||||||
|
) -> AsyncRecordBatchReader:
|
||||||
|
"""Execute SQL and return an asynchronous Arrow reader.
|
||||||
|
|
||||||
Lists history across all jobs when `job_id` is None.
|
This submits through :meth:`execute_query_async` and waits until the
|
||||||
|
initial result stream is readable. It does not wait for the full query
|
||||||
|
to finish.
|
||||||
"""
|
"""
|
||||||
return await self._inner.job_history(job_id)
|
submitted = await self.execute_query_async(
|
||||||
|
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 insalled, which can be done with:
|
For Apple users, you will need the mlx package installed, 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, InstuctorEmbeddingFunction
|
from lancedb.embeddings import get_registry, InstructorEmbeddingFunction
|
||||||
|
|
||||||
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,3 +35,9 @@ 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,11 +470,7 @@ 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
|
||||||
@@ -484,6 +480,13 @@ 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."""
|
||||||
|
|
||||||
@@ -491,6 +494,7 @@ 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
|
||||||
|
|
||||||
@@ -911,8 +915,6 @@ 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]
|
||||||
@@ -924,7 +926,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=False,
|
nullable=field.nullable,
|
||||||
)
|
)
|
||||||
for field in fields
|
for field in fields
|
||||||
),
|
),
|
||||||
@@ -1307,8 +1309,9 @@ 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. Nullable outputs are rejected because V1
|
and ``output_schema`` together. Scalar outputs must be non-nullable. Every
|
||||||
uses physical NULL to represent unassigned computed-column rows.
|
named-struct field may be nullable; Enterprise preserves the struct's
|
||||||
|
validity when the result is expanded into sibling columns.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -1320,8 +1323,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. Must be non-nullable and be
|
Explicit scalar or named-struct output. Scalar outputs must be
|
||||||
provided together with ``input_schema``.
|
non-nullable. Must be 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
|
||||||
@@ -1387,6 +1390,7 @@ 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 divded by
|
the dimension is not evenly divisible by 16 we use the dimension divided 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,15 +4,27 @@
|
|||||||
"""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.
|
||||||
@@ -78,6 +90,149 @@ 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."""
|
||||||
@@ -122,6 +277,75 @@ 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,6 +12,7 @@ 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
|
||||||
@@ -48,8 +49,11 @@ 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,
|
||||||
@@ -1447,6 +1451,37 @@ 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,6 +78,10 @@ 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):
|
||||||
@@ -859,7 +863,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 wouln't be allowed otherwise)
|
# pa.Array wouldn't be allowed otherwise)
|
||||||
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
model_config = pydantic.ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
|
|
||||||
@@ -3893,14 +3897,54 @@ 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 = AsyncFTSQuery(self._inner.to_fts_query(), self._table)
|
fts_query, vec_query, limit, offset = self._create_child_queries()
|
||||||
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
|
||||||
@@ -3920,9 +3964,6 @@ 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),
|
||||||
@@ -3934,8 +3975,9 @@ 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=self._inner.get_limit(),
|
limit=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()
|
||||||
@@ -3964,14 +4006,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]
|
ProjectionExec: expr=[vector@0 as vector, text@3 as text, _distance@2 as _distance, _rowid@1 as _rowid]
|
||||||
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]
|
ProjectionExec: expr=[vector@2 as vector, text@3 as text, _score@1 as _score, _rowid@0 as _rowid]
|
||||||
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]
|
||||||
@@ -3986,8 +4028,9 @@ class AsyncHybridQuery(AsyncStandardQuery, AsyncVectorQueryBase):
|
|||||||
plan : str
|
plan : str
|
||||||
""" # noqa: E501
|
""" # noqa: E501
|
||||||
|
|
||||||
vector_plan = await self._inner.to_vector_query().explain_plan(verbose)
|
fts_query, vec_query, _, _ = self._create_child_queries()
|
||||||
fts_plan = await self._inner.to_fts_query().explain_plan(verbose)
|
vector_plan = await vec_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())
|
||||||
@@ -4014,14 +4057,12 @@ 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(
|
results.append(await vec_query.analyze_plan(distributed_metrics))
|
||||||
await self._inner.to_vector_query().analyze_plan(distributed_metrics)
|
|
||||||
)
|
|
||||||
results.append("FTS Search Query:")
|
results.append("FTS Search Query:")
|
||||||
results.append(
|
results.append(await fts_query.analyze_plan(distributed_metrics))
|
||||||
await self._inner.to_fts_query().analyze_plan(distributed_metrics)
|
|
||||||
)
|
|
||||||
|
|
||||||
return "\n".join(results)
|
return "\n".join(results)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ 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):
|
||||||
@@ -25,10 +26,12 @@ 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 JobDescription, JobInfo
|
from .._lancedb import JobInfo
|
||||||
from ..embeddings import EmbeddingFunctionConfig
|
from ..embeddings import EmbeddingFunctionConfig
|
||||||
from lance_namespace import (
|
from lance_namespace import (
|
||||||
LanceNamespace,
|
LanceNamespace,
|
||||||
@@ -116,6 +119,7 @@ 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):
|
||||||
@@ -161,6 +165,7 @@ 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
|
||||||
|
|
||||||
@@ -175,6 +180,7 @@ 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,
|
||||||
@@ -193,6 +199,7 @@ 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,
|
||||||
}
|
}
|
||||||
@@ -732,14 +739,11 @@ class RemoteDBConnection(DBConnection):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def job(self, job_id: str) -> Job:
|
def open_job(self, job_id: str) -> Job:
|
||||||
"""A [Job][lancedb.job.Job] handle for a server-side job by id.
|
"""Open a server-side job by id. See
|
||||||
|
[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(self._conn.job(job_id))
|
return Job(LOOP.run(self._conn.open_job(job_id)))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def create_function_async(self, definition: UdfDefinition) -> Job[FunctionVersion]:
|
def create_function_async(self, definition: UdfDefinition) -> Job[FunctionVersion]:
|
||||||
@@ -762,14 +766,6 @@ 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.
|
||||||
@@ -781,12 +777,35 @@ class RemoteDBConnection(DBConnection):
|
|||||||
return LOOP.run(self._conn.cancel_job(job_id))
|
return LOOP.run(self._conn.cancel_job(job_id))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def job_history(self, job_id: Optional[str] = None) -> List[pa.RecordBatch]:
|
def execute_query_async(
|
||||||
"""The lifecycle event history of a server-side job, as Arrow batches.
|
self,
|
||||||
|
query: str,
|
||||||
|
*,
|
||||||
|
default_namespace_path: Optional[List[str]] = None,
|
||||||
|
) -> SqlQuery:
|
||||||
|
"""Start executing SQL through this remote connection.
|
||||||
|
|
||||||
Lists history across all jobs when `job_id` is None.
|
Unqualified tables use this connection's database and the
|
||||||
|
``["public"]`` namespace by default. Fully qualified table names may
|
||||||
|
reference other databases available to the same deployment.
|
||||||
"""
|
"""
|
||||||
return LOOP.run(self._conn.job_history(job_id))
|
return SqlQuery(
|
||||||
|
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,4 +177,7 @@ 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 {"Authorization": f"Bearer {self._current_token}"}
|
return {
|
||||||
|
"Authorization": f"Bearer {self._current_token}",
|
||||||
|
"x-lancedb-credential-type": "oidc",
|
||||||
|
}
|
||||||
|
|||||||
@@ -720,7 +720,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 targetted vector to search for.
|
The targeted 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 inteded to
|
In that case, it can be set to None explicitly. This is intended 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,
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# 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"]
|
||||||
@@ -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 targetted vector to search for.
|
The targeted 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,6 +1793,9 @@ 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`.
|
||||||
"""
|
"""
|
||||||
@@ -1810,6 +1813,9 @@ 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.
|
||||||
"""
|
"""
|
||||||
@@ -1825,6 +1831,9 @@ 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
|
||||||
@@ -3832,7 +3841,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 targetted vector to search for.
|
The targeted 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
|
||||||
@@ -5629,7 +5638,7 @@ class AsyncTable:
|
|||||||
if fill_value is None:
|
if fill_value is None:
|
||||||
fill_value = 0.0
|
fill_value = 0.0
|
||||||
|
|
||||||
# _santitize_data is an old code path, but we will use it until the
|
# _sanitize_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
|
||||||
@@ -5805,7 +5814,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 targetted vector to search for.
|
The targeted 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,6 +66,25 @@ 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)
|
||||||
@@ -278,7 +297,10 @@ def test_blob_v2_projection_sources_use_typed_column_name():
|
|||||||
|
|
||||||
|
|
||||||
def _legacy_v1_table(name):
|
def _legacy_v1_table(name):
|
||||||
db = lancedb.connect("memory:///")
|
# Legacy v1 blob columns are only writable at file version <= 2.1.
|
||||||
|
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()),
|
||||||
@@ -691,6 +713,25 @@ 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",
|
||||||
@@ -739,8 +780,25 @@ 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, match="row IDs"):
|
with pytest.raises(ValueError) as exc_info:
|
||||||
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))]
|
||||||
|
|
||||||
registery = get_registry()
|
registry = get_registry()
|
||||||
func = registery.get("mock-embedding").create()
|
func = registry.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))]
|
||||||
|
|
||||||
registery = get_registry()
|
registry = get_registry()
|
||||||
func1 = registery.get("mock-embedding").create()
|
func1 = registry.get("mock-embedding").create()
|
||||||
func2 = registery.get("mock-embedding2").create()
|
func2 = registry.get("mock-embedding2").create()
|
||||||
|
|
||||||
class TestSchema(LanceModel):
|
class TestSchema(LanceModel):
|
||||||
text: str = func1.SourceField()
|
text: str = func1.SourceField()
|
||||||
|
|||||||
@@ -850,6 +850,43 @@ 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,8 +1011,13 @@ 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("nce", query_type="fts").limit(10).to_list()
|
table.search(
|
||||||
) # spellchecker:disable-line
|
"nce", # 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"}
|
||||||
|
|
||||||
@@ -1034,8 +1039,13 @@ 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("nce", query_type="fts").limit(10).to_list()
|
table.search(
|
||||||
) # spellchecker:disable-line
|
"nce", # 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,7 +54,10 @@ class TestOAuthProvider:
|
|||||||
provider = OAuthProvider(fetcher)
|
provider = OAuthProvider(fetcher)
|
||||||
headers = provider.get_headers()
|
headers = provider.get_headers()
|
||||||
|
|
||||||
assert headers == {"Authorization": "Bearer token123"}
|
assert headers == {
|
||||||
|
"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
|
||||||
|
|
||||||
@@ -73,14 +76,20 @@ class TestOAuthProvider:
|
|||||||
|
|
||||||
# First call
|
# First call
|
||||||
headers1 = provider.get_headers()
|
headers1 = provider.get_headers()
|
||||||
assert headers1 == {"Authorization": "Bearer token1"}
|
assert headers1 == {
|
||||||
|
"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 == {"Authorization": "Bearer token2"}
|
assert headers2 == {
|
||||||
|
"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):
|
||||||
@@ -92,12 +101,18 @@ class TestOAuthProvider:
|
|||||||
provider = OAuthProvider(fetcher)
|
provider = OAuthProvider(fetcher)
|
||||||
headers = provider.get_headers()
|
headers = provider.get_headers()
|
||||||
|
|
||||||
assert headers == {"Authorization": "Bearer permanent_token"}
|
assert headers == {
|
||||||
|
"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 == {"Authorization": "Bearer permanent_token"}
|
assert headers2 == {
|
||||||
|
"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."""
|
||||||
@@ -121,7 +136,10 @@ class TestOAuthProvider:
|
|||||||
provider = OAuthProvider(fetcher)
|
provider = OAuthProvider(fetcher)
|
||||||
headers = provider.get_headers()
|
headers = provider.get_headers()
|
||||||
|
|
||||||
assert headers == {"Authorization": "Bearer sync_token"}
|
assert headers == {
|
||||||
|
"Authorization": "Bearer sync_token",
|
||||||
|
"x-lancedb-credential-type": "oidc",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TestClientConfigIntegration:
|
class TestClientConfigIntegration:
|
||||||
|
|||||||
@@ -203,6 +203,93 @@ 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.
|
||||||
|
|||||||
@@ -193,7 +193,13 @@ class TestNamespaceConnection:
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
table = db.create_table("blob_table", data, namespace_path=["test_ns"])
|
# Legacy v1 blob columns are only writable at file version <= 2.1.
|
||||||
|
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,6 +40,10 @@ 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(
|
||||||
{
|
{
|
||||||
@@ -119,13 +123,17 @@ 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(name, _blob_query_data())
|
return db.create_table(
|
||||||
|
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(name, _blob_query_data())
|
return await db.create_table(
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -275,7 +283,9 @@ 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}", _blob_query_data()
|
f"test_query_to_pandas_blob_{blob_mode}",
|
||||||
|
_blob_query_data(),
|
||||||
|
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
||||||
)
|
)
|
||||||
|
|
||||||
df = (
|
df = (
|
||||||
@@ -322,7 +332,9 @@ 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", _blob_query_data()
|
"test_query_to_pandas_blob_no_arrow_collect",
|
||||||
|
_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"])
|
||||||
|
|
||||||
@@ -347,7 +359,9 @@ 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", _blob_query_data()
|
"test_query_to_pandas_blob_desc_flatten",
|
||||||
|
_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"])
|
||||||
|
|
||||||
@@ -365,7 +379,11 @@ 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("test_query_to_pandas_scanner_state", data.slice(0, 2))
|
table = tmp_db.create_table(
|
||||||
|
"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()
|
||||||
@@ -400,7 +418,9 @@ 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", _blob_query_data()
|
"test_async_query_to_pandas_blob_projection",
|
||||||
|
_blob_query_data(),
|
||||||
|
storage_options=LEGACY_BLOB_STORAGE_OPTIONS,
|
||||||
)
|
)
|
||||||
|
|
||||||
lazy_df = await (
|
lazy_df = await (
|
||||||
@@ -452,7 +472,9 @@ 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", _blob_query_data()
|
"test_async_query_to_pandas_blob_no_arrow_collect",
|
||||||
|
_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"])
|
||||||
|
|
||||||
@@ -474,7 +496,11 @@ 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("test_vector_query_blob_mode", _blob_query_data())
|
table = tmp_db.create_table(
|
||||||
|
"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(
|
||||||
@@ -485,7 +511,9 @@ 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", _blob_query_data()
|
"test_vector_query_blob_descriptions",
|
||||||
|
_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"):
|
||||||
|
|||||||
@@ -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
|
from lancedb.exceptions import JobFailedError, JobNotFoundError
|
||||||
|
|
||||||
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,6 +2475,7 @@ 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))
|
||||||
@@ -2512,6 +2513,22 @@ 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()
|
||||||
@@ -2543,7 +2560,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":
|
||||||
assert payload["job_id"] == "job-1"
|
query_events_payloads.append(payload)
|
||||||
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()
|
||||||
@@ -2559,24 +2576,109 @@ 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
|
||||||
|
|
||||||
batches = db.job_history("job-1")
|
# Opening a job hands back a populated handle; a missing one fails.
|
||||||
assert len(batches) == 1
|
with pytest.raises(JobNotFoundError, match="missing"):
|
||||||
assert batches[0].num_rows == 2
|
db.open_job("missing")
|
||||||
assert batches[0].column("state").to_pylist() == ["created", "done"]
|
finished = db.open_job("job-2")
|
||||||
|
assert finished.state == "finished"
|
||||||
|
assert finished.result == {"rows_assigned": 1000000, "rows_failed": 0}
|
||||||
|
|
||||||
job = db.job("job-1")
|
job = db.open_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",
|
||||||
"campains are not as good as they used to be",
|
"campaigns 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",
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
# 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,15 +64,23 @@ 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(name, data=_blob_test_data())
|
return db.create_table(
|
||||||
|
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(name, data=_blob_test_data())
|
return await db.create_table(
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
@@ -147,7 +155,11 @@ 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(f"test_to_pandas_blob_{blob_mode}", _blob_test_data())
|
table = tmp_db.create_table(
|
||||||
|
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)
|
||||||
|
|
||||||
@@ -3342,7 +3354,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, wihch is the same as default
|
# invalid limist is the same as None, which 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
|
||||||
@@ -3959,7 +3971,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": 633,
|
"total_bytes": 637,
|
||||||
"num_rows": 2,
|
"num_rows": 2,
|
||||||
"num_indices": 0,
|
"num_indices": 0,
|
||||||
"fragment_stats": {
|
"fragment_stats": {
|
||||||
|
|||||||
+68
-36
@@ -13,11 +13,7 @@ use crate::{
|
|||||||
runtime::future_into_py,
|
runtime::future_into_py,
|
||||||
table::Table,
|
table::Table,
|
||||||
};
|
};
|
||||||
use arrow::{
|
use arrow::{datatypes::Schema, ffi_stream::ArrowArrayStreamReader, pyarrow::FromPyArrow};
|
||||||
datatypes::Schema,
|
|
||||||
ffi_stream::ArrowArrayStreamReader,
|
|
||||||
pyarrow::{FromPyArrow, ToPyArrow},
|
|
||||||
};
|
|
||||||
use lancedb::{
|
use lancedb::{
|
||||||
connection::Connection as LanceConnection,
|
connection::Connection as LanceConnection,
|
||||||
connection::NamespaceClientPushdownOperation,
|
connection::NamespaceClientPushdownOperation,
|
||||||
@@ -28,7 +24,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::{PyDict, PyDictMethods, PyList, PyListMethods},
|
types::{PyAnyMethods, PyDict, PyDictMethods, PyList},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[pyclass]
|
#[pyclass]
|
||||||
@@ -86,6 +82,24 @@ 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 {
|
||||||
@@ -108,6 +122,40 @@ 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();
|
||||||
@@ -592,9 +640,12 @@ impl Connection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn job(&self, job_id: String) -> PyResult<crate::job::Job> {
|
pub fn open_job(self_: PyRef<'_, Self>, job_id: String) -> PyResult<Bound<'_, PyAny>> {
|
||||||
let inner = self.get_inner()?.clone();
|
let inner = self_.get_inner()?.clone();
|
||||||
Ok(crate::job::Job::new(inner.job(job_id).infer_error()?))
|
future_into_py(self_.py(), async move {
|
||||||
|
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(
|
||||||
@@ -664,42 +715,16 @@ 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, 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, 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))]
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn connect(
|
pub fn connect(
|
||||||
py: Python<'_>,
|
py: Python<'_>,
|
||||||
@@ -707,6 +732,7 @@ 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>>,
|
||||||
@@ -726,6 +752,12 @@ 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,6 +114,12 @@ 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(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-9
@@ -4,11 +4,50 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::runtime::future_into_py;
|
use crate::runtime::future_into_py;
|
||||||
use pyo3::{Bound, PyAny, PyRef, PyResult, pyclass, pymethods};
|
use arrow::{
|
||||||
|
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>>>,
|
||||||
@@ -67,6 +106,48 @@ 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.
|
||||||
@@ -121,7 +202,7 @@ impl JobFailureInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A described job from `Connection.get_job`.
|
/// The server-side record behind a `Job` handle.
|
||||||
#[pyclass(get_all, skip_from_py_object)]
|
#[pyclass(get_all, skip_from_py_object)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct JobDescription {
|
pub struct JobDescription {
|
||||||
@@ -129,17 +210,49 @@ pub struct JobDescription {
|
|||||||
job_type: String,
|
job_type: String,
|
||||||
state: String,
|
state: String,
|
||||||
creation_ms: i64,
|
creation_ms: i64,
|
||||||
spec_json: Option<String>,
|
/// Internal: the wire form behind the `spec` property.
|
||||||
|
_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 {
|
||||||
fn __repr__(&self) -> String {
|
/// The job-type-specific specification it was submitted with.
|
||||||
format!(
|
#[getter]
|
||||||
"JobDescription(job_id={:?}, job_type={:?}, state={:?}, creation_ms={})",
|
fn spec<'py>(&self, py: Python<'py>) -> PyResult<Option<Bound<'py, PyAny>>> {
|
||||||
self.job_id, self.job_type, self.state, self.creation_ms
|
parse_json_payload(py, self._spec_json.as_deref())
|
||||||
)
|
}
|
||||||
|
|
||||||
|
/// 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)"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +263,11 @@ 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,6 +34,7 @@ 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;
|
||||||
|
|
||||||
@@ -50,6 +51,8 @@ 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 shoud be used (currenty 20)
|
// None means user did not set it and default should be used (currently 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>,
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
// 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.1"
|
version = "0.39.0-beta.6"
|
||||||
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,6 +21,8 @@ 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
|
||||||
@@ -77,6 +79,7 @@ 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"] }
|
||||||
@@ -145,8 +148,11 @@ 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 contigous chunk.
|
/// the costly operation of reallocating each series as a single contiguous 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,7 +7,9 @@
|
|||||||
//! 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 and stable row ids at create.
|
//! Blob tables require Lance file format >= 2.2. `_rowid` values stay valid
|
||||||
|
//! 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;
|
||||||
@@ -324,6 +326,7 @@ 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;
|
||||||
@@ -385,6 +388,30 @@ 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>,
|
||||||
@@ -405,7 +432,8 @@ 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();
|
||||||
@@ -434,7 +462,8 @@ 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();
|
||||||
@@ -458,7 +487,10 @@ pub(crate) async fn take_blob_files_aligned(
|
|||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let handles = dataset.take_blobs(row_ids, column).await?;
|
let handles = dataset
|
||||||
|
.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()
|
||||||
@@ -500,10 +532,27 @@ 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);
|
||||||
assert_eq!(
|
let resolved = params
|
||||||
params.data_storage_version.unwrap().resolve(),
|
.data_storage_version
|
||||||
ConcreteFileVersion::V2_2
|
.unwrap_or(LanceFileVersion::Stable)
|
||||||
);
|
.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]
|
||||||
@@ -576,5 +625,6 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+152
-24
@@ -23,15 +23,18 @@ 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, JobDescription, JobInfo, OpenTableRequest,
|
CloneTableRequest, Database, DatabaseOptions, JobInfo, OpenTableRequest, ReadConsistency,
|
||||||
ReadConsistency, TableNamesRequest,
|
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::{OPT_REMOTE_API_KEY, OPT_REMOTE_HOST_OVERRIDE, OPT_REMOTE_REGION},
|
db::{
|
||||||
|
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;
|
||||||
@@ -322,6 +325,43 @@ 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 {
|
||||||
@@ -405,6 +445,51 @@ 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)
|
||||||
@@ -585,14 +670,34 @@ impl Connection {
|
|||||||
self.internal.read_consistency().await
|
self.internal.read_consistency().await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [`crate::job::Job`] handle for a server-side job by id, suitable for
|
/// Open a server-side job by id, returning a handle with its record
|
||||||
/// waiting on or cancelling the job.
|
/// already populated. Fails with [`crate::Error::JobNotFound`] when the
|
||||||
|
/// server has no such job, the way [`Connection::open_table`] does for a
|
||||||
|
/// missing table.
|
||||||
///
|
///
|
||||||
/// The handle is constructed without a server round trip; an unknown id
|
/// This is the one way in: the returned [`crate::job::Job`] answers for
|
||||||
/// surfaces when the handle is used. Only server-backed databases support
|
/// its own state, specification, result, failure and event history, so
|
||||||
/// job handles by id.
|
/// there is no separate connection-level call for any of them.
|
||||||
pub fn job(&self, job_id: impl AsRef<str>) -> Result<crate::job::Job> {
|
///
|
||||||
self.internal.job(job_id.as_ref())
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```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.
|
||||||
@@ -600,24 +705,12 @@ 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
|
||||||
@@ -734,7 +827,7 @@ impl Connection {
|
|||||||
pub struct ConnectRequest {
|
pub struct ConnectRequest {
|
||||||
/// Database URI
|
/// Database URI
|
||||||
///
|
///
|
||||||
/// ### Accpeted URI formats
|
/// ### Accepted 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
|
||||||
@@ -864,6 +957,19 @@ 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
|
||||||
@@ -1053,6 +1159,7 @@ 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 {
|
||||||
@@ -1094,11 +1201,15 @@ 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,
|
||||||
options.host_override,
|
host_overrides,
|
||||||
client_config,
|
client_config,
|
||||||
storage_options.into(),
|
storage_options.into(),
|
||||||
self.request.read_consistency_interval,
|
self.request.read_consistency_interval,
|
||||||
@@ -1392,6 +1503,23 @@ 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,8 +18,6 @@ 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::{
|
||||||
@@ -206,8 +204,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::get_job`] and [`Database::cancel_job`]
|
/// The job id -- what [`Database::open_job`] and
|
||||||
/// accept.
|
/// [`Database::cancel_job`] 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,
|
||||||
@@ -218,8 +216,8 @@ pub struct JobInfo {
|
|||||||
pub created_at_millis: i64,
|
pub created_at_millis: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A described job from [`Database::get_job`]: lifecycle state plus the
|
/// The server-side record behind a [`crate::job::Job`] handle: lifecycle
|
||||||
/// job-type-specific specification.
|
/// state plus the job-type-specific specification and result.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct JobDescription {
|
pub struct JobDescription {
|
||||||
pub job_id: String,
|
pub job_id: String,
|
||||||
@@ -230,6 +228,10 @@ 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>,
|
||||||
@@ -315,30 +317,37 @@ 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()
|
||||||
}
|
}
|
||||||
/// A [`crate::job::Job`] handle for a server-side job by id, suitable for
|
/// Open a job by id, returning a handle with its record already
|
||||||
/// waiting on or cancelling the job. The handle is constructed without a
|
/// populated. Fails with [`crate::Error::JobNotFound`] when the server has
|
||||||
/// server round trip; an unknown id surfaces when the handle is used.
|
/// no such job.
|
||||||
fn job(&self, _job_id: &str) -> Result<crate::job::Job> {
|
async fn open_job(&self, _job_id: &str) -> Result<crate::job::Job> {
|
||||||
job_op_not_supported("job")
|
job_op_not_supported("open_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")
|
||||||
}
|
}
|
||||||
/// The lifecycle event history of a job (all jobs when `job_id` is
|
/// Start executing a SQL statement on a remote database.
|
||||||
/// `None`), as recorded Arrow batches.
|
async fn execute_query_async(
|
||||||
async fn job_history(&self, _job_id: Option<&str>) -> Result<Vec<RecordBatch>> {
|
&self,
|
||||||
job_op_not_supported("job_history")
|
_query: &str,
|
||||||
|
_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, has_blob_columns};
|
use crate::blob::ensure_blob_storage_version;
|
||||||
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_querys = vec![];
|
let mut filtered_queries = 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_querys.push((key.to_string(), value.to_string()));
|
filtered_queries.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_querys);
|
url.query_pairs_mut().extend_pairs(filtered_queries);
|
||||||
// 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,7 +827,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -897,11 +896,11 @@ impl Database for ListingDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn read_consistency(&self) -> Result<ReadConsistency> {
|
async fn read_consistency(&self) -> Result<ReadConsistency> {
|
||||||
if let Some(read_consistency_inverval) = self.read_consistency_interval {
|
if let Some(interval) = self.read_consistency_interval {
|
||||||
if read_consistency_inverval.is_zero() {
|
if interval.is_zero() {
|
||||||
Ok(ReadConsistency::Strong)
|
Ok(ReadConsistency::Strong)
|
||||||
} else {
|
} else {
|
||||||
Ok(ReadConsistency::Eventual(read_consistency_inverval))
|
Ok(ReadConsistency::Eventual(interval))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(ReadConsistency::Manual)
|
Ok(ReadConsistency::Manual)
|
||||||
@@ -3044,15 +3043,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_querys = Vec::new();
|
let mut filtered_queries = 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_querys.push((key.to_string(), value.to_string()));
|
filtered_queries.push((key.to_string(), value.to_string()));
|
||||||
}
|
}
|
||||||
url.query_pairs_mut().clear();
|
url.query_pairs_mut().clear();
|
||||||
url.query_pairs_mut().extend_pairs(filtered_querys);
|
url.query_pairs_mut().extend_pairs(filtered_queries);
|
||||||
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,6 +3,7 @@
|
|||||||
|
|
||||||
//! 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};
|
||||||
|
|
||||||
@@ -23,7 +24,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, has_blob_columns};
|
use crate::blob::ensure_blob_storage_version;
|
||||||
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};
|
||||||
@@ -217,7 +218,6 @@ 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(read_consistency_inverval) = self.read_consistency_interval {
|
if let Some(interval) = self.read_consistency_interval {
|
||||||
if read_consistency_inverval.is_zero() {
|
if interval.is_zero() {
|
||||||
Ok(ReadConsistency::Strong)
|
Ok(ReadConsistency::Strong)
|
||||||
} else {
|
} else {
|
||||||
Ok(ReadConsistency::Eventual(read_consistency_inverval))
|
Ok(ReadConsistency::Eventual(interval))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok(ReadConsistency::Manual)
|
Ok(ReadConsistency::Manual)
|
||||||
@@ -305,6 +305,10 @@ 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,6 +102,8 @@ 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}"))]
|
||||||
|
|||||||
@@ -582,8 +582,8 @@ pub struct InputBinding {
|
|||||||
|
|
||||||
/// Ordered result-field to table-field mapping for a Function binding.
|
/// Ordered result-field to table-field mapping for a Function binding.
|
||||||
///
|
///
|
||||||
/// Assignment state is not part of the Slice 1 client contract. During the
|
/// `nullable` describes the logical Function result. Physical computed-column
|
||||||
/// NULL transition there is no public Lance cell-flag identifier to persist.
|
/// fields remain nullable while unassigned.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct OutputMapping {
|
pub struct OutputMapping {
|
||||||
pub result_field: String,
|
pub result_field: String,
|
||||||
@@ -594,6 +594,14 @@ pub struct OutputMapping {
|
|||||||
pub nullable: bool,
|
pub nullable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Internal physical column preserving the parent validity of a flattened
|
||||||
|
/// named-struct result.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct AssignmentMapping {
|
||||||
|
pub output_name: String,
|
||||||
|
pub output_field_id: i32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Immutable Function binding persisted by the Enterprise table service.
|
/// Immutable Function binding persisted by the Enterprise table service.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct FunctionBinding {
|
pub struct FunctionBinding {
|
||||||
@@ -601,6 +609,8 @@ pub struct FunctionBinding {
|
|||||||
function: FunctionVersionRef,
|
function: FunctionVersionRef,
|
||||||
inputs: Vec<InputBinding>,
|
inputs: Vec<InputBinding>,
|
||||||
outputs: Vec<OutputMapping>,
|
outputs: Vec<OutputMapping>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
assignment: Option<AssignmentMapping>,
|
||||||
/// Exact Arrow schema presented to the Function, encoded with the Lance
|
/// Exact Arrow schema presented to the Function, encoded with the Lance
|
||||||
/// Namespace Arrow JSON representation.
|
/// Namespace Arrow JSON representation.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
@@ -627,6 +637,10 @@ impl FunctionBinding {
|
|||||||
&self.outputs
|
&self.outputs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn assignment(&self) -> Option<&AssignmentMapping> {
|
||||||
|
self.assignment.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn input_schema(&self) -> Option<&Value> {
|
pub fn input_schema(&self) -> Option<&Value> {
|
||||||
self.input_schema.as_ref()
|
self.input_schema.as_ref()
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user