mirror of
https://github.com/lancedb/lancedb.git
synced 2026-09-12 16:22:24 +00:00
Compare commits
54
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
404b91d4d6 | ||
|
|
d6bcfe52a9 | ||
|
|
82cf9f3b96 | ||
|
|
762eb0f44f | ||
|
|
b9c0446421 | ||
|
|
d67585c245 | ||
|
|
2e34d2742b | ||
|
|
4d09f8ce26 | ||
|
|
7b29fb2f51 | ||
|
|
0111a72dc3 | ||
|
|
a487d4033e | ||
|
|
02ea0dda9f | ||
|
|
c7980dbc40 | ||
|
|
1b0f9329ea | ||
|
|
e5cc7a4d66 | ||
|
|
21f11b4463 | ||
|
|
8c9c5c5a5f | ||
|
|
aab23eb39e | ||
|
|
e639b1b650 | ||
|
|
2779b75d0d | ||
|
|
0d19a6c546 | ||
|
|
c0f33f8627 | ||
|
|
904bd975e5 | ||
|
|
d2ca0ce0ab | ||
|
|
9a1ffb9e02 | ||
|
|
f2eb4a245d | ||
|
|
e6867f7d04 | ||
|
|
193c5e3458 | ||
|
|
7ebd3c222d | ||
|
|
d118ef168b | ||
|
|
19232f9c50 | ||
|
|
5cbd979455 | ||
|
|
e773d1e093 | ||
|
|
c8fd3e97d1 | ||
|
|
c196d033e9 | ||
|
|
16753b805a | ||
|
|
840e1d7313 | ||
|
|
1a9414c47c | ||
|
|
c4ee8ae670 | ||
|
|
57b8d3bf05 | ||
|
|
c6dfe830d9 | ||
|
|
d5dac65a21 | ||
|
|
1b0fc2c465 | ||
|
|
a417e46bfa | ||
|
|
fcdc3f949e | ||
|
|
0c4e0667bc | ||
|
|
101f524e47 | ||
|
|
36c142fa2e | ||
|
|
a87cada90e | ||
|
|
0559108fa9 | ||
|
|
6ab3b9eb30 | ||
|
|
c94d9a2a16 | ||
|
|
6c8aa22704 | ||
|
|
84f46df876 |
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
[tool.bumpversion]
|
[tool.bumpversion]
|
||||||
current_version = "0.38.0-beta.11"
|
current_version = "0.39.0-beta.4"
|
||||||
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*)\\.
|
||||||
|
|||||||
@@ -44,3 +44,27 @@ updates:
|
|||||||
python-deps:
|
python-deps:
|
||||||
patterns:
|
patterns:
|
||||||
- "*"
|
- "*"
|
||||||
|
|
||||||
|
# The npm ecosystem covers pnpm lockfiles. There are two separate installs:
|
||||||
|
# the bindings themselves and the examples, which have their own lockfile.
|
||||||
|
# As with cargo and pip above, only bump the lockfile — the version ranges
|
||||||
|
# in package.json are our consumers' constraints, not ours.
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /nodejs
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
versioning-strategy: lockfile-only
|
||||||
|
groups:
|
||||||
|
nodejs-deps:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
- package-ecosystem: npm
|
||||||
|
directory: /nodejs/examples
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
versioning-strategy: lockfile-only
|
||||||
|
groups:
|
||||||
|
nodejs-examples-deps:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
|
|||||||
@@ -29,12 +29,14 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: "18"
|
node-version: "24"
|
||||||
|
- uses: pnpm/action-setup@v6
|
||||||
|
with:
|
||||||
|
version: 11.1.1
|
||||||
# These rules are disabled because Github will always ensure there
|
# These rules are disabled because Github will always ensure there
|
||||||
# is a blank line between the title and the body and Github will
|
# is a blank line between the title and the body and Github will
|
||||||
# word wrap the description field to ensure a reasonable max line
|
# word wrap the description field to ensure a reasonable max line
|
||||||
# length.
|
# length.
|
||||||
- run: npm install @commitlint/config-conventional
|
|
||||||
- run: >
|
- run: >
|
||||||
echo 'module.exports = {
|
echo 'module.exports = {
|
||||||
"rules": {
|
"rules": {
|
||||||
@@ -43,7 +45,11 @@ jobs:
|
|||||||
"body-leading-blank": [0, "always"]
|
"body-leading-blank": [0, "always"]
|
||||||
}
|
}
|
||||||
}' > .commitlintrc.js
|
}' > .commitlintrc.js
|
||||||
- run: npx commitlint --extends @commitlint/config-conventional --verbose <<< $COMMIT_MSG
|
- run: >
|
||||||
|
pnpm dlx
|
||||||
|
--package @commitlint/cli@21.2.2
|
||||||
|
--package @commitlint/config-conventional@21.2.2
|
||||||
|
commitlint --extends @commitlint/config-conventional --verbose <<< $COMMIT_MSG
|
||||||
env:
|
env:
|
||||||
COMMIT_MSG: >
|
COMMIT_MSG: >
|
||||||
${{ github.event.pull_request.title }}
|
${{ github.event.pull_request.title }}
|
||||||
@@ -54,7 +60,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const message = `**ACTION NEEDED**
|
const message = `**ACTION NEEDED**
|
||||||
|
|
||||||
Lance follows the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) for release automation.
|
Lance follows the [Conventional Commits specification](https://www.conventionalcommits.org/en/v1.0.0/) for release automation.
|
||||||
|
|
||||||
The PR title and description are used as the merge commit message.\
|
The PR title and description are used as the merge commit message.\
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
|
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
|
||||||
with:
|
with:
|
||||||
# Restricted to http(s) on purpose. Much of docs/src is generated
|
# Restricted to http(s) on purpose. Much of docs/src is generated
|
||||||
# API reference (the js/ tree comes from `npm run docs` in nodejs)
|
# API reference (the js/ tree comes from `pnpm run docs` in nodejs)
|
||||||
# and the hand-written pages use mkdocstrings cross-references and
|
# and the hand-written pages use mkdocstrings cross-references and
|
||||||
# nav-relative paths that only resolve in the site mkdocs builds,
|
# nav-relative paths that only resolve in the site mkdocs builds,
|
||||||
# not in this checkout, so relative links would be reported as
|
# not in this checkout, so relative links would be reported as
|
||||||
|
|||||||
@@ -55,9 +55,7 @@ jobs:
|
|||||||
- name: Set up node
|
- name: Set up node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 24
|
||||||
cache: 'npm'
|
|
||||||
cache-dependency-path: docs/package-lock.json
|
|
||||||
- name: Install node dependencies
|
- name: Install node dependencies
|
||||||
working-directory: nodejs
|
working-directory: nodejs
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -47,9 +47,8 @@ jobs:
|
|||||||
version: 11.1.1
|
version: 11.1.1
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
|
# Build on a supported LTS; the matrix job below covers every
|
||||||
# in October. The library itself still supports Node >= 18
|
# Node version the library claims to support.
|
||||||
# (see test matrix below).
|
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: nodejs/pnpm-lock.yaml
|
cache-dependency-path: nodejs/pnpm-lock.yaml
|
||||||
@@ -84,7 +83,7 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [ "18", "20" ]
|
node-version: [ "22", "24", "26" ]
|
||||||
runs-on: "ubuntu-22.04"
|
runs-on: "ubuntu-22.04"
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -101,9 +100,9 @@ jobs:
|
|||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
name: Setup Node.js 24 for build
|
name: Setup Node.js 24 for build
|
||||||
with:
|
with:
|
||||||
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
|
# Build and install once on a fixed version so the generated docs
|
||||||
# in October. Build/install runs on Node 24; tests run on the
|
# are identical across matrix legs; the tests below then run on each
|
||||||
# matrix version below using direct jest invocation.
|
# supported Node version.
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: nodejs/pnpm-lock.yaml
|
cache-dependency-path: nodejs/pnpm-lock.yaml
|
||||||
@@ -152,9 +151,9 @@ jobs:
|
|||||||
S3_TEST: "1"
|
S3_TEST: "1"
|
||||||
# Newer @smithy/core uses dynamic ESM imports.
|
# Newer @smithy/core uses dynamic ESM imports.
|
||||||
NODE_OPTIONS: "--experimental-vm-modules"
|
NODE_OPTIONS: "--experimental-vm-modules"
|
||||||
# Invoke jest directly because pnpm 11 itself requires Node 22+
|
# Invoke the installed jest binary directly; the pnpm shim is set up
|
||||||
# while the matrix tests on older Node versions.
|
# against the build-phase Node, not the version selected above.
|
||||||
run: npx jest --verbose
|
run: node_modules/.bin/jest --verbose
|
||||||
- name: Test examples
|
- name: Test examples
|
||||||
working-directory: ./
|
working-directory: ./
|
||||||
env:
|
env:
|
||||||
@@ -164,7 +163,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python ci/mock_openai.py &
|
python ci/mock_openai.py &
|
||||||
cd nodejs/examples
|
cd nodejs/examples
|
||||||
npx jest --testEnvironment jest-environment-node-single-context --verbose
|
node_modules/.bin/jest --testEnvironment jest-environment-node-single-context --verbose
|
||||||
macos:
|
macos:
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
# macos-15 ships a newer linker; the older macos-14 linker fails to insert
|
# macos-15 ships a newer linker; the older macos-14 linker fails to insert
|
||||||
@@ -185,8 +184,7 @@ jobs:
|
|||||||
version: 11.1.1
|
version: 11.1.1
|
||||||
- uses: actions/setup-node@v6
|
- uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
|
# pnpm 11 requires Node >= 22.13.
|
||||||
# in October.
|
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: nodejs/pnpm-lock.yaml
|
cache-dependency-path: nodejs/pnpm-lock.yaml
|
||||||
|
|||||||
@@ -40,40 +40,31 @@ jobs:
|
|||||||
- target: aarch64-apple-darwin
|
- target: aarch64-apple-darwin
|
||||||
host: macos-latest
|
host: macos-latest
|
||||||
features: fp16kernels
|
features: fp16kernels
|
||||||
|
# Fat LTO was ~111 of this job's ~113 minutes.
|
||||||
|
lto: thin
|
||||||
|
codegen_units: 16
|
||||||
pre_build: |-
|
pre_build: |-
|
||||||
brew install protobuf
|
brew install protobuf
|
||||||
# Fat LTO (the workspace default in .cargo/config.toml) is
|
|
||||||
# single-threaded and is the peak-memory step of the build. On
|
|
||||||
# this runner it accounted for ~111 of the job's ~113 minutes,
|
|
||||||
# making it the critical path of the entire publish pipeline.
|
|
||||||
# ThinLTO parallelizes it across the runner's cores, for a few
|
|
||||||
# percent of runtime performance.
|
|
||||||
export CARGO_PROFILE_RELEASE_LTO=thin
|
|
||||||
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
|
|
||||||
- target: x86_64-pc-windows-msvc
|
- target: x86_64-pc-windows-msvc
|
||||||
host: windows-2025
|
host: windows-2025
|
||||||
features: ","
|
features: ","
|
||||||
|
# The lower peak also keeps this on the standard 4-core runner.
|
||||||
|
lto: thin
|
||||||
|
codegen_units: 16
|
||||||
pre_build: |-
|
pre_build: |-
|
||||||
choco install --no-progress protoc ninja nasm
|
choco install --no-progress protoc ninja nasm
|
||||||
tail -n 1000 /c/ProgramData/chocolatey/logs/chocolatey.log
|
tail -n 1000 /c/ProgramData/chocolatey/logs/chocolatey.log
|
||||||
# There is an issue where choco doesn't add nasm to the path
|
# There is an issue where choco doesn't add nasm to the path
|
||||||
export PATH="$PATH:/c/Program Files/NASM"
|
export PATH="$PATH:/c/Program Files/NASM"
|
||||||
nasm -v
|
nasm -v
|
||||||
# See the ThinLTO note on aarch64-apple-darwin above. Keeping
|
|
||||||
# peak memory down is also what lets this run on the standard
|
|
||||||
# 4-core runner: the 8-core larger runner was only needed to
|
|
||||||
# stop fat LTO from OOMing rustc-LLVM.
|
|
||||||
export CARGO_PROFILE_RELEASE_LTO=thin
|
|
||||||
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
|
|
||||||
- target: aarch64-pc-windows-msvc
|
- target: aarch64-pc-windows-msvc
|
||||||
host: windows-2025
|
host: windows-2025
|
||||||
features: ","
|
features: ","
|
||||||
|
lto: thin
|
||||||
|
codegen_units: 16
|
||||||
pre_build: |-
|
pre_build: |-
|
||||||
choco install --no-progress protoc
|
choco install --no-progress protoc
|
||||||
rustup target add aarch64-pc-windows-msvc
|
rustup target add aarch64-pc-windows-msvc
|
||||||
# See the ThinLTO note on aarch64-apple-darwin above.
|
|
||||||
export CARGO_PROFILE_RELEASE_LTO=thin
|
|
||||||
export CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16
|
|
||||||
- target: x86_64-unknown-linux-gnu
|
- target: x86_64-unknown-linux-gnu
|
||||||
host: ubuntu-latest
|
host: ubuntu-latest
|
||||||
features: fp16kernels
|
features: fp16kernels
|
||||||
@@ -103,6 +94,14 @@ jobs:
|
|||||||
# https://github.com/napi-rs/napi-rs/blob/main/debian-aarch64.Dockerfile
|
# https://github.com/napi-rs/napi-rs/blob/main/debian-aarch64.Dockerfile
|
||||||
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64
|
||||||
features: "fp16kernels"
|
features: "fp16kernels"
|
||||||
|
# Fat LTO OOM-killed rustc every nightly; even with lld it peaked
|
||||||
|
# at 31391 MiB of the runner's 32 GiB.
|
||||||
|
lto: thin
|
||||||
|
codegen_units: 16
|
||||||
|
# arm64 Linux links through GNU `ld` where x86_64 defaults to
|
||||||
|
# `rust-lld`, which is why only arm64 OOM'd. lld cut the largest
|
||||||
|
# linker process 7.0 -> 4.0 GiB (lancedb/sophon#7313).
|
||||||
|
linker: /tmp/aarch64-lld-clang
|
||||||
pre_build: |-
|
pre_build: |-
|
||||||
set -e &&
|
set -e &&
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
@@ -112,9 +111,30 @@ jobs:
|
|||||||
# AT_HWCAP2 (added in Linux 3.17). Define it for aws-lc-sys.
|
# AT_HWCAP2 (added in Linux 3.17). Define it for aws-lc-sys.
|
||||||
export CFLAGS="$CFLAGS -DAT_HWCAP2=26" &&
|
export CFLAGS="$CFLAGS -DAT_HWCAP2=26" &&
|
||||||
rustup target add aarch64-unknown-linux-gnu
|
rustup target add aarch64-unknown-linux-gnu
|
||||||
|
# Not `&&`-chained: in dash, errexit does not fire for a
|
||||||
|
# non-final command in an `&&` list, so failures were ignored.
|
||||||
|
#
|
||||||
|
# A wrapper rather than `-C link-arg` because the per-target
|
||||||
|
# rustflags variable does not reach every unit that links, while
|
||||||
|
# the linker variable does. `clang` because GCC silently ignores
|
||||||
|
# `-fuse-ld=lld` unless built with lld support. Two echoes
|
||||||
|
# because printf's newline escape gets rewritten to `;` between
|
||||||
|
# here and the container.
|
||||||
|
echo '#!/bin/sh' > /tmp/aarch64-lld-clang
|
||||||
|
echo 'exec clang --target=aarch64-unknown-linux-gnu --sysroot=/usr/aarch64-unknown-linux-gnu/aarch64-unknown-linux-gnu/sysroot --gcc-toolchain=/usr/aarch64-unknown-linux-gnu -fuse-ld=lld "$@"' >> /tmp/aarch64-lld-clang
|
||||||
|
chmod 0755 /tmp/aarch64-lld-clang
|
||||||
|
# Fail now, not at the cdylib link ~30 minutes later. Linking at
|
||||||
|
# all also proves lld resolved; clang errors out when it cannot.
|
||||||
|
echo 'int main(void){return 0;}' > /tmp/probe.c
|
||||||
|
/tmp/aarch64-lld-clang /tmp/probe.c -o /tmp/probe
|
||||||
|
readelf -h /tmp/probe | grep AArch64
|
||||||
- target: aarch64-unknown-linux-musl
|
- target: aarch64-unknown-linux-musl
|
||||||
host: ubuntu-2404-8x-x64
|
host: ubuntu-2404-8x-x64
|
||||||
features: ","
|
features: ","
|
||||||
|
# Fat LTO took the whole runner down. lld cannot help: it died
|
||||||
|
# inside rustc's LLVM, before any linker was spawned.
|
||||||
|
lto: thin
|
||||||
|
codegen_units: 16
|
||||||
pre_build: |-
|
pre_build: |-
|
||||||
set -e &&
|
set -e &&
|
||||||
sudo apt-get update &&
|
sudo apt-get update &&
|
||||||
@@ -123,6 +143,19 @@ jobs:
|
|||||||
export EXTRA_ARGS="-x"
|
export EXTRA_ARGS="-x"
|
||||||
name: build - ${{ matrix.settings.target }}
|
name: build - ${{ matrix.settings.target }}
|
||||||
runs-on: ${{ matrix.settings.host }}
|
runs-on: ${{ matrix.settings.host }}
|
||||||
|
# On the job, not exported from `pre_build`: `Swatinem/rust-cache` hashes
|
||||||
|
# `CARGO_*` into its cache key before any step runs, so a step-local export
|
||||||
|
# leaves the key unchanged while cargo still rebuilds cold. The ThinLTO
|
||||||
|
# legs had been doing that every run.
|
||||||
|
#
|
||||||
|
# Not `RUSTFLAGS`: setting it, even to "", discards every config-file
|
||||||
|
# rustflag, silently dropping .cargo/config.toml's `target-cpu` and
|
||||||
|
# `target-feature` from the published binaries.
|
||||||
|
env:
|
||||||
|
CARGO_PROFILE_RELEASE_LTO: ${{ matrix.settings.lto || 'fat' }}
|
||||||
|
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: ${{ matrix.settings.codegen_units || '1' }}
|
||||||
|
# Empty elsewhere: a per-target variable is only read for that triple.
|
||||||
|
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.settings.linker }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: nodejs
|
working-directory: nodejs
|
||||||
@@ -135,8 +168,7 @@ jobs:
|
|||||||
- name: Setup node
|
- name: Setup node
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v6
|
||||||
with:
|
with:
|
||||||
# pnpm 11 requires Node >= 22.13; use 24 since 22 hits EOL
|
# pnpm 11 requires Node >= 22.13.
|
||||||
# in October.
|
|
||||||
node-version: 24
|
node-version: 24
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
cache-dependency-path: nodejs/pnpm-lock.yaml
|
cache-dependency-path: nodejs/pnpm-lock.yaml
|
||||||
@@ -169,19 +201,15 @@ jobs:
|
|||||||
# creating ref). The nightly cadence also keeps entries inside
|
# creating ref). The nightly cadence also keeps entries inside
|
||||||
# GitHub's 7-day eviction window, which a tag-only trigger would not.
|
# GitHub's 7-day eviction window, which a tag-only trigger would not.
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
# Docker builds can use rust-cache too. `target/` already lives on the
|
# Docker builds can use rust-cache too: the workspace is bind-mounted, so
|
||||||
# host because the whole workspace is bind-mounted into the container, and
|
# `target/` lives on the host and rust-cache's prune keeps the entry
|
||||||
# rust-cache's prune and save run host-side, so they can manage it -- which
|
# small.
|
||||||
# is what keeps the entry to dependency artifacts rather than a multi-GB
|
|
||||||
# copy of everything.
|
|
||||||
#
|
#
|
||||||
# Two differences from the native builds. The container's CARGO_HOME is
|
# Two differences from the native builds. The container's CARGO_HOME is
|
||||||
# bind-mounted from `.cargo-cache` rather than the host's ~/.cargo, so that
|
# bind-mounted from `.cargo-cache` rather than ~/.cargo, so that is cached
|
||||||
# has to be cached explicitly. And the key is derived from the *host* rustc
|
# explicitly. And the key uses the *host* rustc version, not the compiler
|
||||||
# version, which is not the compiler that produced these artifacts; that is
|
# that built these artifacts -- safe, since cargo fingerprints the real
|
||||||
# safe because cargo fingerprints the real compiler and rebuilds on a
|
# one; a base-image bump just costs one cold build.
|
||||||
# mismatch, it just means a base-image toolchain bump costs one cold build
|
|
||||||
# instead of invalidating the key.
|
|
||||||
- name: Cache cargo (docker builds)
|
- name: Cache cargo (docker builds)
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
if: ${{ matrix.settings.docker }}
|
if: ${{ matrix.settings.docker }}
|
||||||
@@ -210,14 +238,19 @@ jobs:
|
|||||||
# cache step above saves. Previously the registry mounts pointed at
|
# cache step above saves. Previously the registry mounts pointed at
|
||||||
# `.cargo/...`, a path nothing cached, so the container re-downloaded
|
# `.cargo/...`, a path nothing cached, so the container re-downloaded
|
||||||
# the whole crate registry on every run.
|
# the whole crate registry on every run.
|
||||||
|
#
|
||||||
|
# `docker run` inherits nothing; `-e NAME` carries the job's `env:` in.
|
||||||
options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db \
|
options: "--user 0:0 -v ${{ github.workspace }}/.cargo-cache/git/db:/usr/local/cargo/git/db \
|
||||||
-v ${{ github.workspace }}/.cargo-cache/registry/cache:/usr/local/cargo/registry/cache \
|
-v ${{ github.workspace }}/.cargo-cache/registry/cache:/usr/local/cargo/registry/cache \
|
||||||
-v ${{ github.workspace }}/.cargo-cache/registry/index:/usr/local/cargo/registry/index \
|
-v ${{ github.workspace }}/.cargo-cache/registry/index:/usr/local/cargo/registry/index \
|
||||||
|
-e CARGO_PROFILE_RELEASE_LTO \
|
||||||
|
-e CARGO_PROFILE_RELEASE_CODEGEN_UNITS \
|
||||||
|
-e CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER \
|
||||||
-v ${{ github.workspace }}:/build -w /build/nodejs"
|
-v ${{ github.workspace }}:/build -w /build/nodejs"
|
||||||
run: |
|
run: |
|
||||||
set -e
|
set -e
|
||||||
${{ matrix.settings.pre_build }}
|
${{ matrix.settings.pre_build }}
|
||||||
npx napi build --platform --release \
|
node_modules/.bin/napi build --platform --release \
|
||||||
--features ${{ matrix.settings.features }} \
|
--features ${{ matrix.settings.features }} \
|
||||||
--target ${{ matrix.settings.target }} \
|
--target ${{ matrix.settings.target }} \
|
||||||
--dts ../lancedb/native.d.ts \
|
--dts ../lancedb/native.d.ts \
|
||||||
@@ -237,7 +270,7 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
${{ matrix.settings.pre_build }}
|
${{ matrix.settings.pre_build }}
|
||||||
npx napi build --platform --release \
|
node_modules/.bin/napi build --platform --release \
|
||||||
--features ${{ matrix.settings.features }} \
|
--features ${{ matrix.settings.features }} \
|
||||||
--target ${{ matrix.settings.target }} \
|
--target ${{ matrix.settings.target }} \
|
||||||
--dts ../lancedb/native.d.ts \
|
--dts ../lancedb/native.d.ts \
|
||||||
@@ -256,6 +289,18 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
run: df -h
|
run: df -h
|
||||||
shell: bash
|
shell: bash
|
||||||
|
- name: Report peak memory
|
||||||
|
if: always() && runner.os == 'Linux'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
peak=$(find /sys/fs/cgroup -name memory.peak -readable \
|
||||||
|
-exec cat {} + 2>/dev/null | sort -n | tail -1)
|
||||||
|
if [ -n "$peak" ]; then
|
||||||
|
echo "peak memory: $((peak / 1024 / 1024)) MiB"
|
||||||
|
else
|
||||||
|
echo "peak memory: unavailable (no readable cgroup v2 memory.peak)"
|
||||||
|
fi
|
||||||
|
free -g || true
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
@@ -293,7 +338,7 @@ jobs:
|
|||||||
- target: aarch64-unknown-linux-gnu
|
- target: aarch64-unknown-linux-gnu
|
||||||
host: ubuntu-2404-8x-arm64
|
host: ubuntu-2404-8x-arm64
|
||||||
node:
|
node:
|
||||||
- '20'
|
- '22'
|
||||||
runs-on: ${{ matrix.settings.host }}
|
runs-on: ${{ matrix.settings.host }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -339,9 +384,9 @@ jobs:
|
|||||||
- name: Move built files
|
- name: Move built files
|
||||||
run: cp dist/native.d.ts dist/native.js dist/*.node lancedb/
|
run: cp dist/native.d.ts dist/native.js dist/*.node lancedb/
|
||||||
- name: Test bindings
|
- name: Test bindings
|
||||||
# Invoke jest directly because pnpm 11 itself requires Node 22+
|
# Invoke the installed jest binary directly; the pnpm shim is set up
|
||||||
# while the matrix tests on older Node versions.
|
# against the install-phase Node, not the version selected above.
|
||||||
run: npx jest --verbose
|
run: node_modules/.bin/jest --verbose
|
||||||
publish:
|
publish:
|
||||||
name: Publish
|
name: Publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
name: Update package-lock.json
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
ref: main
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
lfs: true
|
|
||||||
- uses: ./.github/workflows/update_package_lock
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.LANCEDB_RELEASE_TOKEN }}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
name: Update NodeJs package-lock.json
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
publish:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
ref: main
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
lfs: true
|
|
||||||
- uses: ./.github/workflows/update_package_lock_nodejs
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.LANCEDB_RELEASE_TOKEN }}
|
|
||||||
@@ -20,7 +20,10 @@ repos:
|
|||||||
hooks:
|
hooks:
|
||||||
- id: local-biome-check
|
- id: local-biome-check
|
||||||
name: biome check
|
name: biome check
|
||||||
entry: npx @biomejs/biome@1.8.3 check --config-path nodejs/biome.json nodejs/
|
# Use the biome from nodejs/package.json rather than a separately
|
||||||
|
# pinned one: the two drifted apart and disagreed on formatting, so
|
||||||
|
# this hook rejected code that `pnpm lint` accepted.
|
||||||
|
entry: nodejs/node_modules/.bin/biome check --config-path nodejs/biome.json nodejs/
|
||||||
language: system
|
language: system
|
||||||
types: [text]
|
types: [text]
|
||||||
files: "nodejs/.*"
|
files: "nodejs/.*"
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ Before committing changes, run formatting for every language you touched. At min
|
|||||||
* Rust changes: run `cargo fmt --all`.
|
* Rust changes: run `cargo fmt --all`.
|
||||||
* Python changes: run `ruff format .` and `ruff check .` from the repository root,
|
* Python changes: run `ruff format .` and `ruff check .` from the repository root,
|
||||||
and run targeted tests through `cd python && uv run ...`.
|
and run targeted tests through `cd python && uv run ...`.
|
||||||
* TypeScript changes: run the relevant `npm`/`pnpm` lint, format, build, and docs commands in `nodejs`.
|
* TypeScript changes: run the relevant `pnpm` lint, format, build, and docs commands in `nodejs`.
|
||||||
|
|
||||||
Before creating a PR, the exact value passed to `gh pr create --title` must follow
|
Before creating a PR, the exact value passed to `gh pr create --title` must follow
|
||||||
Conventional Commits, such as `fix: support nested field paths in native index creation`
|
Conventional Commits, such as `fix: support nested field paths in native index creation`
|
||||||
@@ -101,12 +101,12 @@ Python bindings changes:
|
|||||||
TypeScript bindings changes:
|
TypeScript bindings changes:
|
||||||
|
|
||||||
1. Add napi-rs method binding on `Table` in `nodejs/src/table.rs`.
|
1. Add napi-rs method binding on `Table` in `nodejs/src/table.rs`.
|
||||||
2. Run `npm run build` to generate TypeScript definitions.
|
2. Run `pnpm build` to generate TypeScript definitions.
|
||||||
3. Add typescript method on abstract class `Table` in `nodejs/src/table.ts`.
|
3. Add typescript method on abstract class `Table` in `nodejs/src/table.ts`.
|
||||||
4. Add concrete method on `LocalTable` class in `nodejs/src/native_table.ts`.
|
4. Add concrete method on `LocalTable` class in `nodejs/src/native_table.ts`.
|
||||||
* Note: despite the name, this class is also used for remote tables.
|
* Note: despite the name, this class is also used for remote tables.
|
||||||
5. Add test in `nodejs/__test__/table.test.ts`.
|
5. Add test in `nodejs/__test__/table.test.ts`.
|
||||||
6. Run `npm run docs` to generate TypeScript documentation.
|
6. Run `pnpm run docs` to generate TypeScript documentation.
|
||||||
|
|
||||||
## Python API reference
|
## Python API reference
|
||||||
|
|
||||||
|
|||||||
Generated
+166
-76
@@ -332,6 +332,34 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrow-flight"
|
||||||
|
version = "58.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2dbe34824c639e43136af8f106992792ab456540d54b880bc320a3192502d2e"
|
||||||
|
dependencies = [
|
||||||
|
"arrow-arith",
|
||||||
|
"arrow-array",
|
||||||
|
"arrow-buffer",
|
||||||
|
"arrow-cast",
|
||||||
|
"arrow-data",
|
||||||
|
"arrow-ipc",
|
||||||
|
"arrow-ord",
|
||||||
|
"arrow-row",
|
||||||
|
"arrow-schema",
|
||||||
|
"arrow-select",
|
||||||
|
"arrow-string",
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"futures",
|
||||||
|
"once_cell",
|
||||||
|
"paste",
|
||||||
|
"prost",
|
||||||
|
"prost-types",
|
||||||
|
"tonic",
|
||||||
|
"tonic-prost",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrow-ipc"
|
name = "arrow-ipc"
|
||||||
version = "58.4.0"
|
version = "58.4.0"
|
||||||
@@ -535,9 +563,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.91"
|
version = "0.1.92"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
|
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -1129,7 +1157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core 0.4.5",
|
||||||
"bytes",
|
"bytes",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.5.0",
|
"http 1.5.0",
|
||||||
@@ -1138,7 +1166,7 @@ dependencies = [
|
|||||||
"hyper 1.9.0",
|
"hyper 1.9.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"itoa",
|
"itoa",
|
||||||
"matchit",
|
"matchit 0.7.3",
|
||||||
"memchr",
|
"memchr",
|
||||||
"mime",
|
"mime",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
@@ -1156,6 +1184,31 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
|
||||||
|
dependencies = [
|
||||||
|
"axum-core 0.5.6",
|
||||||
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
|
"http 1.5.0",
|
||||||
|
"http-body 1.1.0",
|
||||||
|
"http-body-util",
|
||||||
|
"itoa",
|
||||||
|
"matchit 0.8.4",
|
||||||
|
"memchr",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"serde_core",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum-core"
|
name = "axum-core"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@@ -1177,6 +1230,24 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "axum-core"
|
||||||
|
version = "0.5.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"http 1.5.0",
|
||||||
|
"http-body 1.1.0",
|
||||||
|
"http-body-util",
|
||||||
|
"mime",
|
||||||
|
"pin-project-lite",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tower-layer",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backoff"
|
name = "backoff"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -1443,9 +1514,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
version = "1.25.0"
|
version = "1.25.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
|
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck_derive",
|
"bytemuck_derive",
|
||||||
]
|
]
|
||||||
@@ -1597,9 +1668,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chacha20"
|
name = "chacha20"
|
||||||
version = "0.10.0"
|
version = "0.10.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
|
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.4",
|
"cfg-if 1.0.4",
|
||||||
"cpufeatures 0.3.0",
|
"cpufeatures 0.3.0",
|
||||||
@@ -3455,8 +3526,8 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fsst"
|
name = "fsst"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"rand 0.9.5",
|
"rand 0.9.5",
|
||||||
@@ -4815,8 +4886,8 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance"
|
name = "lance"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"arrow",
|
"arrow",
|
||||||
@@ -4888,8 +4959,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-arrow"
|
name = "lance-arrow"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -4911,7 +4982,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-arrow-scalar"
|
name = "lance-arrow-scalar"
|
||||||
version = "58.0.0"
|
version = "58.0.0"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -4925,7 +4996,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-arrow-stats"
|
name = "lance-arrow-stats"
|
||||||
version = "58.0.0"
|
version = "58.0.0"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-schema",
|
"arrow-schema",
|
||||||
@@ -4934,8 +5005,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-bitpacking"
|
name = "lance-bitpacking"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrayref",
|
"arrayref",
|
||||||
"crunchy",
|
"crunchy",
|
||||||
@@ -4945,8 +5016,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-core"
|
name = "lance-core"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -4983,8 +5054,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-datafusion"
|
name = "lance-datafusion"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
@@ -5000,6 +5071,7 @@ dependencies = [
|
|||||||
"datafusion-functions",
|
"datafusion-functions",
|
||||||
"datafusion-physical-expr",
|
"datafusion-physical-expr",
|
||||||
"futures",
|
"futures",
|
||||||
|
"half",
|
||||||
"jsonb",
|
"jsonb",
|
||||||
"lance-arrow",
|
"lance-arrow",
|
||||||
"lance-core",
|
"lance-core",
|
||||||
@@ -5013,8 +5085,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-datagen"
|
name = "lance-datagen"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
@@ -5031,8 +5103,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-derive"
|
name = "lance-derive"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5041,8 +5113,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-encoding"
|
name = "lance-encoding"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-arith",
|
"arrow-arith",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
@@ -5075,8 +5147,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-file"
|
name = "lance-file"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-arith",
|
"arrow-arith",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
@@ -5107,8 +5179,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-index"
|
name = "lance-index"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"arrow",
|
"arrow",
|
||||||
@@ -5172,8 +5244,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-index-core"
|
name = "lance-index-core"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-schema",
|
"arrow-schema",
|
||||||
@@ -5195,8 +5267,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-io"
|
name = "lance-io"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
@@ -5236,8 +5308,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-linalg"
|
name = "lance-linalg"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-schema",
|
"arrow-schema",
|
||||||
@@ -5251,27 +5323,29 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-namespace"
|
name = "lance-namespace"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow",
|
"arrow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"bytes",
|
"bytes",
|
||||||
"lance-core",
|
"lance-core",
|
||||||
"lance-namespace-reqwest-client",
|
"lance-namespace-reqwest-client",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"snafu 0.9.0",
|
"snafu 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-namespace-impls"
|
name = "lance-namespace-impls"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-ipc",
|
"arrow-ipc",
|
||||||
"arrow-schema",
|
"arrow-schema",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum 0.7.9",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -5304,9 +5378,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-namespace-reqwest-client"
|
name = "lance-namespace-reqwest-client"
|
||||||
version = "0.11.0"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a030196da1c994b63a96a4f0bf5b0cfa459fe6dadc9e962320246ca328da22a"
|
checksum = "d8d23e54b1634d5bbb434f8dd33dc3c05f6e58d876a9a27b3b4aef58ddbe11af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -5318,8 +5392,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-select"
|
name = "lance-select"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -5333,8 +5407,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-table"
|
name = "lance-table"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow",
|
"arrow",
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
@@ -5374,8 +5448,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-testing"
|
name = "lance-testing"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-schema",
|
"arrow-schema",
|
||||||
@@ -5388,8 +5462,8 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lance-tokenizer"
|
name = "lance-tokenizer"
|
||||||
version = "12.0.0-beta.2"
|
version = "12.0.0-beta.14"
|
||||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.14#a8a101774a1c9647065cc60137094feadbe55296"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"frostem",
|
"frostem",
|
||||||
"icu_segmenter",
|
"icu_segmenter",
|
||||||
@@ -5402,7 +5476,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lancedb"
|
name = "lancedb"
|
||||||
version = "0.38.0-beta.11"
|
version = "0.39.0-beta.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -5411,6 +5485,7 @@ dependencies = [
|
|||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
"arrow-cast",
|
"arrow-cast",
|
||||||
"arrow-data",
|
"arrow-data",
|
||||||
|
"arrow-flight",
|
||||||
"arrow-ipc",
|
"arrow-ipc",
|
||||||
"arrow-ord",
|
"arrow-ord",
|
||||||
"arrow-schema",
|
"arrow-schema",
|
||||||
@@ -5466,6 +5541,7 @@ dependencies = [
|
|||||||
"polars",
|
"polars",
|
||||||
"polars-arrow",
|
"polars-arrow",
|
||||||
"pprof 0.14.1",
|
"pprof 0.14.1",
|
||||||
|
"prost",
|
||||||
"rand 0.9.5",
|
"rand 0.9.5",
|
||||||
"random_word",
|
"random_word",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -5482,6 +5558,7 @@ dependencies = [
|
|||||||
"test-log",
|
"test-log",
|
||||||
"tokenizers",
|
"tokenizers",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tonic",
|
||||||
"url",
|
"url",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -5490,7 +5567,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lancedb-nodejs"
|
name = "lancedb-nodejs"
|
||||||
version = "0.38.0-beta.11"
|
version = "0.39.0-beta.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow-array",
|
"arrow-array",
|
||||||
"arrow-buffer",
|
"arrow-buffer",
|
||||||
@@ -5515,7 +5592,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lancedb-python"
|
name = "lancedb-python"
|
||||||
version = "0.38.0-beta.11"
|
version = "0.39.0-beta.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arrow",
|
"arrow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -5539,6 +5616,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"snafu 0.8.9",
|
"snafu 0.8.9",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5748,9 +5826,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.33"
|
version = "0.4.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "loom"
|
name = "loom"
|
||||||
@@ -5859,6 +5937,12 @@ version = "0.7.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "matchit"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matrixmultiply"
|
name = "matrixmultiply"
|
||||||
version = "0.3.10"
|
version = "0.3.10"
|
||||||
@@ -6001,9 +6085,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moka"
|
name = "moka"
|
||||||
version = "0.12.15"
|
version = "0.12.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046"
|
checksum = "4293f18e7567a1caf3c584855554377025c65e0aa445344d04171f5ad63d19b9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock",
|
"async-lock",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
@@ -6097,14 +6181,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi"
|
name = "napi"
|
||||||
version = "3.11.0"
|
version = "3.12.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941"
|
checksum = "58c5f4d5375213fdb7be2655e152386e82f026f9a5ba36a75556e11359aafe09"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
"ctor 1.0.12",
|
"ctor 1.0.12",
|
||||||
"futures",
|
"futures",
|
||||||
|
"libc",
|
||||||
"napi-build",
|
"napi-build",
|
||||||
"napi-sys",
|
"napi-sys",
|
||||||
"nohash-hasher",
|
"nohash-hasher",
|
||||||
@@ -6116,15 +6201,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-build"
|
name = "napi-build"
|
||||||
version = "2.4.0"
|
version = "2.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5282704fbe8d49b0cf8b08e3f33233416a528658f205c7e5ace63b582de0b11c"
|
checksum = "60fdf9b392c50e7c4170fa633bd909490ed7835cea4c046776d1a4dd8d2ae0ab"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-derive"
|
name = "napi-derive"
|
||||||
version = "3.6.1"
|
version = "3.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4d5c9c02556ea6dc99dffd36c1ce60141411657438501a125b675776d011ce92"
|
checksum = "0fa55ea69990c90b888e9e77044410e304ce7f35de599dc6d0b5c1923d2e59af"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"ctor 1.0.12",
|
"ctor 1.0.12",
|
||||||
@@ -6136,9 +6221,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "napi-derive-backend"
|
name = "napi-derive-backend"
|
||||||
version = "6.1.1"
|
version = "6.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d60b5d773ad46c698c8cc2cd9fde0b283d39cbb7f71c04bee633c7bdba4423bd"
|
checksum = "df4056ac7c18e4438ccf0edaed4340ca0d269278c8ec19284f7b23cb039fd0ae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"convert_case",
|
"convert_case",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -7733,6 +7818,7 @@ dependencies = [
|
|||||||
"pyo3-build-config",
|
"pyo3-build-config",
|
||||||
"pyo3-ffi",
|
"pyo3-ffi",
|
||||||
"pyo3-macros",
|
"pyo3-macros",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8601,9 +8687,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roaring"
|
name = "roaring"
|
||||||
version = "0.11.4"
|
version = "0.11.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1dedc5658c6ecb3bdb5ef5f3295bb9253f42dcf3fd1402c03f6b1f7659c3c4a9"
|
checksum = "18bd8a37d17a58532776dcdf6041ce64929adca78e8489d5cacbafe99229d3e1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"byteorder",
|
"byteorder",
|
||||||
@@ -9063,9 +9149,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.21.0"
|
version = "3.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c"
|
checksum = "ee78f1fbe43ac4a0e47aadb3dbd357b69eb0d3793e948624cd03dd2750ab1c0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bs58",
|
"bs58",
|
||||||
@@ -9073,6 +9159,7 @@ dependencies = [
|
|||||||
"hex",
|
"hex",
|
||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"indexmap 2.14.0",
|
"indexmap 2.14.0",
|
||||||
|
"jiff",
|
||||||
"schemars 0.9.0",
|
"schemars 0.9.0",
|
||||||
"schemars 1.2.1",
|
"schemars 1.2.1",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -9083,9 +9170,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with_macros"
|
name = "serde_with_macros"
|
||||||
version = "3.21.0"
|
version = "3.22.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
|
checksum = "8705578779c2b6bd90d84d66eb2e206b708b1a4d7b9f17641b293545bf1c7e46"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling 0.23.0",
|
"darling 0.23.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -10084,6 +10171,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
|
checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"axum 0.8.9",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"h2 0.4.16",
|
"h2 0.4.16",
|
||||||
@@ -10095,9 +10183,11 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
|
"rustls-native-certs",
|
||||||
"socket2 0.6.3",
|
"socket2 0.6.3",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-rustls 0.26.4",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
@@ -10452,9 +10542,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.24.0"
|
version = "1.26.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
|
checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.4.2",
|
"getrandom 0.4.2",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
|||||||
+17
-15
@@ -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.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance = { "version" = "=12.0.0-beta.14", default-features = false, "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-core = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-core = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-datagen = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-datagen = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-file = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-file = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-io = { "version" = "=12.0.0-beta.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-io = { "version" = "=12.0.0-beta.14", default-features = false, "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-index = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-index = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-linalg = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-linalg = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-namespace = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-namespace = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-namespace-impls = { "version" = "=12.0.0-beta.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-namespace-impls = { "version" = "=12.0.0-beta.14", default-features = false, "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-table = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-table = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-testing = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-testing = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-datafusion = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-datafusion = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-encoding = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-encoding = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "git" = "https://github.com/lance-format/lance.git" }
|
||||||
lance-arrow = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
lance-arrow = { "version" = "=12.0.0-beta.14", "tag" = "v12.0.0-beta.14", "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 }
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ licenses:
|
|||||||
cd python && cargo about generate ../about.hbs -o RUST_THIRD_PARTY_LICENSES.html -c ../about.toml
|
cd python && cargo about generate ../about.hbs -o RUST_THIRD_PARTY_LICENSES.html -c ../about.toml
|
||||||
cd python && uv sync --all-extras && uv tool run pip-licenses --python .venv/bin/python --format=markdown --with-urls --output-file=PYTHON_THIRD_PARTY_LICENSES.md
|
cd python && uv sync --all-extras && uv tool run pip-licenses --python .venv/bin/python --format=markdown --with-urls --output-file=PYTHON_THIRD_PARTY_LICENSES.md
|
||||||
cd nodejs && cargo about generate ../about.hbs -o RUST_THIRD_PARTY_LICENSES.html -c ../about.toml
|
cd nodejs && cargo about generate ../about.hbs -o RUST_THIRD_PARTY_LICENSES.html -c ../about.toml
|
||||||
cd nodejs && npx license-checker --markdown --out NODEJS_THIRD_PARTY_LICENSES.md
|
cd nodejs && pnpm dlx license-checker@25 --markdown --out NODEJS_THIRD_PARTY_LICENSES.md
|
||||||
cd java && ./mvnw license:aggregate-add-third-party -q
|
cd java && ./mvnw license:aggregate-add-third-party -q
|
||||||
|
|||||||
@@ -12,16 +12,12 @@ done
|
|||||||
# This updates the lockfile without building
|
# This updates the lockfile without building
|
||||||
cargo metadata --quiet > /dev/null
|
cargo metadata --quiet > /dev/null
|
||||||
|
|
||||||
pushd nodejs || exit 1
|
|
||||||
npm install --package-lock-only --silent
|
|
||||||
popd
|
|
||||||
|
|
||||||
if git diff --quiet --exit-code; then
|
if git diff --quiet --exit-code; then
|
||||||
echo "No lockfile changes to commit; skipping amend."
|
echo "No lockfile changes to commit; skipping amend."
|
||||||
elif $AMEND; then
|
elif $AMEND; then
|
||||||
git add Cargo.lock nodejs/package-lock.json
|
git add Cargo.lock
|
||||||
git commit --amend --no-edit
|
git commit --amend --no-edit
|
||||||
else
|
else
|
||||||
git add Cargo.lock nodejs/package-lock.json
|
git add Cargo.lock
|
||||||
git commit -m "Update lockfiles"
|
git commit -m "Update lockfiles"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -131,18 +131,13 @@ allow = [
|
|||||||
"BSD-3-Clause",
|
"BSD-3-Clause",
|
||||||
"ISC",
|
"ISC",
|
||||||
"Unicode-3.0",
|
"Unicode-3.0",
|
||||||
"Unicode-DFS-2016",
|
|
||||||
"Zlib",
|
"Zlib",
|
||||||
"CC0-1.0",
|
"CC0-1.0",
|
||||||
"MPL-2.0",
|
"MPL-2.0",
|
||||||
"BSL-1.0",
|
"BSL-1.0",
|
||||||
"OpenSSL",
|
|
||||||
# 0BSD ("BSD Zero Clause") is effectively public domain — no attribution
|
# 0BSD ("BSD Zero Clause") is effectively public domain — no attribution
|
||||||
# required. Pulled in by `mock_instant`.
|
# required. Pulled in by `mock_instant`.
|
||||||
"0BSD",
|
"0BSD",
|
||||||
# bzip2-1.0.6 is the permissive upstream bzip2 license (BSD-like). Pulled
|
|
||||||
# in by `libbz2-rs-sys`, the pure-Rust bzip2 implementation.
|
|
||||||
"bzip2-1.0.6",
|
|
||||||
# CDLA-Permissive-2.0 is a permissive data license used by `webpki-roots`
|
# CDLA-Permissive-2.0 is a permissive data license used by `webpki-roots`
|
||||||
# for the Mozilla CA root bundle. Data-only, distribution-compatible.
|
# for the Mozilla CA root bundle. Data-only, distribution-compatible.
|
||||||
"CDLA-Permissive-2.0",
|
"CDLA-Permissive-2.0",
|
||||||
@@ -150,12 +145,7 @@ allow = [
|
|||||||
confidence-threshold = 0.8
|
confidence-threshold = 0.8
|
||||||
# Per-crate license exceptions: allow a license for a specific crate only,
|
# Per-crate license exceptions: allow a license for a specific crate only,
|
||||||
# rather than globally via the `allow` list above.
|
# rather than globally via the `allow` list above.
|
||||||
exceptions = [
|
exceptions = []
|
||||||
# CDDL-1.0 (copyleft) is pulled in only as a dev/profiling dependency via
|
|
||||||
# `inferno` -> `pprof` -> `lance-testing`; it is a test dependency that we
|
|
||||||
# do not distribute, so scope the allowance to `inferno` alone.
|
|
||||||
{ allow = ["CDDL-1.0"], crate = "inferno" },
|
|
||||||
]
|
|
||||||
# Crates whose license cannot be determined from Cargo metadata but whose
|
# Crates whose license cannot be determined from Cargo metadata but whose
|
||||||
# license we've manually confirmed from upstream. Keep this list minimal.
|
# license we've manually confirmed from upstream. Keep this list minimal.
|
||||||
[[licenses.clarify]]
|
[[licenses.clarify]]
|
||||||
|
|||||||
+11
-8
@@ -47,22 +47,24 @@ pytest -vv python/tests/docs
|
|||||||
|
|
||||||
### Checking typescript examples
|
### Checking typescript examples
|
||||||
|
|
||||||
The `@lancedb/lancedb` package must be built before running the tests:
|
The examples depend on `@lancedb/lancedb` at `file:../dist`, so the package must be
|
||||||
|
built before running the tests. This uses pnpm; see the
|
||||||
|
[Typescript contributing guide](../nodejs/CONTRIBUTING.md) for the toolchain setup.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pushd nodejs
|
pushd nodejs
|
||||||
npm ci
|
pnpm install
|
||||||
npm run build
|
pnpm build
|
||||||
popd
|
popd
|
||||||
```
|
```
|
||||||
|
|
||||||
Then you can run the examples by going to the `nodejs/examples` directory and
|
Then you can run the examples by going to the `nodejs/examples` directory, which is a
|
||||||
running the tests like a normal npm package:
|
separate pnpm package with its own lockfile:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
pushd nodejs/examples
|
pushd nodejs/examples
|
||||||
npm ci
|
pnpm install
|
||||||
npm test
|
pnpm test
|
||||||
popd
|
popd
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -84,6 +86,7 @@ The new files should be checked into the repository.
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
pushd nodejs
|
pushd nodejs
|
||||||
npm run docs
|
# `pnpm docs` would invoke pnpm's built-in `docs` command, not the script.
|
||||||
|
pnpm run docs
|
||||||
popd
|
popd
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -446,6 +446,15 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
column:
|
column:
|
||||||
type: string
|
type: string
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
description: Optional name for the created index.
|
||||||
|
replace:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
description: |
|
||||||
|
Whether to replace an existing index with the same resolved
|
||||||
|
name. Defaults to true.
|
||||||
metric_type:
|
metric_type:
|
||||||
type: string
|
type: string
|
||||||
nullable: false
|
nullable: false
|
||||||
|
|||||||
Generated
-135
@@ -1,135 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "lancedb-docs-test",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"name": "lancedb-docs-test",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "Apache 2",
|
|
||||||
"dependencies": {
|
|
||||||
"apache-arrow": "file:../node/node_modules/apache-arrow",
|
|
||||||
"vectordb": "file:../node"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.11.8",
|
|
||||||
"typescript": "^5.3.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"../node": {
|
|
||||||
"name": "vectordb",
|
|
||||||
"version": "0.21.2-beta.0",
|
|
||||||
"cpu": [
|
|
||||||
"x64",
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"os": [
|
|
||||||
"darwin",
|
|
||||||
"linux",
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"dependencies": {
|
|
||||||
"@neon-rs/load": "^0.0.74",
|
|
||||||
"axios": "^1.4.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@neon-rs/cli": "^0.0.160",
|
|
||||||
"@types/chai": "^4.3.4",
|
|
||||||
"@types/chai-as-promised": "^7.1.5",
|
|
||||||
"@types/mocha": "^10.0.1",
|
|
||||||
"@types/node": "^18.16.2",
|
|
||||||
"@types/sinon": "^10.0.15",
|
|
||||||
"@types/temp": "^0.9.1",
|
|
||||||
"@types/uuid": "^9.0.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
|
||||||
"apache-arrow-old": "npm:apache-arrow@13.0.0",
|
|
||||||
"cargo-cp-artifact": "^0.1",
|
|
||||||
"chai": "^4.3.7",
|
|
||||||
"chai-as-promised": "^7.1.1",
|
|
||||||
"eslint": "^8.39.0",
|
|
||||||
"eslint-config-standard-with-typescript": "^34.0.1",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
|
||||||
"eslint-plugin-n": "^15.7.0",
|
|
||||||
"eslint-plugin-promise": "^6.1.1",
|
|
||||||
"mocha": "^10.2.0",
|
|
||||||
"openai": "^4.24.1",
|
|
||||||
"sinon": "^15.1.0",
|
|
||||||
"temp": "^0.9.4",
|
|
||||||
"ts-node": "^10.9.1",
|
|
||||||
"ts-node-dev": "^2.0.0",
|
|
||||||
"typedoc": "^0.24.7",
|
|
||||||
"typedoc-plugin-markdown": "^3.15.3",
|
|
||||||
"typescript": "^5.1.0",
|
|
||||||
"uuid": "^9.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@lancedb/vectordb-darwin-arm64": "0.21.2-beta.0",
|
|
||||||
"@lancedb/vectordb-darwin-x64": "0.21.2-beta.0",
|
|
||||||
"@lancedb/vectordb-linux-arm64-gnu": "0.21.2-beta.0",
|
|
||||||
"@lancedb/vectordb-linux-x64-gnu": "0.21.2-beta.0",
|
|
||||||
"@lancedb/vectordb-win32-x64-msvc": "0.21.2-beta.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@apache-arrow/ts": "^14.0.2",
|
|
||||||
"apache-arrow": "^14.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"../node/node_modules/apache-arrow": {
|
|
||||||
"version": "14.0.2",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/command-line-args": "5.2.0",
|
|
||||||
"@types/command-line-usage": "5.0.2",
|
|
||||||
"@types/node": "20.3.0",
|
|
||||||
"@types/pad-left": "2.1.1",
|
|
||||||
"command-line-args": "5.2.1",
|
|
||||||
"command-line-usage": "7.0.1",
|
|
||||||
"flatbuffers": "23.5.26",
|
|
||||||
"json-bignum": "^0.0.3",
|
|
||||||
"pad-left": "^2.1.0",
|
|
||||||
"tslib": "^2.5.3"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"arrow2csv": "bin/arrow2csv.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "20.11.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.8.tgz",
|
|
||||||
"integrity": "sha512-i7omyekpPTNdv4Jb/Rgqg0RU8YqLcNsI12quKSDkRXNfx7Wxdm6HhK1awT3xTgEkgxPn3bvnSpiEAc7a7Lpyow==",
|
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": "~5.26.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/apache-arrow": {
|
|
||||||
"resolved": "../node/node_modules/apache-arrow",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/typescript": {
|
|
||||||
"version": "5.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
|
|
||||||
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
|
|
||||||
"dev": true,
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "5.26.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
|
||||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"node_modules/vectordb": {
|
|
||||||
"resolved": "../node",
|
|
||||||
"link": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "lancedb-docs-test",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "auto-generated tests from doc",
|
|
||||||
"author": "dev@lancedb.com",
|
|
||||||
"license": "Apache 2",
|
|
||||||
"dependencies": {
|
|
||||||
"apache-arrow": "file:../node/node_modules/apache-arrow",
|
|
||||||
"vectordb": "file:../node"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"build": "tsc -b && cd ../node && npm run build-release",
|
|
||||||
"example": "npm run build && node",
|
|
||||||
"test": "npm run build && ls dist/*.js | xargs -n 1 node"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/node": "^20.11.8",
|
|
||||||
"typescript": "^5.3.3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.38.0-beta.11</version>
|
<version>0.39.0-beta.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -676,9 +676,17 @@ List all the versions of the table
|
|||||||
abstract mergeInsert(on): MergeInsertBuilder
|
abstract mergeInsert(on): MergeInsertBuilder
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Create a [MergeInsertBuilder](MergeInsertBuilder.md), which combines new data with the
|
||||||
|
existing table in a single transaction — inserting, updating and deleting
|
||||||
|
rows depending on how they match.
|
||||||
|
|
||||||
#### Parameters
|
#### Parameters
|
||||||
|
|
||||||
* **on**: `string` \| `string`[]
|
* **on**: `string` \| `string`[]
|
||||||
|
The column, or columns, to match source rows against target
|
||||||
|
rows on. Typically a key or id column. Several columns match on the
|
||||||
|
composite key: a source row updates a target row only when it agrees on
|
||||||
|
every one of them.
|
||||||
|
|
||||||
#### Returns
|
#### Returns
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@
|
|||||||
- [IvfFlatOptions](interfaces/IvfFlatOptions.md)
|
- [IvfFlatOptions](interfaces/IvfFlatOptions.md)
|
||||||
- [IvfPqOptions](interfaces/IvfPqOptions.md)
|
- [IvfPqOptions](interfaces/IvfPqOptions.md)
|
||||||
- [IvfRqOptions](interfaces/IvfRqOptions.md)
|
- [IvfRqOptions](interfaces/IvfRqOptions.md)
|
||||||
- [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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ projections: [string, string][];
|
|||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
### sourceNamespace
|
||||||
|
|
||||||
|
```ts
|
||||||
|
sourceNamespace: string[];
|
||||||
|
```
|
||||||
|
|
||||||
|
Namespace holding the source table; empty is the root namespace.
|
||||||
|
|
||||||
|
***
|
||||||
|
|
||||||
### sourceTable
|
### sourceTable
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -72,6 +125,10 @@ listing a storage directory.
|
|||||||
|
|
||||||
::: lancedb.functions.UdfDefinition
|
::: lancedb.functions.UdfDefinition
|
||||||
|
|
||||||
|
::: lancedb.secrets.EnvVarSecret
|
||||||
|
|
||||||
|
::: lancedb.secrets.SecretInfo
|
||||||
|
|
||||||
::: lancedb.functions.FunctionRegistrationRequest
|
::: lancedb.functions.FunctionRegistrationRequest
|
||||||
|
|
||||||
::: lancedb.functions.FunctionArtifactRequest
|
::: lancedb.functions.FunctionArtifactRequest
|
||||||
@@ -94,6 +151,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 +161,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 +320,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
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"include": [
|
|
||||||
"src/*.ts",
|
|
||||||
],
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es2022",
|
|
||||||
"module": "nodenext",
|
|
||||||
"declaration": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"strict": true,
|
|
||||||
"allowJs": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
},
|
|
||||||
"exclude": [
|
|
||||||
"./dist/*",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.lancedb</groupId>
|
<groupId>com.lancedb</groupId>
|
||||||
<artifactId>lancedb-parent</artifactId>
|
<artifactId>lancedb-parent</artifactId>
|
||||||
<version>0.38.0-beta.11</version>
|
<version>0.39.0-beta.4</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.38.0-beta.11</version>
|
<version>0.39.0-beta.4</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.2</lance-core.version>
|
<lance-core.version>12.0.0-beta.14</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.38.0-beta.11"
|
version = "0.39.0-beta.4"
|
||||||
publish = false
|
publish = false
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description.workspace = true
|
description.workspace = true
|
||||||
|
|||||||
@@ -48,6 +48,28 @@ describe("materialized views", () => {
|
|||||||
expect(definitionFromMetadata(safe, "v").limit).toBe(42);
|
expect(definitionFromMetadata(safe, "v").limit).toBe(42);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reads the namespaced select kind and refuses unknown kinds", () => {
|
||||||
|
// "namespaced_select" is the namespaced form of "select": same shape, a
|
||||||
|
// separate kind so readers that predate it refuse instead of resolving
|
||||||
|
// the source at the root.
|
||||||
|
const namespaced = new Map([
|
||||||
|
[
|
||||||
|
DEFINITION_META_KEY,
|
||||||
|
'{"kind":"namespaced_select","source_table":"people","source_namespace":["ns"]}',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
const definition = definitionFromMetadata(namespaced, "v");
|
||||||
|
expect(definition.sourceTable).toBe("people");
|
||||||
|
expect(definition.sourceNamespace).toEqual(["ns"]);
|
||||||
|
|
||||||
|
const unknown = new Map([
|
||||||
|
[DEFINITION_META_KEY, '{"kind":"select_v3","source_table":"people"}'],
|
||||||
|
]);
|
||||||
|
expect(() => definitionFromMetadata(unknown, "v")).toThrow(
|
||||||
|
/cannot refresh/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("creates, refreshes and queries a view", async () => {
|
it("creates, refreshes and queries a view", async () => {
|
||||||
const view = await db.createMaterializedView("adults", "people", {
|
const view = await db.createMaterializedView("adults", "people", {
|
||||||
select: ["name", ["shout", "upper(name)"]],
|
select: ["name", ["shout", "upper(name)"]],
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import packageJson = require("../package.json");
|
|||||||
|
|
||||||
describe("package metadata", () => {
|
describe("package metadata", () => {
|
||||||
it("requires Node.js type declarations compatible with the runtime", () => {
|
it("requires Node.js type declarations compatible with the runtime", () => {
|
||||||
expect(packageJson.engines.node).toBe(">= 18");
|
expect(packageJson.engines.node).toBe(">= 22");
|
||||||
expect(packageJson.peerDependencies["@types/node"]).toBe(">=18");
|
expect(packageJson.peerDependencies["@types/node"]).toBe(">=22");
|
||||||
expect(packageJson.peerDependenciesMeta["@types/node"]).toEqual({
|
expect(packageJson.peerDependenciesMeta["@types/node"]).toEqual({
|
||||||
optional: true,
|
optional: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
import { RequestListener } from "http";
|
import { RequestListener } from "http";
|
||||||
|
import packageJson = require("../package.json");
|
||||||
import {
|
import {
|
||||||
ClientConfig,
|
ClientConfig,
|
||||||
Connection,
|
Connection,
|
||||||
@@ -70,7 +71,13 @@ async function withMockDatabase(
|
|||||||
try {
|
try {
|
||||||
await callback(db);
|
await callback(db);
|
||||||
} finally {
|
} finally {
|
||||||
server.close();
|
// `close()` alone leaves the port bound until keep-alive sockets drain, so
|
||||||
|
// a single failing test would cascade into EADDRINUSE for every test after
|
||||||
|
// it. Destroy the connections and wait for the port to actually be free.
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
server.closeAllConnections();
|
||||||
|
server.close(() => resolve());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +138,7 @@ describe("remote connection", () => {
|
|||||||
(req, res) => {
|
(req, res) => {
|
||||||
expect(req.headers["x-api-key"]).toEqual("fake");
|
expect(req.headers["x-api-key"]).toEqual("fake");
|
||||||
expect(req.headers["user-agent"]).toEqual(
|
expect(req.headers["user-agent"]).toEqual(
|
||||||
`LanceDB-Node-Client/${process.env.npm_package_version}`,
|
`LanceDB-Node-Client/${packageJson.version}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const body = JSON.stringify({ tables: [] });
|
const body = JSON.stringify({ tables: [] });
|
||||||
@@ -932,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) => {
|
||||||
@@ -960,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;
|
||||||
@@ -981,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",
|
||||||
@@ -997,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");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -737,11 +737,12 @@ it("should query documents with LangChain PDF metadata", async () => {
|
|||||||
|
|
||||||
describe("merge insert", () => {
|
describe("merge insert", () => {
|
||||||
let tmpDir: tmp.DirResult;
|
let tmpDir: tmp.DirResult;
|
||||||
|
let conn: Connection;
|
||||||
let table: Table;
|
let table: Table;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
||||||
const conn = await connect(tmpDir.name);
|
conn = await connect(tmpDir.name);
|
||||||
|
|
||||||
table = await conn.createTable("some_table", [
|
table = await conn.createTable("some_table", [
|
||||||
{ a: 1, b: "a" },
|
{ a: 1, b: "a" },
|
||||||
@@ -779,6 +780,38 @@ describe("merge insert", () => {
|
|||||||
|
|
||||||
expect(result.map((row) => ({ ...row }))).toEqual(expected);
|
expect(result.map((row) => ({ ...row }))).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
test("upsert on a composite key", async () => {
|
||||||
|
const composite = await conn.createTable("composite", [
|
||||||
|
{ shard: "a", id: 1, val: "x" },
|
||||||
|
{ shard: "a", id: 2, val: "y" },
|
||||||
|
{ shard: "b", id: 1, val: "z" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ("a", 1) matches an existing row and updates it. ("b", 2) agrees with an
|
||||||
|
// existing row on each key column separately but on neither pair, so it is
|
||||||
|
// an insert.
|
||||||
|
const mergeInsertRes = await composite
|
||||||
|
.mergeInsert(["shard", "id"])
|
||||||
|
.whenMatchedUpdateAll()
|
||||||
|
.whenNotMatchedInsertAll()
|
||||||
|
.execute([
|
||||||
|
{ shard: "a", id: 1, val: "X" },
|
||||||
|
{ shard: "b", id: 2, val: "W" },
|
||||||
|
]);
|
||||||
|
expect(mergeInsertRes.numUpdatedRows).toBe(1);
|
||||||
|
expect(mergeInsertRes.numInsertedRows).toBe(1);
|
||||||
|
|
||||||
|
const result = (await composite.toArrow())
|
||||||
|
.toArray()
|
||||||
|
.sort((a, b) => a.shard.localeCompare(b.shard) || a.id - b.id);
|
||||||
|
|
||||||
|
expect(result.map((row) => ({ ...row }))).toEqual([
|
||||||
|
{ shard: "a", id: 1, val: "X" },
|
||||||
|
{ shard: "a", id: 2, val: "y" },
|
||||||
|
{ shard: "b", id: 1, val: "z" },
|
||||||
|
{ shard: "b", id: 2, val: "W" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
test("conditional update", async () => {
|
test("conditional update", async () => {
|
||||||
const newData = [
|
const newData = [
|
||||||
{ a: 2, b: "x" },
|
{ a: 2, b: "x" },
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
"//1": "--experimental-vm-modules is needed to run jest with sentence-transformers",
|
"//1": "--experimental-vm-modules is needed to run jest with sentence-transformers",
|
||||||
"//2": "--testEnvironment is needed to run jest with sentence-transformers",
|
"//2": "--testEnvironment is needed to run jest with sentence-transformers",
|
||||||
"//3": "See: https://github.com/huggingface/transformers.js/issues/57",
|
"//3": "See: https://github.com/huggingface/transformers.js/issues/57",
|
||||||
"test": "node --experimental-vm-modules node_modules/.bin/jest --testEnvironment jest-environment-node-single-context --verbose",
|
"//4": "jest is invoked by its JS entry, not node_modules/.bin/jest: under pnpm that path is a shell shim, which `node` cannot execute",
|
||||||
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testEnvironment jest-environment-node-single-context --verbose",
|
||||||
"lint": "biome check *.ts && biome format *.ts",
|
"lint": "biome check *.ts && biome format *.ts",
|
||||||
"lint-ci": "biome ci .",
|
"lint-ci": "biome ci .",
|
||||||
"lint-fix": "biome check --write *.ts && pnpm format",
|
"lint-fix": "biome check --write *.ts && pnpm format",
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -94,13 +94,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,
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ export interface MaterializedViewDefinition {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
/** Source columns the projections and filter read. */
|
/** Source columns the projections and filter read. */
|
||||||
inputs: string[];
|
inputs: string[];
|
||||||
|
/** Namespace holding the source table; empty is the root namespace. */
|
||||||
|
sourceNamespace: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +80,8 @@ export function definitionFromMetadata(
|
|||||||
}
|
}
|
||||||
// biome-ignore lint/suspicious/noExplicitAny: raw JSON
|
// biome-ignore lint/suspicious/noExplicitAny: raw JSON
|
||||||
const value: any = JSON.parse(raw);
|
const value: any = JSON.parse(raw);
|
||||||
if (value.kind !== "select") {
|
// "namespaced_select" keeps older readers from resolving the source at root.
|
||||||
|
if (value.kind !== "select" && value.kind !== "namespaced_select") {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`materialized view '${name}' is defined by '${value.kind}', which this ` +
|
`materialized view '${name}' is defined by '${value.kind}', which this ` +
|
||||||
"version of lancedb cannot refresh",
|
"version of lancedb cannot refresh",
|
||||||
@@ -103,6 +106,7 @@ export function definitionFromMetadata(
|
|||||||
filter: value.filter ?? undefined,
|
filter: value.filter ?? undefined,
|
||||||
limit,
|
limit,
|
||||||
inputs: value.inputs ?? [],
|
inputs: value.inputs ?? [],
|
||||||
|
sourceNamespace: value.source_namespace ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-9
@@ -19,6 +19,7 @@ import {
|
|||||||
|
|
||||||
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 +31,6 @@ import {
|
|||||||
DropColumnsResult,
|
DropColumnsResult,
|
||||||
IndexConfig,
|
IndexConfig,
|
||||||
IndexStatistics,
|
IndexStatistics,
|
||||||
Job,
|
|
||||||
LsmStats,
|
LsmStats,
|
||||||
Branches as NativeBranches,
|
Branches as NativeBranches,
|
||||||
OptimizeStats,
|
OptimizeStats,
|
||||||
@@ -919,6 +919,16 @@ export abstract class Table {
|
|||||||
/** Return the table as an arrow table */
|
/** Return the table as an arrow table */
|
||||||
abstract toArrow(): Promise<ArrowTable>;
|
abstract toArrow(): Promise<ArrowTable>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a {@link MergeInsertBuilder}, which combines new data with the
|
||||||
|
* existing table in a single transaction — inserting, updating and deleting
|
||||||
|
* rows depending on how they match.
|
||||||
|
*
|
||||||
|
* @param on - The column, or columns, to match source rows against target
|
||||||
|
* rows on. Typically a key or id column. Several columns match on the
|
||||||
|
* composite key: a source row updates a target row only when it agrees on
|
||||||
|
* every one of them.
|
||||||
|
*/
|
||||||
abstract mergeInsert(on: string | string[]): MergeInsertBuilder;
|
abstract mergeInsert(on: string | string[]): MergeInsertBuilder;
|
||||||
|
|
||||||
/** List all the stats of a specified index
|
/** List all the stats of a specified index
|
||||||
@@ -1114,13 +1124,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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1303,7 +1315,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(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-darwin-arm64",
|
"name": "@lancedb/lancedb-darwin-arm64",
|
||||||
"version": "0.38.0-beta.11",
|
"version": "0.39.0-beta.4",
|
||||||
"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.38.0-beta.11",
|
"version": "0.39.0-beta.4",
|
||||||
"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.38.0-beta.11",
|
"version": "0.39.0-beta.4",
|
||||||
"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.38.0-beta.11",
|
"version": "0.39.0-beta.4",
|
||||||
"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.38.0-beta.11",
|
"version": "0.39.0-beta.4",
|
||||||
"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.38.0-beta.11",
|
"version": "0.39.0-beta.4",
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@lancedb/lancedb-win32-x64-msvc",
|
"name": "@lancedb/lancedb-win32-x64-msvc",
|
||||||
"version": "0.38.0-beta.11",
|
"version": "0.39.0-beta.4",
|
||||||
"os": ["win32"],
|
"os": ["win32"],
|
||||||
"cpu": ["x64"],
|
"cpu": ["x64"],
|
||||||
"main": "lancedb.win32-x64-msvc.node",
|
"main": "lancedb.win32-x64-msvc.node",
|
||||||
|
|||||||
Generated
-11106
File diff suppressed because it is too large
Load Diff
+5
-5
@@ -11,7 +11,7 @@
|
|||||||
"ann"
|
"ann"
|
||||||
],
|
],
|
||||||
"private": false,
|
"private": false,
|
||||||
"version": "0.38.0-beta.11",
|
"version": "0.39.0-beta.4",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist/index.js",
|
".": "./dist/index.js",
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
"@biomejs/biome": "^1.7.3",
|
"@biomejs/biome": "^1.7.3",
|
||||||
"@jest/globals": "^29.7.0",
|
"@jest/globals": "^29.7.0",
|
||||||
"@napi-rs/cli": "3.7.0",
|
"@napi-rs/cli": "3.7.0",
|
||||||
"@opentelemetry/sdk-metrics": "^1.30.0",
|
"@opentelemetry/sdk-metrics": "^2.10.0",
|
||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/jest": "^29.1.2",
|
"@types/jest": "^29.1.2",
|
||||||
"@types/node": "22.7.4",
|
"@types/node": "22.7.4",
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"tmp": "^0.2.3",
|
"tmp": "^0.2.7",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
"typedoc": "0.26.4",
|
"typedoc": "0.26.4",
|
||||||
"typedoc-plugin-markdown": "4.2.1",
|
"typedoc-plugin-markdown": "4.2.1",
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
"timeout": "3m"
|
"timeout": "3m"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 18"
|
"node": ">= 22"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@11.1.1",
|
"packageManager": "pnpm@11.1.1",
|
||||||
"cpu": ["x64", "arm64"],
|
"cpu": ["x64", "arm64"],
|
||||||
@@ -101,7 +101,7 @@
|
|||||||
"openai": "4.29.2"
|
"openai": "4.29.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/node": ">=18",
|
"@types/node": ">=22",
|
||||||
"apache-arrow": ">=15.0.0 <=18.1.0"
|
"apache-arrow": ">=15.0.0 <=18.1.0"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
|
|||||||
Generated
+600
-444
File diff suppressed because it is too large
Load Diff
@@ -16,3 +16,41 @@ allowBuilds:
|
|||||||
onnxruntime-node: true
|
onnxruntime-node: true
|
||||||
protobufjs: true
|
protobufjs: true
|
||||||
sharp: true
|
sharp: true
|
||||||
|
|
||||||
|
minimumReleaseAgeExclude:
|
||||||
|
- protobufjs@7.5.8
|
||||||
|
- tmp@0.2.6
|
||||||
|
- form-data@4.0.6
|
||||||
|
- tar@7.5.16
|
||||||
|
- markdown-it@14.1.2
|
||||||
|
- linkify-it@5.0.1
|
||||||
|
- js-yaml@3.15.0
|
||||||
|
- js-yaml@4.1.2
|
||||||
|
- protobufjs@7.6.1
|
||||||
|
- protobufjs@7.6.3
|
||||||
|
- '@babel/core@7.29.1'
|
||||||
|
- axios@1.18.0
|
||||||
|
- brace-expansion@2.1.2
|
||||||
|
- brace-expansion@1.1.16
|
||||||
|
- js-yaml@4.3.0
|
||||||
|
- tar@7.5.18
|
||||||
|
- tar@7.5.19
|
||||||
|
- tar@7.5.17
|
||||||
|
- protobufjs@7.6.5
|
||||||
|
- linkify-it@5.0.2
|
||||||
|
- sharp@0.35.0
|
||||||
|
- brace-expansion@1.1.17
|
||||||
|
- brace-expansion@2.1.3
|
||||||
|
- brace-expansion@2.1.4
|
||||||
|
- brace-expansion@1.1.18
|
||||||
|
- js-yaml@3.15.1
|
||||||
|
- js-yaml@4.3.1
|
||||||
|
- tar@7.5.21
|
||||||
|
- '@opentelemetry/core@2.8.0'
|
||||||
|
|
||||||
|
# @huggingface/transformers pins sharp ^0.33.5 and no released version has moved
|
||||||
|
# past ^0.34.5, all of which inherit the libvips CVEs in GHSA-f88m-g3jw-g9cj.
|
||||||
|
# Force the patched line. sharp is only reached by transformers' image pipeline,
|
||||||
|
# which LanceDB's text embedding function never uses.
|
||||||
|
overrides:
|
||||||
|
sharp: ^0.35.4
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+5
-1
@@ -664,7 +664,11 @@ impl JsFullTextQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_fts_query(query: Object) -> napi::Result<FullTextSearchQuery> {
|
fn parse_fts_query(query: Object) -> napi::Result<FullTextSearchQuery> {
|
||||||
if let Ok(Some(query)) = query.get::<&JsFullTextQuery>("query") {
|
// `&JsFullTextQuery` recovers a native class reference through napi's borrow-tracked
|
||||||
|
// path, which is only usable from generated `#[napi]` argument conversion. This is a
|
||||||
|
// manual lookup on a nested `Object` property instead, so use `ClassInstance`, which
|
||||||
|
// unwraps the class without requiring a borrow scope.
|
||||||
|
if let Ok(Some(query)) = query.get::<ClassInstance<JsFullTextQuery>>("query") {
|
||||||
Ok(FullTextSearchQuery::new_query(query.inner.clone()))
|
Ok(FullTextSearchQuery::new_query(query.inner.clone()))
|
||||||
} else if let Ok(Some(query_text)) = query.get::<String>("query") {
|
} else if let Ok(Some(query_text)) = query.get::<String>("query") {
|
||||||
let mut query_text = query_text;
|
let mut query_text = query_text;
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lancedb-python"
|
name = "lancedb-python"
|
||||||
version = "0.38.0-beta.11"
|
version = "0.39.0-beta.4"
|
||||||
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,
|
||||||
@@ -33,6 +37,8 @@ from .functions import (
|
|||||||
UdfDefinition as UdfDefinition,
|
UdfDefinition as UdfDefinition,
|
||||||
udf as udf,
|
udf as udf,
|
||||||
)
|
)
|
||||||
|
from .secrets import EnvVarSecret as EnvVarSecret
|
||||||
|
from .secrets import SecretInfo as SecretInfo
|
||||||
from .materialized_view import (
|
from .materialized_view import (
|
||||||
AsyncMaterializedView,
|
AsyncMaterializedView,
|
||||||
MaterializedView,
|
MaterializedView,
|
||||||
@@ -101,6 +107,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 +136,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 +280,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 +423,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 +437,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 +460,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 +550,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 +573,7 @@ __all__ = [
|
|||||||
"connect_namespace_async",
|
"connect_namespace_async",
|
||||||
"AsyncConnection",
|
"AsyncConnection",
|
||||||
"AsyncJob",
|
"AsyncJob",
|
||||||
|
"AsyncSqlQuery",
|
||||||
"AsyncLanceNamespaceDBConnection",
|
"AsyncLanceNamespaceDBConnection",
|
||||||
"AsyncTable",
|
"AsyncTable",
|
||||||
"FtsToken",
|
"FtsToken",
|
||||||
@@ -570,6 +588,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,15 +148,25 @@ 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 drop_function(self, name: str, version: str) -> bool: ...
|
||||||
|
async def create_secret(self, name: str, value: str) -> None: ...
|
||||||
|
async def alter_secret(self, name: str, value: str) -> None: ...
|
||||||
|
async def list_secrets(self) -> List[str]: ...
|
||||||
|
async def drop_secret(self, name: str) -> None: ...
|
||||||
|
async def describe_secret(self, name: str) -> Dict[str, str]: ...
|
||||||
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,
|
||||||
@@ -234,9 +245,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
|
||||||
@@ -268,10 +290,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: ...
|
||||||
@@ -450,6 +495,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]],
|
||||||
@@ -606,6 +652,7 @@ class FullTextQuery:
|
|||||||
class PyQueryRequest:
|
class PyQueryRequest:
|
||||||
limit: Optional[int]
|
limit: Optional[int]
|
||||||
offset: Optional[int]
|
offset: Optional[int]
|
||||||
|
take_offsets: Optional[List[int]]
|
||||||
filter: Optional[Union[str, bytes]]
|
filter: Optional[Union[str, bytes]]
|
||||||
full_text_search: Optional[FullTextQuery]
|
full_text_search: Optional[FullTextQuery]
|
||||||
select: Optional[Union[str, List[str]]]
|
select: Optional[Union[str, List[str]]]
|
||||||
|
|||||||
+289
-83
@@ -16,10 +16,11 @@ from typing import (
|
|||||||
Iterable,
|
Iterable,
|
||||||
List,
|
List,
|
||||||
Literal,
|
Literal,
|
||||||
Mapping,
|
|
||||||
Optional,
|
Optional,
|
||||||
|
Sequence,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
if sys.version_info >= (3, 12):
|
if sys.version_info >= (3, 12):
|
||||||
from typing import override
|
from typing import override
|
||||||
@@ -48,12 +49,16 @@ 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,
|
||||||
SelectArg,
|
SelectArg,
|
||||||
normalize_select,
|
normalize_select,
|
||||||
)
|
)
|
||||||
|
from .secrets import EnvVarSecret, SecretInfo, validate_secret_name
|
||||||
from .table import (
|
from .table import (
|
||||||
AsyncTable,
|
AsyncTable,
|
||||||
LanceTable,
|
LanceTable,
|
||||||
@@ -69,10 +74,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
|
||||||
@@ -692,16 +698,34 @@ class DBConnection(EnforceOverrides):
|
|||||||
self,
|
self,
|
||||||
definition: UdfDefinition,
|
definition: UdfDefinition,
|
||||||
*,
|
*,
|
||||||
secrets: Optional[Mapping[str, str]] = None,
|
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||||
) -> FunctionVersion:
|
) -> FunctionVersion:
|
||||||
"""Register a scalar Python UDF and wait for its immutable version.
|
"""Register a scalar Python UDF and wait for its immutable version.
|
||||||
|
|
||||||
``secrets`` must contain exactly the names declared by
|
|
||||||
``@udf(secrets=[...])``. Values are sent in the create request and
|
|
||||||
stored server-side in the private execution artifact; returned
|
|
||||||
Function and Job metadata contain only the declared names.
|
|
||||||
This is the blocking counterpart of :meth:`create_function_async`.
|
This is the blocking counterpart of :meth:`create_function_async`.
|
||||||
Local connections raise ``NotImplementedError``.
|
Local connections raise ``NotImplementedError``.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
definition : UdfDefinition
|
||||||
|
A callable decorated with [udf][lancedb.udf].
|
||||||
|
secrets : sequence of EnvVarSecret, optional
|
||||||
|
One [EnvVarSecret][lancedb.secrets.EnvVarSecret] per credential the
|
||||||
|
Function needs, each naming a Secret and the environment variable
|
||||||
|
its value arrives in. The Function's source is unchanged by this;
|
||||||
|
it reads the variable the way it already did.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
```python
|
||||||
|
db.create_secret("openai-prod", os.environ["OPENAI_API_KEY"])
|
||||||
|
db.create_function(
|
||||||
|
analyze_caption,
|
||||||
|
secrets=[
|
||||||
|
EnvVarSecret(secret="openai-prod", env_variable="OPENAI_API_KEY")
|
||||||
|
],
|
||||||
|
)
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
return self.create_function_async(definition, secrets=secrets).wait()
|
return self.create_function_async(definition, secrets=secrets).wait()
|
||||||
|
|
||||||
@@ -709,14 +733,10 @@ class DBConnection(EnforceOverrides):
|
|||||||
self,
|
self,
|
||||||
definition: UdfDefinition,
|
definition: UdfDefinition,
|
||||||
*,
|
*,
|
||||||
secrets: Optional[Mapping[str, str]] = None,
|
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||||
) -> Job[FunctionVersion]:
|
) -> Job[FunctionVersion]:
|
||||||
"""Register a scalar Python UDF through the remote Function catalog.
|
"""Register a scalar Python UDF through the remote Function catalog.
|
||||||
|
|
||||||
``secrets`` must contain exactly the names declared by
|
|
||||||
``@udf(secrets=[...])``. Values are sent in the create request and
|
|
||||||
stored server-side in the private execution artifact; returned
|
|
||||||
Function and Job metadata contain only the declared names.
|
|
||||||
Submission returns a typed job. The immutable Function version becomes
|
Submission returns a typed job. The immutable Function version becomes
|
||||||
available only when :meth:`Job.wait` succeeds. Local connections raise
|
available only when :meth:`Job.wait` succeeds. Local connections raise
|
||||||
``NotImplementedError``.
|
``NotImplementedError``.
|
||||||
@@ -731,26 +751,107 @@ 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 list_functions(self) -> List[FunctionVersion]:
|
||||||
"""A [Job][lancedb.job.Job] handle for a server-side job by id.
|
"""List every published immutable Function version.
|
||||||
|
|
||||||
The handle is constructed without a server round trip; an unknown id
|
Results are ordered by Function name then version. Local connections
|
||||||
surfaces when the handle is used. Dropping the handle has no effect
|
raise ``NotImplementedError``.
|
||||||
on the job itself.
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
List the identities available to use in Function-backed columns:
|
||||||
|
|
||||||
|
```python
|
||||||
|
[(function.name, function.version) for function in db.list_functions()]
|
||||||
|
```
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError("job is not supported for this connection type")
|
raise NotImplementedError(
|
||||||
|
"Function catalog operations are not supported for this connection type"
|
||||||
|
)
|
||||||
|
|
||||||
|
def drop_function(self, name: str, *, version: str) -> bool:
|
||||||
|
"""Drop one exact immutable Function version from the remote catalog.
|
||||||
|
|
||||||
|
Returns True when the version changed to Dropped and False for an
|
||||||
|
idempotent replay. Local connections raise NotImplementedError.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Function catalog operations are not supported for this connection type"
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_secret(self, name: str, value: str) -> None:
|
||||||
|
"""Create a named Secret in this database.
|
||||||
|
|
||||||
|
Fails if the name is taken, so a create never silently becomes a
|
||||||
|
rotation. Nothing reads the value back: it is bound to a Function by
|
||||||
|
name and resolved by the service when that Function runs. Local
|
||||||
|
connections raise ``NotImplementedError``.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Secret operations are not supported for this connection type"
|
||||||
|
)
|
||||||
|
|
||||||
|
def alter_secret(self, name: str, value: str) -> None:
|
||||||
|
"""Replace the credential behind an existing Secret.
|
||||||
|
|
||||||
|
Fails if it does not exist. Every Function bound to the Secret uses the
|
||||||
|
new value from its next job, and no new Function version is created --
|
||||||
|
which is how a rotation reaches columns pinned to a version registered
|
||||||
|
before it. Local connections raise ``NotImplementedError``.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Secret operations are not supported for this connection type"
|
||||||
|
)
|
||||||
|
|
||||||
|
def list_secrets(self) -> List[str]:
|
||||||
|
"""The names of every Secret in this database.
|
||||||
|
|
||||||
|
Names only. No method returns a stored credential, by construction
|
||||||
|
rather than by policy. Local connections raise ``NotImplementedError``.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Secret operations are not supported for this connection type"
|
||||||
|
)
|
||||||
|
|
||||||
|
def drop_secret(self, name: str) -> None:
|
||||||
|
"""Drop a Secret.
|
||||||
|
|
||||||
|
Functions bound to it fail at their next job, naming the Secret; that
|
||||||
|
is the revocation path. The name becomes free to reuse, and a new
|
||||||
|
Secret under it is picked up by everything still bound to that name.
|
||||||
|
Local connections raise ``NotImplementedError``.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Secret operations are not supported for this connection type"
|
||||||
|
)
|
||||||
|
|
||||||
|
def describe_secret(self, name: str) -> SecretInfo:
|
||||||
|
"""What this database records about a Secret: name and timestamps.
|
||||||
|
|
||||||
|
Never the value -- there is no code path that could return one. Local
|
||||||
|
connections raise ``NotImplementedError``.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError(
|
||||||
|
"Secret operations are not supported for this connection type"
|
||||||
|
)
|
||||||
|
|
||||||
|
def open_job(self, job_id: str) -> Job:
|
||||||
|
"""Open a server-side job by id, returning a handle with its record
|
||||||
|
already populated.
|
||||||
|
|
||||||
|
The returned [Job][lancedb.job.Job] answers for its own state,
|
||||||
|
specification, result, failure and event history, so there is no
|
||||||
|
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("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.
|
||||||
|
|
||||||
@@ -762,14 +863,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):
|
||||||
@@ -866,6 +991,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,
|
||||||
@@ -1414,21 +1540,18 @@ 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(
|
def create_function_async(
|
||||||
self,
|
self,
|
||||||
definition: UdfDefinition,
|
definition: UdfDefinition,
|
||||||
*,
|
*,
|
||||||
secrets: Optional[Mapping[str, str]] = None,
|
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||||
) -> Job[FunctionVersion]:
|
) -> Job[FunctionVersion]:
|
||||||
job = LOOP.run(self._conn.create_function_async(definition, secrets=secrets))
|
job = LOOP.run(self._conn.create_function_async(definition, secrets=secrets))
|
||||||
return Job(job)
|
return Job(job)
|
||||||
@@ -1437,19 +1560,39 @@ class LanceDBConnection(DBConnection):
|
|||||||
def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
||||||
return LOOP.run(self._conn.get_function(name, version=version))
|
return LOOP.run(self._conn.get_function(name, version=version))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def list_functions(self) -> List[FunctionVersion]:
|
||||||
|
return LOOP.run(self._conn.list_functions())
|
||||||
|
|
||||||
|
@override
|
||||||
|
def drop_function(self, name: str, *, version: str) -> bool:
|
||||||
|
return LOOP.run(self._conn.drop_function(name, version=version))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def create_secret(self, name: str, value: str) -> None:
|
||||||
|
LOOP.run(self._conn.create_secret(name, value))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def alter_secret(self, name: str, value: str) -> None:
|
||||||
|
LOOP.run(self._conn.alter_secret(name, value))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def list_secrets(self) -> List[str]:
|
||||||
|
return LOOP.run(self._conn.list_secrets())
|
||||||
|
|
||||||
|
@override
|
||||||
|
def drop_secret(self, name: str) -> None:
|
||||||
|
LOOP.run(self._conn.drop_secret(name))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def describe_secret(self, name: str) -> SecretInfo:
|
||||||
|
return LOOP.run(self._conn.describe_secret(name))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
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."""
|
||||||
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.
|
||||||
@@ -1460,14 +1603,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.
|
||||||
@@ -2238,53 +2373,90 @@ 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,
|
self,
|
||||||
definition: UdfDefinition,
|
definition: UdfDefinition,
|
||||||
*,
|
*,
|
||||||
secrets: Optional[Mapping[str, str]] = None,
|
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||||
) -> AsyncJob[FunctionVersion]:
|
) -> AsyncJob[FunctionVersion]:
|
||||||
"""Register a scalar Python UDF through the remote Function catalog.
|
"""Register a scalar Python UDF through the remote Function catalog.
|
||||||
|
|
||||||
``secrets`` must contain exactly the names declared by
|
|
||||||
``@udf(secrets=[...])``. Values are sent in the create request and
|
|
||||||
stored server-side in the private execution artifact; returned
|
|
||||||
Function and Job metadata contain only the declared names.
|
|
||||||
The returned typed job resolves to the immutable Function version.
|
The returned typed job resolves to the immutable Function version.
|
||||||
Local connections raise ``NotImplementedError``.
|
``secrets`` is a sequence of
|
||||||
|
[EnvVarSecret][lancedb.secrets.EnvVarSecret], each naming a Secret and
|
||||||
|
the environment variable its value arrives in. Local connections raise
|
||||||
|
``NotImplementedError``.
|
||||||
"""
|
"""
|
||||||
if not isinstance(definition, UdfDefinition):
|
if not isinstance(definition, UdfDefinition):
|
||||||
raise TypeError("create_function_async requires a @udf definition")
|
raise TypeError("create_function_async requires a @udf definition")
|
||||||
inner = await self._inner.create_function_async(
|
request = definition.bind_secrets(secrets)
|
||||||
definition._submission_json(secrets)
|
inner = await self._inner.create_function_async(request.to_canonical_json())
|
||||||
)
|
|
||||||
return _typed_job(inner, FunctionVersion.from_json)
|
return _typed_job(inner, FunctionVersion.from_json)
|
||||||
|
|
||||||
async def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
async def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
||||||
"""Open one exact immutable Function version from the remote catalog."""
|
"""Open one exact immutable Function version from the remote catalog."""
|
||||||
return FunctionVersion.from_json(await self._inner.get_function(name, version))
|
return FunctionVersion.from_json(await self._inner.get_function(name, version))
|
||||||
|
|
||||||
|
async def list_functions(self) -> List[FunctionVersion]:
|
||||||
|
"""List every published immutable Function version.
|
||||||
|
|
||||||
|
Results are ordered by Function name then version. Local connections
|
||||||
|
raise ``NotImplementedError``.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
FunctionVersion.from_json(value)
|
||||||
|
for value in await self._inner.list_functions()
|
||||||
|
]
|
||||||
|
|
||||||
|
async def drop_function(self, name: str, *, version: str) -> bool:
|
||||||
|
"""Drop one exact immutable Function version from the remote catalog."""
|
||||||
|
return await self._inner.drop_function(name, version)
|
||||||
|
|
||||||
|
async def create_secret(self, name: str, value: str) -> None:
|
||||||
|
"""Create a named Secret in this database.
|
||||||
|
|
||||||
|
Fails if the name is taken, so a create never silently becomes a
|
||||||
|
rotation. Nothing reads the value back.
|
||||||
|
"""
|
||||||
|
await self._inner.create_secret(validate_secret_name(name), value)
|
||||||
|
|
||||||
|
async def alter_secret(self, name: str, value: str) -> None:
|
||||||
|
"""Replace the credential behind an existing Secret.
|
||||||
|
|
||||||
|
Fails if it does not exist. Bound Functions use the new value from
|
||||||
|
their next job, with no new Function version.
|
||||||
|
"""
|
||||||
|
await self._inner.alter_secret(validate_secret_name(name), value)
|
||||||
|
|
||||||
|
async def list_secrets(self) -> List[str]:
|
||||||
|
"""The names of every Secret in this database. Names only."""
|
||||||
|
return await self._inner.list_secrets()
|
||||||
|
|
||||||
|
async def drop_secret(self, name: str) -> None:
|
||||||
|
"""Drop a Secret. Bound Functions fail at their next job."""
|
||||||
|
await self._inner.drop_secret(validate_secret_name(name))
|
||||||
|
|
||||||
|
async def describe_secret(self, name: str) -> SecretInfo:
|
||||||
|
"""What this database records about a Secret. Never the value."""
|
||||||
|
name, created_at_millis, updated_at_millis = await self._inner.describe_secret(
|
||||||
|
validate_secret_name(name)
|
||||||
|
)
|
||||||
|
return SecretInfo(
|
||||||
|
name=name,
|
||||||
|
created_at_millis=created_at_millis,
|
||||||
|
updated_at_millis=updated_at_millis,
|
||||||
|
)
|
||||||
|
|
||||||
async def list_jobs(self) -> List[JobInfo]:
|
async def list_jobs(self) -> List[JobInfo]:
|
||||||
"""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.
|
||||||
|
|
||||||
@@ -2294,12 +2466,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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+427
-129
@@ -25,7 +25,7 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
import types
|
import types
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping, Sequence
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
from typing import (
|
from typing import (
|
||||||
Annotated,
|
Annotated,
|
||||||
@@ -49,11 +49,26 @@ from pydantic import (
|
|||||||
model_validator,
|
model_validator,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .schema import is_blob_v2_field as _is_blob_v2_field
|
||||||
|
from .secrets import EnvVarSecret
|
||||||
|
|
||||||
_Int32 = conint(strict=True, ge=-(2**31), le=2**31 - 1)
|
_Int32 = conint(strict=True, ge=-(2**31), le=2**31 - 1)
|
||||||
_UInt32 = conint(strict=True, ge=0, le=2**32 - 1)
|
_UInt32 = conint(strict=True, ge=0, le=2**32 - 1)
|
||||||
_UInt64 = conint(strict=True, ge=0, le=2**64 - 1)
|
_UInt64 = conint(strict=True, ge=0, le=2**64 - 1)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_gpu_wire_marker(value: Any) -> bool:
|
||||||
|
if value is not True:
|
||||||
|
raise ValueError("runtime.gpu must be true")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_gpu_marker(value: bool) -> Optional[bool]:
|
||||||
|
if not isinstance(value, bool):
|
||||||
|
raise ValueError("gpu must be a boolean")
|
||||||
|
return True if value else None
|
||||||
|
|
||||||
|
|
||||||
class _FrozenDict(dict):
|
class _FrozenDict(dict):
|
||||||
def _immutable(self, *args, **kwargs):
|
def _immutable(self, *args, **kwargs):
|
||||||
raise TypeError("remote canonical values are immutable")
|
raise TypeError("remote canonical values are immutable")
|
||||||
@@ -212,6 +227,20 @@ class FunctionOutput(_OpenRemoteValue):
|
|||||||
fields: tuple[FunctionResultField, ...] = ()
|
fields: tuple[FunctionResultField, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
class SecretBinding(_RemoteValue):
|
||||||
|
"""How a Secret reaches the Function that binds it.
|
||||||
|
|
||||||
|
One list rather than a field per delivery mode: a binding is the concept,
|
||||||
|
and how it arrives is a property of one. ``kind`` is open, so a binding a
|
||||||
|
newer service introduces decodes here instead of failing the whole
|
||||||
|
FunctionVersion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
kind: str
|
||||||
|
variable: Optional[str] = None
|
||||||
|
secret_ref: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class FunctionSignature(_RemoteValue):
|
class FunctionSignature(_RemoteValue):
|
||||||
inputs: tuple[FunctionParameter, ...]
|
inputs: tuple[FunctionParameter, ...]
|
||||||
output: FunctionOutput
|
output: FunctionOutput
|
||||||
@@ -229,7 +258,7 @@ class PythonEnvironmentSpec(_RemoteValue):
|
|||||||
|
|
||||||
|
|
||||||
class PythonRuntimeSpec(_RemoteValue):
|
class PythonRuntimeSpec(_RemoteValue):
|
||||||
"""Remote runtime definition with non-secret environment values.
|
"""Remote runtime definition with environment values.
|
||||||
|
|
||||||
V1 supports ``kind="python"``. Newer runtime kinds remain readable, while
|
V1 supports ``kind="python"``. Newer runtime kinds remain readable, while
|
||||||
their unknown payload fields are intentionally not retained by the client.
|
their unknown payload fields are intentionally not retained by the client.
|
||||||
@@ -239,6 +268,23 @@ class PythonRuntimeSpec(_RemoteValue):
|
|||||||
python_version: Optional[str] = None
|
python_version: Optional[str] = None
|
||||||
environment: Optional[PythonEnvironmentSpec] = None
|
environment: Optional[PythonEnvironmentSpec] = None
|
||||||
env: Optional[Mapping[str, str]] = None
|
env: Optional[Mapping[str, str]] = None
|
||||||
|
gpu: Optional[bool] = None
|
||||||
|
|
||||||
|
@model_validator(mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _discard_unknown_runtime_payload(cls, value):
|
||||||
|
if isinstance(value, Mapping):
|
||||||
|
kind = value.get("kind")
|
||||||
|
if isinstance(kind, str) and kind not in {"python", "python_v2"}:
|
||||||
|
return {"kind": kind}
|
||||||
|
return value
|
||||||
|
|
||||||
|
@field_validator("gpu", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def _validate_gpu_marker(cls, value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
return _validate_gpu_wire_marker(value)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def _validate_runtime_kind(self):
|
def _validate_runtime_kind(self):
|
||||||
@@ -247,18 +293,28 @@ class PythonRuntimeSpec(_RemoteValue):
|
|||||||
raise ValueError("python runtime requires python_version")
|
raise ValueError("python runtime requires python_version")
|
||||||
if self.environment is None:
|
if self.environment is None:
|
||||||
raise ValueError("python runtime requires environment")
|
raise ValueError("python runtime requires environment")
|
||||||
|
if self.gpu is not None:
|
||||||
|
raise ValueError("python runtime with gpu requires kind='python_v2'")
|
||||||
|
elif self.kind == "python_v2":
|
||||||
|
if self.python_version is None:
|
||||||
|
raise ValueError("python_v2 runtime requires python_version")
|
||||||
|
if self.environment is None:
|
||||||
|
raise ValueError("python_v2 runtime requires environment")
|
||||||
|
if self.gpu is None:
|
||||||
|
raise ValueError("python_v2 runtime requires gpu")
|
||||||
else:
|
else:
|
||||||
object.__setattr__(self, "python_version", None)
|
object.__setattr__(self, "python_version", None)
|
||||||
object.__setattr__(self, "environment", None)
|
object.__setattr__(self, "environment", None)
|
||||||
object.__setattr__(self, "env", None)
|
object.__setattr__(self, "env", None)
|
||||||
|
object.__setattr__(self, "gpu", None)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
class FunctionVersion(_RemoteValue):
|
class FunctionVersion(_RemoteValue):
|
||||||
"""An exact immutable Function version returned by Enterprise.
|
"""An exact immutable Function version returned by Enterprise.
|
||||||
|
|
||||||
Scheduling resources, priority, concurrency, and retry policy belong to
|
The GPU execution requirement is part of this identity. CPU and memory sizing,
|
||||||
the submitting Job and are not part of this identity.
|
priority, concurrency, and retry policy belong to the execution platform.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
@@ -268,7 +324,7 @@ class FunctionVersion(_RemoteValue):
|
|||||||
runtime: PythonRuntimeSpec
|
runtime: PythonRuntimeSpec
|
||||||
runtime_digest: str
|
runtime_digest: str
|
||||||
environment_digest: str
|
environment_digest: str
|
||||||
required_secrets: tuple[str, ...] = ()
|
secret_bindings: tuple[SecretBinding, ...] = ()
|
||||||
created_at: str
|
created_at: str
|
||||||
|
|
||||||
def __call__(self, **inputs: Any) -> FunctionApplication:
|
def __call__(self, **inputs: Any) -> FunctionApplication:
|
||||||
@@ -332,15 +388,16 @@ class FunctionVersion(_RemoteValue):
|
|||||||
class FunctionRegistrationRequest(_RemoteValue):
|
class FunctionRegistrationRequest(_RemoteValue):
|
||||||
"""Stable remote registration envelope produced by :func:`udf`.
|
"""Stable remote registration envelope produced by :func:`udf`.
|
||||||
|
|
||||||
Only secret names are represented. Secret values are supplied separately
|
Credential values deliberately have no field here. The only secret-shaped
|
||||||
when the definition is submitted and are not part of this durable value.
|
thing a client sends is ``secret_bindings``: the name of a Secret the
|
||||||
|
database already holds, which the remote service resolves at execution.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
artifact: FunctionArtifactRequest
|
artifact: FunctionArtifactRequest
|
||||||
signature: FunctionSignature
|
signature: FunctionSignature
|
||||||
runtime: PythonRuntimeSpec
|
runtime: PythonRuntimeSpec
|
||||||
required_secrets: tuple[str, ...] = ()
|
secret_bindings: tuple[SecretBinding, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
class FunctionVersionRef(_OpenRemoteValue):
|
class FunctionVersionRef(_OpenRemoteValue):
|
||||||
@@ -435,11 +492,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
|
||||||
@@ -449,6 +502,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."""
|
||||||
|
|
||||||
@@ -456,6 +516,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
|
||||||
|
|
||||||
@@ -485,27 +546,15 @@ class RefreshColumnResult(_RemoteValue):
|
|||||||
|
|
||||||
|
|
||||||
_FUNCTION_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$")
|
_FUNCTION_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_.-]*$")
|
||||||
_SECRET_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
_DECLARED_SECRET = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||||
# Keep this byte limit aligned with Sophon's MAX_FUNCTION_SECRET_VALUE_BYTES.
|
|
||||||
_MAX_FUNCTION_SECRET_VALUE_BYTES = 64 * 1024
|
|
||||||
_MAX_FUNCTION_SECRET_VALUES_BYTES = 512 * 1024
|
|
||||||
|
|
||||||
|
_FUNCTION_BLOB_V2_TYPE = "blob_v2"
|
||||||
def _validate_secret_value(name: str, value: Any) -> str:
|
_ARROW_EXTENSION_NAME_KEY = "ARROW:extension:name"
|
||||||
"""Validate one secret value before building the create request."""
|
_BLOB_V2_EXTENSION_NAME = "lance.blob.v2"
|
||||||
if not isinstance(value, str):
|
_NESTED_BLOB_COLLECTION_ERROR = (
|
||||||
raise TypeError(f"Function secret {name!r} value must be a string")
|
"unsupported Arrow type for Function signature: Blob v2 fields nested under "
|
||||||
if not value:
|
"collection types are not supported"
|
||||||
raise ValueError(f"Function secret {name!r} value must be non-empty")
|
)
|
||||||
if "\0" in value:
|
|
||||||
raise ValueError(f"Function secret {name!r} value must not contain NUL")
|
|
||||||
value_bytes = len(value.encode("utf-8"))
|
|
||||||
if value_bytes > _MAX_FUNCTION_SECRET_VALUE_BYTES:
|
|
||||||
raise ValueError(
|
|
||||||
f"Function secret {name!r} value exceeds the "
|
|
||||||
f"{_MAX_FUNCTION_SECRET_VALUE_BYTES}-byte limit"
|
|
||||||
)
|
|
||||||
return value
|
|
||||||
|
|
||||||
|
|
||||||
_GRAMMAR_PRIMITIVES = (
|
_GRAMMAR_PRIMITIVES = (
|
||||||
@@ -522,6 +571,7 @@ _GRAMMAR_PRIMITIVES = (
|
|||||||
(pa.float32(), "float32"),
|
(pa.float32(), "float32"),
|
||||||
(pa.float64(), "float64"),
|
(pa.float64(), "float64"),
|
||||||
(pa.string(), "utf8"),
|
(pa.string(), "utf8"),
|
||||||
|
(pa.large_string(), "large_utf8"),
|
||||||
(pa.binary(), "binary"),
|
(pa.binary(), "binary"),
|
||||||
(pa.date32(), "date32"),
|
(pa.date32(), "date32"),
|
||||||
(pa.date64(), "date64"),
|
(pa.date64(), "date64"),
|
||||||
@@ -529,31 +579,258 @@ _GRAMMAR_PRIMITIVES = (
|
|||||||
|
|
||||||
|
|
||||||
def _canonical_arrow_type(data_type: pa.DataType) -> str:
|
def _canonical_arrow_type(data_type: pa.DataType) -> str:
|
||||||
"""The server's V1 Function type grammar. Anything outside it is rejected
|
"""The compact Function grammar, or canonical exact JSON for nested types."""
|
||||||
here rather than at registration."""
|
grammar = _grammar_arrow_type(data_type)
|
||||||
|
if grammar is not None:
|
||||||
|
return grammar
|
||||||
|
exact = _exact_arrow_type(data_type)
|
||||||
|
return json.dumps(exact, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
def _grammar_arrow_type(data_type: pa.DataType) -> Optional[str]:
|
||||||
for candidate, name in _GRAMMAR_PRIMITIVES:
|
for candidate, name in _GRAMMAR_PRIMITIVES:
|
||||||
if data_type == candidate:
|
if data_type == candidate:
|
||||||
return name
|
return name
|
||||||
if pa.types.is_list(data_type) or pa.types.is_large_list(data_type):
|
if pa.types.is_list(data_type) or pa.types.is_large_list(data_type):
|
||||||
|
item = _grammar_list_item(data_type)
|
||||||
|
if item is None:
|
||||||
|
return None
|
||||||
prefix = "list" if pa.types.is_list(data_type) else "large_list"
|
prefix = "list" if pa.types.is_list(data_type) else "large_list"
|
||||||
return f"{prefix}<{_canonical_list_item(data_type)}>"
|
return f"{prefix}<{item}>"
|
||||||
if pa.types.is_fixed_size_list(data_type) and data_type.list_size > 0:
|
if pa.types.is_fixed_size_list(data_type) and data_type.list_size > 0:
|
||||||
return (
|
item = _grammar_list_item(data_type)
|
||||||
f"fixed_size_list<{_canonical_list_item(data_type)}, {data_type.list_size}>"
|
if item is not None:
|
||||||
)
|
return f"fixed_size_list<{item}, {data_type.list_size}>"
|
||||||
raise TypeError(f"unsupported Arrow type for Function signature: {data_type}")
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _canonical_list_item(data_type: pa.DataType) -> str:
|
def _grammar_list_item(data_type: pa.DataType) -> Optional[str]:
|
||||||
"""The grammar names only the item type; it always means a non-nullable
|
"""The grammar names only the item type; it always means a non-nullable
|
||||||
child called `item`, so any other child metadata cannot be represented."""
|
child called `item`, so other child properties require exact JSON."""
|
||||||
child = data_type.value_field
|
child = data_type.value_field
|
||||||
if child.name != "item" or child.nullable or child.metadata:
|
if child.name != "item" or child.nullable or child.metadata:
|
||||||
|
return None
|
||||||
|
return _grammar_arrow_type(child.type)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_exact_arrow_field(field: pa.Field) -> None:
|
||||||
|
if not field.name:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"unsupported Arrow type for Function signature: list items must be a "
|
"unsupported Arrow type for Function signature: field names "
|
||||||
f"non-nullable field named 'item', got {child}"
|
"must not be empty"
|
||||||
)
|
)
|
||||||
return _canonical_arrow_type(child.type)
|
if _is_blob_v2_field(field):
|
||||||
|
if not _has_supported_blob_v2_layout(field):
|
||||||
|
raise TypeError(
|
||||||
|
"unsupported Arrow type for Function signature: lance.blob.v2 "
|
||||||
|
f"requires a supported Blob storage layout, got {field}"
|
||||||
|
)
|
||||||
|
metadata = {
|
||||||
|
(key.decode() if isinstance(key, bytes) else key): (
|
||||||
|
value.decode() if isinstance(value, bytes) else value
|
||||||
|
)
|
||||||
|
for key, value in (field.metadata or {}).items()
|
||||||
|
}
|
||||||
|
if metadata and metadata != {
|
||||||
|
_ARROW_EXTENSION_NAME_KEY: _BLOB_V2_EXTENSION_NAME
|
||||||
|
}:
|
||||||
|
raise TypeError(
|
||||||
|
"unsupported Arrow type for Function signature: lance.blob.v2 "
|
||||||
|
"field metadata must contain only its canonical extension marker"
|
||||||
|
)
|
||||||
|
elif field.metadata:
|
||||||
|
raise TypeError(
|
||||||
|
"unsupported Arrow type for Function signature: field metadata "
|
||||||
|
f"is not supported, got {field}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _has_supported_blob_v2_layout(field: pa.Field) -> bool:
|
||||||
|
data_type = field.type
|
||||||
|
if isinstance(data_type, pa.ExtensionType):
|
||||||
|
data_type = data_type.storage_type
|
||||||
|
if not pa.types.is_struct(data_type):
|
||||||
|
return False
|
||||||
|
|
||||||
|
fields = tuple(data_type)
|
||||||
|
|
||||||
|
def matches(spec, compare_nullable) -> bool:
|
||||||
|
return len(fields) == len(spec) and all(
|
||||||
|
actual.name == name
|
||||||
|
and actual.type == expected_type
|
||||||
|
and (not check_nullable or actual.nullable == nullable)
|
||||||
|
for actual, (name, expected_type, nullable), check_nullable in zip(
|
||||||
|
fields, spec, compare_nullable
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logical_minimal = (
|
||||||
|
("data", pa.large_binary(), True),
|
||||||
|
("uri", pa.utf8(), True),
|
||||||
|
)
|
||||||
|
logical_full = logical_minimal + (
|
||||||
|
("position", pa.uint64(), True),
|
||||||
|
("size", pa.uint64(), True),
|
||||||
|
)
|
||||||
|
prepared = (
|
||||||
|
("kind", pa.uint8(), True),
|
||||||
|
("data", pa.large_binary(), True),
|
||||||
|
("uri", pa.utf8(), True),
|
||||||
|
("blob_id", pa.uint32(), True),
|
||||||
|
("blob_size", pa.uint64(), True),
|
||||||
|
("position", pa.uint64(), True),
|
||||||
|
)
|
||||||
|
descriptor = (
|
||||||
|
("kind", pa.uint8(), False),
|
||||||
|
("position", pa.uint64(), False),
|
||||||
|
("size", pa.uint64(), False),
|
||||||
|
("blob_id", pa.uint32(), False),
|
||||||
|
("blob_uri", pa.utf8(), False),
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
matches(logical_minimal, (True, True))
|
||||||
|
or matches(logical_full, (True, True, False, False))
|
||||||
|
or matches(prepared, (True,) * len(prepared))
|
||||||
|
or matches(descriptor, (False,) * len(descriptor))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _canonical_arrow_field(field: pa.Field) -> str:
|
||||||
|
_validate_exact_arrow_field(field)
|
||||||
|
if _is_blob_v2_field(field):
|
||||||
|
return _FUNCTION_BLOB_V2_TYPE
|
||||||
|
return _canonical_arrow_type(field.type)
|
||||||
|
|
||||||
|
|
||||||
|
def _blob_storage_type(field: pa.Field) -> pa.DataType:
|
||||||
|
data_type = field.type
|
||||||
|
if isinstance(data_type, pa.ExtensionType):
|
||||||
|
return data_type.storage_type
|
||||||
|
return data_type
|
||||||
|
|
||||||
|
|
||||||
|
def _exact_blob_storage_type(field: pa.Field) -> dict[str, Any]:
|
||||||
|
storage = _blob_storage_type(field)
|
||||||
|
if not pa.types.is_struct(storage):
|
||||||
|
raise TypeError(
|
||||||
|
"unsupported Arrow type for Function signature: lance.blob.v2 "
|
||||||
|
"requires struct storage"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"type": "struct",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": child.name,
|
||||||
|
"nullable": child.nullable,
|
||||||
|
"type": (
|
||||||
|
{"type": "large_binary"}
|
||||||
|
if pa.types.is_large_binary(child.type)
|
||||||
|
else _exact_arrow_type(child.type)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for child in storage
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _data_type_has_blob_v2(data_type: pa.DataType) -> bool:
|
||||||
|
if pa.types.is_struct(data_type):
|
||||||
|
return any(
|
||||||
|
_is_blob_v2_field(field) or _data_type_has_blob_v2(field.type)
|
||||||
|
for field in data_type
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
pa.types.is_list(data_type)
|
||||||
|
or pa.types.is_large_list(data_type)
|
||||||
|
or pa.types.is_fixed_size_list(data_type)
|
||||||
|
):
|
||||||
|
field = data_type.value_field
|
||||||
|
return _is_blob_v2_field(field) or _data_type_has_blob_v2(field.type)
|
||||||
|
if pa.types.is_map(data_type):
|
||||||
|
return any(
|
||||||
|
_is_blob_v2_field(field) or _data_type_has_blob_v2(field.type)
|
||||||
|
for field in (data_type.key_field, data_type.item_field)
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _exact_arrow_field(
|
||||||
|
field: pa.Field, *, inside_collection: bool = False
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
_validate_exact_arrow_field(field)
|
||||||
|
if _is_blob_v2_field(field):
|
||||||
|
if inside_collection:
|
||||||
|
raise TypeError(_NESTED_BLOB_COLLECTION_ERROR)
|
||||||
|
return {
|
||||||
|
"name": field.name,
|
||||||
|
"nullable": field.nullable,
|
||||||
|
"type": _exact_blob_storage_type(field),
|
||||||
|
"metadata": {
|
||||||
|
_ARROW_EXTENSION_NAME_KEY: _BLOB_V2_EXTENSION_NAME,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
value = {
|
||||||
|
"name": field.name,
|
||||||
|
"nullable": field.nullable,
|
||||||
|
"type": _exact_arrow_type(field.type, inside_collection=inside_collection),
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _exact_arrow_type(
|
||||||
|
data_type: pa.DataType, *, inside_collection: bool = False
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
for candidate, name in _GRAMMAR_PRIMITIVES:
|
||||||
|
if data_type == candidate:
|
||||||
|
return {"type": name}
|
||||||
|
if pa.types.is_struct(data_type):
|
||||||
|
fields = list(data_type)
|
||||||
|
names = [field.name for field in fields]
|
||||||
|
if not fields or len(set(names)) != len(names):
|
||||||
|
raise TypeError(
|
||||||
|
"unsupported Arrow type for Function signature: structs must have "
|
||||||
|
"non-empty, uniquely named fields"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"type": "struct",
|
||||||
|
"fields": [
|
||||||
|
_exact_arrow_field(field, inside_collection=inside_collection)
|
||||||
|
for field in fields
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
pa.types.is_list(data_type)
|
||||||
|
or pa.types.is_large_list(data_type)
|
||||||
|
or pa.types.is_fixed_size_list(data_type)
|
||||||
|
):
|
||||||
|
if pa.types.is_fixed_size_list(data_type):
|
||||||
|
if data_type.value_field.name != "item":
|
||||||
|
raise TypeError(
|
||||||
|
"unsupported Arrow type for Function signature: fixed-size list "
|
||||||
|
"items must be named 'item'"
|
||||||
|
)
|
||||||
|
if data_type.list_size <= 0:
|
||||||
|
raise TypeError(
|
||||||
|
f"unsupported Arrow type for Function signature: {data_type}"
|
||||||
|
)
|
||||||
|
value: dict[str, Any] = {
|
||||||
|
"type": (
|
||||||
|
"list"
|
||||||
|
if pa.types.is_list(data_type)
|
||||||
|
else "large_list"
|
||||||
|
if pa.types.is_large_list(data_type)
|
||||||
|
else "fixed_size_list"
|
||||||
|
),
|
||||||
|
"fields": [
|
||||||
|
_exact_arrow_field(data_type.value_field, inside_collection=True)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
if pa.types.is_fixed_size_list(data_type):
|
||||||
|
value["length"] = data_type.list_size
|
||||||
|
return value
|
||||||
|
if pa.types.is_map(data_type) and _data_type_has_blob_v2(data_type):
|
||||||
|
raise TypeError(_NESTED_BLOB_COLLECTION_ERROR)
|
||||||
|
raise TypeError(f"unsupported Arrow type for Function signature: {data_type}")
|
||||||
|
|
||||||
|
|
||||||
def _list_of(item: pa.DataType) -> pa.DataType:
|
def _list_of(item: pa.DataType) -> pa.DataType:
|
||||||
@@ -627,8 +904,15 @@ def _callable_parameters(function: Callable[..., Any]) -> tuple[inspect.Paramete
|
|||||||
|
|
||||||
def _function_output(output: pa.DataType | pa.Field | pa.Schema) -> FunctionOutput:
|
def _function_output(output: pa.DataType | pa.Field | pa.Schema) -> FunctionOutput:
|
||||||
if isinstance(output, pa.Schema):
|
if isinstance(output, pa.Schema):
|
||||||
|
if output.metadata:
|
||||||
|
raise TypeError("Function output schema metadata is not supported")
|
||||||
fields = tuple(output)
|
fields = tuple(output)
|
||||||
elif isinstance(output, pa.Field) and pa.types.is_struct(output.type):
|
elif (
|
||||||
|
isinstance(output, pa.Field)
|
||||||
|
and not _is_blob_v2_field(output)
|
||||||
|
and pa.types.is_struct(output.type)
|
||||||
|
):
|
||||||
|
_validate_exact_arrow_field(output)
|
||||||
if output.nullable:
|
if output.nullable:
|
||||||
raise ValueError("Function output must be non-nullable")
|
raise ValueError("Function output must be non-nullable")
|
||||||
fields = tuple(output.type)
|
fields = tuple(output.type)
|
||||||
@@ -644,18 +928,19 @@ def _function_output(output: pa.DataType | pa.Field | pa.Schema) -> FunctionOutp
|
|||||||
raise TypeError(
|
raise TypeError(
|
||||||
"output_schema must be a PyArrow DataType, Field, or Schema"
|
"output_schema must be a PyArrow DataType, Field, or Schema"
|
||||||
)
|
)
|
||||||
|
_validate_exact_arrow_field(field)
|
||||||
if field.nullable:
|
if field.nullable:
|
||||||
raise ValueError("Function output must be non-nullable")
|
raise ValueError("Function output must be non-nullable")
|
||||||
return FunctionOutput(
|
return FunctionOutput(
|
||||||
kind="scalar",
|
kind="scalar",
|
||||||
arrow_type=_canonical_arrow_type(field.type),
|
arrow_type=_canonical_arrow_field(field),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
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):
|
for field in fields:
|
||||||
raise ValueError("Function output fields must be non-nullable")
|
_validate_exact_arrow_field(field)
|
||||||
names = [field.name for field in fields]
|
names = [field.name for field in fields]
|
||||||
if len(set(names)) != len(names):
|
if len(set(names)) != len(names):
|
||||||
raise ValueError("Function output field names must be unique")
|
raise ValueError("Function output field names must be unique")
|
||||||
@@ -664,8 +949,8 @@ def _function_output(output: pa.DataType | pa.Field | pa.Schema) -> FunctionOutp
|
|||||||
fields=tuple(
|
fields=tuple(
|
||||||
FunctionResultField(
|
FunctionResultField(
|
||||||
name=field.name,
|
name=field.name,
|
||||||
arrow_type=_canonical_arrow_type(field.type),
|
arrow_type=_canonical_arrow_field(field),
|
||||||
nullable=False,
|
nullable=field.nullable,
|
||||||
)
|
)
|
||||||
for field in fields
|
for field in fields
|
||||||
),
|
),
|
||||||
@@ -684,6 +969,10 @@ def _infer_signature(
|
|||||||
if input_schema is not None:
|
if input_schema is not None:
|
||||||
if not isinstance(input_schema, pa.Schema):
|
if not isinstance(input_schema, pa.Schema):
|
||||||
raise TypeError("input_schema must be a PyArrow Schema")
|
raise TypeError("input_schema must be a PyArrow Schema")
|
||||||
|
if input_schema.metadata:
|
||||||
|
raise TypeError("Function input schema metadata is not supported")
|
||||||
|
for field in input_schema:
|
||||||
|
_validate_exact_arrow_field(field)
|
||||||
expected = tuple(parameter.name for parameter in parameters)
|
expected = tuple(parameter.name for parameter in parameters)
|
||||||
actual = tuple(input_schema.names)
|
actual = tuple(input_schema.names)
|
||||||
if actual != expected:
|
if actual != expected:
|
||||||
@@ -694,7 +983,7 @@ def _infer_signature(
|
|||||||
inputs = tuple(
|
inputs = tuple(
|
||||||
FunctionParameter(
|
FunctionParameter(
|
||||||
name=field.name,
|
name=field.name,
|
||||||
arrow_type=_canonical_arrow_type(field.type),
|
arrow_type=_canonical_arrow_field(field),
|
||||||
nullable=field.nullable,
|
nullable=field.nullable,
|
||||||
)
|
)
|
||||||
for field in input_schema
|
for field in input_schema
|
||||||
@@ -717,7 +1006,9 @@ def _infer_signature(
|
|||||||
inputs.append(
|
inputs.append(
|
||||||
FunctionParameter(
|
FunctionParameter(
|
||||||
name=parameter.name,
|
name=parameter.name,
|
||||||
arrow_type=_canonical_arrow_type(data_type),
|
arrow_type=_canonical_arrow_field(
|
||||||
|
pa.field(parameter.name, data_type, nullable=nullable)
|
||||||
|
),
|
||||||
nullable=nullable,
|
nullable=nullable,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -936,8 +1227,8 @@ class UdfDefinition:
|
|||||||
output_schema: Optional[pa.DataType | pa.Field | pa.Schema],
|
output_schema: Optional[pa.DataType | pa.Field | pa.Schema],
|
||||||
pip: tuple[str, ...],
|
pip: tuple[str, ...],
|
||||||
env: Mapping[str, str],
|
env: Mapping[str, str],
|
||||||
secrets: tuple[str, ...],
|
|
||||||
python_version: Optional[str],
|
python_version: Optional[str],
|
||||||
|
gpu: bool = False,
|
||||||
conda: tuple[str, ...] = (),
|
conda: tuple[str, ...] = (),
|
||||||
conda_channels: tuple[str, ...] = (),
|
conda_channels: tuple[str, ...] = (),
|
||||||
):
|
):
|
||||||
@@ -963,26 +1254,17 @@ class UdfDefinition:
|
|||||||
for key, value in environment.items()
|
for key, value in environment.items()
|
||||||
):
|
):
|
||||||
raise TypeError("Function env keys and values must be strings")
|
raise TypeError("Function env keys and values must be strings")
|
||||||
required_secrets = tuple(sorted(set(secrets)))
|
|
||||||
invalid_secrets = [
|
|
||||||
secret for secret in required_secrets if not _SECRET_NAME.fullmatch(secret)
|
|
||||||
]
|
|
||||||
if invalid_secrets:
|
|
||||||
raise ValueError(f"invalid Function secret names: {invalid_secrets!r}")
|
|
||||||
overlap = set(environment) & set(required_secrets)
|
|
||||||
if overlap:
|
|
||||||
raise ValueError(
|
|
||||||
f"Function env and secret names must be disjoint: {sorted(overlap)!r}"
|
|
||||||
)
|
|
||||||
signature = _infer_signature(function, input_schema, output_schema)
|
signature = _infer_signature(function, input_schema, output_schema)
|
||||||
source = _package_source(function)
|
source = _package_source(function)
|
||||||
digest = f"sha256:{hashlib.sha256(source).hexdigest()}"
|
digest = f"sha256:{hashlib.sha256(source).hexdigest()}"
|
||||||
|
gpu_marker = _normalize_gpu_marker(gpu)
|
||||||
runtime = PythonRuntimeSpec(
|
runtime = PythonRuntimeSpec(
|
||||||
kind="python",
|
kind="python_v2" if gpu_marker is not None else "python",
|
||||||
python_version=python_version
|
python_version=python_version
|
||||||
or f"{sys.version_info.major}.{sys.version_info.minor}",
|
or f"{sys.version_info.major}.{sys.version_info.minor}",
|
||||||
environment=environment_spec,
|
environment=environment_spec,
|
||||||
env=environment,
|
env=environment,
|
||||||
|
gpu=gpu_marker,
|
||||||
)
|
)
|
||||||
self._function = function
|
self._function = function
|
||||||
self._request = FunctionRegistrationRequest(
|
self._request = FunctionRegistrationRequest(
|
||||||
@@ -1002,64 +1284,74 @@ class UdfDefinition:
|
|||||||
),
|
),
|
||||||
signature=signature,
|
signature=signature,
|
||||||
runtime=runtime,
|
runtime=runtime,
|
||||||
required_secrets=required_secrets,
|
|
||||||
)
|
)
|
||||||
functools.update_wrapper(self, function)
|
functools.update_wrapper(self, function)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def registration_request(self) -> FunctionRegistrationRequest:
|
def registration_request(self) -> FunctionRegistrationRequest:
|
||||||
"""The immutable, value-free client model for a Function submission."""
|
"""The immutable request sent by ``create_function_async``.
|
||||||
|
|
||||||
|
Carries no secret bindings. Binding is a registration-time decision,
|
||||||
|
so a Function bound to Secrets is registered through :meth:`bind_secrets`,
|
||||||
|
which is what ``create_function`` calls.
|
||||||
|
"""
|
||||||
return self._request
|
return self._request
|
||||||
|
|
||||||
def _submission_json(self, secrets: Optional[Mapping[str, str]]) -> str:
|
def bind_secrets(
|
||||||
"""Build one registration submission without retaining values on self."""
|
self, secrets: Optional[Sequence[EnvVarSecret]]
|
||||||
if secrets is None:
|
) -> FunctionRegistrationRequest:
|
||||||
secret_values: Mapping[str, str] = {}
|
"""The registration request for this definition bound to ``secrets``.
|
||||||
elif not isinstance(secrets, Mapping):
|
|
||||||
raise TypeError("Function secrets must be a mapping of names to strings")
|
|
||||||
else:
|
|
||||||
secret_values = secrets
|
|
||||||
|
|
||||||
if any(not isinstance(name, str) for name in secret_values):
|
Binding does not change the Function's source: each
|
||||||
raise TypeError("Function secret names must be strings")
|
[EnvVarSecret][lancedb.secrets.EnvVarSecret] names a Secret and the
|
||||||
expected = set(self._request.required_secrets)
|
environment variable its value should arrive in, and the Function reads
|
||||||
provided = set(secret_values)
|
that variable the way it already did. Whether the named Secrets exist is
|
||||||
if provided != expected:
|
the server's answer, not this one.
|
||||||
missing = sorted(expected - provided)
|
"""
|
||||||
unexpected = sorted(provided - expected)
|
bindings = () if secrets is None else tuple(secrets)
|
||||||
details = []
|
wrong_type = [
|
||||||
if missing:
|
binding for binding in bindings if not isinstance(binding, EnvVarSecret)
|
||||||
details.append(f"missing: {missing!r}")
|
]
|
||||||
if unexpected:
|
if wrong_type:
|
||||||
details.append(f"unexpected: {unexpected!r}")
|
kinds = sorted({type(binding).__name__ for binding in wrong_type})
|
||||||
|
raise TypeError(
|
||||||
|
f"Function secrets must be EnvVarSecret values, not {kinds!r}; a "
|
||||||
|
"credential value is never sent to this API"
|
||||||
|
)
|
||||||
|
variables = [binding.env_variable for binding in bindings]
|
||||||
|
duplicates = sorted({name for name in variables if variables.count(name) > 1})
|
||||||
|
if duplicates:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Function secret values must exactly match the declared secrets ("
|
"a Function binds each environment variable once; duplicated: "
|
||||||
+ "; ".join(details)
|
f"{duplicates!r}"
|
||||||
+ ")"
|
)
|
||||||
|
# `env` is ordinary configuration carried in the definition, so a name in
|
||||||
|
# both would have a value visible in the Function's record and a value
|
||||||
|
# that is not. Refuse rather than pick.
|
||||||
|
environment = self._request.runtime.env or {}
|
||||||
|
overlap = sorted(set(environment) & set(variables))
|
||||||
|
if overlap:
|
||||||
|
raise ValueError(
|
||||||
|
f"Function env and secret bindings must be disjoint: {overlap!r}"
|
||||||
|
)
|
||||||
|
if not bindings:
|
||||||
|
return self._request
|
||||||
|
# Sorted, because the list is carried in the FunctionVersion hash and a
|
||||||
|
# caller's argument order is not part of what a Function is.
|
||||||
|
resolved = tuple(
|
||||||
|
sorted(
|
||||||
|
(
|
||||||
|
SecretBinding(
|
||||||
|
kind="env",
|
||||||
|
variable=binding.env_variable,
|
||||||
|
secret_ref=binding.secret,
|
||||||
|
)
|
||||||
|
for binding in bindings
|
||||||
|
),
|
||||||
|
key=lambda binding: (binding.kind, binding.variable or ""),
|
||||||
)
|
)
|
||||||
|
|
||||||
canonical_values = {}
|
|
||||||
total_bytes = 0
|
|
||||||
for name in sorted(secret_values):
|
|
||||||
value = _validate_secret_value(name, secret_values[name])
|
|
||||||
total_bytes += len(value.encode("utf-8"))
|
|
||||||
if total_bytes > _MAX_FUNCTION_SECRET_VALUES_BYTES:
|
|
||||||
raise ValueError(
|
|
||||||
"Function secret values exceed the "
|
|
||||||
f"{_MAX_FUNCTION_SECRET_VALUES_BYTES}-byte request limit"
|
|
||||||
)
|
|
||||||
canonical_values[name] = value
|
|
||||||
|
|
||||||
submission = self._request._known_dict()
|
|
||||||
if canonical_values:
|
|
||||||
submission["secret_values"] = canonical_values
|
|
||||||
return json.dumps(
|
|
||||||
submission,
|
|
||||||
ensure_ascii=False,
|
|
||||||
allow_nan=False,
|
|
||||||
sort_keys=True,
|
|
||||||
separators=(",", ":"),
|
|
||||||
)
|
)
|
||||||
|
return self._request._copy(update={"secret_bindings": resolved})
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
return self._function(*args, **kwargs)
|
return self._function(*args, **kwargs)
|
||||||
@@ -1078,8 +1370,8 @@ def udf(
|
|||||||
output_schema: Optional[pa.DataType | pa.Field | pa.Schema] = None,
|
output_schema: Optional[pa.DataType | pa.Field | pa.Schema] = None,
|
||||||
pip: tuple[str, ...] | list[str] = (),
|
pip: tuple[str, ...] | list[str] = (),
|
||||||
env: Optional[Mapping[str, str]] = None,
|
env: Optional[Mapping[str, str]] = None,
|
||||||
secrets: tuple[str, ...] | list[str] = (),
|
|
||||||
python_version: Optional[str] = None,
|
python_version: Optional[str] = None,
|
||||||
|
gpu: bool = False,
|
||||||
conda: tuple[str, ...] | list[str] = (),
|
conda: tuple[str, ...] | list[str] = (),
|
||||||
conda_channels: tuple[str, ...] | list[str] = (),
|
conda_channels: tuple[str, ...] | list[str] = (),
|
||||||
) -> Callable[[Callable[..., Any]], UdfDefinition]: ...
|
) -> Callable[[Callable[..., Any]], UdfDefinition]: ...
|
||||||
@@ -1093,8 +1385,8 @@ def udf(
|
|||||||
output_schema: Optional[pa.DataType | pa.Field | pa.Schema] = None,
|
output_schema: Optional[pa.DataType | pa.Field | pa.Schema] = None,
|
||||||
pip: tuple[str, ...] | list[str] = (),
|
pip: tuple[str, ...] | list[str] = (),
|
||||||
env: Optional[Mapping[str, str]] = None,
|
env: Optional[Mapping[str, str]] = None,
|
||||||
secrets: tuple[str, ...] | list[str] = (),
|
|
||||||
python_version: Optional[str] = None,
|
python_version: Optional[str] = None,
|
||||||
|
gpu: bool = False,
|
||||||
conda: tuple[str, ...] | list[str] = (),
|
conda: tuple[str, ...] | list[str] = (),
|
||||||
conda_channels: tuple[str, ...] | list[str] = (),
|
conda_channels: tuple[str, ...] | list[str] = (),
|
||||||
):
|
):
|
||||||
@@ -1102,8 +1394,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
|
||||||
----------
|
----------
|
||||||
@@ -1115,8 +1408,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
|
||||||
@@ -1124,12 +1417,15 @@ def udf(
|
|||||||
conda_channels : sequence of str, optional
|
conda_channels : sequence of str, optional
|
||||||
Conda channels in priority order; requires ``conda``.
|
Conda channels in priority order; requires ``conda``.
|
||||||
env : mapping of str to str, optional
|
env : mapping of str to str, optional
|
||||||
Non-secret environment variables. Use ``secrets`` for credentials.
|
Environment variables included in the Function definition. Not for
|
||||||
secrets : sequence of str, optional
|
credentials -- these are ordinary configuration, stored with the
|
||||||
Names of secrets required by the callable. Supply their values separately
|
Function and visible wherever it is.
|
||||||
to ``create_function`` or ``create_function_async``.
|
|
||||||
python_version : str, optional
|
python_version : str, optional
|
||||||
Remote Python major/minor version. Defaults to the client version.
|
Remote Python major/minor version. Defaults to the client version.
|
||||||
|
gpu : bool, default False
|
||||||
|
Whether every remote execution requires a GPU. The execution platform
|
||||||
|
selects one compatible GPU for each worker. The requirement is part of
|
||||||
|
the immutable Function version.
|
||||||
|
|
||||||
The packaged artifact is a snapshot: the function source plus exactly
|
The packaged artifact is a snapshot: the function source plus exactly
|
||||||
the module-level names it references (modules as imports, importable
|
the module-level names it references (modules as imports, importable
|
||||||
@@ -1149,15 +1445,16 @@ def udf(
|
|||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
>>> from lancedb import udf
|
>>> from lancedb import udf
|
||||||
>>> @udf(pip=["numpy==2.2.0"], secrets=["MODEL_TOKEN"])
|
>>> @udf(pip=["numpy==2.2.0"])
|
||||||
... def score(value: float) -> float:
|
... def score(value: float) -> float:
|
||||||
... return value * 2
|
... return value * 2
|
||||||
>>> score(1.5)
|
>>> score(1.5)
|
||||||
3.0
|
3.0
|
||||||
>>> db.create_function( # doctest: +SKIP
|
>>> @udf(pip=["cupy-cuda12x"], gpu=True)
|
||||||
... score, secrets={"MODEL_TOKEN": "user-secret-value"}
|
... def gpu_score(value: int) -> int:
|
||||||
... )
|
... return value * 2
|
||||||
|
>>> gpu_score.registration_request.runtime.gpu
|
||||||
|
True
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorate(target: Callable[..., Any]) -> UdfDefinition:
|
def decorate(target: Callable[..., Any]) -> UdfDefinition:
|
||||||
@@ -1168,8 +1465,8 @@ def udf(
|
|||||||
output_schema=output_schema,
|
output_schema=output_schema,
|
||||||
pip=tuple(pip),
|
pip=tuple(pip),
|
||||||
env={} if env is None else env,
|
env={} if env is None else env,
|
||||||
secrets=tuple(secrets),
|
|
||||||
python_version=python_version,
|
python_version=python_version,
|
||||||
|
gpu=gpu,
|
||||||
conda=tuple(conda),
|
conda=tuple(conda),
|
||||||
conda_channels=tuple(conda_channels),
|
conda_channels=tuple(conda_channels),
|
||||||
)
|
)
|
||||||
@@ -1180,6 +1477,7 @@ def udf(
|
|||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"AssignmentMapping",
|
||||||
"ApplicationInput",
|
"ApplicationInput",
|
||||||
"FunctionApplication",
|
"FunctionApplication",
|
||||||
"FunctionArtifact",
|
"FunctionArtifact",
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ class MaterializedViewDefinition:
|
|||||||
"""Cap on the number of rows the view holds."""
|
"""Cap on the number of rows the view holds."""
|
||||||
inputs: List[str] = field(default_factory=list)
|
inputs: List[str] = field(default_factory=list)
|
||||||
"""Source columns the projections and filter read."""
|
"""Source columns the projections and filter read."""
|
||||||
|
source_namespace: List[str] = field(default_factory=list)
|
||||||
|
"""Namespace holding the source table; empty is the root namespace."""
|
||||||
|
|
||||||
|
|
||||||
def _definition_from_schema(
|
def _definition_from_schema(
|
||||||
@@ -53,7 +55,8 @@ def _definition_from_schema(
|
|||||||
raise ValueError(f"Table '{name}' is not a materialized view")
|
raise ValueError(f"Table '{name}' is not a materialized view")
|
||||||
value = json.loads(raw)
|
value = json.loads(raw)
|
||||||
kind = value.get("kind")
|
kind = value.get("kind")
|
||||||
if kind != "select":
|
# "namespaced_select" keeps older readers from resolving the source at root.
|
||||||
|
if kind not in ("select", "namespaced_select"):
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"materialized view '{name}' is defined by '{kind}', which this "
|
f"materialized view '{name}' is defined by '{kind}', which this "
|
||||||
"version of lancedb cannot refresh"
|
"version of lancedb cannot refresh"
|
||||||
@@ -66,6 +69,7 @@ def _definition_from_schema(
|
|||||||
filter=value.get("filter"),
|
filter=value.get("filter"),
|
||||||
limit=value.get("limit"),
|
limit=value.get("limit"),
|
||||||
inputs=value.get("inputs", []),
|
inputs=value.get("inputs", []),
|
||||||
|
source_namespace=value.get("source_namespace", []),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,7 @@ def _query_is_plain_scan(query: Query) -> bool:
|
|||||||
return (
|
return (
|
||||||
query.vector is None
|
query.vector is None
|
||||||
and query.full_text_query is None
|
and query.full_text_query is None
|
||||||
|
and query.take_offsets is None
|
||||||
and not query.postfilter
|
and not query.postfilter
|
||||||
and not query.order_by
|
and not query.order_by
|
||||||
)
|
)
|
||||||
@@ -804,6 +805,10 @@ class Query(pydantic.BaseModel):
|
|||||||
# offset to start fetching results from
|
# offset to start fetching results from
|
||||||
offset: Optional[int] = None
|
offset: Optional[int] = None
|
||||||
|
|
||||||
|
# Dataset offsets whose duplicate occurrences must be restored after lookup.
|
||||||
|
# This is populated when a take query is converted to this serializable form.
|
||||||
|
take_offsets: Optional[List[int]] = None
|
||||||
|
|
||||||
# if true, will only search the indexed data
|
# if true, will only search the indexed data
|
||||||
fast_search: Optional[bool] = None
|
fast_search: Optional[bool] = None
|
||||||
|
|
||||||
@@ -825,6 +830,7 @@ class Query(pydantic.BaseModel):
|
|||||||
query = cls()
|
query = cls()
|
||||||
query.limit = req.limit
|
query.limit = req.limit
|
||||||
query.offset = req.offset
|
query.offset = req.offset
|
||||||
|
query.take_offsets = req.take_offsets
|
||||||
query.filter = req.filter
|
query.filter = req.filter
|
||||||
query.full_text_query = req.full_text_search
|
query.full_text_query = req.full_text_search
|
||||||
query.columns = req.select
|
query.columns = req.select
|
||||||
|
|||||||
@@ -7,8 +7,18 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Mapping, Optional, Union
|
from typing import (
|
||||||
|
TYPE_CHECKING,
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
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 +35,13 @@ 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
|
||||||
|
from ..secrets import EnvVarSecret, SecretInfo
|
||||||
|
|
||||||
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 +129,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 +175,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 +190,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 +209,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,43 +749,59 @@ 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(
|
def create_function_async(
|
||||||
self,
|
self,
|
||||||
definition: UdfDefinition,
|
definition: UdfDefinition,
|
||||||
*,
|
*,
|
||||||
secrets: Optional[Mapping[str, str]] = None,
|
secrets: Optional[Sequence[EnvVarSecret]] = None,
|
||||||
) -> Job[FunctionVersion]:
|
) -> Job[FunctionVersion]:
|
||||||
return Job(
|
job = LOOP.run(self._conn.create_function_async(definition, secrets=secrets))
|
||||||
LOOP.run(self._conn.create_function_async(definition, secrets=secrets))
|
return Job(job)
|
||||||
)
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
def get_function(self, name: str, *, version: str) -> FunctionVersion:
|
||||||
return LOOP.run(self._conn.get_function(name, version=version))
|
return LOOP.run(self._conn.get_function(name, version=version))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def list_functions(self) -> List[FunctionVersion]:
|
||||||
|
return LOOP.run(self._conn.list_functions())
|
||||||
|
|
||||||
|
@override
|
||||||
|
def drop_function(self, name: str, *, version: str) -> bool:
|
||||||
|
return LOOP.run(self._conn.drop_function(name, version=version))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def create_secret(self, name: str, value: str) -> None:
|
||||||
|
LOOP.run(self._conn.create_secret(name, value))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def alter_secret(self, name: str, value: str) -> None:
|
||||||
|
LOOP.run(self._conn.alter_secret(name, value))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def describe_secret(self, name: str) -> SecretInfo:
|
||||||
|
return LOOP.run(self._conn.describe_secret(name))
|
||||||
|
|
||||||
|
@override
|
||||||
|
def list_secrets(self) -> List[str]:
|
||||||
|
return LOOP.run(self._conn.list_secrets())
|
||||||
|
|
||||||
|
@override
|
||||||
|
def drop_secret(self, name: str) -> None:
|
||||||
|
LOOP.run(self._conn.drop_secret(name))
|
||||||
|
|
||||||
@override
|
@override
|
||||||
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."""
|
||||||
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.
|
||||||
@@ -780,12 +813,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",
|
||||||
|
}
|
||||||
|
|||||||
@@ -67,7 +67,15 @@ from ..query import (
|
|||||||
LanceTakeQueryBuilder,
|
LanceTakeQueryBuilder,
|
||||||
LanceVectorQueryBuilder,
|
LanceVectorQueryBuilder,
|
||||||
)
|
)
|
||||||
from ..table import AsyncTable, BlobMode, Branches, IndexStatistics, Query, Table, Tags
|
from ..table import (
|
||||||
|
AsyncTable,
|
||||||
|
BlobMode,
|
||||||
|
Branches,
|
||||||
|
IndexStatistics,
|
||||||
|
Query,
|
||||||
|
Table,
|
||||||
|
Tags,
|
||||||
|
)
|
||||||
from ..types import BaseTokenizerType
|
from ..types import BaseTokenizerType
|
||||||
|
|
||||||
|
|
||||||
@@ -540,6 +548,7 @@ class RemoteTable(Table):
|
|||||||
LOOP.run(
|
LOOP.run(
|
||||||
self._table.create_index(
|
self._table.create_index(
|
||||||
column,
|
column,
|
||||||
|
replace=replace,
|
||||||
config=config,
|
config=config,
|
||||||
wait_timeout=wait_timeout,
|
wait_timeout=wait_timeout,
|
||||||
name=name,
|
name=name,
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||||
|
|
||||||
|
"""Named Secrets, and the bindings that deliver them to Functions.
|
||||||
|
|
||||||
|
A Secret is a database-scoped named credential. Nothing in this module holds a
|
||||||
|
value: :class:`EnvVarSecret` names one and says which environment variable it
|
||||||
|
should arrive in, and the value is resolved by the remote service when a
|
||||||
|
Function bound to it runs. No API returns a stored credential, by construction
|
||||||
|
rather than by policy -- there is no code path that could.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
# The same characters LanceDB already admits in a namespace or table name, and
|
||||||
|
# no positional rule on top of them: a segment may begin with `_`, `-` or `.`
|
||||||
|
# today, so anything narrower would put Secrets out of reach inside namespaces
|
||||||
|
# that already exist. Matches the service, which admits the same set.
|
||||||
|
_SECRET_NAME = re.compile(r"^[A-Za-z0-9_.-]{1,255}$")
|
||||||
|
_ENV_VARIABLE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_secret_name(name: str) -> str:
|
||||||
|
"""Check a Secret name locally and return it unchanged."""
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise TypeError(f"Secret name must be a string, not {type(name).__name__}")
|
||||||
|
if not _SECRET_NAME.fullmatch(name):
|
||||||
|
raise ValueError(f"invalid Secret name: {name!r}")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def validate_env_variable(name: str) -> str:
|
||||||
|
"""Check an environment variable name locally and return it unchanged."""
|
||||||
|
if not isinstance(name, str):
|
||||||
|
raise TypeError(
|
||||||
|
f"environment variable name must be a string, not {type(name).__name__}"
|
||||||
|
)
|
||||||
|
if not _ENV_VARIABLE.fullmatch(name):
|
||||||
|
raise ValueError(f"invalid environment variable name: {name!r}")
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
class EnvVarSecret:
|
||||||
|
"""A Secret bound to the environment variable a Function's library reads.
|
||||||
|
|
||||||
|
Pass these in the ``secrets`` sequence of
|
||||||
|
[DBConnection.create_function][lancedb.db.DBConnection.create_function]. The
|
||||||
|
Function's source is unchanged by binding: it reads ``OPENAI_API_KEY`` the
|
||||||
|
way it always did, and the binding is what puts a value there.
|
||||||
|
|
||||||
|
This is a local value. Constructing it contacts no server, so it always
|
||||||
|
succeeds and says nothing about whether the Secret exists; that is checked
|
||||||
|
at registration, where a mistyped Secret name surfaces as a clear "does not
|
||||||
|
exist" naming both the Secret and the variable bound to it. A mistyped
|
||||||
|
*variable* name cannot be caught anywhere -- nothing knows which variables a
|
||||||
|
Function reads -- so it surfaces on the first rows instead.
|
||||||
|
|
||||||
|
The type exists so a credential cannot be passed by accident. A bare string
|
||||||
|
in the same position is a plausible-looking mistake with the opposite
|
||||||
|
meaning, and it reads identically in a diff.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
secret : str
|
||||||
|
The Secret's database-scoped name.
|
||||||
|
env_variable : str
|
||||||
|
The environment variable the Function reads it from.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
>>> from lancedb import EnvVarSecret
|
||||||
|
>>> binding = EnvVarSecret(secret="openai-prod", env_variable="OPENAI_API_KEY")
|
||||||
|
>>> binding.secret, binding.env_variable
|
||||||
|
('openai-prod', 'OPENAI_API_KEY')
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_secret", "_env_variable")
|
||||||
|
|
||||||
|
def __init__(self, secret: str, env_variable: str):
|
||||||
|
self._secret = validate_secret_name(secret)
|
||||||
|
self._env_variable = validate_env_variable(env_variable)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def secret(self) -> str:
|
||||||
|
"""The Secret's database-scoped name."""
|
||||||
|
return self._secret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def env_variable(self) -> str:
|
||||||
|
"""The environment variable the value is delivered in."""
|
||||||
|
return self._env_variable
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"EnvVarSecret(secret={self._secret!r}, "
|
||||||
|
f"env_variable={self._env_variable!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
return (
|
||||||
|
isinstance(other, EnvVarSecret)
|
||||||
|
and other._secret == self._secret
|
||||||
|
and other._env_variable == self._env_variable
|
||||||
|
)
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return hash((EnvVarSecret, self._secret, self._env_variable))
|
||||||
|
|
||||||
|
|
||||||
|
class SecretInfo:
|
||||||
|
"""What a database records about a Secret. Never its value.
|
||||||
|
|
||||||
|
Returned by
|
||||||
|
[DBConnection.describe_secret][lancedb.db.DBConnection.describe_secret].
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ("_name", "_created_at_millis", "_updated_at_millis")
|
||||||
|
|
||||||
|
def __init__(self, name: str, created_at_millis: int, updated_at_millis: int):
|
||||||
|
self._name = name
|
||||||
|
self._created_at_millis = created_at_millis
|
||||||
|
self._updated_at_millis = updated_at_millis
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""The Secret's database-scoped name."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def created_at_millis(self) -> int:
|
||||||
|
"""When the Secret was created, in milliseconds since the Unix epoch."""
|
||||||
|
return self._created_at_millis
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updated_at_millis(self) -> int:
|
||||||
|
"""When the Secret's value was last rotated, in epoch milliseconds.
|
||||||
|
|
||||||
|
The only observable that a rotation landed: no API returns a credential,
|
||||||
|
so a caller confirms ``alter_secret`` took effect by watching this move.
|
||||||
|
"""
|
||||||
|
return self._updated_at_millis
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_json(cls, value: dict) -> "SecretInfo":
|
||||||
|
return cls(
|
||||||
|
name=value["name"],
|
||||||
|
created_at_millis=value["created_at_millis"],
|
||||||
|
updated_at_millis=value["updated_at_millis"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return (
|
||||||
|
f"SecretInfo(name={self._name!r}, "
|
||||||
|
f"created_at_millis={self._created_at_millis!r}, "
|
||||||
|
f"updated_at_millis={self._updated_at_millis!r})"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __eq__(self, other: object) -> bool:
|
||||||
|
return (
|
||||||
|
isinstance(other, SecretInfo)
|
||||||
|
and other._name == self._name
|
||||||
|
and other._created_at_millis == self._created_at_millis
|
||||||
|
and other._updated_at_millis == self._updated_at_millis
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"EnvVarSecret",
|
||||||
|
"SecretInfo",
|
||||||
|
"validate_env_variable",
|
||||||
|
"validate_secret_name",
|
||||||
|
]
|
||||||
@@ -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"]
|
||||||
@@ -1547,7 +1547,9 @@ class Table(ABC):
|
|||||||
on: Union[str, Iterable[str]]
|
on: Union[str, Iterable[str]]
|
||||||
A column (or columns) to join on. This is how records from the
|
A column (or columns) to join on. This is how records from the
|
||||||
source table and target table are matched. Typically this is some
|
source table and target table are matched. Typically this is some
|
||||||
kind of key or id column.
|
kind of key or id column. Passing several columns matches on the
|
||||||
|
composite key: a source row updates a target row only when it
|
||||||
|
agrees on every one of them.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
@@ -1678,9 +1680,9 @@ class Table(ABC):
|
|||||||
Offsets are mostly useful for sampling as the set of all valid offsets is easily
|
Offsets are mostly useful for sampling as the set of all valid offsets is easily
|
||||||
known in advance to be [0, len(table)).
|
known in advance to be [0, len(table)).
|
||||||
|
|
||||||
No guarantees are made regarding the order in which results are returned. If
|
No guarantees are made regarding the order in which results are returned.
|
||||||
you desire an output order that matches the order of the given offsets, you will
|
Repeated offsets produce repeated rows, which makes this method suitable for
|
||||||
need to add the row offset column to the output and align it yourself.
|
sampling with replacement.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -1791,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`.
|
||||||
"""
|
"""
|
||||||
@@ -1808,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.
|
||||||
"""
|
"""
|
||||||
@@ -1823,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
|
||||||
@@ -2165,9 +2176,11 @@ class Table(ABC):
|
|||||||
Function columns are supported only on LanceDB Cloud and
|
Function columns are supported only on LanceDB Cloud and
|
||||||
Enterprise.
|
Enterprise.
|
||||||
computed: Dict[str, str], optional
|
computed: Dict[str, str], optional
|
||||||
A map of column name to a SQL expression defining the column. The
|
A mapping from output column names to SQL expressions derives each
|
||||||
column's type and inputs are derived from the expression, so no
|
output field from its expression. A direct projection of a Blob v2
|
||||||
data type is supplied.
|
field inherits Blob v2 semantics; other expressions derive their
|
||||||
|
ordinary Arrow type. Mapping order is declaration and dependency
|
||||||
|
order.
|
||||||
|
|
||||||
Unlike ``transforms``, the expression is stored rather than
|
Unlike ``transforms``, the expression is stored rather than
|
||||||
evaluated now: the column is committed with no values, and rows get
|
evaluated now: the column is committed with no values, and rows get
|
||||||
@@ -4088,6 +4101,7 @@ class LanceTable(Table):
|
|||||||
)
|
)
|
||||||
and not self._route_pushdown_to_rust
|
and not self._route_pushdown_to_rust
|
||||||
and self.current_branch() is None
|
and self.current_branch() is None
|
||||||
|
and query.take_offsets is None
|
||||||
):
|
):
|
||||||
from lancedb.namespace import _execute_server_side_query
|
from lancedb.namespace import _execute_server_side_query
|
||||||
|
|
||||||
@@ -5698,7 +5712,9 @@ class AsyncTable:
|
|||||||
on: Union[str, Iterable[str]]
|
on: Union[str, Iterable[str]]
|
||||||
A column (or columns) to join on. This is how records from the
|
A column (or columns) to join on. This is how records from the
|
||||||
source table and target table are matched. Typically this is some
|
source table and target table are matched. Typically this is some
|
||||||
kind of key or id column.
|
kind of key or id column. Passing several columns matches on the
|
||||||
|
composite key: a source row updates a target row only when it
|
||||||
|
agrees on every one of them.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
@@ -5981,7 +5997,23 @@ class AsyncTable:
|
|||||||
|
|
||||||
def _sync_query_to_async(
|
def _sync_query_to_async(
|
||||||
self, query: Query
|
self, query: Query
|
||||||
) -> AsyncHybridQuery | AsyncFTSQuery | AsyncVectorQuery | AsyncQuery:
|
) -> (
|
||||||
|
AsyncHybridQuery
|
||||||
|
| AsyncFTSQuery
|
||||||
|
| AsyncVectorQuery
|
||||||
|
| AsyncQuery
|
||||||
|
| AsyncTakeQuery
|
||||||
|
):
|
||||||
|
if query.take_offsets is not None:
|
||||||
|
take_query = self.take_offsets(query.take_offsets)
|
||||||
|
if query.columns:
|
||||||
|
take_query = take_query.select(query.columns)
|
||||||
|
if query.use_lsm is not None:
|
||||||
|
take_query = take_query.use_lsm(query.use_lsm)
|
||||||
|
if query.with_row_id:
|
||||||
|
take_query = take_query.with_row_id()
|
||||||
|
return take_query
|
||||||
|
|
||||||
async_query = self.query()
|
async_query = self.query()
|
||||||
if query.limit is not None:
|
if query.limit is not None:
|
||||||
async_query = async_query.limit(query.limit)
|
async_query = async_query.limit(query.limit)
|
||||||
@@ -6046,6 +6078,7 @@ class AsyncTable:
|
|||||||
self._namespace_client, self._pushdown_operations
|
self._namespace_client, self._pushdown_operations
|
||||||
)
|
)
|
||||||
and not self._route_pushdown_to_rust
|
and not self._route_pushdown_to_rust
|
||||||
|
and query.take_offsets is None
|
||||||
):
|
):
|
||||||
from lancedb.namespace import _execute_server_side_query
|
from lancedb.namespace import _execute_server_side_query
|
||||||
|
|
||||||
@@ -6268,8 +6301,11 @@ class AsyncTable:
|
|||||||
Function columns are supported only on LanceDB Cloud and
|
Function columns are supported only on LanceDB Cloud and
|
||||||
Enterprise.
|
Enterprise.
|
||||||
computed: Dict[str, str], optional
|
computed: Dict[str, str], optional
|
||||||
A map of column name to a SQL expression defining the column. The
|
A mapping from output column names to SQL expressions derives each
|
||||||
column's type and inputs are derived from the expression.
|
output field from its expression. A direct projection of a Blob v2
|
||||||
|
field inherits Blob v2 semantics; other expressions derive their
|
||||||
|
ordinary Arrow type. Mapping order is declaration and dependency
|
||||||
|
order.
|
||||||
|
|
||||||
Unlike ``transforms``, the expression is stored rather than
|
Unlike ``transforms``, the expression is stored rather than
|
||||||
evaluated now: the column is committed with no values, and rows get
|
evaluated now: the column is committed with no values, and rows get
|
||||||
@@ -6540,6 +6576,9 @@ class AsyncTable:
|
|||||||
Offsets are mostly useful for sampling as the set of all valid offsets is easily
|
Offsets are mostly useful for sampling as the set of all valid offsets is easily
|
||||||
known in advance to be [0, len(table)).
|
known in advance to be [0, len(table)).
|
||||||
|
|
||||||
|
No guarantees are made regarding the order in which results are returned.
|
||||||
|
Repeated offsets produce repeated rows.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
offsets: list[int]
|
offsets: list[int]
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -691,6 +710,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 +777,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():
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from lancedb.functions import (
|
|||||||
FunctionBinding,
|
FunctionBinding,
|
||||||
FunctionVersion,
|
FunctionVersion,
|
||||||
PythonRuntimeSpec,
|
PythonRuntimeSpec,
|
||||||
|
SecretBinding,
|
||||||
RefreshColumnResult,
|
RefreshColumnResult,
|
||||||
)
|
)
|
||||||
from lancedb.table import AsyncTable
|
from lancedb.table import AsyncTable
|
||||||
@@ -38,6 +39,7 @@ def job_result(name: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def assert_no_secret_values(value):
|
def assert_no_secret_values(value):
|
||||||
|
"""No client value models a resolved credential, at any nesting depth."""
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
for key, child in value.items():
|
for key, child in value.items():
|
||||||
assert key not in {
|
assert key not in {
|
||||||
@@ -109,7 +111,9 @@ def test_function_version_identity_is_immutable_and_exact():
|
|||||||
version = FunctionVersion.from_json(json.dumps(value))
|
version = FunctionVersion.from_json(json.dumps(value))
|
||||||
assert version.name == "embed"
|
assert version.name == "embed"
|
||||||
assert version.version == "fv_01K3EXACT"
|
assert version.version == "fv_01K3EXACT"
|
||||||
assert version.required_secrets == ("HF_TOKEN",)
|
assert list(version.secret_bindings) == [
|
||||||
|
SecretBinding(kind="env", variable="HF_TOKEN", secret_ref="hf-prod")
|
||||||
|
]
|
||||||
|
|
||||||
with pytest.raises((TypeError, ValueError)):
|
with pytest.raises((TypeError, ValueError)):
|
||||||
version.version = "fv_changed"
|
version.version = "fv_changed"
|
||||||
@@ -292,15 +296,27 @@ def test_refresh_result_rejects_non_u64_values(field):
|
|||||||
RefreshColumnResult.from_json(json.dumps(value))
|
RefreshColumnResult.from_json(json.dumps(value))
|
||||||
|
|
||||||
|
|
||||||
def test_canonical_client_values_contain_secret_names_only():
|
def test_canonical_client_values_carry_bindings_and_no_credentials():
|
||||||
|
"""A binding names a Secret; the credential behind it has no client field."""
|
||||||
version = FunctionVersion.from_json(
|
version = FunctionVersion.from_json(
|
||||||
json.dumps(job_result("remote_function_job.json"))
|
json.dumps(job_result("remote_function_job.json"))
|
||||||
)
|
)
|
||||||
canonical = json.loads(version.to_canonical_json())
|
canonical = json.loads(version.to_canonical_json())
|
||||||
assert canonical["required_secrets"] == ["HF_TOKEN"]
|
assert canonical["secret_bindings"] == [
|
||||||
|
{"kind": "env", "variable": "HF_TOKEN", "secret_ref": "hf-prod"}
|
||||||
|
]
|
||||||
assert_no_secret_values(canonical)
|
assert_no_secret_values(canonical)
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_version_without_bindings_keeps_the_original_wire_shape():
|
||||||
|
"""Every Function registered before Secrets existed serializes unchanged."""
|
||||||
|
value = job_result("remote_function_job.json")
|
||||||
|
del value["secret_bindings"]
|
||||||
|
version = FunctionVersion.from_json(json.dumps(value))
|
||||||
|
assert list(version.secret_bindings) == []
|
||||||
|
assert "secret_bindings" not in json.loads(version.to_canonical_json())
|
||||||
|
|
||||||
|
|
||||||
class _FunctionDeclarationInner:
|
class _FunctionDeclarationInner:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.calls = []
|
self.calls = []
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||||
|
|||||||
@@ -266,3 +266,38 @@ async def test_async_namespace_connection_materialized_views(tmp_path):
|
|||||||
handle._route_pushdown_to_rust == through_namespace._route_pushdown_to_rust
|
handle._route_pushdown_to_rust == through_namespace._route_pushdown_to_rust
|
||||||
)
|
)
|
||||||
assert handle._namespace_path == through_namespace._namespace_path
|
assert handle._namespace_path == through_namespace._namespace_path
|
||||||
|
|
||||||
|
|
||||||
|
def test_namespaced_select_kind_is_read_and_unknown_kinds_are_refused():
|
||||||
|
import json
|
||||||
|
|
||||||
|
import pyarrow as pa
|
||||||
|
|
||||||
|
from lancedb.materialized_view import _definition_from_schema
|
||||||
|
|
||||||
|
def schema_with(definition: dict) -> pa.Schema:
|
||||||
|
return pa.schema([pa.field("id", pa.int32())]).with_metadata(
|
||||||
|
{b"mv.definition": json.dumps(definition).encode()}
|
||||||
|
)
|
||||||
|
|
||||||
|
# "namespaced_select" is the namespaced form of "select": same shape,
|
||||||
|
# a separate kind so readers that predate it refuse instead of
|
||||||
|
# resolving the source at the root.
|
||||||
|
definition = _definition_from_schema(
|
||||||
|
schema_with(
|
||||||
|
{
|
||||||
|
"kind": "namespaced_select",
|
||||||
|
"source_table": "people",
|
||||||
|
"source_namespace": ["ns"],
|
||||||
|
"projections": [{"output": "name", "expression": "name"}],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"v",
|
||||||
|
)
|
||||||
|
assert definition.source_table == "people"
|
||||||
|
assert definition.source_namespace == ["ns"]
|
||||||
|
|
||||||
|
with pytest.raises(NotImplementedError, match="cannot refresh"):
|
||||||
|
_definition_from_schema(
|
||||||
|
schema_with({"kind": "select_v3", "source_table": "people"}), "v"
|
||||||
|
)
|
||||||
|
|||||||
@@ -1923,6 +1923,21 @@ def test_take_queries(tmp_path):
|
|||||||
17,
|
17,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Duplicate offsets are occurrences, not set members. Ordering is unspecified.
|
||||||
|
assert sorted(table.take_offsets([5, 2, 5, 17]).to_pandas()["idx"].to_list()) == [
|
||||||
|
2,
|
||||||
|
5,
|
||||||
|
5,
|
||||||
|
17,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Converting a take builder to its serializable query representation must
|
||||||
|
# retain occurrence metadata and execute with the same multiplicity.
|
||||||
|
query = table.take_offsets([5, 2, 5, 17]).select(["idx"]).to_query_object()
|
||||||
|
assert query.take_offsets == [5, 2, 5, 17]
|
||||||
|
converted = table._execute_query(query).read_all()
|
||||||
|
assert sorted(converted["idx"].to_pylist()) == [2, 5, 5, 17]
|
||||||
|
|
||||||
# Take by row id
|
# Take by row id
|
||||||
assert list(
|
assert list(
|
||||||
sorted(table.take_row_ids([5, 2, 17]).to_pandas()["idx"].to_list())
|
sorted(table.take_row_ids([5, 2, 17]).to_pandas()["idx"].to_list())
|
||||||
|
|||||||
@@ -479,24 +479,49 @@ def test_remote_permutation_is_picklable():
|
|||||||
match = re.search(
|
match = re.search(
|
||||||
r"_rowoffset\s+in\s+\((.*?)\)", body["filter"], re.IGNORECASE
|
r"_rowoffset\s+in\s+\((.*?)\)", body["filter"], re.IGNORECASE
|
||||||
)
|
)
|
||||||
offsets = [int(o.strip()) for o in match.group(1).split(",")]
|
offsets = list(
|
||||||
|
dict.fromkeys(int(o.strip()) for o in match.group(1).split(","))
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
offsets = list(range(len(rows)))
|
offsets = list(range(len(rows)))
|
||||||
table = pa.table({"a": [rows[offset] for offset in offsets]})
|
columns = body.get("columns") or ["a"]
|
||||||
|
table = pa.table(
|
||||||
|
{
|
||||||
|
column: (
|
||||||
|
[rows[offset] for offset in offsets]
|
||||||
|
if column == "a"
|
||||||
|
else offsets
|
||||||
|
)
|
||||||
|
for column in columns
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
request.send_response(200)
|
request.send_response(200)
|
||||||
request.send_header("Content-Type", "application/vnd.apache.arrow.file")
|
request.send_header("Content-Type", "application/vnd.apache.arrow.file")
|
||||||
request.end_headers()
|
request.end_headers()
|
||||||
with pa.ipc.new_file(request.wfile, schema=table.schema) as writer:
|
with pa.ipc.new_file(request.wfile, schema=table.schema) as writer:
|
||||||
writer.write_table(table)
|
writer.write_table(table, max_chunksize=2)
|
||||||
else:
|
else:
|
||||||
request.send_response(404)
|
request.send_response(404)
|
||||||
request.end_headers()
|
request.end_headers()
|
||||||
|
|
||||||
with mock_lancedb_connection(handler) as db:
|
with mock_lancedb_connection(handler) as db:
|
||||||
permutation = Permutation.identity(db.open_table("test"))
|
table = db.open_table("test")
|
||||||
|
assert table.take_offsets([0, 2, 0, 4]).to_list() == [
|
||||||
|
{"a": 0},
|
||||||
|
{"a": 0},
|
||||||
|
{"a": 2},
|
||||||
|
{"a": 4},
|
||||||
|
]
|
||||||
|
|
||||||
|
permutation = Permutation.identity(table)
|
||||||
restored = pickle.loads(pickle.dumps(permutation))
|
restored = pickle.loads(pickle.dumps(permutation))
|
||||||
assert restored.__getitems__([0, 2, 4]) == [{"a": 0}, {"a": 2}, {"a": 4}]
|
assert restored.__getitems__([0, 2, 0, 4]) == [
|
||||||
|
{"a": 0},
|
||||||
|
{"a": 2},
|
||||||
|
{"a": 0},
|
||||||
|
{"a": 4},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_create_table_exist_ok():
|
def test_create_table_exist_ok():
|
||||||
@@ -795,11 +820,13 @@ def test_table_create_indices():
|
|||||||
scalar_req = received_requests[0]
|
scalar_req = received_requests[0]
|
||||||
assert "name" in scalar_req
|
assert "name" in scalar_req
|
||||||
assert scalar_req["name"] == "custom_scalar_idx"
|
assert scalar_req["name"] == "custom_scalar_idx"
|
||||||
|
assert scalar_req["replace"] is False
|
||||||
|
|
||||||
# Check FTS index request has custom name
|
# Check FTS index request has custom name
|
||||||
fts_req = received_requests[1]
|
fts_req = received_requests[1]
|
||||||
assert "name" in fts_req
|
assert "name" in fts_req
|
||||||
assert fts_req["name"] == "custom_fts_idx"
|
assert fts_req["name"] == "custom_fts_idx"
|
||||||
|
assert fts_req["replace"] is False
|
||||||
assert fts_req["block_size"] == 256
|
assert fts_req["block_size"] == 256
|
||||||
assert fts_req["custom_stop_words"] == ["cloud"]
|
assert fts_req["custom_stop_words"] == ["cloud"]
|
||||||
|
|
||||||
@@ -807,6 +834,7 @@ def test_table_create_indices():
|
|||||||
vector_req = received_requests[2]
|
vector_req = received_requests[2]
|
||||||
assert "name" in vector_req
|
assert "name" in vector_req
|
||||||
assert vector_req["name"] == "custom_vector_idx"
|
assert vector_req["name"] == "custom_vector_idx"
|
||||||
|
assert "replace" not in vector_req
|
||||||
|
|
||||||
table.wait_for_index(["custom_scalar_idx"], timedelta(seconds=2))
|
table.wait_for_index(["custom_scalar_idx"], timedelta(seconds=2))
|
||||||
table.wait_for_index(
|
table.wait_for_index(
|
||||||
@@ -1079,6 +1107,9 @@ def test_remote_create_index_new_api():
|
|||||||
table.create_index("text", config=FTS(block_size=256))
|
table.create_index("text", config=FTS(block_size=256))
|
||||||
# IvfRq via new API
|
# IvfRq via new API
|
||||||
table.create_index("vector", config=IvfRq(distance_type="l2"))
|
table.create_index("vector", config=IvfRq(distance_type="l2"))
|
||||||
|
table.create_index(
|
||||||
|
"vector", config=IvfPq(distance_type="l2"), replace=False
|
||||||
|
)
|
||||||
|
|
||||||
# Legacy index_type="IVF_RQ" routes to IvfRq config under the hood.
|
# Legacy index_type="IVF_RQ" routes to IvfRq config under the hood.
|
||||||
with pytest.warns(DeprecationWarning, match="create_index"):
|
with pytest.warns(DeprecationWarning, match="create_index"):
|
||||||
@@ -1088,15 +1119,17 @@ def test_remote_create_index_new_api():
|
|||||||
num_partitions=8,
|
num_partitions=8,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(received_requests) == 5
|
assert len(received_requests) == 6
|
||||||
assert [req["column"] for req in received_requests] == [
|
assert [req["column"] for req in received_requests] == [
|
||||||
"vector",
|
"vector",
|
||||||
"category",
|
"category",
|
||||||
"text",
|
"text",
|
||||||
"vector",
|
"vector",
|
||||||
"vector",
|
"vector",
|
||||||
|
"vector",
|
||||||
]
|
]
|
||||||
assert received_requests[2]["block_size"] == 256
|
assert received_requests[2]["block_size"] == 256
|
||||||
|
assert received_requests[4]["replace"] is False
|
||||||
|
|
||||||
|
|
||||||
def test_table_wait_for_index_timeout():
|
def test_table_wait_for_index_timeout():
|
||||||
@@ -2434,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)
|
||||||
@@ -2442,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))
|
||||||
@@ -2479,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()
|
||||||
@@ -2510,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()
|
||||||
@@ -2526,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'",
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -2682,6 +2682,43 @@ def test_merge_insert(mem_db: DBConnection):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_insert_composite_key(mem_db: DBConnection):
|
||||||
|
table = mem_db.create_table(
|
||||||
|
"my_table",
|
||||||
|
data=pa.table(
|
||||||
|
{
|
||||||
|
"shard": ["a", "a", "b"],
|
||||||
|
"id": [1, 2, 1],
|
||||||
|
"val": ["x", "y", "z"],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ("a", 1) matches an existing row and updates it. ("b", 2) agrees with an
|
||||||
|
# existing row on each key column separately but on neither pair, so it is
|
||||||
|
# an insert.
|
||||||
|
new_data = pa.table({"shard": ["a", "b"], "id": [1, 2], "val": ["X", "W"]})
|
||||||
|
res = (
|
||||||
|
table.merge_insert(["shard", "id"])
|
||||||
|
.when_matched_update_all()
|
||||||
|
.when_not_matched_insert_all()
|
||||||
|
.execute(new_data)
|
||||||
|
)
|
||||||
|
assert res.num_updated_rows == 1
|
||||||
|
assert res.num_inserted_rows == 1
|
||||||
|
|
||||||
|
expected = pa.table(
|
||||||
|
{
|
||||||
|
"shard": ["a", "a", "b", "b"],
|
||||||
|
"id": [1, 2, 1, 2],
|
||||||
|
"val": ["X", "y", "z", "W"],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert table.to_arrow().sort_by([("shard", "ascending"), ("id", "ascending")]) == (
|
||||||
|
expected
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_merge_insert_nullable_pandas_into_pydantic_schema(mem_db: DBConnection):
|
def test_merge_insert_nullable_pandas_into_pydantic_schema(mem_db: DBConnection):
|
||||||
# Regression test for https://github.com/lancedb/lancedb/issues/2366
|
# Regression test for https://github.com/lancedb/lancedb/issues/2366
|
||||||
pd = pytest.importorskip("pandas")
|
pd = pytest.importorskip("pandas")
|
||||||
@@ -4087,6 +4124,29 @@ def test_computed_column_rejects_transforms_and_computed_together(tmp_path):
|
|||||||
table.add_columns({"a": "x + 1"}, computed={"b": "x * 2"})
|
table.add_columns({"a": "x + 1"}, computed={"b": "x * 2"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_computed_column_blob_projection_inherits_semantics(tmp_path):
|
||||||
|
schema = pa.schema([pa.field("id", pa.int64()), lancedb.blob("image")])
|
||||||
|
db = lancedb.connect(tmp_path)
|
||||||
|
table = db.create_table("computed_column_blob", schema=schema)
|
||||||
|
table.add(
|
||||||
|
[
|
||||||
|
{"id": 1, "image": b"hello"},
|
||||||
|
{"id": 2, "image": b""},
|
||||||
|
{"id": 3, "image": None},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
table.add_columns(computed={"image_copy": "image", "second_copy": "image_copy"})
|
||||||
|
assert table.refresh_column("image_copy").rows_filled == 2
|
||||||
|
assert table.refresh_column("second_copy").rows_filled == 2
|
||||||
|
assert table.blob_columns() == ["image", "image_copy", "second_copy"]
|
||||||
|
|
||||||
|
hits = table.search().with_row_id(True).limit(10).to_arrow()
|
||||||
|
rows = sorted(zip(hits["id"].to_pylist(), hits["_rowid"].to_pylist()))
|
||||||
|
copied = table.fetch_blobs("second_copy", [row_id for _, row_id in rows])
|
||||||
|
assert copied.to_pylist() == [b"hello", b"", None]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_computed_column_async(tmp_path):
|
async def test_computed_column_async(tmp_path):
|
||||||
db = await lancedb.connect_async(tmp_path)
|
db = await lancedb.connect_async(tmp_path)
|
||||||
|
|||||||
+139
-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(
|
||||||
@@ -629,6 +680,77 @@ impl Connection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn list_functions(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
inner
|
||||||
|
.list_functions()
|
||||||
|
.await
|
||||||
|
.infer_error()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|function| function.to_canonical_json().infer_error())
|
||||||
|
.collect::<PyResult<Vec<_>>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drop_function(
|
||||||
|
self_: PyRef<'_, Self>,
|
||||||
|
name: String,
|
||||||
|
version: String,
|
||||||
|
) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
inner.drop_function(name, version).await.infer_error()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_secret(
|
||||||
|
self_: PyRef<'_, Self>,
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
inner.create_secret(name, value).await.infer_error()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alter_secret(
|
||||||
|
self_: PyRef<'_, Self>,
|
||||||
|
name: String,
|
||||||
|
value: String,
|
||||||
|
) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
inner.alter_secret(name, value).await.infer_error()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_secrets(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
inner.list_secrets().await.infer_error()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drop_secret(self_: PyRef<'_, Self>, name: String) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
inner.drop_secret(name).await.infer_error()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Name and timestamps as a plain tuple. `SecretInfo` carries no value, so
|
||||||
|
/// there is none to filter out here. Timestamps stay integers rather than
|
||||||
|
/// going through a string, so the caller can compare two without parsing.
|
||||||
|
pub fn describe_secret(self_: PyRef<'_, Self>, name: String) -> PyResult<Bound<'_, PyAny>> {
|
||||||
|
let inner = self_.get_inner()?.clone();
|
||||||
|
future_into_py(self_.py(), async move {
|
||||||
|
let info = inner.describe_secret(name).await.infer_error()?;
|
||||||
|
Ok((info.name, info.created_at_millis, info.updated_at_millis))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_jobs(self_: PyRef<'_, Self>) -> PyResult<Bound<'_, PyAny>> {
|
pub fn list_jobs(self_: PyRef<'_, Self>) -> 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 {
|
||||||
@@ -640,42 +762,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<'_>,
|
||||||
@@ -683,6 +779,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>>,
|
||||||
@@ -702,6 +799,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>()?;
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ impl<'py> IntoPyObject<'py> for PyQueryVectors {
|
|||||||
pub struct PyQueryRequest {
|
pub struct PyQueryRequest {
|
||||||
pub limit: Option<usize>,
|
pub limit: Option<usize>,
|
||||||
pub offset: Option<usize>,
|
pub offset: Option<usize>,
|
||||||
|
pub take_offsets: Option<Vec<u64>>,
|
||||||
pub filter: Option<PyQueryFilter>,
|
pub filter: Option<PyQueryFilter>,
|
||||||
pub full_text_search: Option<PyLanceDB<FtsQuery>>,
|
pub full_text_search: Option<PyLanceDB<FtsQuery>>,
|
||||||
pub select: PySelect,
|
pub select: PySelect,
|
||||||
@@ -353,6 +354,7 @@ impl From<AnyQuery> for PyQueryRequest {
|
|||||||
AnyQuery::Query(query_request) => Self {
|
AnyQuery::Query(query_request) => Self {
|
||||||
limit: query_request.limit,
|
limit: query_request.limit,
|
||||||
offset: query_request.offset,
|
offset: query_request.offset,
|
||||||
|
take_offsets: query_request.take_offsets,
|
||||||
filter: query_request.filter.map(PyQueryFilter),
|
filter: query_request.filter.map(PyQueryFilter),
|
||||||
full_text_search: query_request
|
full_text_search: query_request
|
||||||
.full_text_search
|
.full_text_search
|
||||||
@@ -381,6 +383,7 @@ impl From<AnyQuery> for PyQueryRequest {
|
|||||||
AnyQuery::VectorQuery(vector_query) => Self {
|
AnyQuery::VectorQuery(vector_query) => Self {
|
||||||
limit: vector_query.base.limit,
|
limit: vector_query.base.limit,
|
||||||
offset: vector_query.base.offset,
|
offset: vector_query.base.offset,
|
||||||
|
take_offsets: vector_query.base.take_offsets,
|
||||||
filter: vector_query.base.filter.map(PyQueryFilter),
|
filter: vector_query.base.filter.map(PyQueryFilter),
|
||||||
full_text_search: None,
|
full_text_search: None,
|
||||||
select_source_columns: PySelect::source_columns(&vector_query.base.select),
|
select_source_columns: PySelect::source_columns(&vector_query.base.select),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+98
-95
@@ -10,6 +10,9 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.11'",
|
"python_full_version < '3.11'",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[options]
|
||||||
|
prerelease-mode = "allow"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "accelerate"
|
name = "accelerate"
|
||||||
version = "1.14.0"
|
version = "1.14.0"
|
||||||
@@ -799,7 +802,7 @@ name = "cuda-bindings"
|
|||||||
version = "13.3.1"
|
version = "13.3.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cuda-pathfinder", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
{ name = "cuda-pathfinder" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/21/8464d133752951c154feafb3b65c297e7d80f301183d220bec4c830f1441/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120fcc53d57903df529c3486962c56528cba5b7d6c57c99537320ed9922c8b86", size = 6073403, upload-time = "2026-05-29T23:11:36.22Z" },
|
{ url = "https://files.pythonhosted.org/packages/a9/21/8464d133752951c154feafb3b65c297e7d80f301183d220bec4c830f1441/cuda_bindings-13.3.1-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:120fcc53d57903df529c3486962c56528cba5b7d6c57c99537320ed9922c8b86", size = 6073403, upload-time = "2026-05-29T23:11:36.22Z" },
|
||||||
@@ -834,37 +837,37 @@ wheels = [
|
|||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
cublas = [
|
cublas = [
|
||||||
{ name = "nvidia-cublas", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-cublas" },
|
||||||
]
|
]
|
||||||
cudart = [
|
cudart = [
|
||||||
{ name = "nvidia-cuda-runtime", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-cuda-runtime" },
|
||||||
]
|
]
|
||||||
cufft = [
|
cufft = [
|
||||||
{ name = "nvidia-cufft", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-cufft" },
|
||||||
]
|
]
|
||||||
cufile = [
|
cufile = [
|
||||||
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
|
{ name = "nvidia-cufile" },
|
||||||
]
|
]
|
||||||
cupti = [
|
cupti = [
|
||||||
{ name = "nvidia-cuda-cupti", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-cuda-cupti" },
|
||||||
]
|
]
|
||||||
curand = [
|
curand = [
|
||||||
{ name = "nvidia-curand", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-curand" },
|
||||||
]
|
]
|
||||||
cusolver = [
|
cusolver = [
|
||||||
{ name = "nvidia-cusolver", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-cusolver" },
|
||||||
]
|
]
|
||||||
cusparse = [
|
cusparse = [
|
||||||
{ name = "nvidia-cusparse", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-cusparse" },
|
||||||
]
|
]
|
||||||
nvjitlink = [
|
nvjitlink = [
|
||||||
{ name = "nvidia-nvjitlink", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-nvjitlink" },
|
||||||
]
|
]
|
||||||
nvrtc = [
|
nvrtc = [
|
||||||
{ name = "nvidia-cuda-nvrtc", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-cuda-nvrtc" },
|
||||||
]
|
]
|
||||||
nvtx = [
|
nvtx = [
|
||||||
{ name = "nvidia-nvtx", marker = "(python_full_version < '3.14' and sys_platform == 'win32') or sys_platform == 'linux'" },
|
{ name = "nvidia-nvtx" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1023,7 +1026,7 @@ name = "exceptiongroup"
|
|||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -1440,16 +1443,16 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.11'",
|
"python_full_version < '3.11'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cachetools", marker = "python_full_version < '3.11'" },
|
{ name = "cachetools" },
|
||||||
{ name = "certifi", marker = "python_full_version < '3.11'" },
|
{ name = "certifi" },
|
||||||
{ name = "httpx", marker = "python_full_version < '3.11'" },
|
{ name = "httpx" },
|
||||||
{ name = "ibm-cos-sdk", marker = "python_full_version < '3.11'" },
|
{ name = "ibm-cos-sdk" },
|
||||||
{ name = "lomond", marker = "python_full_version < '3.11'" },
|
{ name = "lomond" },
|
||||||
{ name = "packaging", marker = "python_full_version < '3.11'" },
|
{ name = "packaging" },
|
||||||
{ name = "pandas", version = "2.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "pandas", version = "2.2.3", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "requests", marker = "python_full_version < '3.11'" },
|
{ name = "requests" },
|
||||||
{ name = "tabulate", marker = "python_full_version < '3.11'" },
|
{ name = "tabulate" },
|
||||||
{ name = "urllib3", marker = "python_full_version < '3.11'" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/56/2e3df38a1f13062095d7bde23c87a92f3898982993a15186b1bfecbd206f/ibm_watsonx_ai-1.3.42.tar.gz", hash = "sha256:ee5be59009004245d957ce97d1227355516df95a2640189749487614fef674ff", size = 688651, upload-time = "2025-10-01T13:35:41.527Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c7/56/2e3df38a1f13062095d7bde23c87a92f3898982993a15186b1bfecbd206f/ibm_watsonx_ai-1.3.42.tar.gz", hash = "sha256:ee5be59009004245d957ce97d1227355516df95a2640189749487614fef674ff", size = 688651, upload-time = "2025-10-01T13:35:41.527Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -1468,17 +1471,17 @@ resolution-markers = [
|
|||||||
"python_full_version == '3.11.*'",
|
"python_full_version == '3.11.*'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "cachetools", marker = "python_full_version >= '3.11'" },
|
{ name = "cachetools" },
|
||||||
{ name = "certifi", marker = "python_full_version >= '3.11'" },
|
{ name = "certifi" },
|
||||||
{ name = "httpx", marker = "python_full_version >= '3.11'" },
|
{ name = "httpx" },
|
||||||
{ name = "ibm-cos-sdk", marker = "python_full_version >= '3.11'" },
|
{ name = "ibm-cos-sdk" },
|
||||||
{ name = "lomond", marker = "python_full_version >= '3.11'" },
|
{ name = "lomond" },
|
||||||
{ name = "packaging", marker = "python_full_version >= '3.11'" },
|
{ name = "packaging" },
|
||||||
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
|
||||||
{ name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
|
{ name = "pandas", version = "3.0.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
|
||||||
{ name = "requests", marker = "python_full_version >= '3.11'" },
|
{ name = "requests" },
|
||||||
{ name = "tabulate", marker = "python_full_version >= '3.11'" },
|
{ name = "tabulate" },
|
||||||
{ name = "urllib3", marker = "python_full_version >= '3.11'" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/29/a3/c756b534696ab2f3f29882fdb7ca7198b7a5c94e10c0a3a327853d6d6b79/ibm_watsonx_ai-1.5.14.tar.gz", hash = "sha256:a756488bd57e87c0fc51be42dcba871143cfe0ac1e805c497c5047e1e4f13e9d", size = 735804, upload-time = "2026-06-22T12:32:43.85Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/29/a3/c756b534696ab2f3f29882fdb7ca7198b7a5c94e10c0a3a327853d6d6b79/ibm_watsonx_ai-1.5.14.tar.gz", hash = "sha256:a756488bd57e87c0fc51be42dcba871143cfe0ac1e805c497c5047e1e4f13e9d", size = 735804, upload-time = "2026-06-22T12:32:43.85Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -1554,17 +1557,17 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.11'",
|
"python_full_version < '3.11'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "decorator", marker = "python_full_version < '3.11'" },
|
{ name = "decorator" },
|
||||||
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
{ name = "exceptiongroup" },
|
||||||
{ name = "jedi", marker = "python_full_version < '3.11'" },
|
{ name = "jedi" },
|
||||||
{ name = "matplotlib-inline", marker = "python_full_version < '3.11'" },
|
{ name = "matplotlib-inline" },
|
||||||
{ name = "pexpect", marker = "python_full_version < '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||||
{ name = "prompt-toolkit", marker = "python_full_version < '3.11'" },
|
{ name = "prompt-toolkit" },
|
||||||
{ name = "pygments", marker = "python_full_version < '3.11'" },
|
{ name = "pygments" },
|
||||||
{ name = "stack-data", marker = "python_full_version < '3.11'" },
|
{ name = "stack-data" },
|
||||||
{ name = "traitlets", marker = "python_full_version < '3.11'" },
|
{ name = "traitlets" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/40/18/f8598d287006885e7136451fdea0755af4ebcbfe342836f24deefaed1164/ipython-8.39.0.tar.gz", hash = "sha256:4110ae96012c379b8b6db898a07e186c40a2a1ef5d57a7fa83166047d9da7624", size = 5513971, upload-time = "2026-03-27T10:02:13.94Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -1583,18 +1586,18 @@ resolution-markers = [
|
|||||||
"python_full_version == '3.11.*'",
|
"python_full_version == '3.11.*'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
{ name = "decorator", marker = "python_full_version >= '3.11'" },
|
{ name = "decorator" },
|
||||||
{ name = "ipython-pygments-lexers", marker = "python_full_version >= '3.11'" },
|
{ name = "ipython-pygments-lexers" },
|
||||||
{ name = "jedi", marker = "python_full_version >= '3.11'" },
|
{ name = "jedi" },
|
||||||
{ name = "matplotlib-inline", marker = "python_full_version >= '3.11'" },
|
{ name = "matplotlib-inline" },
|
||||||
{ name = "pexpect", marker = "python_full_version >= '3.11' and sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
{ name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||||
{ name = "prompt-toolkit", marker = "python_full_version >= '3.11'" },
|
{ name = "prompt-toolkit" },
|
||||||
{ name = "psutil", marker = "python_full_version >= '3.11' and sys_platform != 'cygwin' and sys_platform != 'emscripten'" },
|
{ name = "psutil", marker = "sys_platform != 'cygwin' and sys_platform != 'emscripten'" },
|
||||||
{ name = "pygments", marker = "python_full_version >= '3.11'" },
|
{ name = "pygments" },
|
||||||
{ name = "stack-data", marker = "python_full_version >= '3.11'" },
|
{ name = "stack-data" },
|
||||||
{ name = "traitlets", marker = "python_full_version >= '3.11'" },
|
{ name = "traitlets" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
|
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/53/59/165d3b4d75cc34add3122c4417ecb229085140ac573103c223cd01dde96f/ipython-9.15.0.tar.gz", hash = "sha256:da2819ce2aa83135257df830660b1176d986c3d2876db24df01974fa955b2756", size = 4442580, upload-time = "2026-06-26T11:03:35.913Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -1606,7 +1609,7 @@ name = "ipython-pygments-lexers"
|
|||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pygments", marker = "python_full_version >= '3.11'" },
|
{ name = "pygments" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -2858,7 +2861,7 @@ name = "nvidia-cudnn-cu13"
|
|||||||
version = "9.19.0.56"
|
version = "9.19.0.56"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "nvidia-cublas", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
{ name = "nvidia-cublas" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" },
|
{ url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" },
|
||||||
@@ -2870,7 +2873,7 @@ name = "nvidia-cufft"
|
|||||||
version = "12.0.0.61"
|
version = "12.0.0.61"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "nvidia-nvjitlink", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
{ name = "nvidia-nvjitlink" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
|
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
|
||||||
@@ -2900,9 +2903,9 @@ name = "nvidia-cusolver"
|
|||||||
version = "12.0.4.66"
|
version = "12.0.4.66"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "nvidia-cublas", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
{ name = "nvidia-cublas" },
|
||||||
{ name = "nvidia-cusparse", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
{ name = "nvidia-cusparse" },
|
||||||
{ name = "nvidia-nvjitlink", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
{ name = "nvidia-nvjitlink" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
|
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
|
||||||
@@ -2914,7 +2917,7 @@ name = "nvidia-cusparse"
|
|||||||
version = "12.6.3.3"
|
version = "12.6.3.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "nvidia-nvjitlink", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
{ name = "nvidia-nvjitlink" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
|
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
|
||||||
@@ -3091,10 +3094,10 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.11'",
|
"python_full_version < '3.11'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "python-dateutil", marker = "python_full_version < '3.11'" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "pytz", marker = "python_full_version < '3.11'" },
|
{ name = "pytz" },
|
||||||
{ name = "tzdata", marker = "python_full_version < '3.11'" },
|
{ name = "tzdata" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -3143,11 +3146,11 @@ resolution-markers = [
|
|||||||
"python_full_version == '3.11.*'",
|
"python_full_version == '3.11.*'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12' or python_full_version >= '3.14'" },
|
||||||
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" },
|
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12' and python_full_version < '3.14'" },
|
||||||
{ name = "python-dateutil", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "pytz", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
{ name = "pytz" },
|
||||||
{ name = "tzdata", marker = "python_full_version >= '3.11' and python_full_version < '3.14'" },
|
{ name = "tzdata" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -3210,9 +3213,9 @@ resolution-markers = [
|
|||||||
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
|
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "python-dateutil", marker = "python_full_version >= '3.14'" },
|
{ name = "python-dateutil" },
|
||||||
{ name = "tzdata", marker = "(python_full_version >= '3.14' and sys_platform == 'emscripten') or (python_full_version >= '3.14' and sys_platform == 'win32')" },
|
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/87/4341c6252d1c47b08768c3d25ac487362bf403f0313ddae4a2a26c9b1b4c/pandas-3.0.3.tar.gz", hash = "sha256:696a4a00a2a2a35d4e5deb3fc946641b96c944f02230e4f76137fe35d806c4fc", size = 4651414, upload-time = "2026-05-11T18:54:29.21Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -3320,7 +3323,7 @@ name = "pexpect"
|
|||||||
version = "4.9.0"
|
version = "4.9.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "ptyprocess", marker = "(python_full_version < '3.14' and sys_platform == 'emscripten') or (python_full_version < '3.14' and sys_platform == 'win32') or (sys_platform != 'emscripten' and sys_platform != 'win32')" },
|
{ name = "ptyprocess" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -3912,8 +3915,8 @@ crypto = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pylance"
|
name = "pylance"
|
||||||
version = "7.0.0"
|
version = "9.0.0rc1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.fury.io/lance-format/" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "lance-namespace" },
|
{ name = "lance-namespace" },
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||||
@@ -3922,12 +3925,12 @@ dependencies = [
|
|||||||
{ name = "pyarrow" },
|
{ name = "pyarrow" },
|
||||||
]
|
]
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/ad/2f64921bf346e7075aef24a72595db44821724a3d89a9a92dd24e79632aa/pylance-7.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:98422021975be76e72b1572f41b8c9abb3bee5bdc9bfa5e9ce731110a65ed4d1", size = 62134146, upload-time = "2026-05-27T21:59:37.459Z" },
|
{ url = "https://pypi.fury.io/lance-format/-/ver_vEHBE/pylance-9.0.0rc1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0b6b02a1808bb3072ee7fe4e36614cae6f86302513e73ec7f55b2234a963b24" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/1c/c5a01bee0160b55d9a98895cbd33091d038f0a0995b121ab72e629008d02/pylance-7.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bec86ee5b6fbd8bfc493e653f0a1fba0303cfe5492b9b46fc25ab908edc7183", size = 65373684, upload-time = "2026-05-27T22:04:01.584Z" },
|
{ url = "https://pypi.fury.io/lance-format/-/ver_1Jipm4/pylance-9.0.0rc1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30f0ebf0d88034301819eb964f9236ce555aaa58e7ab89c5975a3e2250bbb405" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/da/1fe8b8f7dbfe734d76af76acc994fc360a0d0c79a4874ef69f5a72a58fe3/pylance-7.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881491432c53184e52f8d1db8d5f872f39a03f36fb104bec77b33d379519d8b5", size = 69458555, upload-time = "2026-05-27T22:16:50.567Z" },
|
{ url = "https://pypi.fury.io/lance-format/-/ver_IvKxo/pylance-9.0.0rc1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44609ea2615ea6e684b85478d1694af2026458f61cf7895ecc75e238bfd17aa8" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/f0/dd505cf3fd0226ab9d94759acd713125af1d3bfacfd80bbd52e3b9f89509/pylance-7.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:18453999e7fff4f76b16d6b7882c9df0628bd142ff95e2461bd7dd5ee3fe0af3", size = 65394430, upload-time = "2026-05-27T22:05:30.923Z" },
|
{ url = "https://pypi.fury.io/lance-format/-/ver_2hidj1/pylance-9.0.0rc1-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:182167a8dba9eeabffbffd53bd5b8548613d4d459b7cd7b34a840dd00cbb806f" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/17/ba/2357b81034f28eb00790e258ed140289a6a887a7468ca9df6349fd186b27/pylance-7.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:04a58051d408c60fe76d41a220dcaf8fea8fb6d1aa0ca78a709b60bc3cc8d19a", size = 69473470, upload-time = "2026-05-27T22:17:18.935Z" },
|
{ url = "https://pypi.fury.io/lance-format/-/ver_1dFx3r/pylance-9.0.0rc1-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8a63b11e814b7eab758bcaf0d6f97eb05ea86203d9fb0af718c462c24c7d6c9c" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/ec/5c00b6303a67d787f9475141832cbdc513d674ac3dcaeef8a7b169905e65/pylance-7.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:467d4864af047eaab4e1370e2f1e88e2c6f507c079874421116cb41d78bc3629", size = 74792863, upload-time = "2026-05-27T22:19:23.875Z" },
|
{ url = "https://pypi.fury.io/lance-format/-/ver_2a8dSh/pylance-9.0.0rc1-cp310-abi3-win_amd64.whl", hash = "sha256:2ff8b953ae2b0550490c1a7efd210aa91bc223d200ffac28849056cfd7436d97" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4683,10 +4686,10 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.11'",
|
"python_full_version < '3.11'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "joblib", marker = "python_full_version < '3.11'" },
|
{ name = "joblib" },
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" } },
|
||||||
{ name = "threadpoolctl", marker = "python_full_version < '3.11'" },
|
{ name = "threadpoolctl" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -4734,13 +4737,13 @@ resolution-markers = [
|
|||||||
"python_full_version == '3.11.*'",
|
"python_full_version == '3.11.*'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "joblib", marker = "python_full_version >= '3.11'" },
|
{ name = "joblib" },
|
||||||
{ name = "narwhals", marker = "python_full_version >= '3.11'" },
|
{ name = "narwhals" },
|
||||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||||
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||||
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
{ name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||||
{ name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
{ name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||||
{ name = "threadpoolctl", marker = "python_full_version >= '3.11'" },
|
{ name = "threadpoolctl" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/6f/37092bdb25f712817231799fc5674d8e704066a8a70c1d2d40517e18b4ab/scikit_learn-1.9.0.tar.gz", hash = "sha256:8833266989d3a5110178a9fae30783675460724d0e1efb13b14901d2c660c557", size = 7750767, upload-time = "2026-06-02T11:54:32.706Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -4784,7 +4787,7 @@ resolution-markers = [
|
|||||||
"python_full_version < '3.11'",
|
"python_full_version < '3.11'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -4843,7 +4846,7 @@ resolution-markers = [
|
|||||||
"python_full_version == '3.11.*'",
|
"python_full_version == '3.11.*'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" } },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -4920,7 +4923,7 @@ resolution-markers = [
|
|||||||
"python_full_version >= '3.12' and python_full_version < '3.14'",
|
"python_full_version >= '3.12' and python_full_version < '3.14'",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" } },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lancedb"
|
name = "lancedb"
|
||||||
version = "0.38.0-beta.11"
|
version = "0.39.0-beta.4"
|
||||||
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",
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -504,6 +536,21 @@ mod tests {
|
|||||||
params.data_storage_version.unwrap().resolve(),
|
params.data_storage_version.unwrap().resolve(),
|
||||||
ConcreteFileVersion::V2_2
|
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);
|
||||||
|
assert_eq!(
|
||||||
|
params.data_storage_version.unwrap().resolve(),
|
||||||
|
ConcreteFileVersion::V2_2
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -576,5 +623,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+243
-23
@@ -23,16 +23,20 @@ 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 crate::secrets::SecretInfo;
|
||||||
use lance::io::ObjectStoreParams;
|
use lance::io::ObjectStoreParams;
|
||||||
pub use lance_file::version::LanceFileVersion;
|
pub use lance_file::version::LanceFileVersion;
|
||||||
#[cfg(feature = "remote")]
|
#[cfg(feature = "remote")]
|
||||||
@@ -322,6 +326,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 +446,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)
|
||||||
@@ -501,6 +587,7 @@ impl Connection {
|
|||||||
/// Registration is remote-only and always asynchronous. Waiting on the
|
/// Registration is remote-only and always asynchronous. Waiting on the
|
||||||
/// returned typed job yields the durable [`crate::function::FunctionVersion`].
|
/// returned typed job yields the durable [`crate::function::FunctionVersion`].
|
||||||
/// Local databases return [`Error::NotSupported`].
|
/// Local databases return [`Error::NotSupported`].
|
||||||
|
///
|
||||||
pub async fn create_function_async(
|
pub async fn create_function_async(
|
||||||
&self,
|
&self,
|
||||||
request: crate::function::FunctionRegistrationRequest,
|
request: crate::function::FunctionRegistrationRequest,
|
||||||
@@ -523,6 +610,96 @@ impl Connection {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// List every published immutable Function version in the remote catalog.
|
||||||
|
///
|
||||||
|
/// Results are ordered by Function name then version. The client walks all
|
||||||
|
/// server pages before returning. Local databases return
|
||||||
|
/// [`Error::NotSupported`].
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// # async fn list_functions(
|
||||||
|
/// # connection: &lancedb::Connection,
|
||||||
|
/// # ) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
/// for function in connection.list_functions().await? {
|
||||||
|
/// println!("{} {}", function.name(), function.version());
|
||||||
|
/// }
|
||||||
|
/// # Ok(())
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub async fn list_functions(&self) -> Result<Vec<crate::function::FunctionVersion>> {
|
||||||
|
self.internal.list_functions().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop one exact immutable Function version from the remote catalog.
|
||||||
|
///
|
||||||
|
/// Returns `true` when the server appended a Dropped transition and
|
||||||
|
/// `false` for an idempotent replay. Local databases return
|
||||||
|
/// [`Error::NotSupported`].
|
||||||
|
pub async fn drop_function(
|
||||||
|
&self,
|
||||||
|
name: impl AsRef<str>,
|
||||||
|
version: impl AsRef<str>,
|
||||||
|
) -> Result<bool> {
|
||||||
|
self.internal
|
||||||
|
.drop_function(name.as_ref(), version.as_ref())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a named Secret in this database.
|
||||||
|
///
|
||||||
|
/// Fails if the name is taken, so a create can never silently become a
|
||||||
|
/// rotation. There is no API that reads a stored credential back; the only
|
||||||
|
/// consumer is a Function that binds the Secret by name. Local databases
|
||||||
|
/// return [`Error::NotSupported`].
|
||||||
|
pub async fn create_secret(&self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<()> {
|
||||||
|
self.internal
|
||||||
|
.create_secret(name.as_ref(), value.as_ref())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the credential behind an existing Secret.
|
||||||
|
///
|
||||||
|
/// Fails if it does not exist. Every Function bound to the Secret resolves
|
||||||
|
/// the new value from its next execution, and no new Function version is
|
||||||
|
/// minted -- which is what lets a rotation reach columns pinned to a
|
||||||
|
/// version registered before it. Local databases return
|
||||||
|
/// [`Error::NotSupported`].
|
||||||
|
pub async fn alter_secret(&self, name: impl AsRef<str>, value: impl AsRef<str>) -> Result<()> {
|
||||||
|
self.internal
|
||||||
|
.alter_secret(name.as_ref(), value.as_ref())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The names of every Secret in this database.
|
||||||
|
///
|
||||||
|
/// Names only. No path in this API returns a stored credential, by
|
||||||
|
/// construction rather than by policy. Local databases return
|
||||||
|
/// [`Error::NotSupported`].
|
||||||
|
pub async fn list_secrets(&self) -> Result<Vec<String>> {
|
||||||
|
self.internal.list_secrets().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop a Secret.
|
||||||
|
///
|
||||||
|
/// Functions bound to it fail at their next job, naming the Secret; that
|
||||||
|
/// is the revocation path. The name becomes free to reuse, and a new
|
||||||
|
/// Secret under it is picked up by everything still bound to that name.
|
||||||
|
/// Local databases return [`Error::NotSupported`].
|
||||||
|
pub async fn drop_secret(&self, name: impl AsRef<str>) -> Result<()> {
|
||||||
|
self.internal.drop_secret(name.as_ref()).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What this database records about one Secret: its name and timestamps.
|
||||||
|
///
|
||||||
|
/// Never the value. The type it returns has no field for one, so this is a
|
||||||
|
/// property of the API rather than of what the caller chooses to read.
|
||||||
|
/// Local databases return [`Error::NotSupported`].
|
||||||
|
pub async fn describe_secret(&self, name: impl AsRef<str>) -> Result<SecretInfo> {
|
||||||
|
self.internal.describe_secret(name.as_ref()).await
|
||||||
|
}
|
||||||
|
|
||||||
/// Rename a table in the database.
|
/// Rename a table in the database.
|
||||||
///
|
///
|
||||||
/// This is only supported in LanceDB Cloud.
|
/// This is only supported in LanceDB Cloud.
|
||||||
@@ -548,14 +725,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.
|
||||||
@@ -563,24 +760,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
|
||||||
@@ -827,6 +1012,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
|
||||||
@@ -1016,6 +1214,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 {
|
||||||
@@ -1057,11 +1256,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,
|
||||||
@@ -1355,6 +1558,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::{
|
||||||
@@ -30,6 +28,7 @@ use lance_namespace::models::{
|
|||||||
|
|
||||||
use crate::data::scannable::Scannable;
|
use crate::data::scannable::Scannable;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
|
use crate::secrets::SecretInfo;
|
||||||
use crate::table::{BaseTable, WriteOptions};
|
use crate::table::{BaseTable, WriteOptions};
|
||||||
|
|
||||||
pub mod listing;
|
pub mod listing;
|
||||||
@@ -206,8 +205,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 +217,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 +229,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>,
|
||||||
@@ -247,6 +250,12 @@ fn function_catalog_not_supported<T>() -> Result<T> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn secret_catalog_not_supported<T>() -> Result<T> {
|
||||||
|
Err(crate::error::Error::NotSupported {
|
||||||
|
message: "Secret operations are not supported by this database".to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// The `Database` trait defines the interface for database implementations.
|
/// The `Database` trait defines the interface for database implementations.
|
||||||
///
|
///
|
||||||
/// A database is responsible for managing tables and their metadata.
|
/// A database is responsible for managing tables and their metadata.
|
||||||
@@ -307,30 +316,73 @@ pub trait Database:
|
|||||||
) -> Result<crate::function::FunctionVersion> {
|
) -> Result<crate::function::FunctionVersion> {
|
||||||
function_catalog_not_supported()
|
function_catalog_not_supported()
|
||||||
}
|
}
|
||||||
/// A [`crate::job::Job`] handle for a server-side job by id, suitable for
|
/// List every published immutable Function version in the remote catalog.
|
||||||
/// waiting on or cancelling the job. The handle is constructed without a
|
async fn list_functions(&self) -> Result<Vec<crate::function::FunctionVersion>> {
|
||||||
/// server round trip; an unknown id surfaces when the handle is used.
|
function_catalog_not_supported()
|
||||||
fn job(&self, _job_id: &str) -> Result<crate::job::Job> {
|
}
|
||||||
job_op_not_supported("job")
|
/// Drop one exact immutable Function version from the remote catalog.
|
||||||
|
async fn drop_function(&self, _name: &str, _version: &str) -> Result<bool> {
|
||||||
|
function_catalog_not_supported()
|
||||||
|
}
|
||||||
|
/// Create a named Secret in this database. Fails if the name is taken, so
|
||||||
|
/// a create can never silently become a rotation.
|
||||||
|
async fn create_secret(&self, _name: &str, _value: &str) -> Result<()> {
|
||||||
|
secret_catalog_not_supported()
|
||||||
|
}
|
||||||
|
/// Replace the credential behind an existing Secret. Fails if it does not
|
||||||
|
/// exist. Every Function bound to it resolves the new value from its next
|
||||||
|
/// execution, with no new Function version.
|
||||||
|
async fn alter_secret(&self, _name: &str, _value: &str) -> Result<()> {
|
||||||
|
secret_catalog_not_supported()
|
||||||
|
}
|
||||||
|
/// The names of every Secret in this database.
|
||||||
|
///
|
||||||
|
/// Names only. No API path returns a stored credential, by construction
|
||||||
|
/// rather than by policy.
|
||||||
|
async fn list_secrets(&self) -> Result<Vec<String>> {
|
||||||
|
secret_catalog_not_supported()
|
||||||
|
}
|
||||||
|
/// Drop a Secret. Functions bound to it fail at their next job, which is
|
||||||
|
/// the revocation path.
|
||||||
|
async fn drop_secret(&self, _name: &str) -> Result<()> {
|
||||||
|
secret_catalog_not_supported()
|
||||||
|
}
|
||||||
|
/// What the database records about one Secret: its name and timestamps,
|
||||||
|
/// never its value.
|
||||||
|
async fn describe_secret(&self, _name: &str) -> Result<SecretInfo> {
|
||||||
|
secret_catalog_not_supported()
|
||||||
|
}
|
||||||
|
/// Open a job by id, returning a handle with its record already
|
||||||
|
/// populated. Fails with [`crate::Error::JobNotFound`] when the server has
|
||||||
|
/// no such job.
|
||||||
|
async fn open_job(&self, _job_id: &str) -> Result<crate::job::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;
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use lance_namespace_impls::ConnectBuilder;
|
|||||||
use lance_table::io::commit::CommitHandler;
|
use lance_table::io::commit::CommitHandler;
|
||||||
use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler;
|
use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler;
|
||||||
|
|
||||||
use crate::blob::{ensure_blob_storage_version, 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 +217,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;
|
||||||
}
|
}
|
||||||
@@ -539,9 +538,7 @@ impl Database for LanceNamespaceDatabase {
|
|||||||
self.namespace
|
self.namespace
|
||||||
.drop_table(drop_request)
|
.drop_table(drop_request)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::Runtime {
|
.map_err(|e| map_namespace_lance_error(e, name))?;
|
||||||
message: format!("Failed to drop table: {}", e),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1495,6 +1492,15 @@ mod tests {
|
|||||||
.expect("Failed to list tables");
|
.expect("Failed to list tables");
|
||||||
assert!(!table_names_after.contains(&"drop_test".to_string()));
|
assert!(!table_names_after.contains(&"drop_test".to_string()));
|
||||||
|
|
||||||
|
let error = conn
|
||||||
|
.drop_table("drop_test", &["test_ns".into()])
|
||||||
|
.await
|
||||||
|
.expect_err("dropping a missing table should fail");
|
||||||
|
assert!(
|
||||||
|
matches!(error, Error::TableNotFound { ref name, .. } if name == "drop_test"),
|
||||||
|
"expected TableNotFound, got: {error:?}"
|
||||||
|
);
|
||||||
|
|
||||||
// Verify: Cannot open dropped table
|
// Verify: Cannot open dropped table
|
||||||
let open_result = conn.open_table("drop_test").execute().await;
|
let open_result = conn.open_table("drop_test").execute().await;
|
||||||
assert!(open_result.is_err());
|
assert!(open_result.is_err());
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ use lance::io::RecordBatchStream;
|
|||||||
use lance_arrow::RecordBatchExt;
|
use lance_arrow::RecordBatchExt;
|
||||||
use lance_core::ROW_ID;
|
use lance_core::ROW_ID;
|
||||||
use lance_core::error::LanceOptionExt;
|
use lance_core::error::LanceOptionExt;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Reads a permutation of a source table based on row IDs stored in a separate table
|
/// Reads a permutation of a source table based on row IDs stored in a separate table
|
||||||
@@ -234,7 +234,14 @@ impl PermutationReader {
|
|||||||
.expect_ok()?
|
.expect_ok()?
|
||||||
.values();
|
.values();
|
||||||
|
|
||||||
let in_list: Vec<Expr> = row_ids.iter().map(|id| lit(*id)).collect();
|
let mut unique_row_ids = HashSet::with_capacity(num_rows);
|
||||||
|
let in_list: Vec<Expr> = row_ids
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter(|row_id| unique_row_ids.insert(*row_id))
|
||||||
|
.map(lit)
|
||||||
|
.collect();
|
||||||
|
let num_unique_row_ids = unique_row_ids.len();
|
||||||
|
|
||||||
let base_query = QueryRequest {
|
let base_query = QueryRequest {
|
||||||
filter: Some(QueryFilter::Datafusion(col(ROW_ID).in_list(in_list, false))),
|
filter: Some(QueryFilter::Datafusion(col(ROW_ID).in_list(in_list, false))),
|
||||||
@@ -247,7 +254,7 @@ impl PermutationReader {
|
|||||||
.query(
|
.query(
|
||||||
&AnyQuery::Query(base_query),
|
&AnyQuery::Query(base_query),
|
||||||
QueryExecutionOptions {
|
QueryExecutionOptions {
|
||||||
max_batch_length: num_rows as u32,
|
max_batch_length: num_unique_row_ids as u32,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -262,9 +269,9 @@ impl PermutationReader {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if batches.iter().map(|b| b.num_rows()).sum::<usize>() != num_rows {
|
if batches.iter().map(|b| b.num_rows()).sum::<usize>() != num_unique_row_ids {
|
||||||
return Err(Error::InvalidInput {
|
return Err(Error::InvalidInput {
|
||||||
message: "Base table returned different number of rows than the number of row IDs"
|
message: "Base table returned a different number of rows than the number of unique row IDs"
|
||||||
.to_string(),
|
.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -504,6 +511,7 @@ impl PermutationReader {
|
|||||||
let table = Table::from(self.base_table.clone());
|
let table = Table::from(self.base_table.clone());
|
||||||
let batches = table
|
let batches = table
|
||||||
.take_offsets(offsets.to_vec())
|
.take_offsets(offsets.to_vec())
|
||||||
|
.preserve_order()
|
||||||
.select(selection.clone())
|
.select(selection.clone())
|
||||||
.execute()
|
.execute()
|
||||||
.await?
|
.await?
|
||||||
@@ -803,10 +811,10 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Take offsets in reverse order and verify returned rows match that order
|
// Take offsets in reverse order and verify returned rows match that order
|
||||||
let offsets = vec![5, 3, 1, 0];
|
let offsets = vec![5, 3, 5, 1, 0];
|
||||||
let batch = reader.take_offsets(&offsets, Select::All).await.unwrap();
|
let batch = reader.take_offsets(&offsets, Select::All).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(batch.num_rows(), 4);
|
assert_eq!(batch.num_rows(), 5);
|
||||||
|
|
||||||
let idx_values = batch
|
let idx_values = batch
|
||||||
.column(0)
|
.column(0)
|
||||||
@@ -820,6 +828,52 @@ mod tests {
|
|||||||
assert_eq!(idx_values, expected);
|
assert_eq!(idx_values, expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_take_offsets_preserves_repeated_rows_in_permutation() {
|
||||||
|
let base_table = lance_datagen::gen_batch()
|
||||||
|
.col("idx", lance_datagen::array::step::<Int32Type>())
|
||||||
|
.into_mem_table("tbl", RowCount::from(5), BatchCount::from(1))
|
||||||
|
.await;
|
||||||
|
let base_row_ids = collect_column::<UInt64Type>(&base_table, "_rowid").await;
|
||||||
|
let permutation_row_ids = vec![
|
||||||
|
base_row_ids[3],
|
||||||
|
base_row_ids[1],
|
||||||
|
base_row_ids[3],
|
||||||
|
base_row_ids[2],
|
||||||
|
];
|
||||||
|
let permutation_batch = RecordBatch::try_new(
|
||||||
|
Arc::new(Schema::new(vec![
|
||||||
|
Field::new("row_id", DataType::UInt64, false),
|
||||||
|
Field::new(SPLIT_ID_COLUMN, DataType::UInt64, false),
|
||||||
|
])),
|
||||||
|
vec![
|
||||||
|
Arc::new(UInt64Array::from(permutation_row_ids)),
|
||||||
|
Arc::new(UInt64Array::from(vec![0; 4])),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let permutation_table = virtual_table("row_ids", &permutation_batch).await;
|
||||||
|
let reader = PermutationReader::try_from_tables(
|
||||||
|
base_table.base_table().clone(),
|
||||||
|
permutation_table.base_table().clone(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let batch = reader
|
||||||
|
.take_offsets(&[0, 1, 2, 3], Select::All)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let idx_values = batch
|
||||||
|
.column(0)
|
||||||
|
.as_primitive::<Int32Type>()
|
||||||
|
.values()
|
||||||
|
.to_vec();
|
||||||
|
|
||||||
|
assert_eq!(idx_values, vec![3, 1, 3, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_take_offsets_with_column_selection() {
|
async fn test_take_offsets_with_column_selection() {
|
||||||
let (base_table, row_ids_table, row_ids) = setup_permutation_tables(10).await;
|
let (base_table, row_ids_table, row_ids) = setup_permutation_tables(10).await;
|
||||||
@@ -883,17 +937,17 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// With no permutation table, take_offsets uses the base table directly
|
// With no permutation table, take_offsets uses the base table directly
|
||||||
let offsets = vec![0, 2, 4, 6];
|
let offsets = vec![0, 2, 0, 4, 6];
|
||||||
let batch = reader.take_offsets(&offsets, Select::All).await.unwrap();
|
let batch = reader.take_offsets(&offsets, Select::All).await.unwrap();
|
||||||
|
|
||||||
assert_eq!(batch.num_rows(), 4);
|
assert_eq!(batch.num_rows(), 5);
|
||||||
|
|
||||||
let idx_values = batch
|
let idx_values = batch
|
||||||
.column(0)
|
.column(0)
|
||||||
.as_primitive::<Int32Type>()
|
.as_primitive::<Int32Type>()
|
||||||
.values()
|
.values()
|
||||||
.to_vec();
|
.to_vec();
|
||||||
assert_eq!(idx_values, vec![0, 2, 4, 6]);
|
assert_eq!(idx_values, vec![0, 2, 0, 4, 6]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -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}"))]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user