Compare commits

...

19 Commits

Author SHA1 Message Date
Lance Release
ff20d12f20 Bump version: 0.26.0-beta.2 → 0.26.0 2025-12-16 16:57:09 +00:00
Lance Release
5f3e133470 Bump version: 0.26.0-beta.1 → 0.26.0-beta.2 2025-12-16 16:57:07 +00:00
Jack Ye
332e722a64 feat: upgrade lance-namespace python to 0.3.2 (#2868)
Includes fix https://github.com/lance-format/lance-namespace/pull/281
2025-12-16 08:56:04 -08:00
LanceDB Robot
3f63c4f8d9 chore: update lance dependency to v1.0.0 (#2867)
## Summary
- update all lance crates to v1.0.0 using the helper script (fallbacks
to the v1.0.0 tag)
- refresh Cargo.lock to pull the new release
- add script fallback to retry with the git tag when a crates.io release
is unavailable

## Testing
- cargo clippy --workspace --tests --all-features -- -D warnings
- cargo fmt --all

Tag: https://github.com/lance-format/lance/releases/tag/v1.0.0

---------

Co-authored-by: Jack Ye <yezhaoqin@gmail.com>
2025-12-15 20:36:19 -08:00
BubbleCal
39a18baf59 feat: infer vector type to float32 if integers are out of uint8 range (#2856)
## Summary
- infer integer vector columns as float32 when any value exceeds uint8
range or is negative
- keep uint8 for integer vectors within range and nulls only
- add sync/async tests covering large integer vector inference

## Testing
- ./.venv/bin/pytest python/python/tests/test_table.py -k
"large_int_vectors"
2025-12-08 17:10:25 +08:00
Lance Release
0960e19559 Bump version: 0.23.0-beta.0 → 0.23.0-beta.1 2025-12-05 00:36:39 +00:00
Lance Release
e5321ba311 Bump version: 0.26.0-beta.0 → 0.26.0-beta.1 2025-12-05 00:35:17 +00:00
Jack Ye
f523191d21 feat: make java client builder generic (#2851)
In #2845 we ported the lancedb integration in lance-namespace to
lancedb. But that is too specific to RestNamespace. We can improve the
user entry point so that we can put local mode and future version of the
Flight SQL-based LanceDB server all behind this single
`LanceDbNamespaceClientBuilder` API.

Also I renamed `namespace` to `namesapceClient` to avoid confusion with
the namespace path.
2025-12-04 16:34:32 -08:00
Jack Ye
4c3790cde4 chore: remove java-jni from cargo workspace (#2849)
Fixes
https://github.com/lancedb/lancedb/actions/runs/19945349063/job/57193307680
2025-12-04 16:31:37 -08:00
Jack Ye
ff75f2467b feat: use rest namespace for lancedb java sdk (#2845)
After the refactoring on both client and server side, we should have the
ability to fully use lance REST namespace to call into LanceDB cloud and
enterprise. We can avoid having a JNI implementation (which today does
not really do anything except for vending a connection object), and just
use lance-core's RestNamespace.

We will at this moment have a LanceDbRestNamespaceBuilder to allow users
to more easily build the RestNamespace to talk to LanceDB Cloud or
Enterprise endpoint.

In the future, we could extend this further to also support the local
mode through DirectoryNamespace. That will be a separated PR.
2025-12-04 13:53:47 -08:00
Lance Release
6f79770248 Bump version: 0.22.4-beta.3 → 0.23.0-beta.0 2025-12-04 19:33:37 +00:00
Lance Release
a497db66f9 Bump version: 0.25.4-beta.3 → 0.26.0-beta.0 2025-12-04 19:32:04 +00:00
LanceDB Robot
f5076269fe chore: update lance dependency to v1.1.0-beta.1 (#2844)
## Summary
- bump Lance workspace dependencies to v1.1.0-beta.1 via
ci/set_lance_version.py
- verified `cargo clippy --workspace --tests --all-features -- -D
warnings` and `cargo fmt --all` after the bump

Link: refs/tags/v1.1.0-beta.1
2025-12-04 00:44:50 -08:00
BubbleCal
a61461331c feat: add IVF SQ index support and HNSW aliases (#2832)
Adds IVF_SQ index config through Rust core and Python bindings, plus
alias names IvfHnswSq/Pq for backward compatibility. Updates
remote/table helpers and types to accept the new index type. Includes
tests covering IVF SQ creation and alias usage.
2025-12-04 00:25:44 +08:00
Jack Ye
b0170ea86a fix: table_names error at root namespace (#2842)
Root namepace should be passed in as an empty vector, not None.
2025-12-02 23:53:29 -08:00
Jack Ye
d1efc6ad8a refactor!: use namespace models directly for namespace operations (#2806)
1. Use generated models in lance-namespace for request response models
to avoid multiple layers of conversions
2. Make sure the API is consistent with the namespace spec
3. Deprecate the table_names API in favor of the list_tables API in
namespace that allows full pagination support without the need to have
sorted table names
4. Add describe_namespace API which was a miss in the original
implementation
2025-12-02 22:41:04 -08:00
Jack Ye
9d638cb3c7 feat: support namespace server side query (#2811)
Currently a table in a namespace is still backed with a `NativeTable`,
which means after getting the location of the table and optional storage
options override from `namespace.describe_table`, all things work like a
normal local table. However, namespace also supports `query_table`,
which is exactly the same API as remote table. This PR adds a
`server_side_query` capability, when enabled, it runs the query by
calling `namespace.query_table`. For namespace that implements the
operation (e.g. REST namespace), this could hit a backend server that
could execute the query faster (e.g. using a distributed engine).
2025-12-02 21:04:12 -08:00
Jack Ye
3cd73f9f5a refactor!: deprecate mac x86 support (#2836)
We have very low download stats for mac x86, and also latest github
runners for mac are all arm, so it makes sense at this point to
deprecate x86 support in general.
2025-12-02 14:12:51 -08:00
Lance Release
b2d06a3a73 Bump version: 0.22.4-beta.2 → 0.22.4-beta.3 2025-12-02 22:01:59 +00:00
91 changed files with 3061 additions and 1868 deletions

View File

@@ -1,5 +1,5 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "0.22.4-beta.2" current_version = "0.23.0-beta.1"
parse = """(?x) parse = """(?x)
(?P<major>0|[1-9]\\d*)\\. (?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\. (?P<minor>0|[1-9]\\d*)\\.
@@ -72,3 +72,9 @@ search = "\nversion = \"{current_version}\""
filename = "nodejs/Cargo.toml" filename = "nodejs/Cargo.toml"
replace = "\nversion = \"{new_version}\"" replace = "\nversion = \"{new_version}\""
search = "\nversion = \"{current_version}\"" search = "\nversion = \"{current_version}\""
# Java documentation
[[tool.bumpversion.files]]
filename = "docs/src/java/java.md"
replace = "<version>{new_version}</version>"
search = "<version>{current_version}</version>"

View File

@@ -1,76 +1,35 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Build and publish Java packages name: Build and publish Java packages
on: on:
release: push:
types: [released] tags:
- "v*"
pull_request: pull_request:
paths: paths:
- .github/workflows/java-publish.yml - .github/workflows/java-publish.yml
jobs: jobs:
macos-arm64: publish:
name: Build on MacOS Arm64 name: Build and Publish
runs-on: macos-14 runs-on: ubuntu-24.04
timeout-minutes: 45
defaults:
run:
working-directory: ./java/core/lancedb-jni
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Install dependencies
run: |
brew install protobuf
- name: Build release
run: |
cargo build --release
- uses: actions/upload-artifact@v4
with:
name: liblancedb_jni_darwin_aarch64.zip
path: target/release/liblancedb_jni.dylib
retention-days: 1
if-no-files-found: error
linux-arm64:
name: Build on Linux Arm64
runs-on: warp-ubuntu-2204-arm64-8x
timeout-minutes: 45
defaults:
run:
working-directory: ./java/core/lancedb-jni
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
cache-workspaces: "./java/core/lancedb-jni"
# Disable full debug symbol generation to speed up CI build and keep memory down
# "1" means line tables only, which is useful for panic tracebacks.
rustflags: "-C debuginfo=1"
- name: Install dependencies
run: |
sudo apt -y -qq update
sudo apt install -y protobuf-compiler libssl-dev pkg-config
- name: Build release
run: |
cargo build --release
- uses: actions/upload-artifact@v4
with:
name: liblancedb_jni_linux_aarch64.zip
path: target/release/liblancedb_jni.so
retention-days: 1
if-no-files-found: error
linux-x86:
runs-on: warp-ubuntu-2204-x64-8x
timeout-minutes: 30 timeout-minutes: 30
needs: [macos-arm64, linux-arm64]
defaults: defaults:
run: run:
working-directory: ./java working-directory: ./java
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
- name: Set up Java 8 - name: Set up Java 8
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
@@ -82,40 +41,30 @@ jobs:
server-password: SONATYPE_TOKEN server-password: SONATYPE_TOKEN
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }} gpg-passphrase: ${{ secrets.GPG_PASSPHRASE }}
- name: Install dependencies - name: Set git config
run: | run: |
sudo apt -y -qq update git config --global user.email "dev+gha@lancedb.com"
sudo apt install -y protobuf-compiler libssl-dev pkg-config git config --global user.name "LanceDB Github Runner"
- name: Download artifact
uses: actions/download-artifact@v4
- name: Copy native libs
run: |
mkdir -p ./core/target/classes/nativelib/darwin-aarch64 ./core/target/classes/nativelib/linux-aarch64
cp ../liblancedb_jni_darwin_aarch64.zip/liblancedb_jni.dylib ./core/target/classes/nativelib/darwin-aarch64/liblancedb_jni.dylib
cp ../liblancedb_jni_linux_aarch64.zip/liblancedb_jni.so ./core/target/classes/nativelib/linux-aarch64/liblancedb_jni.so
- name: Dry run - name: Dry run
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
run: | run: |
mvn --batch-mode -DskipTests -Drust.release.build=true package ./mvnw --batch-mode -DskipTests package -pl lancedb-core -am
- name: Set github - name: Publish
run: | if: startsWith(github.ref, 'refs/tags/v')
git config --global user.email "LanceDB Github Runner"
git config --global user.name "dev+gha@lancedb.com"
- name: Publish with Java 8
if: github.event_name == 'release'
run: | run: |
echo "use-agent" >> ~/.gnupg/gpg.conf echo "use-agent" >> ~/.gnupg/gpg.conf
echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf echo "pinentry-mode loopback" >> ~/.gnupg/gpg.conf
export GPG_TTY=$(tty) export GPG_TTY=$(tty)
mvn --batch-mode -DskipTests -Drust.release.build=true -DpushChanges=false -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} deploy -P deploy-to-ossrh ./mvnw --batch-mode -DskipTests -DpushChanges=false -Dgpg.passphrase=${{ secrets.GPG_PASSPHRASE }} deploy -pl lancedb-core -am -P deploy-to-ossrh
env: env:
SONATYPE_USER: ${{ secrets.SONATYPE_USER }} SONATYPE_USER: ${{ secrets.SONATYPE_USER }}
SONATYPE_TOKEN: ${{ secrets.SONATYPE_TOKEN }} SONATYPE_TOKEN: ${{ secrets.SONATYPE_TOKEN }}
report-failure: report-failure:
name: Report Workflow Failure name: Report Workflow Failure
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [linux-arm64, linux-x86, macos-arm64] needs: [publish]
if: always() && (github.event_name == 'release' || github.event_name == 'workflow_dispatch') if: always() && failure() && startsWith(github.ref, 'refs/tags/v')
permissions: permissions:
contents: read contents: read
issues: write issues: write

View File

@@ -1,118 +1,46 @@
name: Build and Run Java JNI Tests # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: Build Java LanceDB Core
on: on:
push: push:
branches: branches:
- main - main
paths: paths:
- java/** - java/**
- .github/workflows/java.yml
pull_request: pull_request:
paths: paths:
- java/** - java/**
- rust/**
- .github/workflows/java.yml - .github/workflows/java.yml
env:
# This env var is used by Swatinem/rust-cache@v2 for the cache
# key, so we set it to make sure it is always consistent.
CARGO_TERM_COLOR: always
# Disable full debug symbol generation to speed up CI build and keep memory down
# "1" means line tables only, which is useful for panic tracebacks.
RUSTFLAGS: "-C debuginfo=1"
RUST_BACKTRACE: "1"
# according to: https://matklad.github.io/2021/09/04/fast-rust-builds.html
# CI builds are faster with incremental disabled.
CARGO_INCREMENTAL: "0"
CARGO_BUILD_JOBS: "1"
jobs: jobs:
linux-build-java-11: build-java:
runs-on: ubuntu-22.04 runs-on: ubuntu-24.04
name: ubuntu-22.04 + Java 11 name: Build
defaults: defaults:
run: run:
working-directory: ./java working-directory: ./java
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2 - name: Set up Java 17
with:
workspaces: java/core/lancedb-jni
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt
- name: Run cargo fmt
run: cargo fmt --check
working-directory: ./java/core/lancedb-jni
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y protobuf-compiler libssl-dev
- name: Install Java 11
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 11
cache: "maven"
- name: Java Style Check
run: mvn checkstyle:check
# Disable because of issues in lancedb rust core code
# - name: Rust Clippy
# working-directory: java/core/lancedb-jni
# run: cargo clippy --all-targets -- -D warnings
- name: Running tests with Java 11
run: mvn clean test
linux-build-java-17:
runs-on: ubuntu-22.04
name: ubuntu-22.04 + Java 17
defaults:
run:
working-directory: ./java
steps:
- name: Checkout repository
uses: actions/checkout@v4
- uses: Swatinem/rust-cache@v2
with:
workspaces: java/core/lancedb-jni
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt
- name: Run cargo fmt
run: cargo fmt --check
working-directory: ./java/core/lancedb-jni
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y protobuf-compiler libssl-dev
- name: Install Java 17
uses: actions/setup-java@v4 uses: actions/setup-java@v4
with: with:
distribution: temurin distribution: temurin
java-version: 17 java-version: 17
cache: "maven" cache: "maven"
- run: echo "JAVA_17=$JAVA_HOME" >> $GITHUB_ENV
- name: Java Style Check - name: Java Style Check
run: mvn checkstyle:check run: ./mvnw checkstyle:check
# Disable because of issues in lancedb rust core code - name: Build and install
# - name: Rust Clippy run: ./mvnw clean install
# working-directory: java/core/lancedb-jni
# run: cargo clippy --all-targets -- -D warnings
- name: Running tests with Java 17
run: |
export JAVA_TOOL_OPTIONS="$JAVA_TOOL_OPTIONS \
-XX:+IgnoreUnrecognizedVMOptions \
--add-opens=java.base/java.lang=ALL-UNNAMED \
--add-opens=java.base/java.lang.invoke=ALL-UNNAMED \
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED \
--add-opens=java.base/java.io=ALL-UNNAMED \
--add-opens=java.base/java.net=ALL-UNNAMED \
--add-opens=java.base/java.nio=ALL-UNNAMED \
--add-opens=java.base/java.util=ALL-UNNAMED \
--add-opens=java.base/java.util.concurrent=ALL-UNNAMED \
--add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED \
--add-opens=java.base/jdk.internal.ref=ALL-UNNAMED \
--add-opens=java.base/sun.nio.ch=ALL-UNNAMED \
--add-opens=java.base/sun.nio.cs=ALL-UNNAMED \
--add-opens=java.base/sun.security.action=ALL-UNNAMED \
--add-opens=java.base/sun.util.calendar=ALL-UNNAMED \
--add-opens=java.security.jgss/sun.security.krb5=ALL-UNNAMED \
-Djdk.reflect.useDirectMethodHandle=false \
-Dio.netty.tryReflectionSetAccessible=true"
JAVA_HOME=$JAVA_17 mvn clean test

View File

@@ -88,7 +88,7 @@ jobs:
npm install -g @napi-rs/cli npm install -g @napi-rs/cli
- name: Build - name: Build
run: | run: |
npm ci npm ci --include=optional
npm run build:debug -- --profile ci npm run build:debug -- --profile ci
npm run tsc npm run tsc
- name: Setup localstack - name: Setup localstack
@@ -146,7 +146,7 @@ jobs:
npm install -g @napi-rs/cli npm install -g @napi-rs/cli
- name: Build - name: Build
run: | run: |
npm ci npm ci --include=optional
npm run build:debug -- --profile ci npm run build:debug -- --profile ci
npm run tsc npm run tsc
- name: Test - name: Test

View File

@@ -97,12 +97,6 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
settings: settings:
- target: x86_64-apple-darwin
host: macos-latest
features: ","
pre_build: |-
brew install protobuf
rustup target add x86_64-apple-darwin
- target: aarch64-apple-darwin - target: aarch64-apple-darwin
host: macos-latest host: macos-latest
features: fp16kernels features: fp16kernels

View File

@@ -64,8 +64,6 @@ jobs:
strategy: strategy:
matrix: matrix:
config: config:
- target: x86_64-apple-darwin
runner: macos-15-large
- target: aarch64-apple-darwin - target: aarch64-apple-darwin
runner: warp-macos-14-arm64-6x runner: warp-macos-14-arm64-6x
env: env:

View File

@@ -49,8 +49,8 @@ jobs:
type-check: type-check:
name: "Type Check" name: "Type Check"
timeout-minutes: 30 timeout-minutes: 60
runs-on: "ubuntu-22.04" runs-on: ubuntu-2404-8x-x64
defaults: defaults:
run: run:
shell: bash shell: bash
@@ -78,7 +78,7 @@ jobs:
doctest: doctest:
name: "Doctest" name: "Doctest"
timeout-minutes: 30 timeout-minutes: 60
runs-on: ubuntu-2404-8x-x64 runs-on: ubuntu-2404-8x-x64
defaults: defaults:
run: run:
@@ -143,16 +143,9 @@ jobs:
- name: Delete wheels - name: Delete wheels
run: rm -rf target/wheels run: rm -rf target/wheels
platform: platform:
name: "Mac: ${{ matrix.config.name }}" name: "Mac"
timeout-minutes: 30 timeout-minutes: 30
strategy: runs-on: macos-14
matrix:
config:
- name: x86
runner: macos-15-large
- name: Arm
runner: macos-14
runs-on: "${{ matrix.config.runner }}"
defaults: defaults:
run: run:
shell: bash shell: bash

195
Cargo.lock generated
View File

@@ -1456,12 +1456,6 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0" checksum = "4f4c707c6a209cbe82d10abd08e1ea8995e9ea937d2550646e02798948992be0"
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]] [[package]]
name = "cexpr" name = "cexpr"
version = "0.6.0" version = "0.6.0"
@@ -1565,16 +1559,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]] [[package]]
name = "comfy-table" name = "comfy-table"
version = "7.1.2" version = "7.1.2"
@@ -3102,8 +3086,8 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]] [[package]]
name = "fsst" name = "fsst"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow-array", "arrow-array",
"rand 0.9.2", "rand 0.9.2",
@@ -4381,28 +4365,6 @@ dependencies = [
"jiff-tzdb", "jiff-tzdb",
] ]
[[package]]
name = "jni"
version = "0.21.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.34" version = "0.1.34"
@@ -4460,8 +4422,8 @@ dependencies = [
[[package]] [[package]]
name = "lance" name = "lance"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow", "arrow",
"arrow-arith", "arrow-arith",
@@ -4526,8 +4488,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-arrow" name = "lance-arrow"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow-array", "arrow-array",
"arrow-buffer", "arrow-buffer",
@@ -4545,8 +4507,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-bitpacking" name = "lance-bitpacking"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrayref", "arrayref",
"paste", "paste",
@@ -4555,8 +4517,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-core" name = "lance-core"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow-array", "arrow-array",
"arrow-buffer", "arrow-buffer",
@@ -4592,8 +4554,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-datafusion" name = "lance-datafusion"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow", "arrow",
"arrow-array", "arrow-array",
@@ -4623,8 +4585,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-datagen" name = "lance-datagen"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow", "arrow",
"arrow-array", "arrow-array",
@@ -4641,8 +4603,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-encoding" name = "lance-encoding"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow-arith", "arrow-arith",
"arrow-array", "arrow-array",
@@ -4679,8 +4641,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-file" name = "lance-file"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow-arith", "arrow-arith",
"arrow-array", "arrow-array",
@@ -4712,8 +4674,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-geo" name = "lance-geo"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"datafusion", "datafusion",
"geo-types", "geo-types",
@@ -4724,8 +4686,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-index" name = "lance-index"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow", "arrow",
"arrow-arith", "arrow-arith",
@@ -4786,8 +4748,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-io" name = "lance-io"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow", "arrow",
"arrow-arith", "arrow-arith",
@@ -4827,8 +4789,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-linalg" name = "lance-linalg"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow-array", "arrow-array",
"arrow-buffer", "arrow-buffer",
@@ -4844,8 +4806,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-namespace" name = "lance-namespace"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow", "arrow",
"async-trait", "async-trait",
@@ -4857,8 +4819,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-namespace-impls" name = "lance-namespace-impls"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow", "arrow",
"arrow-ipc", "arrow-ipc",
@@ -4896,8 +4858,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-table" name = "lance-table"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow", "arrow",
"arrow-array", "arrow-array",
@@ -4936,8 +4898,8 @@ dependencies = [
[[package]] [[package]]
name = "lance-testing" name = "lance-testing"
version = "1.0.0-beta.16" version = "1.0.0"
source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0-beta.16#ff89675ebaca362412c3ec9d62d2de3885a57727" source = "git+https://github.com/lance-format/lance.git?tag=v1.0.0#a0979691926f57afd5d3ac90bf6e5bb11188c0c3"
dependencies = [ dependencies = [
"arrow-array", "arrow-array",
"arrow-schema", "arrow-schema",
@@ -4948,7 +4910,7 @@ dependencies = [
[[package]] [[package]]
name = "lancedb" name = "lancedb"
version = "0.22.4-beta.2" version = "0.23.0-beta.1"
dependencies = [ dependencies = [
"ahash", "ahash",
"anyhow", "anyhow",
@@ -5025,25 +4987,9 @@ dependencies = [
"walkdir", "walkdir",
] ]
[[package]]
name = "lancedb-jni"
version = "0.10.0"
dependencies = [
"arrow",
"arrow-schema",
"jni",
"lance",
"lancedb",
"lazy_static",
"serde",
"serde_json",
"snafu",
"tokio",
]
[[package]] [[package]]
name = "lancedb-nodejs" name = "lancedb-nodejs"
version = "0.22.4-beta.2" version = "0.23.0-beta.1"
dependencies = [ dependencies = [
"arrow-array", "arrow-array",
"arrow-ipc", "arrow-ipc",
@@ -5063,7 +5009,7 @@ dependencies = [
[[package]] [[package]]
name = "lancedb-python" name = "lancedb-python"
version = "0.25.4-beta.2" version = "0.26.0-beta.1"
dependencies = [ dependencies = [
"arrow", "arrow",
"async-trait", "async-trait",
@@ -5071,6 +5017,7 @@ dependencies = [
"futures", "futures",
"lance-core", "lance-core",
"lance-io", "lance-io",
"lance-namespace",
"lancedb", "lancedb",
"pin-project", "pin-project",
"pyo3", "pyo3",
@@ -9505,15 +9452,6 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -9550,21 +9488,6 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -9607,12 +9530,6 @@ dependencies = [
"windows-link 0.1.3", "windows-link 0.1.3",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -9625,12 +9542,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -9643,12 +9554,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@@ -9673,12 +9578,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@@ -9691,12 +9590,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@@ -9709,12 +9602,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -9727,12 +9614,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"

View File

@@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["rust/lancedb", "nodejs", "python", "java/core/lancedb-jni"] members = ["rust/lancedb", "nodejs", "python"]
# Python package needs to be built by maturin. # Python package needs to be built by maturin.
exclude = ["python"] exclude = ["python"]
resolver = "2" resolver = "2"
@@ -15,20 +15,20 @@ categories = ["database-implementations"]
rust-version = "1.78.0" rust-version = "1.78.0"
[workspace.dependencies] [workspace.dependencies]
lance = { "version" = "=1.0.0-beta.16", default-features = false, "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance = { "version" = "=1.0.0", default-features = false, "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-core = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-datagen = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-file = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=1.0.0-beta.16", default-features = false, "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-io = { "version" = "=1.0.0", default-features = false, "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-index = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-linalg = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-namespace = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=1.0.0-beta.16", default-features = false, "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-namespace-impls = { "version" = "=1.0.0", default-features = false, "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-table = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-testing = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-datafusion = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-encoding = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=1.0.0-beta.16", "tag" = "v1.0.0-beta.16", "git" = "https://github.com/lance-format/lance.git" } lance-arrow = { "version" = "=1.0.0", "tag" = "v1.0.0", "git" = "https://github.com/lance-format/lance.git" }
ahash = "0.8" ahash = "0.8"
# Note that this one does not include pyarrow # Note that this one does not include pyarrow
arrow = { version = "56.2", optional = false } arrow = { version = "56.2", optional = false }

View File

@@ -229,6 +229,29 @@ def set_local_version():
update_cargo_toml(line_updater) update_cargo_toml(line_updater)
def update_lockfiles(version: str, fallback_to_git: bool = False):
"""
Update Cargo metadata and optionally fall back to using the git tag if the
requested crates.io version is unavailable.
"""
try:
print("Updating lockfiles...", file=sys.stderr, end="")
run_command("cargo metadata > /dev/null")
print(" done.", file=sys.stderr)
except Exception as e:
if fallback_to_git and "failed to select a version" in str(e):
print(
f" failed for crates.io v{version}, retrying with git tag...",
file=sys.stderr,
)
set_preview_version(version)
print("Updating lockfiles...", file=sys.stderr, end="")
run_command("cargo metadata > /dev/null")
print(" done.", file=sys.stderr)
else:
raise
parser = argparse.ArgumentParser(description="Set the version of the Lance package.") parser = argparse.ArgumentParser(description="Set the version of the Lance package.")
parser.add_argument( parser.add_argument(
"version", "version",
@@ -244,6 +267,7 @@ if args.version == "stable":
file=sys.stderr, file=sys.stderr,
) )
set_stable_version(latest_stable_version) set_stable_version(latest_stable_version)
update_lockfiles(latest_stable_version)
elif args.version == "preview": elif args.version == "preview":
latest_preview_version = get_latest_preview_version() latest_preview_version = get_latest_preview_version()
print( print(
@@ -251,8 +275,10 @@ elif args.version == "preview":
file=sys.stderr, file=sys.stderr,
) )
set_preview_version(latest_preview_version) set_preview_version(latest_preview_version)
update_lockfiles(latest_preview_version)
elif args.version == "local": elif args.version == "local":
set_local_version() set_local_version()
update_lockfiles("local")
else: else:
# Parse the version number. # Parse the version number.
version = args.version version = args.version
@@ -262,9 +288,7 @@ else:
if "beta" in version: if "beta" in version:
set_preview_version(version) set_preview_version(version)
update_lockfiles(version)
else: else:
set_stable_version(version) set_stable_version(version)
update_lockfiles(version, fallback_to_git=True)
print("Updating lockfiles...", file=sys.stderr, end="")
run_command("cargo metadata > /dev/null")
print(" done.", file=sys.stderr)

View File

@@ -123,6 +123,7 @@ nav:
- Overview: index.md - Overview: index.md
- Python: python/python.md - Python: python/python.md
- Javascript/TypeScript: js/globals.md - Javascript/TypeScript: js/globals.md
- Java: java/java.md
- Rust: https://docs.rs/lancedb/latest/lancedb/index.html - Rust: https://docs.rs/lancedb/latest/lancedb/index.html
extra_css: extra_css:

View File

@@ -4,4 +4,5 @@ This page contains the API reference for the SDKs supported by the LanceDB team.
- [Python](python/python.md) - [Python](python/python.md)
- [JavaScript/TypeScript](js/globals.md) - [JavaScript/TypeScript](js/globals.md)
- [Java](java/java.md)
- [Rust](https://docs.rs/lancedb/latest/lancedb/index.html) - [Rust](https://docs.rs/lancedb/latest/lancedb/index.html)

499
docs/src/java/java.md Normal file
View File

@@ -0,0 +1,499 @@
# Java SDK
The LanceDB Java SDK provides a convenient way to interact with LanceDB Cloud and Enterprise deployments using the Lance REST Namespace API.
!!! note
The Java SDK currently only works for LanceDB remote database that connects to LanceDB Cloud and Enterprise.
Local database support is a work in progress. Check [LANCEDB-2848](https://github.com/lancedb/lancedb/issues/2848) for the latest progress.
## Installation
Add the following dependency to your `pom.xml`:
```xml
<dependency>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-core</artifactId>
<version>0.23.0-beta.1</version>
</dependency>
```
## Quick Start
### Connecting to LanceDB Cloud
```java
import com.lancedb.LanceDbNamespaceClientBuilder;
import org.lance.namespace.LanceNamespace;
// If your DB url is db://example-db, then your database here is example-db
LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
.apiKey("your_lancedb_cloud_api_key")
.database("your_database_name")
.build();
```
### Connecting to LanceDB Enterprise
For LanceDB Enterprise deployments with a custom endpoint:
```java
LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
.apiKey("your_lancedb_enterprise_api_key")
.database("your_database_name")
.endpoint("<your_enterprise_endpoint>")
.build();
```
### Configuration Options
| Method | Description | Required |
|--------|-------------|----------|
| `apiKey(String)` | LanceDB API key | Yes |
| `database(String)` | Database name | Yes |
| `endpoint(String)` | Custom endpoint URL for Enterprise deployments | No |
| `region(String)` | AWS region (default: "us-east-1") | No |
| `config(String, String)` | Additional configuration parameters | No |
## Metadata Operations
### Creating a Namespace
Namespaces organize tables hierarchically. Create a namespace before creating tables within it:
```java
import org.lance.namespace.model.CreateNamespaceRequest;
import org.lance.namespace.model.CreateNamespaceResponse;
// Create a child namespace
CreateNamespaceRequest request = new CreateNamespaceRequest();
request.setId(Arrays.asList("my_namespace"));
CreateNamespaceResponse response = namespaceClient.createNamespace(request);
```
You can also create nested namespaces:
```java
// Create a nested namespace: parent/child
CreateNamespaceRequest request = new CreateNamespaceRequest();
request.setId(Arrays.asList("parent_namespace", "child_namespace"));
CreateNamespaceResponse response = namespaceClient.createNamespace(request);
```
### Describing a Namespace
```java
import org.lance.namespace.model.DescribeNamespaceRequest;
import org.lance.namespace.model.DescribeNamespaceResponse;
DescribeNamespaceRequest request = new DescribeNamespaceRequest();
request.setId(Arrays.asList("my_namespace"));
DescribeNamespaceResponse response = namespaceClient.describeNamespace(request);
System.out.println("Namespace properties: " + response.getProperties());
```
### Listing Namespaces
```java
import org.lance.namespace.model.ListNamespacesRequest;
import org.lance.namespace.model.ListNamespacesResponse;
// List all namespaces at root level
ListNamespacesRequest request = new ListNamespacesRequest();
request.setId(Arrays.asList()); // Empty for root
ListNamespacesResponse response = namespaceClient.listNamespaces(request);
for (String ns : response.getNamespaces()) {
System.out.println("Namespace: " + ns);
}
// List child namespaces under a parent
ListNamespacesRequest childRequest = new ListNamespacesRequest();
childRequest.setId(Arrays.asList("parent_namespace"));
ListNamespacesResponse childResponse = namespaceClient.listNamespaces(childRequest);
```
### Listing Tables
```java
import org.lance.namespace.model.ListTablesRequest;
import org.lance.namespace.model.ListTablesResponse;
// List tables in a namespace
ListTablesRequest request = new ListTablesRequest();
request.setId(Arrays.asList("my_namespace"));
ListTablesResponse response = namespaceClient.listTables(request);
for (String table : response.getTables()) {
System.out.println("Table: " + table);
}
```
### Dropping a Namespace
```java
import org.lance.namespace.model.DropNamespaceRequest;
import org.lance.namespace.model.DropNamespaceResponse;
DropNamespaceRequest request = new DropNamespaceRequest();
request.setId(Arrays.asList("my_namespace"));
DropNamespaceResponse response = namespaceClient.dropNamespace(request);
```
### Describing a Table
```java
import org.lance.namespace.model.DescribeTableRequest;
import org.lance.namespace.model.DescribeTableResponse;
DescribeTableRequest request = new DescribeTableRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
DescribeTableResponse response = namespaceClient.describeTable(request);
System.out.println("Table version: " + response.getVersion());
System.out.println("Schema fields: " + response.getSchema().getFields());
```
### Dropping a Table
```java
import org.lance.namespace.model.DropTableRequest;
import org.lance.namespace.model.DropTableResponse;
DropTableRequest request = new DropTableRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
DropTableResponse response = namespaceClient.dropTable(request);
```
## Writing Data
### Creating a Table
Tables are created within a namespace by providing data in Apache Arrow IPC format:
```java
import org.lance.namespace.LanceNamespace;
import org.lance.namespace.model.CreateTableRequest;
import org.lance.namespace.model.CreateTableResponse;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.IntVector;
import org.apache.arrow.vector.VarCharVector;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.vector.complex.FixedSizeListVector;
import org.apache.arrow.vector.Float4Vector;
import org.apache.arrow.vector.ipc.ArrowStreamWriter;
import org.apache.arrow.vector.types.FloatingPointPrecision;
import org.apache.arrow.vector.types.pojo.ArrowType;
import org.apache.arrow.vector.types.pojo.Field;
import org.apache.arrow.vector.types.pojo.FieldType;
import org.apache.arrow.vector.types.pojo.Schema;
import java.io.ByteArrayOutputStream;
import java.nio.channels.Channels;
import java.util.Arrays;
// Create schema with id, name, and embedding fields
Schema schema = new Schema(Arrays.asList(
new Field("id", FieldType.nullable(new ArrowType.Int(32, true)), null),
new Field("name", FieldType.nullable(new ArrowType.Utf8()), null),
new Field("embedding",
FieldType.nullable(new ArrowType.FixedSizeList(128)),
Arrays.asList(new Field("item",
FieldType.nullable(new ArrowType.FloatingPoint(FloatingPointPrecision.SINGLE)),
null)))
));
try (BufferAllocator allocator = new RootAllocator();
VectorSchemaRoot root = VectorSchemaRoot.create(schema, allocator)) {
// Populate data
root.setRowCount(3);
IntVector idVector = (IntVector) root.getVector("id");
VarCharVector nameVector = (VarCharVector) root.getVector("name");
FixedSizeListVector embeddingVector = (FixedSizeListVector) root.getVector("embedding");
Float4Vector embeddingData = (Float4Vector) embeddingVector.getDataVector();
for (int i = 0; i < 3; i++) {
idVector.setSafe(i, i + 1);
nameVector.setSafe(i, ("item_" + i).getBytes());
embeddingVector.setNotNull(i);
for (int j = 0; j < 128; j++) {
embeddingData.setSafe(i * 128 + j, (float) i);
}
}
idVector.setValueCount(3);
nameVector.setValueCount(3);
embeddingData.setValueCount(3 * 128);
embeddingVector.setValueCount(3);
// Serialize to Arrow IPC format
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (ArrowStreamWriter writer = new ArrowStreamWriter(root, null, Channels.newChannel(out))) {
writer.start();
writer.writeBatch();
writer.end();
}
byte[] tableData = out.toByteArray();
// Create table in a namespace
CreateTableRequest request = new CreateTableRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
CreateTableResponse response = namespaceClient.createTable(request, tableData);
}
```
### Insert
```java
import org.lance.namespace.model.InsertIntoTableRequest;
import org.lance.namespace.model.InsertIntoTableResponse;
// Prepare data in Arrow IPC format (similar to create table example)
byte[] insertData = prepareArrowData();
InsertIntoTableRequest request = new InsertIntoTableRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
request.setMode(InsertIntoTableRequest.ModeEnum.APPEND);
InsertIntoTableResponse response = namespaceClient.insertIntoTable(request, insertData);
System.out.println("New version: " + response.getVersion());
```
### Update
Update rows matching a predicate condition:
```java
import org.lance.namespace.model.UpdateTableRequest;
import org.lance.namespace.model.UpdateTableResponse;
UpdateTableRequest request = new UpdateTableRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
// Predicate to select rows to update
request.setPredicate("id = 1");
// Set new values using SQL expressions as [column_name, expression] pairs
request.setUpdates(Arrays.asList(
Arrays.asList("name", "'updated_name'")
));
UpdateTableResponse response = namespaceClient.updateTable(request);
System.out.println("Updated rows: " + response.getUpdatedRows());
```
### Delete
Delete rows matching a predicate condition:
```java
import org.lance.namespace.model.DeleteFromTableRequest;
import org.lance.namespace.model.DeleteFromTableResponse;
DeleteFromTableRequest request = new DeleteFromTableRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
// Predicate to select rows to delete
request.setPredicate("id > 100");
DeleteFromTableResponse response = namespaceClient.deleteFromTable(request);
System.out.println("New version: " + response.getVersion());
```
### Merge Insert (Upsert)
Merge insert allows you to update existing rows and insert new rows in a single operation based on a key column:
```java
import org.lance.namespace.model.MergeInsertIntoTableRequest;
import org.lance.namespace.model.MergeInsertIntoTableResponse;
// Prepare data with rows to update (id=2,3) and new rows (id=4)
byte[] mergeData = prepareArrowData(); // Contains rows with id=2,3,4
MergeInsertIntoTableRequest request = new MergeInsertIntoTableRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
// Match on the "id" column
request.setOn("id");
// Update all columns when a matching row is found
request.setWhenMatchedUpdateAll(true);
// Insert new rows when no match is found
request.setWhenNotMatchedInsertAll(true);
MergeInsertIntoTableResponse response = namespaceClient.mergeInsertIntoTable(request, mergeData);
System.out.println("Updated rows: " + response.getNumUpdatedRows());
System.out.println("Inserted rows: " + response.getNumInsertedRows());
```
## Querying Data
### Counting Rows
```java
import org.lance.namespace.model.CountTableRowsRequest;
CountTableRowsRequest request = new CountTableRowsRequest();
request.setId(Arrays.asList("my_namespace", "my_table"));
Long rowCount = namespaceClient.countTableRows(request);
System.out.println("Row count: " + rowCount);
```
### Vector Search
```java
import org.lance.namespace.model.QueryTableRequest;
import org.lance.namespace.model.QueryTableRequestVector;
QueryTableRequest query = new QueryTableRequest();
query.setId(Arrays.asList("my_namespace", "my_table"));
query.setK(10); // Return top 10 results
// Set the query vector
List<Float> queryVector = new ArrayList<>();
for (int i = 0; i < 128; i++) {
queryVector.add(1.0f);
}
QueryTableRequestVector vector = new QueryTableRequestVector();
vector.setSingleVector(queryVector);
query.setVector(vector);
// Specify columns to return
query.setColumns(Arrays.asList("id", "name", "embedding"));
// Execute query - returns Arrow IPC format
byte[] result = namespaceClient.queryTable(query);
```
### Full Text Search
```java
import org.lance.namespace.model.QueryTableRequest;
import org.lance.namespace.model.QueryTableRequestFullTextQuery;
import org.lance.namespace.model.StringFtsQuery;
QueryTableRequest query = new QueryTableRequest();
query.setId(Arrays.asList("my_namespace", "my_table"));
query.setK(10);
// Set full text search query
StringFtsQuery stringQuery = new StringFtsQuery();
stringQuery.setQuery("search terms");
stringQuery.setColumns(Arrays.asList("text_column"));
QueryTableRequestFullTextQuery fts = new QueryTableRequestFullTextQuery();
fts.setStringQuery(stringQuery);
query.setFullTextQuery(fts);
// Specify columns to return
query.setColumns(Arrays.asList("id", "text_column"));
byte[] result = namespaceClient.queryTable(query);
```
### Query with Filter
```java
QueryTableRequest query = new QueryTableRequest();
query.setId(Arrays.asList("my_namespace", "my_table"));
query.setK(10);
query.setFilter("id > 50");
query.setColumns(Arrays.asList("id", "name"));
byte[] result = namespaceClient.queryTable(query);
```
### Query with Prefilter
```java
QueryTableRequest query = new QueryTableRequest();
query.setId(Arrays.asList("my_namespace", "my_table"));
query.setK(5);
query.setPrefilter(true); // Apply filter before vector search
query.setFilter("category = 'electronics'");
// Set query vector
QueryTableRequestVector vector = new QueryTableRequestVector();
vector.setSingleVector(queryVector);
query.setVector(vector);
byte[] result = namespaceClient.queryTable(query);
```
### Reading Query Results
Query results are returned in Apache Arrow IPC file format. Here's how to read them:
```java
import org.apache.arrow.vector.ipc.ArrowFileReader;
import org.apache.arrow.vector.VectorSchemaRoot;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
// Helper class to read Arrow data from byte array
class ByteArraySeekableByteChannel implements SeekableByteChannel {
private final byte[] data;
private long position = 0;
private boolean isOpen = true;
public ByteArraySeekableByteChannel(byte[] data) {
this.data = data;
}
@Override
public int read(ByteBuffer dst) {
int remaining = dst.remaining();
int available = (int) (data.length - position);
if (available <= 0) return -1;
int toRead = Math.min(remaining, available);
dst.put(data, (int) position, toRead);
position += toRead;
return toRead;
}
@Override public long position() { return position; }
@Override public SeekableByteChannel position(long newPosition) { position = newPosition; return this; }
@Override public long size() { return data.length; }
@Override public boolean isOpen() { return isOpen; }
@Override public void close() { isOpen = false; }
@Override public int write(ByteBuffer src) { throw new UnsupportedOperationException(); }
@Override public SeekableByteChannel truncate(long size) { throw new UnsupportedOperationException(); }
}
// Read query results
byte[] queryResult = namespaceClient.queryTable(query);
try (BufferAllocator allocator = new RootAllocator();
ArrowFileReader reader = new ArrowFileReader(
new ByteArraySeekableByteChannel(queryResult), allocator)) {
for (int i = 0; i < reader.getRecordBlocks().size(); i++) {
reader.loadRecordBatch(reader.getRecordBlocks().get(i));
VectorSchemaRoot root = reader.getVectorSchemaRoot();
// Access data
IntVector idVector = (IntVector) root.getVector("id");
VarCharVector nameVector = (VarCharVector) root.getVector("name");
for (int row = 0; row < root.getRowCount(); row++) {
int id = idVector.get(row);
String name = new String(nameVector.get(row));
System.out.println("Row " + row + ": id=" + id + ", name=" + name);
}
}
}
```

28
java/Makefile Normal file
View File

@@ -0,0 +1,28 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
.PHONY: build-lancedb
build-lancedb:
./mvnw spotless:apply -pl lancedb-core -am
./mvnw install -pl lancedb-core -am
.PHONY: test-lancedb
test-lancedb:
# Requires LANCEDB_DB and LANCEDB_API_KEY environment variables
./mvnw test -pl lancedb-core -P integration-tests
.PHONY: clean
clean:
./mvnw clean
.PHONY: build
build: build-lancedb

View File

@@ -7,10 +7,11 @@
For LanceDB Cloud, use the simplified builder API: For LanceDB Cloud, use the simplified builder API:
```java ```java
import com.lancedb.lance.namespace.LanceRestNamespace; import com.lancedb.LanceDbNamespaceClientBuilder;
import org.lance.namespace.LanceNamespace;
// If your DB url is db://example-db, then your database here is example-db // If your DB url is db://example-db, then your database here is example-db
LanceRestNamespace namespace = LanceDBRestNamespaces.builder() LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
.apiKey("your_lancedb_cloud_api_key") .apiKey("your_lancedb_cloud_api_key")
.database("your_database_name") .database("your_database_name")
.build(); .build();
@@ -18,13 +19,13 @@ LanceRestNamespace namespace = LanceDBRestNamespaces.builder()
### LanceDB Enterprise ### LanceDB Enterprise
For Enterprise deployments, use your VPC endpoint: For Enterprise deployments, use your custom endpoint:
```java ```java
LanceRestNamespace namespace = LanceDBRestNamespaces.builder() LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
.apiKey("your_lancedb_enterprise_api_key") .apiKey("your_lancedb_enterprise_api_key")
.database("your-top-dir") // Your top level folder under your cloud bucket, e.g. s3://your-bucket/your-top-dir/ .database("your_database_name")
.hostOverride("http://<vpc_endpoint_dns_name>:80") .endpoint("<your_enterprise_endpoint>")
.build(); .build();
``` ```
@@ -33,5 +34,11 @@ LanceRestNamespace namespace = LanceDBRestNamespaces.builder()
Build: Build:
```shell ```shell
./mvnw install ./mvnw install -pl lancedb-core -am
```
Run tests:
```shell
./mvnw test -pl lancedb-core
``` ```

View File

@@ -1,30 +0,0 @@
[package]
name = "lancedb-jni"
description = "JNI bindings for LanceDB"
# TODO modify lancedb/Cargo.toml for version and dependencies
version = "0.10.0"
edition.workspace = true
repository.workspace = true
readme.workspace = true
license.workspace = true
keywords.workspace = true
categories.workspace = true
publish = false
[lib]
crate-type = ["cdylib"]
[dependencies]
lancedb = { path = "../../../rust/lancedb", default-features = false }
lance = { workspace = true }
arrow = { workspace = true, features = ["ffi"] }
arrow-schema.workspace = true
tokio = "1.46"
jni = "0.21.1"
snafu.workspace = true
lazy_static.workspace = true
serde = { version = "^1" }
serde_json = { version = "1" }
[features]
default = ["lancedb/default"]

View File

@@ -1,133 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
use crate::ffi::JNIEnvExt;
use crate::traits::IntoJava;
use crate::{Error, RT};
use jni::objects::{JObject, JString, JValue};
use jni::JNIEnv;
pub const NATIVE_CONNECTION: &str = "nativeConnectionHandle";
use crate::Result;
use lancedb::connection::{connect, Connection};
#[derive(Clone)]
pub struct BlockingConnection {
pub(crate) inner: Connection,
}
impl BlockingConnection {
pub fn create(dataset_uri: &str) -> Result<Self> {
let inner = RT.block_on(connect(dataset_uri).execute())?;
Ok(Self { inner })
}
pub fn table_names(
&self,
start_after: Option<String>,
limit: Option<i32>,
) -> Result<Vec<String>> {
let mut op = self.inner.table_names();
if let Some(start_after) = start_after {
op = op.start_after(start_after);
}
if let Some(limit) = limit {
op = op.limit(limit as u32);
}
Ok(RT.block_on(op.execute())?)
}
}
impl IntoJava for BlockingConnection {
fn into_java<'a>(self, env: &mut JNIEnv<'a>) -> JObject<'a> {
attach_native_connection(env, self)
}
}
fn attach_native_connection<'local>(
env: &mut JNIEnv<'local>,
connection: BlockingConnection,
) -> JObject<'local> {
let j_connection = create_java_connection_object(env);
// This block sets a native Rust object (Connection) as a field in the Java object (j_Connection).
// Caution: This creates a potential for memory leaks. The Rust object (Connection) is not
// automatically garbage-collected by Java, and its memory will not be freed unless
// explicitly handled.
//
// To prevent memory leaks, ensure the following:
// 1. The Java object (`j_Connection`) should implement the `java.io.Closeable` interface.
// 2. Users of this Java object should be instructed to always use it within a try-with-resources
// statement (or manually call the `close()` method) to ensure that `self.close()` is invoked.
match unsafe { env.set_rust_field(&j_connection, NATIVE_CONNECTION, connection) } {
Ok(_) => j_connection,
Err(err) => {
env.throw_new(
"java/lang/RuntimeException",
format!("Failed to set native handle for Connection: {}", err),
)
.expect("Error throwing exception");
JObject::null()
}
}
}
fn create_java_connection_object<'a>(env: &mut JNIEnv<'a>) -> JObject<'a> {
env.new_object("com/lancedb/lancedb/Connection", "()V", &[])
.expect("Failed to create Java Lance Connection instance")
}
#[no_mangle]
pub extern "system" fn Java_com_lancedb_lancedb_Connection_releaseNativeConnection(
mut env: JNIEnv,
j_connection: JObject,
) {
let _: BlockingConnection = unsafe {
env.take_rust_field(j_connection, NATIVE_CONNECTION)
.expect("Failed to take native Connection handle")
};
}
#[no_mangle]
pub extern "system" fn Java_com_lancedb_lancedb_Connection_connect<'local>(
mut env: JNIEnv<'local>,
_obj: JObject,
dataset_uri_object: JString,
) -> JObject<'local> {
let dataset_uri: String = ok_or_throw!(env, env.get_string(&dataset_uri_object)).into();
let blocking_connection = ok_or_throw!(env, BlockingConnection::create(&dataset_uri));
blocking_connection.into_java(&mut env)
}
#[no_mangle]
pub extern "system" fn Java_com_lancedb_lancedb_Connection_tableNames<'local>(
mut env: JNIEnv<'local>,
j_connection: JObject,
start_after_obj: JObject, // Optional<String>
limit_obj: JObject, // Optional<Integer>
) -> JObject<'local> {
ok_or_throw!(
env,
inner_table_names(&mut env, j_connection, start_after_obj, limit_obj)
)
}
fn inner_table_names<'local>(
env: &mut JNIEnv<'local>,
j_connection: JObject,
start_after_obj: JObject, // Optional<String>
limit_obj: JObject, // Optional<Integer>
) -> Result<JObject<'local>> {
let start_after = env.get_string_opt(&start_after_obj)?;
let limit = env.get_int_opt(&limit_obj)?;
let conn =
unsafe { env.get_rust_field::<_, _, BlockingConnection>(j_connection, NATIVE_CONNECTION) }?;
let table_names = conn.table_names(start_after, limit)?;
drop(conn);
let j_names = env.new_object("java/util/ArrayList", "()V", &[])?;
for item in table_names {
let jstr_item = env.new_string(item)?;
let item_jobj = JObject::from(jstr_item);
let item_gen = JValue::Object(&item_jobj);
env.call_method(&j_names, "add", "(Ljava/lang/Object;)Z", &[item_gen])?;
}
Ok(j_names)
}

View File

@@ -1,217 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
use std::str::Utf8Error;
use arrow_schema::ArrowError;
use jni::errors::Error as JniError;
use serde_json::Error as JsonError;
use snafu::{Location, Snafu};
type BoxedError = Box<dyn std::error::Error + Send + Sync + 'static>;
/// Java Exception types
pub enum JavaException {
IllegalArgumentException,
IOException,
RuntimeException,
}
impl JavaException {
pub fn as_str(&self) -> &str {
match self {
Self::IllegalArgumentException => "java/lang/IllegalArgumentException",
Self::IOException => "java/io/IOException",
Self::RuntimeException => "java/lang/RuntimeException",
}
}
}
/// TODO(lu) change to lancedb-jni
#[derive(Debug, Snafu)]
#[snafu(visibility(pub))]
pub enum Error {
#[snafu(display("JNI error: {message}, {location}"))]
Jni { message: String, location: Location },
#[snafu(display("Invalid argument: {message}, {location}"))]
InvalidArgument { message: String, location: Location },
#[snafu(display("IO error: {source}, {location}"))]
IO {
source: BoxedError,
location: Location,
},
#[snafu(display("Arrow error: {message}, {location}"))]
Arrow { message: String, location: Location },
#[snafu(display("Index error: {message}, {location}"))]
Index { message: String, location: Location },
#[snafu(display("JSON error: {message}, {location}"))]
JSON { message: String, location: Location },
#[snafu(display("Dataset at path {path} was not found, {location}"))]
DatasetNotFound { path: String, location: Location },
#[snafu(display("Dataset already exists: {uri}, {location}"))]
DatasetAlreadyExists { uri: String, location: Location },
#[snafu(display("Table '{name}' already exists"))]
TableAlreadyExists { name: String },
#[snafu(display("Table '{name}' was not found: {source}"))]
TableNotFound {
name: String,
source: Box<dyn std::error::Error + Send + Sync>,
},
#[snafu(display("Invalid table name '{name}': {reason}"))]
InvalidTableName { name: String, reason: String },
#[snafu(display("Embedding function '{name}' was not found: {reason}, {location}"))]
EmbeddingFunctionNotFound {
name: String,
reason: String,
location: Location,
},
#[snafu(display("Other Lance error: {message}, {location}"))]
OtherLance { message: String, location: Location },
#[snafu(display("Other LanceDB error: {message}, {location}"))]
OtherLanceDB { message: String, location: Location },
}
impl Error {
/// Throw as Java Exception
pub fn throw(&self, env: &mut jni::JNIEnv) {
match self {
Self::InvalidArgument { .. }
| Self::DatasetNotFound { .. }
| Self::DatasetAlreadyExists { .. }
| Self::TableAlreadyExists { .. }
| Self::TableNotFound { .. }
| Self::InvalidTableName { .. }
| Self::EmbeddingFunctionNotFound { .. } => {
self.throw_as(env, JavaException::IllegalArgumentException)
}
Self::IO { .. } | Self::Index { .. } => self.throw_as(env, JavaException::IOException),
Self::Arrow { .. }
| Self::JSON { .. }
| Self::OtherLance { .. }
| Self::OtherLanceDB { .. }
| Self::Jni { .. } => self.throw_as(env, JavaException::RuntimeException),
}
}
/// Throw as an concrete Java Exception
pub fn throw_as(&self, env: &mut jni::JNIEnv, exception: JavaException) {
let message = &format!(
"Error when throwing Java exception: {}:{}",
exception.as_str(),
self
);
env.throw_new(exception.as_str(), self.to_string())
.expect(message);
}
}
pub type Result<T> = std::result::Result<T, Error>;
trait ToSnafuLocation {
fn to_snafu_location(&'static self) -> snafu::Location;
}
impl ToSnafuLocation for std::panic::Location<'static> {
fn to_snafu_location(&'static self) -> snafu::Location {
snafu::Location::new(self.file(), self.line(), self.column())
}
}
impl From<JniError> for Error {
#[track_caller]
fn from(source: JniError) -> Self {
Self::Jni {
message: source.to_string(),
location: std::panic::Location::caller().to_snafu_location(),
}
}
}
impl From<Utf8Error> for Error {
#[track_caller]
fn from(source: Utf8Error) -> Self {
Self::InvalidArgument {
message: source.to_string(),
location: std::panic::Location::caller().to_snafu_location(),
}
}
}
impl From<ArrowError> for Error {
#[track_caller]
fn from(source: ArrowError) -> Self {
Self::Arrow {
message: source.to_string(),
location: std::panic::Location::caller().to_snafu_location(),
}
}
}
impl From<JsonError> for Error {
#[track_caller]
fn from(source: JsonError) -> Self {
Self::JSON {
message: source.to_string(),
location: std::panic::Location::caller().to_snafu_location(),
}
}
}
impl From<lance::Error> for Error {
#[track_caller]
fn from(source: lance::Error) -> Self {
match source {
lance::Error::DatasetNotFound {
path,
source: _,
location,
} => Self::DatasetNotFound { path, location },
lance::Error::DatasetAlreadyExists { uri, location } => {
Self::DatasetAlreadyExists { uri, location }
}
lance::Error::IO { source, location } => Self::IO { source, location },
lance::Error::Arrow { message, location } => Self::Arrow { message, location },
lance::Error::Index { message, location } => Self::Index { message, location },
lance::Error::InvalidInput { source, location } => Self::InvalidArgument {
message: source.to_string(),
location,
},
_ => Self::OtherLance {
message: source.to_string(),
location: std::panic::Location::caller().to_snafu_location(),
},
}
}
}
impl From<lancedb::Error> for Error {
#[track_caller]
fn from(source: lancedb::Error) -> Self {
match source {
lancedb::Error::InvalidTableName { name, reason } => {
Self::InvalidTableName { name, reason }
}
lancedb::Error::InvalidInput { message } => Self::InvalidArgument {
message,
location: std::panic::Location::caller().to_snafu_location(),
},
lancedb::Error::TableNotFound { name, source } => Self::TableNotFound { name, source },
lancedb::Error::TableAlreadyExists { name } => Self::TableAlreadyExists { name },
lancedb::Error::EmbeddingFunctionNotFound { name, reason } => {
Self::EmbeddingFunctionNotFound {
name,
reason,
location: std::panic::Location::caller().to_snafu_location(),
}
}
lancedb::Error::Arrow { source } => Self::Arrow {
message: source.to_string(),
location: std::panic::Location::caller().to_snafu_location(),
},
lancedb::Error::Lance { source } => Self::from(source),
_ => Self::OtherLanceDB {
message: source.to_string(),
location: std::panic::Location::caller().to_snafu_location(),
},
}
}
}

View File

@@ -1,194 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
use core::slice;
use jni::objects::{JByteBuffer, JObjectArray, JString};
use jni::sys::jobjectArray;
use jni::{objects::JObject, JNIEnv};
use crate::error::{Error, Result};
/// TODO(lu) import from lance-jni without duplicate
/// Extend JNIEnv with helper functions.
pub trait JNIEnvExt {
/// Get integers from Java List<Integer> object.
fn get_integers(&mut self, obj: &JObject) -> Result<Vec<i32>>;
/// Get strings from Java List<String> object.
#[allow(dead_code)]
fn get_strings(&mut self, obj: &JObject) -> Result<Vec<String>>;
/// Get strings from Java String[] object.
/// Note that get Option<Vec<String>> from Java Optional<String[]> just doesn't work.
#[allow(unused)]
fn get_strings_array(&mut self, obj: jobjectArray) -> Result<Vec<String>>;
/// Get Option<String> from Java Optional<String>.
fn get_string_opt(&mut self, obj: &JObject) -> Result<Option<String>>;
/// Get Option<Vec<String>> from Java Optional<List<String>>.
#[allow(unused)]
fn get_strings_opt(&mut self, obj: &JObject) -> Result<Option<Vec<String>>>;
/// Get Option<i32> from Java Optional<Integer>.
fn get_int_opt(&mut self, obj: &JObject) -> Result<Option<i32>>;
/// Get Option<Vec<i32>> from Java Optional<List<Integer>>.
fn get_ints_opt(&mut self, obj: &JObject) -> Result<Option<Vec<i32>>>;
/// Get Option<i64> from Java Optional<Long>.
#[allow(unused)]
fn get_long_opt(&mut self, obj: &JObject) -> Result<Option<i64>>;
/// Get Option<u64> from Java Optional<Long>.
#[allow(unused)]
fn get_u64_opt(&mut self, obj: &JObject) -> Result<Option<u64>>;
/// Get Option<&[u8]> from Java Optional<ByteBuffer>.
#[allow(unused)]
fn get_bytes_opt(&mut self, obj: &JObject) -> Result<Option<&[u8]>>;
fn get_optional<T, F>(&mut self, obj: &JObject, f: F) -> Result<Option<T>>
where
F: FnOnce(&mut JNIEnv, &JObject) -> Result<T>;
}
impl JNIEnvExt for JNIEnv<'_> {
fn get_integers(&mut self, obj: &JObject) -> Result<Vec<i32>> {
let list = self.get_list(obj)?;
let mut iter = list.iter(self)?;
let mut results = Vec::with_capacity(list.size(self)? as usize);
while let Some(elem) = iter.next(self)? {
let int_obj = self.call_method(elem, "intValue", "()I", &[])?;
let int_value = int_obj.i()?;
results.push(int_value);
}
Ok(results)
}
fn get_strings(&mut self, obj: &JObject) -> Result<Vec<String>> {
let list = self.get_list(obj)?;
let mut iter = list.iter(self)?;
let mut results = Vec::with_capacity(list.size(self)? as usize);
while let Some(elem) = iter.next(self)? {
let jstr = JString::from(elem);
let val = self.get_string(&jstr)?;
results.push(val.to_str()?.to_string())
}
Ok(results)
}
fn get_strings_array(&mut self, obj: jobjectArray) -> Result<Vec<String>> {
let jobject_array = unsafe { JObjectArray::from_raw(obj) };
let array_len = self.get_array_length(&jobject_array)?;
let mut res: Vec<String> = Vec::new();
for i in 0..array_len {
let item: JString = self.get_object_array_element(&jobject_array, i)?.into();
res.push(self.get_string(&item)?.into());
}
Ok(res)
}
fn get_string_opt(&mut self, obj: &JObject) -> Result<Option<String>> {
self.get_optional(obj, |env, inner_obj| {
let java_obj_gen = env.call_method(inner_obj, "get", "()Ljava/lang/Object;", &[])?;
let java_string_obj = java_obj_gen.l()?;
let jstr = JString::from(java_string_obj);
let val = env.get_string(&jstr)?;
Ok(val.to_str()?.to_string())
})
}
fn get_strings_opt(&mut self, obj: &JObject) -> Result<Option<Vec<String>>> {
self.get_optional(obj, |env, inner_obj| {
let java_obj_gen = env.call_method(inner_obj, "get", "()Ljava/lang/Object;", &[])?;
let java_list_obj = java_obj_gen.l()?;
env.get_strings(&java_list_obj)
})
}
fn get_int_opt(&mut self, obj: &JObject) -> Result<Option<i32>> {
self.get_optional(obj, |env, inner_obj| {
let java_obj_gen = env.call_method(inner_obj, "get", "()Ljava/lang/Object;", &[])?;
let java_int_obj = java_obj_gen.l()?;
let int_obj = env.call_method(java_int_obj, "intValue", "()I", &[])?;
let int_value = int_obj.i()?;
Ok(int_value)
})
}
fn get_ints_opt(&mut self, obj: &JObject) -> Result<Option<Vec<i32>>> {
self.get_optional(obj, |env, inner_obj| {
let java_obj_gen = env.call_method(inner_obj, "get", "()Ljava/lang/Object;", &[])?;
let java_list_obj = java_obj_gen.l()?;
env.get_integers(&java_list_obj)
})
}
fn get_long_opt(&mut self, obj: &JObject) -> Result<Option<i64>> {
self.get_optional(obj, |env, inner_obj| {
let java_obj_gen = env.call_method(inner_obj, "get", "()Ljava/lang/Object;", &[])?;
let java_long_obj = java_obj_gen.l()?;
let long_obj = env.call_method(java_long_obj, "longValue", "()J", &[])?;
let long_value = long_obj.j()?;
Ok(long_value)
})
}
fn get_u64_opt(&mut self, obj: &JObject) -> Result<Option<u64>> {
self.get_optional(obj, |env, inner_obj| {
let java_obj_gen = env.call_method(inner_obj, "get", "()Ljava/lang/Object;", &[])?;
let java_long_obj = java_obj_gen.l()?;
let long_obj = env.call_method(java_long_obj, "longValue", "()J", &[])?;
let long_value = long_obj.j()?;
Ok(long_value as u64)
})
}
fn get_bytes_opt(&mut self, obj: &JObject) -> Result<Option<&[u8]>> {
self.get_optional(obj, |env, inner_obj| {
let java_obj_gen = env.call_method(inner_obj, "get", "()Ljava/lang/Object;", &[])?;
let java_byte_buffer_obj = java_obj_gen.l()?;
let j_byte_buffer = JByteBuffer::from(java_byte_buffer_obj);
let raw_data = env.get_direct_buffer_address(&j_byte_buffer)?;
let capacity = env.get_direct_buffer_capacity(&j_byte_buffer)?;
let data = unsafe { slice::from_raw_parts(raw_data, capacity) };
Ok(data)
})
}
fn get_optional<T, F>(&mut self, obj: &JObject, f: F) -> Result<Option<T>>
where
F: FnOnce(&mut JNIEnv, &JObject) -> Result<T>,
{
if obj.is_null() {
return Ok(None);
}
let is_present = self.call_method(obj, "isPresent", "()Z", &[])?;
if !is_present.z()? {
// TODO(lu): put get java object into here cuz can only get java Object
Ok(None)
} else {
f(self, obj).map(Some)
}
}
}
#[no_mangle]
pub extern "system" fn Java_com_lancedb_lance_test_JniTestHelper_parseInts(
mut env: JNIEnv,
_obj: JObject,
list_obj: JObject, // List<Integer>
) {
ok_or_throw_without_return!(env, env.get_integers(&list_obj));
}
#[no_mangle]
pub extern "system" fn Java_com_lancedb_lance_test_JniTestHelper_parseIntsOpt(
mut env: JNIEnv,
_obj: JObject,
list_obj: JObject, // Optional<List<Integer>>
) {
ok_or_throw_without_return!(env, env.get_ints_opt(&list_obj));
}

View File

@@ -1,57 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
use lazy_static::lazy_static;
// TODO import from lance-jni without duplicate
#[macro_export]
macro_rules! ok_or_throw {
($env:expr, $result:expr) => {
match $result {
Ok(value) => value,
Err(err) => {
Error::from(err).throw(&mut $env);
return JObject::null();
}
}
};
}
macro_rules! ok_or_throw_without_return {
($env:expr, $result:expr) => {
match $result {
Ok(value) => value,
Err(err) => {
Error::from(err).throw(&mut $env);
return;
}
}
};
}
#[macro_export]
macro_rules! ok_or_throw_with_return {
($env:expr, $result:expr, $ret:expr) => {
match $result {
Ok(value) => value,
Err(err) => {
Error::from(err).throw(&mut $env);
return $ret;
}
}
};
}
mod connection;
pub mod error;
mod ffi;
mod traits;
pub use error::{Error, Result};
lazy_static! {
static ref RT: tokio::runtime::Runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed to create tokio runtime");
}

View File

@@ -1,114 +0,0 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
use jni::objects::{JMap, JObject, JString, JValue};
use jni::JNIEnv;
use crate::Result;
#[allow(dead_code)]
pub trait FromJObject<T> {
fn extract(&self) -> Result<T>;
}
/// Convert a Rust type into a Java Object.
pub trait IntoJava {
fn into_java<'a>(self, env: &mut JNIEnv<'a>) -> JObject<'a>;
}
impl FromJObject<i32> for JObject<'_> {
fn extract(&self) -> Result<i32> {
Ok(JValue::from(self).i()?)
}
}
impl FromJObject<i64> for JObject<'_> {
fn extract(&self) -> Result<i64> {
Ok(JValue::from(self).j()?)
}
}
impl FromJObject<f32> for JObject<'_> {
fn extract(&self) -> Result<f32> {
Ok(JValue::from(self).f()?)
}
}
impl FromJObject<f64> for JObject<'_> {
fn extract(&self) -> Result<f64> {
Ok(JValue::from(self).d()?)
}
}
#[allow(dead_code)]
pub trait FromJString {
fn extract(&self, env: &mut JNIEnv) -> Result<String>;
}
impl FromJString for JString<'_> {
fn extract(&self, env: &mut JNIEnv) -> Result<String> {
Ok(env.get_string(self)?.into())
}
}
pub trait JMapExt {
#[allow(dead_code)]
fn get_string(&self, env: &mut JNIEnv, key: &str) -> Result<Option<String>>;
#[allow(dead_code)]
fn get_i32(&self, env: &mut JNIEnv, key: &str) -> Result<Option<i32>>;
#[allow(dead_code)]
fn get_i64(&self, env: &mut JNIEnv, key: &str) -> Result<Option<i64>>;
#[allow(dead_code)]
fn get_f32(&self, env: &mut JNIEnv, key: &str) -> Result<Option<f32>>;
#[allow(dead_code)]
fn get_f64(&self, env: &mut JNIEnv, key: &str) -> Result<Option<f64>>;
}
#[allow(dead_code)]
fn get_map_value<T>(env: &mut JNIEnv, map: &JMap, key: &str) -> Result<Option<T>>
where
for<'a> JObject<'a>: FromJObject<T>,
{
let key_obj: JObject = env.new_string(key)?.into();
if let Some(value) = map.get(env, &key_obj)? {
if value.is_null() {
Ok(None)
} else {
Ok(Some(value.extract()?))
}
} else {
Ok(None)
}
}
impl JMapExt for JMap<'_, '_, '_> {
fn get_string(&self, env: &mut JNIEnv, key: &str) -> Result<Option<String>> {
let key_obj: JObject = env.new_string(key)?.into();
if let Some(value) = self.get(env, &key_obj)? {
let value_str: JString = value.into();
Ok(Some(value_str.extract(env)?))
} else {
Ok(None)
}
}
fn get_i32(&self, env: &mut JNIEnv, key: &str) -> Result<Option<i32>> {
get_map_value(env, self, key)
}
fn get_i64(&self, env: &mut JNIEnv, key: &str) -> Result<Option<i64>> {
get_map_value(env, self, key)
}
fn get_f32(&self, env: &mut JNIEnv, key: &str) -> Result<Option<f32>> {
get_map_value(env, self, key)
}
fn get_f64(&self, env: &mut JNIEnv, key: &str) -> Result<Option<f64>> {
get_map_value(env, self, key)
}
}

View File

@@ -1,103 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.22.4-beta.2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>lancedb-core</artifactId>
<name>${project.artifactId}</name>
<description>LanceDB Core</description>
<packaging>jar</packaging>
<properties>
<rust.release.build>false</rust.release.build>
</properties>
<dependencies>
<dependency>
<groupId>com.lancedb</groupId>
<artifactId>lance-namespace-core</artifactId>
<version>0.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-vector</artifactId>
</dependency>
<dependency>
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-memory-netty</artifactId>
</dependency>
<dependency>
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-c-data</artifactId>
</dependency>
<dependency>
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-dataset</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>org.questdb</groupId>
<artifactId>jar-jni</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<profiles>
<profile>
<id>build-jni</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.questdb</groupId>
<artifactId>rust-maven-plugin</artifactId>
<version>1.1.1</version>
<executions>
<execution>
<id>lancedb-jni</id>
<goals>
<goal>build</goal>
</goals>
<configuration>
<path>lancedb-jni</path>
<release>${rust.release.build}</release>
<!-- Copy native libraries to target/classes for runtime access -->
<copyTo>${project.build.directory}/classes/nativelib</copyTo>
<copyWithPlatformDir>true</copyWithPlatformDir>
</configuration>
</execution>
<execution>
<id>lancedb-jni-test</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<path>lancedb-jni</path>
<release>false</release>
<verbosity>-v</verbosity>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@@ -1,108 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb.lancedb;
import io.questdb.jar.jni.JarJniLoader;
import java.io.Closeable;
import java.util.List;
import java.util.Optional;
/** Represents LanceDB database. */
public class Connection implements Closeable {
static {
JarJniLoader.loadLib(Connection.class, "/nativelib", "lancedb_jni");
}
private long nativeConnectionHandle;
/** Connect to a LanceDB instance. */
public static native Connection connect(String uri);
/**
* Get the names of all tables in the database. The names are sorted in ascending order.
*
* @return the table names
*/
public List<String> tableNames() {
return tableNames(Optional.empty(), Optional.empty());
}
/**
* Get the names of filtered tables in the database. The names are sorted in ascending order.
*
* @param limit The number of results to return.
* @return the table names
*/
public List<String> tableNames(int limit) {
return tableNames(Optional.empty(), Optional.of(limit));
}
/**
* Get the names of filtered tables in the database. The names are sorted in ascending order.
*
* @param startAfter If present, only return names that come lexicographically after the supplied
* value. This can be combined with limit to implement pagination by setting this to the last
* table name from the previous page.
* @return the table names
*/
public List<String> tableNames(String startAfter) {
return tableNames(Optional.of(startAfter), Optional.empty());
}
/**
* Get the names of filtered tables in the database. The names are sorted in ascending order.
*
* @param startAfter If present, only return names that come lexicographically after the supplied
* value. This can be combined with limit to implement pagination by setting this to the last
* table name from the previous page.
* @param limit The number of results to return.
* @return the table names
*/
public List<String> tableNames(String startAfter, int limit) {
return tableNames(Optional.of(startAfter), Optional.of(limit));
}
/**
* Get the names of filtered tables in the database. The names are sorted in ascending order.
*
* @param startAfter If present, only return names that come lexicographically after the supplied
* value. This can be combined with limit to implement pagination by setting this to the last
* table name from the previous page.
* @param limit The number of results to return.
* @return the table names
*/
public native List<String> tableNames(Optional<String> startAfter, Optional<Integer> limit);
/**
* Closes this connection and releases any system resources associated with it. If the connection
* is already closed, then invoking this method has no effect.
*/
@Override
public void close() {
if (nativeConnectionHandle != 0) {
releaseNativeConnection(nativeConnectionHandle);
nativeConnectionHandle = 0;
}
}
/**
* Native method to release the Lance connection resources associated with the given handle.
*
* @param handle The native handle to the connection resource.
*/
private native void releaseNativeConnection(long handle);
private Connection() {}
}

View File

@@ -1,135 +0,0 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb.lancedb;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.net.URL;
import java.nio.file.Path;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ConnectionTest {
private static final String[] TABLE_NAMES = {
"dataset_version", "new_empty_dataset", "test", "write_stream"
};
@TempDir static Path tempDir; // Temporary directory for the tests
private static URL lanceDbURL;
@BeforeAll
static void setUp() {
ClassLoader classLoader = ConnectionTest.class.getClassLoader();
lanceDbURL = classLoader.getResource("example_db");
}
@Test
void emptyDB() {
String databaseUri = tempDir.resolve("emptyDB").toString();
try (Connection conn = Connection.connect(databaseUri)) {
List<String> tableNames = conn.tableNames();
assertTrue(tableNames.isEmpty());
}
}
@Test
void tableNames() {
try (Connection conn = Connection.connect(lanceDbURL.toString())) {
List<String> tableNames = conn.tableNames();
assertEquals(4, tableNames.size());
for (int i = 0; i < TABLE_NAMES.length; i++) {
assertEquals(TABLE_NAMES[i], tableNames.get(i));
}
}
}
@Test
void tableNamesStartAfter() {
try (Connection conn = Connection.connect(lanceDbURL.toString())) {
assertTableNamesStartAfter(
conn, TABLE_NAMES[0], 3, TABLE_NAMES[1], TABLE_NAMES[2], TABLE_NAMES[3]);
assertTableNamesStartAfter(conn, TABLE_NAMES[1], 2, TABLE_NAMES[2], TABLE_NAMES[3]);
assertTableNamesStartAfter(conn, TABLE_NAMES[2], 1, TABLE_NAMES[3]);
assertTableNamesStartAfter(conn, TABLE_NAMES[3], 0);
assertTableNamesStartAfter(
conn, "a_dataset", 4, TABLE_NAMES[0], TABLE_NAMES[1], TABLE_NAMES[2], TABLE_NAMES[3]);
assertTableNamesStartAfter(conn, "o_dataset", 2, TABLE_NAMES[2], TABLE_NAMES[3]);
assertTableNamesStartAfter(conn, "v_dataset", 1, TABLE_NAMES[3]);
assertTableNamesStartAfter(conn, "z_dataset", 0);
}
}
private void assertTableNamesStartAfter(
Connection conn, String startAfter, int expectedSize, String... expectedNames) {
List<String> tableNames = conn.tableNames(startAfter);
assertEquals(expectedSize, tableNames.size());
for (int i = 0; i < expectedNames.length; i++) {
assertEquals(expectedNames[i], tableNames.get(i));
}
}
@Test
void tableNamesLimit() {
try (Connection conn = Connection.connect(lanceDbURL.toString())) {
for (int i = 0; i <= TABLE_NAMES.length; i++) {
List<String> tableNames = conn.tableNames(i);
assertEquals(i, tableNames.size());
for (int j = 0; j < i; j++) {
assertEquals(TABLE_NAMES[j], tableNames.get(j));
}
}
}
}
@Test
void tableNamesStartAfterLimit() {
try (Connection conn = Connection.connect(lanceDbURL.toString())) {
List<String> tableNames = conn.tableNames(TABLE_NAMES[0], 2);
assertEquals(2, tableNames.size());
assertEquals(TABLE_NAMES[1], tableNames.get(0));
assertEquals(TABLE_NAMES[2], tableNames.get(1));
tableNames = conn.tableNames(TABLE_NAMES[1], 1);
assertEquals(1, tableNames.size());
assertEquals(TABLE_NAMES[2], tableNames.get(0));
tableNames = conn.tableNames(TABLE_NAMES[2], 2);
assertEquals(1, tableNames.size());
assertEquals(TABLE_NAMES[3], tableNames.get(0));
tableNames = conn.tableNames(TABLE_NAMES[3], 2);
assertEquals(0, tableNames.size());
tableNames = conn.tableNames(TABLE_NAMES[0], 0);
assertEquals(0, tableNames.size());
// Limit larger than the number of remaining tables
tableNames = conn.tableNames(TABLE_NAMES[0], 10);
assertEquals(3, tableNames.size());
assertEquals(TABLE_NAMES[1], tableNames.get(0));
assertEquals(TABLE_NAMES[2], tableNames.get(1));
assertEquals(TABLE_NAMES[3], tableNames.get(2));
// Start after a value not in the list
tableNames = conn.tableNames("non_existent_table", 2);
assertEquals(2, tableNames.size());
assertEquals(TABLE_NAMES[2], tableNames.get(0));
assertEquals(TABLE_NAMES[3], tableNames.get(1));
// Start after the last table with a limit
tableNames = conn.tableNames(TABLE_NAMES[3], 1);
assertEquals(0, tableNames.size());
}
}
}

View File

@@ -1 +0,0 @@
$d51afd07-e3cd-4c76-9b9b-787e13fd55b0<62>=id <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*int3208name <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*string08

View File

@@ -1 +0,0 @@
$15648e72-076f-4ef1-8b90-10d305b95b3b<33>=id <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*int3208name <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*string08

View File

@@ -1 +0,0 @@
$a3689caf-4f6b-4afc-a3c7-97af75661843<34>oitem <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*string8price <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*double80vector <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>*fixed_size_list:float:28

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.22.4-beta.2</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>lancedb-lance-namespace</artifactId>
<name>${project.artifactId}</name>
<description>LanceDB Java Integration with Lance Namespace</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.lancedb</groupId>
<artifactId>lance-namespace-core</artifactId>
</dependency>
</dependencies>
</project>

99
java/lancedb-core/pom.xml Normal file
View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.23.0-beta.1</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>lancedb-core</artifactId>
<name>${project.artifactId}</name>
<description>Utilities to work with LanceDB Cloud and Enterprise via Lance REST Namespace</description>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>org.lance</groupId>
<artifactId>lance-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-vector</artifactId>
</dependency>
<dependency>
<groupId>org.apache.arrow</groupId>
<artifactId>arrow-memory-netty</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.18.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j2-impl</artifactId>
<version>2.24.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.24.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.24.3</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -11,35 +11,58 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package com.lancedb.lancedb; package com.lancedb;
import com.lancedb.lance.namespace.LanceRestNamespace; import org.lance.namespace.LanceNamespace;
import com.lancedb.lance.namespace.client.apache.ApiClient;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
/** Util class to help construct a {@link LanceRestNamespace} for LanceDB. */ /**
public class LanceDbRestNamespaces { * Util class to help construct a {@link LanceNamespace} for LanceDB.
*
* <p>For LanceDB Cloud, use the simplified builder API:
*
* <pre>{@code
* import org.lance.namespace.LanceNamespace;
*
* // If your DB url is db://example-db, then your database here is example-db
* LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
* .apiKey("your_lancedb_cloud_api_key")
* .database("your_database_name")
* .build();
* }</pre>
*
* <p>For LanceDB Enterprise deployments, use your custom endpoint:
*
* <pre>{@code
* LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
* .apiKey("your_lancedb_enterprise_api_key")
* .database("your_database_name")
* .endpoint("<your_enterprise_endpoint>")
* .build();
* }</pre>
*/
public class LanceDbNamespaceClientBuilder {
private static final String DEFAULT_REGION = "us-east-1"; private static final String DEFAULT_REGION = "us-east-1";
private static final String CLOUD_URL_PATTERN = "https://%s.%s.api.lancedb.com"; private static final String CLOUD_URL_PATTERN = "https://%s.%s.api.lancedb.com";
private String apiKey; private String apiKey;
private String database; private String database;
private Optional<String> hostOverride = Optional.empty(); private Optional<String> endpoint = Optional.empty();
private Optional<String> region = Optional.empty(); private Optional<String> region = Optional.empty();
private Map<String, String> additionalConfig = new HashMap<>(); private Map<String, String> additionalConfig = new HashMap<>();
private LanceDbRestNamespaces() {} private LanceDbNamespaceClientBuilder() {}
/** /**
* Create a new builder instance. * Create a new builder instance.
* *
* @return A new LanceRestNamespaceBuilder * @return A new LanceDbNamespaceClientBuilder
*/ */
public static LanceDbRestNamespaces builder() { public static LanceDbNamespaceClientBuilder newBuilder() {
return new LanceDbRestNamespaces(); return new LanceDbNamespaceClientBuilder();
} }
/** /**
@@ -48,7 +71,7 @@ public class LanceDbRestNamespaces {
* @param apiKey The LanceDB API key * @param apiKey The LanceDB API key
* @return This builder * @return This builder
*/ */
public LanceDbRestNamespaces apiKey(String apiKey) { public LanceDbNamespaceClientBuilder apiKey(String apiKey) {
if (apiKey == null || apiKey.trim().isEmpty()) { if (apiKey == null || apiKey.trim().isEmpty()) {
throw new IllegalArgumentException("API key cannot be null or empty"); throw new IllegalArgumentException("API key cannot be null or empty");
} }
@@ -62,7 +85,7 @@ public class LanceDbRestNamespaces {
* @param database The database name * @param database The database name
* @return This builder * @return This builder
*/ */
public LanceDbRestNamespaces database(String database) { public LanceDbNamespaceClientBuilder database(String database) {
if (database == null || database.trim().isEmpty()) { if (database == null || database.trim().isEmpty()) {
throw new IllegalArgumentException("Database cannot be null or empty"); throw new IllegalArgumentException("Database cannot be null or empty");
} }
@@ -71,25 +94,25 @@ public class LanceDbRestNamespaces {
} }
/** /**
* Set a custom host override (optional). When set, this overrides the default LanceDB Cloud URL * Set a custom endpoint URL (optional). When set, this overrides the default LanceDB Cloud URL
* construction. Use this for LanceDB Enterprise deployments. * construction. Use this for LanceDB Enterprise deployments.
* *
* @param hostOverride The complete base URL (e.g., "http://your-vpc-endpoint:80") * @param endpoint The complete base URL for your LanceDB Enterprise deployment
* @return This builder * @return This builder
*/ */
public LanceDbRestNamespaces hostOverride(String hostOverride) { public LanceDbNamespaceClientBuilder endpoint(String endpoint) {
this.hostOverride = Optional.ofNullable(hostOverride); this.endpoint = Optional.ofNullable(endpoint);
return this; return this;
} }
/** /**
* Set the region for LanceDB Cloud (optional). Defaults to "us-east-1" if not specified. This is * Set the region for LanceDB Cloud (optional). Defaults to "us-east-1" if not specified. This is
* ignored when hostOverride is set. * ignored when endpoint is set.
* *
* @param region The AWS region (e.g., "us-east-1", "eu-west-1") * @param region The AWS region (e.g., "us-east-1", "eu-west-1")
* @return This builder * @return This builder
*/ */
public LanceDbRestNamespaces region(String region) { public LanceDbNamespaceClientBuilder region(String region) {
this.region = Optional.ofNullable(region); this.region = Optional.ofNullable(region);
return this; return this;
} }
@@ -101,18 +124,18 @@ public class LanceDbRestNamespaces {
* @param value The configuration value * @param value The configuration value
* @return This builder * @return This builder
*/ */
public LanceDbRestNamespaces config(String key, String value) { public LanceDbNamespaceClientBuilder config(String key, String value) {
this.additionalConfig.put(key, value); this.additionalConfig.put(key, value);
return this; return this;
} }
/** /**
* Build the LanceRestNamespace instance. * Build the LanceNamespace instance.
* *
* @return A configured LanceRestNamespace * @return A configured LanceNamespace
* @throws IllegalStateException if required parameters are missing * @throws IllegalStateException if required parameters are missing
*/ */
public LanceRestNamespace build() { public LanceNamespace build() {
// Validate required fields // Validate required fields
if (apiKey == null) { if (apiKey == null) {
throw new IllegalStateException("API key is required"); throw new IllegalStateException("API key is required");
@@ -123,24 +146,19 @@ public class LanceDbRestNamespaces {
// Build configuration map // Build configuration map
Map<String, String> config = new HashMap<>(additionalConfig); Map<String, String> config = new HashMap<>(additionalConfig);
config.put("headers.x-lancedb-database", database); config.put("header.x-lancedb-database", database);
config.put("headers.x-api-key", apiKey); config.put("header.x-api-key", apiKey);
// Determine base URL // Determine base URL
String baseUrl; String uri;
if (hostOverride.isPresent()) { if (endpoint.isPresent()) {
baseUrl = hostOverride.get(); uri = endpoint.get();
config.put("host_override", hostOverride.get());
} else { } else {
String effectiveRegion = region.orElse(DEFAULT_REGION); String effectiveRegion = region.orElse(DEFAULT_REGION);
baseUrl = String.format(CLOUD_URL_PATTERN, database, effectiveRegion); uri = String.format(CLOUD_URL_PATTERN, database, effectiveRegion);
config.put("region", effectiveRegion);
} }
config.put("uri", uri);
// Create and configure ApiClient return LanceNamespace.connect("rest", config, null);
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(baseUrl);
return new LanceRestNamespace(apiClient, config);
} }
} }

View File

@@ -0,0 +1,96 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lancedb;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
/** Unit tests for LanceDbNamespaceClientBuilder. */
public class LanceDbNamespaceClientBuilderTest {
@Test
public void testBuilderRequiresApiKey() {
LanceDbNamespaceClientBuilder builder =
LanceDbNamespaceClientBuilder.newBuilder().database("test-db");
IllegalStateException exception = assertThrows(IllegalStateException.class, builder::build);
assertEquals("API key is required", exception.getMessage());
}
@Test
public void testBuilderRequiresDatabase() {
LanceDbNamespaceClientBuilder builder =
LanceDbNamespaceClientBuilder.newBuilder().apiKey("test-api-key");
IllegalStateException exception = assertThrows(IllegalStateException.class, builder::build);
assertEquals("Database is required", exception.getMessage());
}
@Test
public void testApiKeyCannotBeNull() {
IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() -> LanceDbNamespaceClientBuilder.newBuilder().apiKey(null));
assertEquals("API key cannot be null or empty", exception.getMessage());
}
@Test
public void testApiKeyCannotBeEmpty() {
IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() -> LanceDbNamespaceClientBuilder.newBuilder().apiKey(" "));
assertEquals("API key cannot be null or empty", exception.getMessage());
}
@Test
public void testDatabaseCannotBeNull() {
IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() -> LanceDbNamespaceClientBuilder.newBuilder().database(null));
assertEquals("Database cannot be null or empty", exception.getMessage());
}
@Test
public void testDatabaseCannotBeEmpty() {
IllegalArgumentException exception =
assertThrows(
IllegalArgumentException.class,
() -> LanceDbNamespaceClientBuilder.newBuilder().database(" "));
assertEquals("Database cannot be null or empty", exception.getMessage());
}
@Test
public void testBuilderFluentApi() {
// Verify the builder returns itself for chaining
LanceDbNamespaceClientBuilder builder = LanceDbNamespaceClientBuilder.newBuilder();
assertSame(builder, builder.apiKey("test-key"));
assertSame(builder, builder.database("test-db"));
assertSame(builder, builder.endpoint("http://localhost:8080"));
assertSame(builder, builder.region("eu-west-1"));
assertSame(builder, builder.config("custom-key", "custom-value"));
}
@Test
public void testNewBuilderCreatesNewInstance() {
LanceDbNamespaceClientBuilder builder1 = LanceDbNamespaceClientBuilder.newBuilder();
LanceDbNamespaceClientBuilder builder2 = LanceDbNamespaceClientBuilder.newBuilder();
assertNotSame(builder1, builder2);
}
}

View File

@@ -0,0 +1,32 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<configuration monitorInterval="30">
<appenders>
<Console name='Console' target='SYSTEM_ERR'>
<PatternLayout pattern='%d{HH:mm:ss.SSS} %p [%t] %C{1}.%M: %m%n'/>
</Console>
</appenders>
<loggers>
<logger name='com.lancedb' level='DEBUG' additivity='false'>
<appender-ref ref='Console'/>
</logger>
<root level='INFO'>
<appender-ref ref='Console'/>
</root>
</loggers>
</configuration>

View File

@@ -6,7 +6,7 @@
<groupId>com.lancedb</groupId> <groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId> <artifactId>lancedb-parent</artifactId>
<version>0.22.4-beta.2</version> <version>0.23.0-beta.1</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>${project.artifactId}</name> <name>${project.artifactId}</name>
<description>LanceDB Java SDK Parent POM</description> <description>LanceDB Java SDK Parent POM</description>
@@ -28,7 +28,7 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<arrow.version>15.0.0</arrow.version> <arrow.version>15.0.0</arrow.version>
<lance-namespace.verison>0.0.1</lance-namespace.verison> <lance-core.version>1.0.0-rc.2</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>
@@ -51,8 +51,7 @@
</properties> </properties>
<modules> <modules>
<module>core</module> <module>lancedb-core</module>
<module>lance-namespace</module>
</modules> </modules>
<scm> <scm>
@@ -64,9 +63,9 @@
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>com.lancedb</groupId> <groupId>org.lance</groupId>
<artifactId>lance-namespace-core</artifactId> <artifactId>lance-core</artifactId>
<version>${lance-namespace.verison}</version> <version>${lance-core.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.arrow</groupId> <groupId>org.apache.arrow</groupId>
@@ -88,21 +87,11 @@
<artifactId>arrow-dataset</artifactId> <artifactId>arrow-dataset</artifactId>
<version>${arrow.version}</version> <version>${arrow.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.questdb</groupId>
<artifactId>jar-jni</artifactId>
<version>1.1.1</version>
</dependency>
<dependency> <dependency>
<groupId>org.junit.jupiter</groupId> <groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId> <artifactId>junit-jupiter</artifactId>
<version>5.10.1</version> <version>5.10.1</version>
</dependency> </dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lancedb-nodejs" name = "lancedb-nodejs"
edition.workspace = true edition.workspace = true
version = "0.22.4-beta.2" version = "0.23.0-beta.1"
license.workspace = true license.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true

View File

@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-darwin-arm64", "name": "@lancedb/lancedb-darwin-arm64",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"os": ["darwin"], "os": ["darwin"],
"cpu": ["arm64"], "cpu": ["arm64"],
"main": "lancedb.darwin-arm64.node", "main": "lancedb.darwin-arm64.node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-darwin-x64", "name": "@lancedb/lancedb-darwin-x64",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"os": ["darwin"], "os": ["darwin"],
"cpu": ["x64"], "cpu": ["x64"],
"main": "lancedb.darwin-x64.node", "main": "lancedb.darwin-x64.node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-linux-arm64-gnu", "name": "@lancedb/lancedb-linux-arm64-gnu",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"os": ["linux"], "os": ["linux"],
"cpu": ["arm64"], "cpu": ["arm64"],
"main": "lancedb.linux-arm64-gnu.node", "main": "lancedb.linux-arm64-gnu.node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-linux-arm64-musl", "name": "@lancedb/lancedb-linux-arm64-musl",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"os": ["linux"], "os": ["linux"],
"cpu": ["arm64"], "cpu": ["arm64"],
"main": "lancedb.linux-arm64-musl.node", "main": "lancedb.linux-arm64-musl.node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-linux-x64-gnu", "name": "@lancedb/lancedb-linux-x64-gnu",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"os": ["linux"], "os": ["linux"],
"cpu": ["x64"], "cpu": ["x64"],
"main": "lancedb.linux-x64-gnu.node", "main": "lancedb.linux-x64-gnu.node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-linux-x64-musl", "name": "@lancedb/lancedb-linux-x64-musl",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"os": ["linux"], "os": ["linux"],
"cpu": ["x64"], "cpu": ["x64"],
"main": "lancedb.linux-x64-musl.node", "main": "lancedb.linux-x64-musl.node",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-win32-arm64-msvc", "name": "@lancedb/lancedb-win32-arm64-msvc",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"os": [ "os": [
"win32" "win32"
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-win32-x64-msvc", "name": "@lancedb/lancedb-win32-x64-msvc",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"os": ["win32"], "os": ["win32"],
"cpu": ["x64"], "cpu": ["x64"],
"main": "lancedb.win32-x64-msvc.node", "main": "lancedb.win32-x64-msvc.node",

View File

@@ -1,12 +1,12 @@
{ {
"name": "@lancedb/lancedb", "name": "@lancedb/lancedb",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@lancedb/lancedb", "name": "@lancedb/lancedb",
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"cpu": [ "cpu": [
"x64", "x64",
"arm64" "arm64"

View File

@@ -11,7 +11,7 @@
"ann" "ann"
], ],
"private": false, "private": false,
"version": "0.22.4-beta.2", "version": "0.23.0-beta.1",
"main": "dist/index.js", "main": "dist/index.js",
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",

View File

@@ -1,5 +1,5 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "0.25.4-beta.3" current_version = "0.26.0"
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*)\\.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lancedb-python" name = "lancedb-python"
version = "0.25.4-beta.3" version = "0.26.0"
edition.workspace = true edition.workspace = true
description = "Python bindings for LanceDB" description = "Python bindings for LanceDB"
license.workspace = true license.workspace = true
@@ -18,6 +18,7 @@ arrow = { version = "56.2", features = ["pyarrow"] }
async-trait = "0.1" async-trait = "0.1"
lancedb = { path = "../rust/lancedb", default-features = false } lancedb = { path = "../rust/lancedb", default-features = false }
lance-core.workspace = true lance-core.workspace = true
lance-namespace.workspace = true
lance-io.workspace = true lance-io.workspace = true
env_logger.workspace = true env_logger.workspace = true
pyo3 = { version = "0.25", features = ["extension-module", "abi3-py39"] } pyo3 = { version = "0.25", features = ["extension-module", "abi3-py39"] }

View File

@@ -10,7 +10,7 @@ dependencies = [
"pyarrow>=16", "pyarrow>=16",
"pydantic>=1.10", "pydantic>=1.10",
"tqdm>=4.27.0", "tqdm>=4.27.0",
"lance-namespace>=0.2.1" "lance-namespace>=0.3.2"
] ]
description = "lancedb" description = "lancedb"
authors = [{ name = "LanceDB Devs", email = "dev@lancedb.com" }] authors = [{ name = "LanceDB Devs", email = "dev@lancedb.com" }]

View File

@@ -3,10 +3,30 @@ from typing import Dict, List, Optional, Tuple, Any, TypedDict, Union, Literal
import pyarrow as pa import pyarrow as pa
from .index import BTree, IvfFlat, IvfPq, Bitmap, LabelList, HnswPq, HnswSq, FTS from .index import (
BTree,
IvfFlat,
IvfPq,
IvfSq,
Bitmap,
LabelList,
HnswPq,
HnswSq,
FTS,
)
from .io import StorageOptionsProvider from .io import StorageOptionsProvider
from lance_namespace import (
ListNamespacesResponse,
CreateNamespaceResponse,
DropNamespaceResponse,
DescribeNamespaceResponse,
ListTablesResponse,
)
from .remote import ClientConfig from .remote import ClientConfig
IvfHnswPq: type[HnswPq] = HnswPq
IvfHnswSq: type[HnswSq] = HnswSq
class Session: class Session:
def __init__( def __init__(
self, self,
@@ -26,18 +46,38 @@ class Connection(object):
async def close(self): ... async def close(self): ...
async def list_namespaces( async def list_namespaces(
self, self,
namespace: Optional[List[str]], namespace: Optional[List[str]] = None,
page_token: Optional[str], page_token: Optional[str] = None,
limit: Optional[int], limit: Optional[int] = None,
) -> List[str]: ... ) -> ListNamespacesResponse: ...
async def create_namespace(self, namespace: List[str]) -> None: ... async def create_namespace(
async def drop_namespace(self, namespace: List[str]) -> None: ... self,
namespace: List[str],
mode: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> CreateNamespaceResponse: ...
async def drop_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
behavior: Optional[str] = None,
) -> DropNamespaceResponse: ...
async def describe_namespace(
self,
namespace: List[str],
) -> DescribeNamespaceResponse: ...
async def list_tables(
self,
namespace: Optional[List[str]] = None,
page_token: Optional[str] = None,
limit: Optional[int] = None,
) -> ListTablesResponse: ...
async def table_names( async def table_names(
self, self,
namespace: Optional[List[str]], namespace: Optional[List[str]],
start_after: Optional[str], start_after: Optional[str],
limit: Optional[int], limit: Optional[int],
) -> list[str]: ... ) -> list[str]: ... # Deprecated: Use list_tables instead
async def create_table( async def create_table(
self, self,
name: str, name: str,
@@ -104,7 +144,17 @@ class Table:
async def create_index( async def create_index(
self, self,
column: str, column: str,
index: Union[IvfFlat, IvfPq, HnswPq, HnswSq, BTree, Bitmap, LabelList, FTS], index: Union[
IvfFlat,
IvfSq,
IvfPq,
HnswPq,
HnswSq,
BTree,
Bitmap,
LabelList,
FTS,
],
replace: Optional[bool], replace: Optional[bool],
wait_timeout: Optional[object], wait_timeout: Optional[object],
*, *,

View File

@@ -22,6 +22,13 @@ from lancedb.embeddings.registry import EmbeddingFunctionRegistry
from lancedb.common import data_to_reader, sanitize_uri, validate_schema from lancedb.common import data_to_reader, sanitize_uri, validate_schema
from lancedb.background_loop import LOOP from lancedb.background_loop import LOOP
from lance_namespace import (
ListNamespacesResponse,
CreateNamespaceResponse,
DropNamespaceResponse,
DescribeNamespaceResponse,
ListTablesResponse,
)
from . import __version__ from . import __version__
from ._lancedb import connect as lancedb_connect # type: ignore from ._lancedb import connect as lancedb_connect # type: ignore
@@ -48,6 +55,12 @@ if TYPE_CHECKING:
from .io import StorageOptionsProvider from .io import StorageOptionsProvider
from ._lancedb import Session from ._lancedb import Session
from .namespace_utils import (
_normalize_create_namespace_mode,
_normalize_drop_namespace_mode,
_normalize_drop_namespace_behavior,
)
class DBConnection(EnforceOverrides): class DBConnection(EnforceOverrides):
"""An active LanceDB connection interface.""" """An active LanceDB connection interface."""
@@ -56,8 +69,8 @@ class DBConnection(EnforceOverrides):
self, self,
namespace: Optional[List[str]] = None, namespace: Optional[List[str]] = None,
page_token: Optional[str] = None, page_token: Optional[str] = None,
limit: int = 10, limit: Optional[int] = None,
) -> Iterable[str]: ) -> ListNamespacesResponse:
"""List immediate child namespace names in the given namespace. """List immediate child namespace names in the given namespace.
Parameters Parameters
@@ -66,43 +79,119 @@ class DBConnection(EnforceOverrides):
The parent namespace to list namespaces in. The parent namespace to list namespaces in.
Empty list represents root namespace. Empty list represents root namespace.
page_token: str, optional page_token: str, optional
The token to use for pagination. If not present, start from the beginning. Token for pagination. Use the token from a previous response
limit: int, default 10 to get the next page of results.
The size of the page to return. limit: int, optional
The maximum number of results to return.
Returns Returns
------- -------
Iterable of str ListNamespacesResponse
List of immediate child namespace names Response containing namespace names and optional page_token for pagination.
""" """
if namespace is None: if namespace is None:
namespace = [] namespace = []
return [] return ListNamespacesResponse(namespaces=[], page_token=None)
def create_namespace(self, namespace: List[str]) -> None: def create_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> CreateNamespaceResponse:
"""Create a new namespace. """Create a new namespace.
Parameters Parameters
---------- ----------
namespace: List[str] namespace: List[str]
The namespace identifier to create. The namespace identifier to create.
mode: str, optional
Creation mode - "create" (fail if exists), "exist_ok" (skip if exists),
or "overwrite" (replace if exists). Case insensitive.
properties: Dict[str, str], optional
Properties to set on the namespace.
Returns
-------
CreateNamespaceResponse
Response containing the properties of the created namespace.
""" """
raise NotImplementedError( raise NotImplementedError(
"Namespace operations are not supported for this connection type" "Namespace operations are not supported for this connection type"
) )
def drop_namespace(self, namespace: List[str]) -> None: def drop_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
behavior: Optional[str] = None,
) -> DropNamespaceResponse:
"""Drop a namespace. """Drop a namespace.
Parameters Parameters
---------- ----------
namespace: List[str] namespace: List[str]
The namespace identifier to drop. The namespace identifier to drop.
mode: str, optional
Whether to skip if not exists ("SKIP") or fail ("FAIL"). Case insensitive.
behavior: str, optional
Whether to restrict drop if not empty ("RESTRICT") or cascade ("CASCADE").
Case insensitive.
Returns
-------
DropNamespaceResponse
Response containing properties and transaction_id if applicable.
""" """
raise NotImplementedError( raise NotImplementedError(
"Namespace operations are not supported for this connection type" "Namespace operations are not supported for this connection type"
) )
def describe_namespace(self, namespace: List[str]) -> DescribeNamespaceResponse:
"""Describe a namespace.
Parameters
----------
namespace: List[str]
The namespace identifier to describe.
Returns
-------
DescribeNamespaceResponse
Response containing the namespace properties.
"""
raise NotImplementedError(
"Namespace operations are not supported for this connection type"
)
def list_tables(
self,
namespace: Optional[List[str]] = None,
page_token: Optional[str] = None,
limit: Optional[int] = None,
) -> ListTablesResponse:
"""List all tables in this database with pagination support.
Parameters
----------
namespace: List[str], optional
The namespace to list tables in.
None or empty list represents root namespace.
page_token: str, optional
Token for pagination. Use the token from a previous response
to get the next page of results.
limit: int, optional
The maximum number of results to return.
Returns
-------
ListTablesResponse
Response containing table names and optional page_token for pagination.
"""
raise NotImplementedError(
"list_tables is not supported for this connection type"
)
@abstractmethod @abstractmethod
def table_names( def table_names(
self, self,
@@ -557,8 +646,8 @@ class LanceDBConnection(DBConnection):
self, self,
namespace: Optional[List[str]] = None, namespace: Optional[List[str]] = None,
page_token: Optional[str] = None, page_token: Optional[str] = None,
limit: int = 10, limit: Optional[int] = None,
) -> Iterable[str]: ) -> ListNamespacesResponse:
"""List immediate child namespace names in the given namespace. """List immediate child namespace names in the given namespace.
Parameters Parameters
@@ -567,14 +656,15 @@ class LanceDBConnection(DBConnection):
The parent namespace to list namespaces in. The parent namespace to list namespaces in.
None or empty list represents root namespace. None or empty list represents root namespace.
page_token: str, optional page_token: str, optional
The token to use for pagination. If not present, start from the beginning. Token for pagination. Use the token from a previous response
limit: int, default 10 to get the next page of results.
The size of the page to return. limit: int, optional
The maximum number of results to return.
Returns Returns
------- -------
Iterable of str ListNamespacesResponse
List of immediate child namespace names Response containing namespace names and optional page_token for pagination.
""" """
if namespace is None: if namespace is None:
namespace = [] namespace = []
@@ -585,26 +675,111 @@ class LanceDBConnection(DBConnection):
) )
@override @override
def create_namespace(self, namespace: List[str]) -> None: def create_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> CreateNamespaceResponse:
"""Create a new namespace. """Create a new namespace.
Parameters Parameters
---------- ----------
namespace: List[str] namespace: List[str]
The namespace identifier to create. The namespace identifier to create.
mode: str, optional
Creation mode - "create" (fail if exists), "exist_ok" (skip if exists),
or "overwrite" (replace if exists). Case insensitive.
properties: Dict[str, str], optional
Properties to set on the namespace.
Returns
-------
CreateNamespaceResponse
Response containing the properties of the created namespace.
""" """
LOOP.run(self._conn.create_namespace(namespace=namespace)) return LOOP.run(
self._conn.create_namespace(
namespace=namespace, mode=mode, properties=properties
)
)
@override @override
def drop_namespace(self, namespace: List[str]) -> None: def drop_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
behavior: Optional[str] = None,
) -> DropNamespaceResponse:
"""Drop a namespace. """Drop a namespace.
Parameters Parameters
---------- ----------
namespace: List[str] namespace: List[str]
The namespace identifier to drop. The namespace identifier to drop.
mode: str, optional
Whether to skip if not exists ("SKIP") or fail ("FAIL"). Case insensitive.
behavior: str, optional
Whether to restrict drop if not empty ("RESTRICT") or cascade ("CASCADE").
Case insensitive.
Returns
-------
DropNamespaceResponse
Response containing properties and transaction_id if applicable.
""" """
return LOOP.run(self._conn.drop_namespace(namespace=namespace)) return LOOP.run(
self._conn.drop_namespace(namespace=namespace, mode=mode, behavior=behavior)
)
@override
def describe_namespace(self, namespace: List[str]) -> DescribeNamespaceResponse:
"""Describe a namespace.
Parameters
----------
namespace: List[str]
The namespace identifier to describe.
Returns
-------
DescribeNamespaceResponse
Response containing the namespace properties.
"""
return LOOP.run(self._conn.describe_namespace(namespace=namespace))
@override
def list_tables(
self,
namespace: Optional[List[str]] = None,
page_token: Optional[str] = None,
limit: Optional[int] = None,
) -> ListTablesResponse:
"""List all tables in this database with pagination support.
Parameters
----------
namespace: List[str], optional
The namespace to list tables in.
None or empty list represents root namespace.
page_token: str, optional
Token for pagination. Use the token from a previous response
to get the next page of results.
limit: int, optional
The maximum number of results to return.
Returns
-------
ListTablesResponse
Response containing table names and optional page_token for pagination.
"""
if namespace is None:
namespace = []
return LOOP.run(
self._conn.list_tables(
namespace=namespace, page_token=page_token, limit=limit
)
)
@override @override
def table_names( def table_names(
@@ -616,6 +791,9 @@ class LanceDBConnection(DBConnection):
) -> Iterable[str]: ) -> Iterable[str]:
"""Get the names of all tables in the database. The names are sorted. """Get the names of all tables in the database. The names are sorted.
.. deprecated::
Use :meth:`list_tables` instead, which provides proper pagination support.
Parameters Parameters
---------- ----------
namespace: List[str], optional namespace: List[str], optional
@@ -630,6 +808,13 @@ class LanceDBConnection(DBConnection):
Iterator of str. Iterator of str.
A list of table names. A list of table names.
""" """
import warnings
warnings.warn(
"table_names() is deprecated, use list_tables() instead",
DeprecationWarning,
stacklevel=2,
)
if namespace is None: if namespace is None:
namespace = [] namespace = []
return LOOP.run( return LOOP.run(
@@ -944,8 +1129,8 @@ class AsyncConnection(object):
self, self,
namespace: Optional[List[str]] = None, namespace: Optional[List[str]] = None,
page_token: Optional[str] = None, page_token: Optional[str] = None,
limit: int = 10, limit: Optional[int] = None,
) -> Iterable[str]: ) -> ListNamespacesResponse:
"""List immediate child namespace names in the given namespace. """List immediate child namespace names in the given namespace.
Parameters Parameters
@@ -955,39 +1140,128 @@ class AsyncConnection(object):
None or empty list represents root namespace. None or empty list represents root namespace.
page_token: str, optional page_token: str, optional
The token to use for pagination. If not present, start from the beginning. The token to use for pagination. If not present, start from the beginning.
limit: int, default 10 limit: int, optional
The size of the page to return. The maximum number of results to return.
Returns Returns
------- -------
Iterable of str ListNamespacesResponse
List of immediate child namespace names (not full paths) Response containing namespace names and optional pagination token
""" """
if namespace is None: if namespace is None:
namespace = [] namespace = []
return await self._inner.list_namespaces( result = await self._inner.list_namespaces(
namespace=namespace, page_token=page_token, limit=limit namespace=namespace, page_token=page_token, limit=limit
) )
return ListNamespacesResponse(**result)
async def create_namespace(self, namespace: List[str]) -> None: async def create_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> CreateNamespaceResponse:
"""Create a new namespace. """Create a new namespace.
Parameters Parameters
---------- ----------
namespace: List[str] namespace: List[str]
The namespace identifier to create. The namespace identifier to create.
""" mode: str, optional
await self._inner.create_namespace(namespace) Creation mode - "create", "exist_ok", or "overwrite". Case insensitive.
properties: Dict[str, str], optional
Properties to associate with the namespace
async def drop_namespace(self, namespace: List[str]) -> None: Returns
-------
CreateNamespaceResponse
Response containing namespace properties
"""
result = await self._inner.create_namespace(
namespace,
mode=_normalize_create_namespace_mode(mode),
properties=properties,
)
return CreateNamespaceResponse(**result)
async def drop_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
behavior: Optional[str] = None,
) -> DropNamespaceResponse:
"""Drop a namespace. """Drop a namespace.
Parameters Parameters
---------- ----------
namespace: List[str] namespace: List[str]
The namespace identifier to drop. The namespace identifier to drop.
mode: str, optional
Whether to skip if not exists ("SKIP") or fail ("FAIL"). Case insensitive.
behavior: str, optional
Whether to restrict drop if not empty ("RESTRICT") or cascade ("CASCADE").
Case insensitive.
Returns
-------
DropNamespaceResponse
Response containing properties and transaction_id if applicable.
""" """
await self._inner.drop_namespace(namespace) result = await self._inner.drop_namespace(
namespace,
mode=_normalize_drop_namespace_mode(mode),
behavior=_normalize_drop_namespace_behavior(behavior),
)
return DropNamespaceResponse(**result)
async def describe_namespace(
self, namespace: List[str]
) -> DescribeNamespaceResponse:
"""Describe a namespace.
Parameters
----------
namespace: List[str]
The namespace identifier to describe.
Returns
-------
DescribeNamespaceResponse
Response containing the namespace properties.
"""
result = await self._inner.describe_namespace(namespace)
return DescribeNamespaceResponse(**result)
async def list_tables(
self,
namespace: Optional[List[str]] = None,
page_token: Optional[str] = None,
limit: Optional[int] = None,
) -> ListTablesResponse:
"""List all tables in this database with pagination support.
Parameters
----------
namespace: List[str], optional
The namespace to list tables in.
None or empty list represents root namespace.
page_token: str, optional
Token for pagination. Use the token from a previous response
to get the next page of results.
limit: int, optional
The maximum number of results to return.
Returns
-------
ListTablesResponse
Response containing table names and optional page_token for pagination.
"""
if namespace is None:
namespace = []
result = await self._inner.list_tables(
namespace=namespace, page_token=page_token, limit=limit
)
return ListTablesResponse(**result)
async def table_names( async def table_names(
self, self,
@@ -998,6 +1272,9 @@ class AsyncConnection(object):
) -> Iterable[str]: ) -> Iterable[str]:
"""List all tables in this database, in sorted order """List all tables in this database, in sorted order
.. deprecated::
Use :meth:`list_tables` instead, which provides proper pagination support.
Parameters Parameters
---------- ----------
namespace: List[str], optional namespace: List[str], optional
@@ -1016,6 +1293,13 @@ class AsyncConnection(object):
------- -------
Iterable of str Iterable of str
""" """
import warnings
warnings.warn(
"table_names() is deprecated, use list_tables() instead",
DeprecationWarning,
stacklevel=2,
)
if namespace is None: if namespace is None:
namespace = [] namespace = []
return await self._inner.table_names( return await self._inner.table_names(

View File

@@ -376,6 +376,11 @@ class HnswSq:
target_partition_size: Optional[int] = None target_partition_size: Optional[int] = None
# Backwards-compatible aliases
IvfHnswPq = HnswPq
IvfHnswSq = HnswSq
@dataclass @dataclass
class IvfFlat: class IvfFlat:
"""Describes an IVF Flat Index """Describes an IVF Flat Index
@@ -475,6 +480,36 @@ class IvfFlat:
target_partition_size: Optional[int] = None target_partition_size: Optional[int] = None
@dataclass
class IvfSq:
"""Describes an IVF Scalar Quantization (SQ) index.
This index applies scalar quantization to compress vectors and organizes the
quantized vectors into IVF partitions. It offers a balance between search
speed and storage efficiency while keeping good recall.
Attributes
----------
distance_type: str, default "l2"
The distance metric used to train and search the index. Supported values
are "l2", "cosine", and "dot".
num_partitions: int, default sqrt(num_rows)
Number of IVF partitions to create.
max_iterations: int, default 50
Maximum iterations for kmeans during partition training.
sample_rate: int, default 256
Controls the number of training vectors: sample_rate * num_partitions.
target_partition_size: int, optional
Target size for each partition; adjusts the balance between speed and accuracy.
"""
distance_type: Literal["l2", "cosine", "dot"] = "l2"
num_partitions: Optional[int] = None
max_iterations: int = 50
sample_rate: int = 256
target_partition_size: Optional[int] = None
@dataclass @dataclass
class IvfPq: class IvfPq:
"""Describes an IVF PQ Index """Describes an IVF PQ Index
@@ -661,6 +696,9 @@ class IvfRq:
__all__ = [ __all__ = [
"BTree", "BTree",
"IvfPq", "IvfPq",
"IvfHnswPq",
"IvfHnswSq",
"IvfSq",
"IvfRq", "IvfRq",
"IvfFlat", "IvfFlat",
"HnswPq", "HnswPq",

View File

@@ -23,7 +23,29 @@ from datetime import timedelta
import pyarrow as pa import pyarrow as pa
from lancedb.db import DBConnection, LanceDBConnection from lancedb.db import DBConnection, LanceDBConnection
from lancedb.namespace_utils import (
_normalize_create_namespace_mode,
_normalize_drop_namespace_mode,
_normalize_drop_namespace_behavior,
)
from lancedb.io import StorageOptionsProvider from lancedb.io import StorageOptionsProvider
from lance_namespace import (
LanceNamespace,
connect as namespace_connect,
CreateNamespaceResponse,
DescribeNamespaceResponse,
DropNamespaceResponse,
ListNamespacesResponse,
ListTablesResponse,
ListTablesRequest,
DescribeTableRequest,
DescribeNamespaceRequest,
DropTableRequest,
ListNamespacesRequest,
CreateNamespaceRequest,
DropNamespaceRequest,
CreateEmptyTableRequest,
)
from lancedb.table import AsyncTable, LanceTable, Table from lancedb.table import AsyncTable, LanceTable, Table
from lancedb.util import validate_table_name from lancedb.util import validate_table_name
from lancedb.common import DATA from lancedb.common import DATA
@@ -31,19 +53,9 @@ from lancedb.pydantic import LanceModel
from lancedb.embeddings import EmbeddingFunctionConfig from lancedb.embeddings import EmbeddingFunctionConfig
from ._lancedb import Session from ._lancedb import Session
from lance_namespace import LanceNamespace, connect as namespace_connect from lance_namespace_urllib3_client.models.json_arrow_schema import JsonArrowSchema
from lance_namespace_urllib3_client.models import ( from lance_namespace_urllib3_client.models.json_arrow_field import JsonArrowField
ListTablesRequest, from lance_namespace_urllib3_client.models.json_arrow_data_type import JsonArrowDataType
DescribeTableRequest,
DropTableRequest,
ListNamespacesRequest,
CreateNamespaceRequest,
DropNamespaceRequest,
CreateEmptyTableRequest,
JsonArrowSchema,
JsonArrowField,
JsonArrowDataType,
)
def _convert_pyarrow_type_to_json(arrow_type: pa.DataType) -> JsonArrowDataType: def _convert_pyarrow_type_to_json(arrow_type: pa.DataType) -> JsonArrowDataType:
@@ -241,6 +253,19 @@ class LanceNamespaceDBConnection(DBConnection):
*, *,
namespace: Optional[List[str]] = None, namespace: Optional[List[str]] = None,
) -> Iterable[str]: ) -> Iterable[str]:
"""
List table names in the database.
.. deprecated::
Use :meth:`list_tables` instead, which provides proper pagination support.
"""
import warnings
warnings.warn(
"table_names() is deprecated, use list_tables() instead",
DeprecationWarning,
stacklevel=2,
)
if namespace is None: if namespace is None:
namespace = [] namespace = []
request = ListTablesRequest(id=namespace, page_token=page_token, limit=limit) request = ListTablesRequest(id=namespace, page_token=page_token, limit=limit)
@@ -433,8 +458,8 @@ class LanceNamespaceDBConnection(DBConnection):
self, self,
namespace: Optional[List[str]] = None, namespace: Optional[List[str]] = None,
page_token: Optional[str] = None, page_token: Optional[str] = None,
limit: int = 10, limit: Optional[int] = None,
) -> Iterable[str]: ) -> ListNamespacesResponse:
""" """
List child namespaces under the given namespace. List child namespaces under the given namespace.
@@ -444,14 +469,15 @@ class LanceNamespaceDBConnection(DBConnection):
The parent namespace to list children from. The parent namespace to list children from.
If None, lists root-level namespaces. If None, lists root-level namespaces.
page_token : Optional[str] page_token : Optional[str]
Pagination token for listing results. Token for pagination. Use the token from a previous response
limit : int to get the next page of results.
limit : int, optional
Maximum number of namespaces to return. Maximum number of namespaces to return.
Returns Returns
------- -------
Iterable[str] ListNamespacesResponse
Names of child namespaces. Response containing namespace names and optional page_token for pagination.
""" """
if namespace is None: if namespace is None:
namespace = [] namespace = []
@@ -459,10 +485,18 @@ class LanceNamespaceDBConnection(DBConnection):
id=namespace, page_token=page_token, limit=limit id=namespace, page_token=page_token, limit=limit
) )
response = self._ns.list_namespaces(request) response = self._ns.list_namespaces(request)
return response.namespaces if response.namespaces else [] return ListNamespacesResponse(
namespaces=response.namespaces if response.namespaces else [],
page_token=response.page_token,
)
@override @override
def create_namespace(self, namespace: List[str]) -> None: def create_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> CreateNamespaceResponse:
""" """
Create a new namespace. Create a new namespace.
@@ -470,12 +504,34 @@ class LanceNamespaceDBConnection(DBConnection):
---------- ----------
namespace : List[str] namespace : List[str]
The namespace path to create. The namespace path to create.
mode : str, optional
Creation mode - "create" (fail if exists), "exist_ok" (skip if exists),
or "overwrite" (replace if exists). Case insensitive.
properties : Dict[str, str], optional
Properties to set on the namespace.
Returns
-------
CreateNamespaceResponse
Response containing the properties of the created namespace.
""" """
request = CreateNamespaceRequest(id=namespace) request = CreateNamespaceRequest(
self._ns.create_namespace(request) id=namespace,
mode=_normalize_create_namespace_mode(mode),
properties=properties,
)
response = self._ns.create_namespace(request)
return CreateNamespaceResponse(
properties=response.properties if hasattr(response, "properties") else None
)
@override @override
def drop_namespace(self, namespace: List[str]) -> None: def drop_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
behavior: Optional[str] = None,
) -> DropNamespaceResponse:
""" """
Drop a namespace. Drop a namespace.
@@ -483,9 +539,87 @@ class LanceNamespaceDBConnection(DBConnection):
---------- ----------
namespace : List[str] namespace : List[str]
The namespace path to drop. The namespace path to drop.
mode : str, optional
Whether to skip if not exists ("SKIP") or fail ("FAIL"). Case insensitive.
behavior : str, optional
Whether to restrict drop if not empty ("RESTRICT") or cascade ("CASCADE").
Case insensitive.
Returns
-------
DropNamespaceResponse
Response containing properties and transaction_id if applicable.
""" """
request = DropNamespaceRequest(id=namespace) request = DropNamespaceRequest(
self._ns.drop_namespace(request) id=namespace,
mode=_normalize_drop_namespace_mode(mode),
behavior=_normalize_drop_namespace_behavior(behavior),
)
response = self._ns.drop_namespace(request)
return DropNamespaceResponse(
properties=(
response.properties if hasattr(response, "properties") else None
),
transaction_id=(
response.transaction_id if hasattr(response, "transaction_id") else None
),
)
@override
def describe_namespace(self, namespace: List[str]) -> DescribeNamespaceResponse:
"""
Describe a namespace.
Parameters
----------
namespace : List[str]
The namespace identifier to describe.
Returns
-------
DescribeNamespaceResponse
Response containing the namespace properties.
"""
request = DescribeNamespaceRequest(id=namespace)
response = self._ns.describe_namespace(request)
return DescribeNamespaceResponse(
properties=response.properties if hasattr(response, "properties") else None
)
@override
def list_tables(
self,
namespace: Optional[List[str]] = None,
page_token: Optional[str] = None,
limit: Optional[int] = None,
) -> ListTablesResponse:
"""
List all tables in this database with pagination support.
Parameters
----------
namespace : List[str], optional
The namespace to list tables in.
None or empty list represents root namespace.
page_token : str, optional
Token for pagination. Use the token from a previous response
to get the next page of results.
limit : int, optional
The maximum number of results to return.
Returns
-------
ListTablesResponse
Response containing table names and optional page_token for pagination.
"""
if namespace is None:
namespace = []
request = ListTablesRequest(id=namespace, page_token=page_token, limit=limit)
response = self._ns.list_tables(request)
return ListTablesResponse(
tables=response.tables if response.tables else [],
page_token=response.page_token,
)
def _lance_table_from_uri( def _lance_table_from_uri(
self, self,
@@ -563,7 +697,19 @@ class AsyncLanceNamespaceDBConnection:
*, *,
namespace: Optional[List[str]] = None, namespace: Optional[List[str]] = None,
) -> Iterable[str]: ) -> Iterable[str]:
"""List table names in the namespace.""" """
List table names in the namespace.
.. deprecated::
Use :meth:`list_tables` instead, which provides proper pagination support.
"""
import warnings
warnings.warn(
"table_names() is deprecated, use list_tables() instead",
DeprecationWarning,
stacklevel=2,
)
if namespace is None: if namespace is None:
namespace = [] namespace = []
request = ListTablesRequest(id=namespace, page_token=page_token, limit=limit) request = ListTablesRequest(id=namespace, page_token=page_token, limit=limit)
@@ -771,8 +917,8 @@ class AsyncLanceNamespaceDBConnection:
self, self,
namespace: Optional[List[str]] = None, namespace: Optional[List[str]] = None,
page_token: Optional[str] = None, page_token: Optional[str] = None,
limit: int = 10, limit: Optional[int] = None,
) -> Iterable[str]: ) -> ListNamespacesResponse:
""" """
List child namespaces under the given namespace. List child namespaces under the given namespace.
@@ -782,14 +928,15 @@ class AsyncLanceNamespaceDBConnection:
The parent namespace to list children from. The parent namespace to list children from.
If None, lists root-level namespaces. If None, lists root-level namespaces.
page_token : Optional[str] page_token : Optional[str]
Pagination token for listing results. Token for pagination. Use the token from a previous response
limit : int to get the next page of results.
limit : int, optional
Maximum number of namespaces to return. Maximum number of namespaces to return.
Returns Returns
------- -------
Iterable[str] ListNamespacesResponse
Names of child namespaces. Response containing namespace names and optional page_token for pagination.
""" """
if namespace is None: if namespace is None:
namespace = [] namespace = []
@@ -797,9 +944,17 @@ class AsyncLanceNamespaceDBConnection:
id=namespace, page_token=page_token, limit=limit id=namespace, page_token=page_token, limit=limit
) )
response = self._ns.list_namespaces(request) response = self._ns.list_namespaces(request)
return response.namespaces if response.namespaces else [] return ListNamespacesResponse(
namespaces=response.namespaces if response.namespaces else [],
page_token=response.page_token,
)
async def create_namespace(self, namespace: List[str]) -> None: async def create_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> CreateNamespaceResponse:
""" """
Create a new namespace. Create a new namespace.
@@ -807,11 +962,33 @@ class AsyncLanceNamespaceDBConnection:
---------- ----------
namespace : List[str] namespace : List[str]
The namespace path to create. The namespace path to create.
""" mode : str, optional
request = CreateNamespaceRequest(id=namespace) Creation mode - "create" (fail if exists), "exist_ok" (skip if exists),
self._ns.create_namespace(request) or "overwrite" (replace if exists). Case insensitive.
properties : Dict[str, str], optional
Properties to set on the namespace.
async def drop_namespace(self, namespace: List[str]) -> None: Returns
-------
CreateNamespaceResponse
Response containing the properties of the created namespace.
"""
request = CreateNamespaceRequest(
id=namespace,
mode=_normalize_create_namespace_mode(mode),
properties=properties,
)
response = self._ns.create_namespace(request)
return CreateNamespaceResponse(
properties=response.properties if hasattr(response, "properties") else None
)
async def drop_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
behavior: Optional[str] = None,
) -> DropNamespaceResponse:
""" """
Drop a namespace. Drop a namespace.
@@ -819,9 +996,87 @@ class AsyncLanceNamespaceDBConnection:
---------- ----------
namespace : List[str] namespace : List[str]
The namespace path to drop. The namespace path to drop.
mode : str, optional
Whether to skip if not exists ("SKIP") or fail ("FAIL"). Case insensitive.
behavior : str, optional
Whether to restrict drop if not empty ("RESTRICT") or cascade ("CASCADE").
Case insensitive.
Returns
-------
DropNamespaceResponse
Response containing properties and transaction_id if applicable.
""" """
request = DropNamespaceRequest(id=namespace) request = DropNamespaceRequest(
self._ns.drop_namespace(request) id=namespace,
mode=_normalize_drop_namespace_mode(mode),
behavior=_normalize_drop_namespace_behavior(behavior),
)
response = self._ns.drop_namespace(request)
return DropNamespaceResponse(
properties=(
response.properties if hasattr(response, "properties") else None
),
transaction_id=(
response.transaction_id if hasattr(response, "transaction_id") else None
),
)
async def describe_namespace(
self, namespace: List[str]
) -> DescribeNamespaceResponse:
"""
Describe a namespace.
Parameters
----------
namespace : List[str]
The namespace identifier to describe.
Returns
-------
DescribeNamespaceResponse
Response containing the namespace properties.
"""
request = DescribeNamespaceRequest(id=namespace)
response = self._ns.describe_namespace(request)
return DescribeNamespaceResponse(
properties=response.properties if hasattr(response, "properties") else None
)
async def list_tables(
self,
namespace: Optional[List[str]] = None,
page_token: Optional[str] = None,
limit: Optional[int] = None,
) -> ListTablesResponse:
"""
List all tables in this database with pagination support.
Parameters
----------
namespace : List[str], optional
The namespace to list tables in.
None or empty list represents root namespace.
page_token : str, optional
Token for pagination. Use the token from a previous response
to get the next page of results.
limit : int, optional
The maximum number of results to return.
Returns
-------
ListTablesResponse
Response containing table names and optional page_token for pagination.
"""
if namespace is None:
namespace = []
request = ListTablesRequest(id=namespace, page_token=page_token, limit=limit)
response = self._ns.list_tables(request)
return ListTablesResponse(
tables=response.tables if response.tables else [],
page_token=response.page_token,
)
def connect_namespace( def connect_namespace(

View File

@@ -0,0 +1,27 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Utility functions for namespace operations."""
from typing import Optional
def _normalize_create_namespace_mode(mode: Optional[str]) -> Optional[str]:
"""Normalize create namespace mode to lowercase (API expects lowercase)."""
if mode is None:
return None
return mode.lower()
def _normalize_drop_namespace_mode(mode: Optional[str]) -> Optional[str]:
"""Normalize drop namespace mode to uppercase (API expects uppercase)."""
if mode is None:
return None
return mode.upper()
def _normalize_drop_namespace_behavior(behavior: Optional[str]) -> Optional[str]:
"""Normalize drop namespace behavior to uppercase (API expects uppercase)."""
if behavior is None:
return None
return behavior.upper()

View File

@@ -23,6 +23,13 @@ import pyarrow as pa
from ..common import DATA from ..common import DATA
from ..db import DBConnection, LOOP from ..db import DBConnection, LOOP
from ..embeddings import EmbeddingFunctionConfig from ..embeddings import EmbeddingFunctionConfig
from lance_namespace import (
CreateNamespaceResponse,
DescribeNamespaceResponse,
DropNamespaceResponse,
ListNamespacesResponse,
ListTablesResponse,
)
from ..pydantic import LanceModel from ..pydantic import LanceModel
from ..table import Table from ..table import Table
from ..util import validate_table_name from ..util import validate_table_name
@@ -106,8 +113,8 @@ class RemoteDBConnection(DBConnection):
self, self,
namespace: Optional[List[str]] = None, namespace: Optional[List[str]] = None,
page_token: Optional[str] = None, page_token: Optional[str] = None,
limit: int = 10, limit: Optional[int] = None,
) -> Iterable[str]: ) -> ListNamespacesResponse:
"""List immediate child namespace names in the given namespace. """List immediate child namespace names in the given namespace.
Parameters Parameters
@@ -116,14 +123,15 @@ class RemoteDBConnection(DBConnection):
The parent namespace to list namespaces in. The parent namespace to list namespaces in.
None or empty list represents root namespace. None or empty list represents root namespace.
page_token: str, optional page_token: str, optional
The token to use for pagination. If not present, start from the beginning. Token for pagination. Use the token from a previous response
limit: int, default 10 to get the next page of results.
The size of the page to return. limit: int, optional
The maximum number of results to return.
Returns Returns
------- -------
Iterable of str ListNamespacesResponse
List of immediate child namespace names Response containing namespace names and optional page_token for pagination.
""" """
if namespace is None: if namespace is None:
namespace = [] namespace = []
@@ -134,26 +142,111 @@ class RemoteDBConnection(DBConnection):
) )
@override @override
def create_namespace(self, namespace: List[str]) -> None: def create_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
properties: Optional[Dict[str, str]] = None,
) -> CreateNamespaceResponse:
"""Create a new namespace. """Create a new namespace.
Parameters Parameters
---------- ----------
namespace: List[str] namespace: List[str]
The namespace identifier to create. The namespace identifier to create.
mode: str, optional
Creation mode - "create" (fail if exists), "exist_ok" (skip if exists),
or "overwrite" (replace if exists). Case insensitive.
properties: Dict[str, str], optional
Properties to set on the namespace.
Returns
-------
CreateNamespaceResponse
Response containing the properties of the created namespace.
""" """
LOOP.run(self._conn.create_namespace(namespace=namespace)) return LOOP.run(
self._conn.create_namespace(
namespace=namespace, mode=mode, properties=properties
)
)
@override @override
def drop_namespace(self, namespace: List[str]) -> None: def drop_namespace(
self,
namespace: List[str],
mode: Optional[str] = None,
behavior: Optional[str] = None,
) -> DropNamespaceResponse:
"""Drop a namespace. """Drop a namespace.
Parameters Parameters
---------- ----------
namespace: List[str] namespace: List[str]
The namespace identifier to drop. The namespace identifier to drop.
mode: str, optional
Whether to skip if not exists ("SKIP") or fail ("FAIL"). Case insensitive.
behavior: str, optional
Whether to restrict drop if not empty ("RESTRICT") or cascade ("CASCADE").
Case insensitive.
Returns
-------
DropNamespaceResponse
Response containing properties and transaction_id if applicable.
""" """
return LOOP.run(self._conn.drop_namespace(namespace=namespace)) return LOOP.run(
self._conn.drop_namespace(namespace=namespace, mode=mode, behavior=behavior)
)
@override
def describe_namespace(self, namespace: List[str]) -> DescribeNamespaceResponse:
"""Describe a namespace.
Parameters
----------
namespace: List[str]
The namespace identifier to describe.
Returns
-------
DescribeNamespaceResponse
Response containing the namespace properties.
"""
return LOOP.run(self._conn.describe_namespace(namespace=namespace))
@override
def list_tables(
self,
namespace: Optional[List[str]] = None,
page_token: Optional[str] = None,
limit: Optional[int] = None,
) -> ListTablesResponse:
"""List all tables in this database with pagination support.
Parameters
----------
namespace: List[str], optional
The namespace to list tables in.
None or empty list represents root namespace.
page_token: str, optional
Token for pagination. Use the token from a previous response
to get the next page of results.
limit: int, optional
The maximum number of results to return.
Returns
-------
ListTablesResponse
Response containing table names and optional page_token for pagination.
"""
if namespace is None:
namespace = []
return LOOP.run(
self._conn.list_tables(
namespace=namespace, page_token=page_token, limit=limit
)
)
@override @override
def table_names( def table_names(
@@ -165,6 +258,9 @@ class RemoteDBConnection(DBConnection):
) -> Iterable[str]: ) -> Iterable[str]:
"""List the names of all tables in the database. """List the names of all tables in the database.
.. deprecated::
Use :meth:`list_tables` instead, which provides proper pagination support.
Parameters Parameters
---------- ----------
namespace: List[str], default [] namespace: List[str], default []
@@ -179,6 +275,13 @@ class RemoteDBConnection(DBConnection):
------- -------
An iterator of table names. An iterator of table names.
""" """
import warnings
warnings.warn(
"table_names() is deprecated, use list_tables() instead",
DeprecationWarning,
stacklevel=2,
)
if namespace is None: if namespace is None:
namespace = [] namespace = []
return LOOP.run( return LOOP.run(

View File

@@ -18,7 +18,7 @@ from lancedb._lancedb import (
UpdateResult, UpdateResult,
) )
from lancedb.embeddings.base import EmbeddingFunctionConfig from lancedb.embeddings.base import EmbeddingFunctionConfig
from lancedb.index import FTS, BTree, Bitmap, HnswSq, IvfFlat, IvfPq, LabelList from lancedb.index import FTS, BTree, Bitmap, HnswSq, IvfFlat, IvfPq, IvfSq, LabelList
from lancedb.remote.db import LOOP from lancedb.remote.db import LOOP
import pyarrow as pa import pyarrow as pa
@@ -265,6 +265,8 @@ class RemoteTable(Table):
num_sub_vectors=num_sub_vectors, num_sub_vectors=num_sub_vectors,
num_bits=num_bits, num_bits=num_bits,
) )
elif index_type == "IVF_SQ":
config = IvfSq(distance_type=metric, num_partitions=num_partitions)
elif index_type == "IVF_HNSW_PQ": elif index_type == "IVF_HNSW_PQ":
raise ValueError( raise ValueError(
"IVF_HNSW_PQ is not supported on LanceDB cloud." "IVF_HNSW_PQ is not supported on LanceDB cloud."
@@ -277,7 +279,7 @@ class RemoteTable(Table):
else: else:
raise ValueError( raise ValueError(
f"Unknown vector index type: {index_type}. Valid options are" f"Unknown vector index type: {index_type}. Valid options are"
" 'IVF_FLAT', 'IVF_PQ', 'IVF_HNSW_PQ', 'IVF_HNSW_SQ'" " 'IVF_FLAT', 'IVF_SQ', 'IVF_PQ', 'IVF_HNSW_PQ', 'IVF_HNSW_SQ'"
) )
LOOP.run( LOOP.run(

View File

@@ -44,7 +44,18 @@ import numpy as np
from .common import DATA, VEC, VECTOR_COLUMN_NAME from .common import DATA, VEC, VECTOR_COLUMN_NAME
from .embeddings import EmbeddingFunctionConfig, EmbeddingFunctionRegistry from .embeddings import EmbeddingFunctionConfig, EmbeddingFunctionRegistry
from .index import BTree, IvfFlat, IvfPq, Bitmap, IvfRq, LabelList, HnswPq, HnswSq, FTS from .index import (
BTree,
IvfFlat,
IvfPq,
IvfSq,
Bitmap,
IvfRq,
LabelList,
HnswPq,
HnswSq,
FTS,
)
from .merge import LanceMergeInsertBuilder from .merge import LanceMergeInsertBuilder
from .pydantic import LanceModel, model_to_dict from .pydantic import LanceModel, model_to_dict
from .query import ( from .query import (
@@ -2054,7 +2065,7 @@ class LanceTable(Table):
index_cache_size: Optional[int] = None, index_cache_size: Optional[int] = None,
num_bits: int = 8, num_bits: int = 8,
index_type: Literal[ index_type: Literal[
"IVF_FLAT", "IVF_PQ", "IVF_RQ", "IVF_HNSW_SQ", "IVF_HNSW_PQ" "IVF_FLAT", "IVF_SQ", "IVF_PQ", "IVF_RQ", "IVF_HNSW_SQ", "IVF_HNSW_PQ"
] = "IVF_PQ", ] = "IVF_PQ",
max_iterations: int = 50, max_iterations: int = 50,
sample_rate: int = 256, sample_rate: int = 256,
@@ -2092,6 +2103,14 @@ class LanceTable(Table):
sample_rate=sample_rate, sample_rate=sample_rate,
target_partition_size=target_partition_size, target_partition_size=target_partition_size,
) )
elif index_type == "IVF_SQ":
config = IvfSq(
distance_type=metric,
num_partitions=num_partitions,
max_iterations=max_iterations,
sample_rate=sample_rate,
target_partition_size=target_partition_size,
)
elif index_type == "IVF_PQ": elif index_type == "IVF_PQ":
config = IvfPq( config = IvfPq(
distance_type=metric, distance_type=metric,
@@ -3189,6 +3208,26 @@ def _infer_target_schema(
if pa.types.is_floating(field.type.value_type): if pa.types.is_floating(field.type.value_type):
target_type = pa.list_(pa.float32(), dim) target_type = pa.list_(pa.float32(), dim)
elif pa.types.is_integer(field.type.value_type): elif pa.types.is_integer(field.type.value_type):
values = peeked.column(i)
if isinstance(values, pa.ChunkedArray):
values = values.combine_chunks()
flattened = values.flatten()
valid_count = pc.count(flattened, mode="only_valid").as_py()
if valid_count == 0:
target_type = pa.list_(pa.uint8(), dim)
else:
min_max = pc.min_max(flattened)
min_value = min_max["min"].as_py()
max_value = min_max["max"].as_py()
if (min_value is not None and min_value < 0) or (
max_value is not None and max_value > 255
):
target_type = pa.list_(pa.float32(), dim)
else:
target_type = pa.list_(pa.uint8(), dim) target_type = pa.list_(pa.uint8(), dim)
else: else:
continue # Skip non-numeric types continue # Skip non-numeric types
@@ -3456,11 +3495,22 @@ class AsyncTable:
if config is not None: if config is not None:
if not isinstance( if not isinstance(
config, config,
(IvfFlat, IvfPq, IvfRq, HnswPq, HnswSq, BTree, Bitmap, LabelList, FTS), (
IvfFlat,
IvfSq,
IvfPq,
IvfRq,
HnswPq,
HnswSq,
BTree,
Bitmap,
LabelList,
FTS,
),
): ):
raise TypeError( raise TypeError(
"config must be an instance of IvfPq, IvfRq, HnswPq, HnswSq, BTree," "config must be an instance of IvfSq, IvfPq, IvfRq, HnswPq, HnswSq,"
" Bitmap, LabelList, or FTS, but got " + str(type(config)) " BTree, Bitmap, LabelList, or FTS, but got " + str(type(config))
) )
try: try:
await self._inner.create_index( await self._inner.create_index(

View File

@@ -18,12 +18,20 @@ AddMode = Literal["append", "overwrite"]
CreateMode = Literal["create", "overwrite"] CreateMode = Literal["create", "overwrite"]
# Index type literals # Index type literals
VectorIndexType = Literal["IVF_FLAT", "IVF_PQ", "IVF_HNSW_SQ", "IVF_HNSW_PQ", "IVF_RQ"] VectorIndexType = Literal[
"IVF_FLAT",
"IVF_SQ",
"IVF_PQ",
"IVF_HNSW_SQ",
"IVF_HNSW_PQ",
"IVF_RQ",
]
ScalarIndexType = Literal["BTREE", "BITMAP", "LABEL_LIST"] ScalarIndexType = Literal["BTREE", "BITMAP", "LABEL_LIST"]
IndexType = Literal[ IndexType = Literal[
"IVF_PQ", "IVF_PQ",
"IVF_HNSW_PQ", "IVF_HNSW_PQ",
"IVF_HNSW_SQ", "IVF_HNSW_SQ",
"IVF_SQ",
"FTS", "FTS",
"BTREE", "BTREE",
"BITMAP", "BITMAP",

View File

@@ -892,7 +892,7 @@ def test_local_namespace_operations(tmp_path):
db = lancedb.connect(tmp_path) db = lancedb.connect(tmp_path)
# Test list_namespaces returns empty list for root namespace # Test list_namespaces returns empty list for root namespace
namespaces = list(db.list_namespaces()) namespaces = db.list_namespaces().namespaces
assert namespaces == [] assert namespaces == []
# Test list_namespaces with non-empty namespace raises NotImplementedError # Test list_namespaces with non-empty namespace raises NotImplementedError
@@ -900,7 +900,7 @@ def test_local_namespace_operations(tmp_path):
NotImplementedError, NotImplementedError,
match="Namespace operations are not supported for listing database", match="Namespace operations are not supported for listing database",
): ):
list(db.list_namespaces(namespace=["test"])) db.list_namespaces(namespace=["test"])
def test_local_create_namespace_not_supported(tmp_path): def test_local_create_namespace_not_supported(tmp_path):

View File

@@ -12,6 +12,9 @@ from lancedb.index import (
BTree, BTree,
IvfFlat, IvfFlat,
IvfPq, IvfPq,
IvfSq,
IvfHnswPq,
IvfHnswSq,
IvfRq, IvfRq,
Bitmap, Bitmap,
LabelList, LabelList,
@@ -229,6 +232,35 @@ async def test_create_hnswsq_index(some_table: AsyncTable):
assert len(indices) == 1 assert len(indices) == 1
@pytest.mark.asyncio
async def test_create_hnswsq_alias_index(some_table: AsyncTable):
await some_table.create_index("vector", config=IvfHnswSq(num_partitions=5))
indices = await some_table.list_indices()
assert len(indices) == 1
assert indices[0].index_type in {"HnswSq", "IvfHnswSq"}
@pytest.mark.asyncio
async def test_create_hnswpq_alias_index(some_table: AsyncTable):
await some_table.create_index("vector", config=IvfHnswPq(num_partitions=5))
indices = await some_table.list_indices()
assert len(indices) == 1
assert indices[0].index_type in {"HnswPq", "IvfHnswPq"}
@pytest.mark.asyncio
async def test_create_ivfsq_index(some_table: AsyncTable):
await some_table.create_index("vector", config=IvfSq(num_partitions=10))
indices = await some_table.list_indices()
assert len(indices) == 1
assert indices[0].index_type == "IvfSq"
stats = await some_table.index_stats(indices[0].name)
assert stats.index_type == "IVF_SQ"
assert stats.distance_type == "l2"
assert stats.num_indexed_rows == await some_table.count_rows()
assert stats.num_unindexed_rows == 0
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_create_index_with_binary_vectors(binary_table: AsyncTable): async def test_create_index_with_binary_vectors(binary_table: AsyncTable):
await binary_table.create_index( await binary_table.create_index(

View File

@@ -279,13 +279,13 @@ class TestNamespaceConnection:
db = lancedb.connect_namespace("dir", {"root": self.temp_dir}) db = lancedb.connect_namespace("dir", {"root": self.temp_dir})
# Initially no namespaces # Initially no namespaces
assert len(list(db.list_namespaces())) == 0 assert len(db.list_namespaces().namespaces) == 0
# Create a namespace # Create a namespace
db.create_namespace(["test_namespace"]) db.create_namespace(["test_namespace"])
# Verify namespace exists # Verify namespace exists
namespaces = list(db.list_namespaces()) namespaces = db.list_namespaces().namespaces
assert "test_namespace" in namespaces assert "test_namespace" in namespaces
assert len(namespaces) == 1 assert len(namespaces) == 1
@@ -322,7 +322,7 @@ class TestNamespaceConnection:
db.drop_namespace(["test_namespace"]) db.drop_namespace(["test_namespace"])
# Verify namespace no longer exists # Verify namespace no longer exists
namespaces = list(db.list_namespaces()) namespaces = db.list_namespaces().namespaces
assert len(namespaces) == 0 assert len(namespaces) == 0
def test_namespace_with_tables_cannot_be_dropped(self): def test_namespace_with_tables_cannot_be_dropped(self):
@@ -570,13 +570,13 @@ class TestAsyncNamespaceConnection:
# Initially no namespaces # Initially no namespaces
namespaces = await db.list_namespaces() namespaces = await db.list_namespaces()
assert len(list(namespaces)) == 0 assert len(namespaces.namespaces) == 0
# Create a namespace # Create a namespace
await db.create_namespace(["test_namespace"]) await db.create_namespace(["test_namespace"])
# Verify namespace exists # Verify namespace exists
namespaces = list(await db.list_namespaces()) namespaces = (await db.list_namespaces()).namespaces
assert "test_namespace" in namespaces assert "test_namespace" in namespaces
assert len(namespaces) == 1 assert len(namespaces) == 1
@@ -608,7 +608,7 @@ class TestAsyncNamespaceConnection:
await db.drop_namespace(["test_namespace"]) await db.drop_namespace(["test_namespace"])
# Verify namespace no longer exists # Verify namespace no longer exists
namespaces = list(await db.list_namespaces()) namespaces = (await db.list_namespaces()).namespaces
assert len(namespaces) == 0 assert len(namespaces) == 0
async def test_drop_all_tables_async(self): async def test_drop_all_tables_async(self):

View File

@@ -46,6 +46,39 @@ def test_basic(mem_db: DBConnection):
assert table.to_arrow() == expected_data assert table.to_arrow() == expected_data
def test_create_table_infers_large_int_vectors(mem_db: DBConnection):
data = [{"vector": [0, 300]}]
table = mem_db.create_table(
"int_vector_overflow", data=data, mode="overwrite", exist_ok=True
)
vector_field = table.schema.field("vector")
assert vector_field.type == pa.list_(pa.float32(), 2)
vector_column = table.to_arrow().column("vector")
assert vector_column.type == pa.list_(pa.float32(), 2)
assert vector_column.to_pylist() == [[0.0, 300.0]]
@pytest.mark.asyncio
async def test_create_table_async_infers_large_int_vectors(
mem_db_async: AsyncConnection,
):
data = [{"vector": [256, 257]}]
table = await mem_db_async.create_table(
"int_vector_overflow_async", data=data, mode="overwrite", exist_ok=True
)
schema = await table.schema()
assert schema.field("vector").type == pa.list_(pa.float32(), 2)
vector_column = (await table.to_arrow()).column("vector")
assert vector_column.type == pa.list_(pa.float32(), 2)
assert vector_column.to_pylist() == [[256.0, 257.0]]
def test_input_data_type(mem_db: DBConnection, tmp_path): def test_input_data_type(mem_db: DBConnection, tmp_path):
schema = pa.schema( schema = pa.schema(
{ {

View File

@@ -10,8 +10,9 @@ use lancedb::{
}; };
use pyo3::{ use pyo3::{
exceptions::{PyRuntimeError, PyValueError}, exceptions::{PyRuntimeError, PyValueError},
pyclass, pyfunction, pymethods, Bound, FromPyObject, Py, PyAny, PyObject, PyRef, PyResult, pyclass, pyfunction, pymethods,
Python, types::{PyDict, PyDictMethods},
Bound, FromPyObject, Py, PyAny, PyObject, PyRef, PyResult, Python,
}; };
use pyo3_async_runtimes::tokio::future_into_py; use pyo3_async_runtimes::tokio::future_into_py;
@@ -292,40 +293,155 @@ impl Connection {
limit: Option<u32>, limit: Option<u32>,
) -> PyResult<Bound<'_, PyAny>> { ) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone(); let inner = self_.get_inner()?.clone();
future_into_py(self_.py(), async move { let py = self_.py();
use lancedb::database::ListNamespacesRequest; future_into_py(py, async move {
use lance_namespace::models::ListNamespacesRequest;
let request = ListNamespacesRequest { let request = ListNamespacesRequest {
namespace, id: if namespace.is_empty() {
None
} else {
Some(namespace)
},
page_token, page_token,
limit, limit: limit.map(|l| l as i32),
}; };
inner.list_namespaces(request).await.infer_error() let response = inner.list_namespaces(request).await.infer_error()?;
Python::with_gil(|py| -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("namespaces", response.namespaces)?;
dict.set_item("page_token", response.page_token)?;
Ok(dict.unbind())
})
}) })
} }
#[pyo3(signature = (namespace,))] #[pyo3(signature = (namespace, mode=None, properties=None))]
pub fn create_namespace( pub fn create_namespace(
self_: PyRef<'_, Self>, self_: PyRef<'_, Self>,
namespace: Vec<String>, namespace: Vec<String>,
mode: Option<String>,
properties: Option<std::collections::HashMap<String, String>>,
) -> PyResult<Bound<'_, PyAny>> { ) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone(); let inner = self_.get_inner()?.clone();
future_into_py(self_.py(), async move { let py = self_.py();
use lancedb::database::CreateNamespaceRequest; future_into_py(py, async move {
let request = CreateNamespaceRequest { namespace }; use lance_namespace::models::{create_namespace_request, CreateNamespaceRequest};
inner.create_namespace(request).await.infer_error() let mode_enum = mode.and_then(|m| match m.to_lowercase().as_str() {
"create" => Some(create_namespace_request::Mode::Create),
"exist_ok" => Some(create_namespace_request::Mode::ExistOk),
"overwrite" => Some(create_namespace_request::Mode::Overwrite),
_ => None,
});
let request = CreateNamespaceRequest {
id: if namespace.is_empty() {
None
} else {
Some(namespace)
},
mode: mode_enum,
properties,
};
let response = inner.create_namespace(request).await.infer_error()?;
Python::with_gil(|py| -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("properties", response.properties)?;
Ok(dict.unbind())
})
})
}
#[pyo3(signature = (namespace, mode=None, behavior=None))]
pub fn drop_namespace(
self_: PyRef<'_, Self>,
namespace: Vec<String>,
mode: Option<String>,
behavior: Option<String>,
) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone();
let py = self_.py();
future_into_py(py, async move {
use lance_namespace::models::{drop_namespace_request, DropNamespaceRequest};
let mode_enum = mode.and_then(|m| match m.to_uppercase().as_str() {
"SKIP" => Some(drop_namespace_request::Mode::Skip),
"FAIL" => Some(drop_namespace_request::Mode::Fail),
_ => None,
});
let behavior_enum = behavior.and_then(|b| match b.to_uppercase().as_str() {
"RESTRICT" => Some(drop_namespace_request::Behavior::Restrict),
"CASCADE" => Some(drop_namespace_request::Behavior::Cascade),
_ => None,
});
let request = DropNamespaceRequest {
id: if namespace.is_empty() {
None
} else {
Some(namespace)
},
mode: mode_enum,
behavior: behavior_enum,
};
let response = inner.drop_namespace(request).await.infer_error()?;
Python::with_gil(|py| -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("properties", response.properties)?;
dict.set_item("transaction_id", response.transaction_id)?;
Ok(dict.unbind())
})
}) })
} }
#[pyo3(signature = (namespace,))] #[pyo3(signature = (namespace,))]
pub fn drop_namespace( pub fn describe_namespace(
self_: PyRef<'_, Self>, self_: PyRef<'_, Self>,
namespace: Vec<String>, namespace: Vec<String>,
) -> PyResult<Bound<'_, PyAny>> { ) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone(); let inner = self_.get_inner()?.clone();
future_into_py(self_.py(), async move { let py = self_.py();
use lancedb::database::DropNamespaceRequest; future_into_py(py, async move {
let request = DropNamespaceRequest { namespace }; use lance_namespace::models::DescribeNamespaceRequest;
inner.drop_namespace(request).await.infer_error() let request = DescribeNamespaceRequest {
id: if namespace.is_empty() {
None
} else {
Some(namespace)
},
};
let response = inner.describe_namespace(request).await.infer_error()?;
Python::with_gil(|py| -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("properties", response.properties)?;
Ok(dict.unbind())
})
})
}
#[pyo3(signature = (namespace=vec![], page_token=None, limit=None))]
pub fn list_tables(
self_: PyRef<'_, Self>,
namespace: Vec<String>,
page_token: Option<String>,
limit: Option<u32>,
) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.get_inner()?.clone();
let py = self_.py();
future_into_py(py, async move {
use lance_namespace::models::ListTablesRequest;
let request = ListTablesRequest {
id: if namespace.is_empty() {
None
} else {
Some(namespace)
},
page_token,
limit: limit.map(|l| l as i32),
};
let response = inner.list_tables(request).await.infer_error()?;
Python::with_gil(|py| -> PyResult<Py<PyDict>> {
let dict = PyDict::new(py);
dict.set_item("tables", response.tables)?;
dict.set_item("page_token", response.page_token)?;
Ok(dict.unbind())
})
}) })
} }
} }

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors // SPDX-FileCopyrightText: Copyright The LanceDB Authors
use lancedb::index::vector::{IvfFlatIndexBuilder, IvfRqIndexBuilder}; use lancedb::index::vector::{IvfFlatIndexBuilder, IvfRqIndexBuilder, IvfSqIndexBuilder};
use lancedb::index::{ use lancedb::index::{
scalar::{BTreeIndexBuilder, FtsIndexBuilder}, scalar::{BTreeIndexBuilder, FtsIndexBuilder},
vector::{IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder}, vector::{IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder},
@@ -87,6 +87,21 @@ pub fn extract_index_params(source: &Option<Bound<'_, PyAny>>) -> PyResult<Lance
} }
Ok(LanceDbIndex::IvfPq(ivf_pq_builder)) Ok(LanceDbIndex::IvfPq(ivf_pq_builder))
}, },
"IvfSq" => {
let params = source.extract::<IvfSqParams>()?;
let distance_type = parse_distance_type(params.distance_type)?;
let mut ivf_sq_builder = IvfSqIndexBuilder::default()
.distance_type(distance_type)
.max_iterations(params.max_iterations)
.sample_rate(params.sample_rate);
if let Some(num_partitions) = params.num_partitions {
ivf_sq_builder = ivf_sq_builder.num_partitions(num_partitions);
}
if let Some(target_partition_size) = params.target_partition_size {
ivf_sq_builder = ivf_sq_builder.target_partition_size(target_partition_size);
}
Ok(LanceDbIndex::IvfSq(ivf_sq_builder))
},
"IvfRq" => { "IvfRq" => {
let params = source.extract::<IvfRqParams>()?; let params = source.extract::<IvfRqParams>()?;
let distance_type = parse_distance_type(params.distance_type)?; let distance_type = parse_distance_type(params.distance_type)?;
@@ -142,7 +157,7 @@ pub fn extract_index_params(source: &Option<Bound<'_, PyAny>>) -> PyResult<Lance
Ok(LanceDbIndex::IvfHnswSq(hnsw_sq_builder)) Ok(LanceDbIndex::IvfHnswSq(hnsw_sq_builder))
}, },
not_supported => Err(PyValueError::new_err(format!( not_supported => Err(PyValueError::new_err(format!(
"Invalid index type '{}'. Must be one of BTree, Bitmap, LabelList, FTS, IvfPq, IvfHnswPq, or IvfHnswSq", "Invalid index type '{}'. Must be one of BTree, Bitmap, LabelList, FTS, IvfPq, IvfSq, IvfHnswPq, or IvfHnswSq",
not_supported not_supported
))), ))),
} }
@@ -186,6 +201,15 @@ struct IvfPqParams {
target_partition_size: Option<u32>, target_partition_size: Option<u32>,
} }
#[derive(FromPyObject)]
struct IvfSqParams {
distance_type: String,
num_partitions: Option<u32>,
max_iterations: u32,
sample_rate: u32,
target_partition_size: Option<u32>,
}
#[derive(FromPyObject)] #[derive(FromPyObject)]
struct IvfRqParams { struct IvfRqParams {
distance_type: String, distance_type: String,

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lancedb" name = "lancedb"
version = "0.22.4-beta.2" version = "0.23.0-beta.1"
edition.workspace = true edition.workspace = true
description = "LanceDB: A serverless, low-latency vector database for AI applications" description = "LanceDB: A serverless, low-latency vector database for AI applications"
license.workspace = true license.workspace = true

View File

@@ -9,6 +9,11 @@ use std::sync::Arc;
use arrow_array::RecordBatchReader; use arrow_array::RecordBatchReader;
use arrow_schema::{Field, SchemaRef}; use arrow_schema::{Field, SchemaRef};
use lance::dataset::ReadParams; use lance::dataset::ReadParams;
use lance_namespace::models::{
CreateNamespaceRequest, CreateNamespaceResponse, DescribeNamespaceRequest,
DescribeNamespaceResponse, DropNamespaceRequest, DropNamespaceResponse, ListNamespacesRequest,
ListNamespacesResponse, ListTablesRequest, ListTablesResponse,
};
#[cfg(feature = "aws")] #[cfg(feature = "aws")]
use object_store::aws::AwsCredential; use object_store::aws::AwsCredential;
@@ -17,9 +22,8 @@ use crate::database::listing::{
ListingDatabase, OPT_NEW_TABLE_STORAGE_VERSION, OPT_NEW_TABLE_V2_MANIFEST_PATHS, ListingDatabase, OPT_NEW_TABLE_STORAGE_VERSION, OPT_NEW_TABLE_V2_MANIFEST_PATHS,
}; };
use crate::database::{ use crate::database::{
CloneTableRequest, CreateNamespaceRequest, CreateTableData, CreateTableMode, CloneTableRequest, CreateTableData, CreateTableMode, CreateTableRequest, Database,
CreateTableRequest, Database, DatabaseOptions, DropNamespaceRequest, ListNamespacesRequest, DatabaseOptions, OpenTableRequest, ReadConsistency, TableNamesRequest,
OpenTableRequest, ReadConsistency, TableNamesRequest,
}; };
use crate::embeddings::{ use crate::embeddings::{
EmbeddingDefinition, EmbeddingFunction, EmbeddingRegistry, MemoryRegistry, WithEmbeddings, EmbeddingDefinition, EmbeddingFunction, EmbeddingRegistry, MemoryRegistry, WithEmbeddings,
@@ -74,6 +78,7 @@ impl TableNamesBuilder {
} }
/// Execute the table names operation /// Execute the table names operation
#[allow(deprecated)]
pub async fn execute(self) -> Result<Vec<String>> { pub async fn execute(self) -> Result<Vec<String>> {
self.parent.clone().table_names(self.request).await self.parent.clone().table_names(self.request).await
} }
@@ -408,6 +413,7 @@ impl OpenTableBuilder {
index_cache_size: None, index_cache_size: None,
lance_read_params: None, lance_read_params: None,
location: None, location: None,
namespace_client: None,
}, },
embedding_registry, embedding_registry,
} }
@@ -767,20 +773,42 @@ impl Connection {
} }
/// List immediate child namespace names in the given namespace /// List immediate child namespace names in the given namespace
pub async fn list_namespaces(&self, request: ListNamespacesRequest) -> Result<Vec<String>> { pub async fn list_namespaces(
&self,
request: ListNamespacesRequest,
) -> Result<ListNamespacesResponse> {
self.internal.list_namespaces(request).await self.internal.list_namespaces(request).await
} }
/// Create a new namespace /// Create a new namespace
pub async fn create_namespace(&self, request: CreateNamespaceRequest) -> Result<()> { pub async fn create_namespace(
&self,
request: CreateNamespaceRequest,
) -> Result<CreateNamespaceResponse> {
self.internal.create_namespace(request).await self.internal.create_namespace(request).await
} }
/// Drop a namespace /// Drop a namespace
pub async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<()> { pub async fn drop_namespace(
&self,
request: DropNamespaceRequest,
) -> Result<DropNamespaceResponse> {
self.internal.drop_namespace(request).await self.internal.drop_namespace(request).await
} }
/// Describe a namespace
pub async fn describe_namespace(
&self,
request: DescribeNamespaceRequest,
) -> Result<DescribeNamespaceResponse> {
self.internal.describe_namespace(request).await
}
/// List tables with pagination support
pub async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
self.internal.list_tables(request).await
}
/// Get the in-memory embedding registry. /// Get the in-memory embedding registry.
/// It's important to note that the embedding registry is not persisted across connections. /// It's important to note that the embedding registry is not persisted across connections.
/// So if a table contains embeddings, you will need to make sure that you are using a connection that has the same embedding functions registered /// So if a table contains embeddings, you will need to make sure that you are using a connection that has the same embedding functions registered
@@ -1086,6 +1114,7 @@ pub struct ConnectNamespaceBuilder {
read_consistency_interval: Option<std::time::Duration>, read_consistency_interval: Option<std::time::Duration>,
embedding_registry: Option<Arc<dyn EmbeddingRegistry>>, embedding_registry: Option<Arc<dyn EmbeddingRegistry>>,
session: Option<Arc<lance::session::Session>>, session: Option<Arc<lance::session::Session>>,
server_side_query_enabled: bool,
} }
impl ConnectNamespaceBuilder { impl ConnectNamespaceBuilder {
@@ -1097,6 +1126,7 @@ impl ConnectNamespaceBuilder {
read_consistency_interval: None, read_consistency_interval: None,
embedding_registry: None, embedding_registry: None,
session: None, session: None,
server_side_query_enabled: false,
} }
} }
@@ -1151,6 +1181,18 @@ impl ConnectNamespaceBuilder {
self self
} }
/// Enable server-side query execution.
///
/// When enabled, queries will be executed on the namespace server instead of
/// locally. This can improve performance by reducing data transfer and
/// leveraging server-side compute resources.
///
/// Default is `false` (queries executed locally).
pub fn server_side_query(mut self, enabled: bool) -> Self {
self.server_side_query_enabled = enabled;
self
}
/// Execute the connection /// Execute the connection
pub async fn execute(self) -> Result<Connection> { pub async fn execute(self) -> Result<Connection> {
use crate::database::namespace::LanceNamespaceDatabase; use crate::database::namespace::LanceNamespaceDatabase;
@@ -1162,6 +1204,7 @@ impl ConnectNamespaceBuilder {
self.storage_options, self.storage_options,
self.read_consistency_interval, self.read_consistency_interval,
self.session, self.session,
self.server_side_query_enabled,
) )
.await?, .await?,
); );

View File

@@ -24,6 +24,12 @@ use datafusion_physical_plan::stream::RecordBatchStreamAdapter;
use futures::stream; use futures::stream;
use lance::dataset::ReadParams; use lance::dataset::ReadParams;
use lance_datafusion::utils::StreamingWriteSource; use lance_datafusion::utils::StreamingWriteSource;
use lance_namespace::models::{
CreateNamespaceRequest, CreateNamespaceResponse, DescribeNamespaceRequest,
DescribeNamespaceResponse, DropNamespaceRequest, DropNamespaceResponse, ListNamespacesRequest,
ListNamespacesResponse, ListTablesRequest, ListTablesResponse,
};
use lance_namespace::LanceNamespace;
use crate::arrow::{SendableRecordBatchStream, SendableRecordBatchStreamExt}; use crate::arrow::{SendableRecordBatchStream, SendableRecordBatchStreamExt};
use crate::error::Result; use crate::error::Result;
@@ -36,32 +42,7 @@ pub trait DatabaseOptions {
fn serialize_into_map(&self, map: &mut HashMap<String, String>); fn serialize_into_map(&self, map: &mut HashMap<String, String>);
} }
/// A request to list namespaces in the database /// A request to list names of tables in the database (deprecated, use ListTablesRequest)
#[derive(Clone, Debug, Default)]
pub struct ListNamespacesRequest {
/// The parent namespace to list namespaces in. Empty list represents root namespace.
pub namespace: Vec<String>,
/// If present, only return names that come lexicographically after the supplied value.
pub page_token: Option<String>,
/// The maximum number of namespace names to return
pub limit: Option<u32>,
}
/// A request to create a namespace
#[derive(Clone, Debug)]
pub struct CreateNamespaceRequest {
/// The namespace identifier to create
pub namespace: Vec<String>,
}
/// A request to drop a namespace
#[derive(Clone, Debug)]
pub struct DropNamespaceRequest {
/// The namespace identifier to drop
pub namespace: Vec<String>,
}
/// A request to list names of tables in the database
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct TableNamesRequest { pub struct TableNamesRequest {
/// The namespace to list tables in. Empty list represents root namespace. /// The namespace to list tables in. Empty list represents root namespace.
@@ -77,7 +58,7 @@ pub struct TableNamesRequest {
} }
/// A request to open a table /// A request to open a table
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct OpenTableRequest { pub struct OpenTableRequest {
pub name: String, pub name: String,
/// The namespace to open the table from. Empty list represents root namespace. /// The namespace to open the table from. Empty list represents root namespace.
@@ -87,6 +68,22 @@ pub struct OpenTableRequest {
/// Optional custom location for the table. If not provided, the database will /// Optional custom location for the table. If not provided, the database will
/// derive a location based on its URI and the table name. /// derive a location based on its URI and the table name.
pub location: Option<String>, pub location: Option<String>,
/// Optional namespace client for server-side query execution.
/// When set, queries will be executed on the namespace server instead of locally.
pub namespace_client: Option<Arc<dyn LanceNamespace>>,
}
impl std::fmt::Debug for OpenTableRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("OpenTableRequest")
.field("name", &self.name)
.field("namespace", &self.namespace)
.field("index_cache_size", &self.index_cache_size)
.field("lance_read_params", &self.lance_read_params)
.field("location", &self.location)
.field("namespace_client", &self.namespace_client)
.finish()
}
} }
pub type TableBuilderCallback = Box<dyn FnOnce(OpenTableRequest) -> OpenTableRequest + Send>; pub type TableBuilderCallback = Box<dyn FnOnce(OpenTableRequest) -> OpenTableRequest + Send>;
@@ -170,6 +167,9 @@ pub struct CreateTableRequest {
/// Optional custom location for the table. If not provided, the database will /// Optional custom location for the table. If not provided, the database will
/// derive a location based on its URI and the table name. /// derive a location based on its URI and the table name.
pub location: Option<String>, pub location: Option<String>,
/// Optional namespace client for server-side query execution.
/// When set, queries will be executed on the namespace server instead of locally.
pub namespace_client: Option<Arc<dyn LanceNamespace>>,
} }
impl CreateTableRequest { impl CreateTableRequest {
@@ -181,6 +181,7 @@ impl CreateTableRequest {
mode: CreateTableMode::default(), mode: CreateTableMode::default(),
write_options: WriteOptions::default(), write_options: WriteOptions::default(),
location: None, location: None,
namespace_client: None,
} }
} }
} }
@@ -247,13 +248,30 @@ pub trait Database:
/// Get the read consistency of the database /// Get the read consistency of the database
async fn read_consistency(&self) -> Result<ReadConsistency>; async fn read_consistency(&self) -> Result<ReadConsistency>;
/// List immediate child namespace names in the given namespace /// List immediate child namespace names in the given namespace
async fn list_namespaces(&self, request: ListNamespacesRequest) -> Result<Vec<String>>; async fn list_namespaces(
&self,
request: ListNamespacesRequest,
) -> Result<ListNamespacesResponse>;
/// Create a new namespace /// Create a new namespace
async fn create_namespace(&self, request: CreateNamespaceRequest) -> Result<()>; async fn create_namespace(
&self,
request: CreateNamespaceRequest,
) -> Result<CreateNamespaceResponse>;
/// Drop a namespace /// Drop a namespace
async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<()>; async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<DropNamespaceResponse>;
/// Describe a namespace (get its properties)
async fn describe_namespace(
&self,
request: DescribeNamespaceRequest,
) -> Result<DescribeNamespaceResponse>;
/// List the names of tables in the database /// List the names of tables in the database
///
/// # Deprecated
/// Use `list_tables` instead for pagination support
#[deprecated(note = "Use list_tables instead")]
async fn table_names(&self, request: TableNamesRequest) -> Result<Vec<String>>; async fn table_names(&self, request: TableNamesRequest) -> Result<Vec<String>>;
/// List tables in the database with pagination support
async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse>;
/// Create a table in the database /// Create a table in the database
async fn create_table(&self, request: CreateTableRequest) -> Result<Arc<dyn BaseTable>>; async fn create_table(&self, request: CreateTableRequest) -> Result<Arc<dyn BaseTable>>;
/// Clone a table in the database. /// Clone a table in the database.

View File

@@ -24,10 +24,15 @@ use crate::io::object_store::MirroringObjectStoreWrapper;
use crate::table::NativeTable; use crate::table::NativeTable;
use crate::utils::validate_table_name; use crate::utils::validate_table_name;
use lance_namespace::models::{
CreateNamespaceRequest, CreateNamespaceResponse, DescribeNamespaceRequest,
DescribeNamespaceResponse, DropNamespaceRequest, DropNamespaceResponse, ListNamespacesRequest,
ListNamespacesResponse, ListTablesRequest, ListTablesResponse,
};
use super::{ use super::{
BaseTable, CloneTableRequest, CreateNamespaceRequest, CreateTableMode, CreateTableRequest, BaseTable, CloneTableRequest, CreateTableMode, CreateTableRequest, Database, DatabaseOptions,
Database, DatabaseOptions, DropNamespaceRequest, ListNamespacesRequest, OpenTableRequest, OpenTableRequest, TableNamesRequest,
TableNamesRequest,
}; };
/// File extension to indicate a lance table /// File extension to indicate a lance table
@@ -641,6 +646,7 @@ impl ListingDatabase {
index_cache_size: None, index_cache_size: None,
lance_read_params: None, lance_read_params: None,
location: None, location: None,
namespace_client: None,
}; };
let req = (callback)(req); let req = (callback)(req);
let table = self.open_table(req).await?; let table = self.open_table(req).await?;
@@ -662,14 +668,20 @@ impl ListingDatabase {
#[async_trait::async_trait] #[async_trait::async_trait]
impl Database for ListingDatabase { impl Database for ListingDatabase {
async fn list_namespaces(&self, request: ListNamespacesRequest) -> Result<Vec<String>> { async fn list_namespaces(
if !request.namespace.is_empty() { &self,
request: ListNamespacesRequest,
) -> Result<ListNamespacesResponse> {
if request.id.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
return Err(Error::NotSupported { return Err(Error::NotSupported {
message: "Namespace operations are not supported for listing database".into(), message: "Namespace operations are not supported for listing database".into(),
}); });
} }
Ok(Vec::new()) Ok(ListNamespacesResponse {
namespaces: Vec::new(),
page_token: None,
})
} }
fn uri(&self) -> &str { fn uri(&self) -> &str {
@@ -688,13 +700,28 @@ impl Database for ListingDatabase {
} }
} }
async fn create_namespace(&self, _request: CreateNamespaceRequest) -> Result<()> { async fn create_namespace(
&self,
_request: CreateNamespaceRequest,
) -> Result<CreateNamespaceResponse> {
Err(Error::NotSupported { Err(Error::NotSupported {
message: "Namespace operations are not supported for listing database".into(), message: "Namespace operations are not supported for listing database".into(),
}) })
} }
async fn drop_namespace(&self, _request: DropNamespaceRequest) -> Result<()> { async fn drop_namespace(
&self,
_request: DropNamespaceRequest,
) -> Result<DropNamespaceResponse> {
Err(Error::NotSupported {
message: "Namespace operations are not supported for listing database".into(),
})
}
async fn describe_namespace(
&self,
_request: DescribeNamespaceRequest,
) -> Result<DescribeNamespaceResponse> {
Err(Error::NotSupported { Err(Error::NotSupported {
message: "Namespace operations are not supported for listing database".into(), message: "Namespace operations are not supported for listing database".into(),
}) })
@@ -735,6 +762,57 @@ impl Database for ListingDatabase {
Ok(f) Ok(f)
} }
async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
if request.id.as_ref().map(|v| !v.is_empty()).unwrap_or(false) {
return Err(Error::NotSupported {
message: "Namespace parameter is not supported for listing database. Only root namespace is supported.".into(),
});
}
let mut f = self
.object_store
.read_dir(self.base_path.clone())
.await?
.iter()
.map(Path::new)
.filter(|path| {
let is_lance = path
.extension()
.and_then(|e| e.to_str())
.map(|e| e == LANCE_EXTENSION);
is_lance.unwrap_or(false)
})
.filter_map(|p| p.file_stem().and_then(|s| s.to_str().map(String::from)))
.collect::<Vec<String>>();
f.sort();
// Handle pagination with page_token
if let Some(ref page_token) = request.page_token {
let index = f
.iter()
.position(|name| name.as_str() > page_token.as_str())
.unwrap_or(f.len());
f.drain(0..index);
}
// Determine if there's a next page
let next_page_token = if let Some(limit) = request.limit {
if f.len() > limit as usize {
let token = f[limit as usize].clone();
f.truncate(limit as usize);
Some(token)
} else {
None
}
} else {
None
};
Ok(ListTablesResponse {
tables: f,
page_token: next_page_token,
})
}
async fn create_table(&self, request: CreateTableRequest) -> Result<Arc<dyn BaseTable>> { async fn create_table(&self, request: CreateTableRequest) -> Result<Arc<dyn BaseTable>> {
// When namespace is not empty, location must be provided // When namespace is not empty, location must be provided
if !request.namespace.is_empty() && request.location.is_none() { if !request.namespace.is_empty() && request.location.is_none() {
@@ -768,6 +846,7 @@ impl Database for ListingDatabase {
self.store_wrapper.clone(), self.store_wrapper.clone(),
Some(write_params), Some(write_params),
self.read_consistency_interval, self.read_consistency_interval,
request.namespace_client,
) )
.await .await
{ {
@@ -839,6 +918,7 @@ impl Database for ListingDatabase {
self.store_wrapper.clone(), self.store_wrapper.clone(),
None, None,
self.read_consistency_interval, self.read_consistency_interval,
None,
) )
.await?; .await?;
@@ -910,6 +990,7 @@ impl Database for ListingDatabase {
self.store_wrapper.clone(), self.store_wrapper.clone(),
Some(read_params), Some(read_params),
self.read_consistency_interval, self.read_consistency_interval,
request.namespace_client,
) )
.await?, .await?,
); );
@@ -947,6 +1028,7 @@ impl Database for ListingDatabase {
self.drop_tables(vec![name.to_string()]).await self.drop_tables(vec![name.to_string()]).await
} }
#[allow(deprecated)]
async fn drop_all_tables(&self, namespace: &[String]) -> Result<()> { async fn drop_all_tables(&self, namespace: &[String]) -> Result<()> {
// Check if namespace parameter is provided // Check if namespace parameter is provided
if !namespace.is_empty() { if !namespace.is_empty() {
@@ -1011,6 +1093,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1032,6 +1115,7 @@ mod tests {
.unwrap(); .unwrap();
// Verify both tables exist // Verify both tables exist
#[allow(deprecated)]
let table_names = db.table_names(TableNamesRequest::default()).await.unwrap(); let table_names = db.table_names(TableNamesRequest::default()).await.unwrap();
assert!(table_names.contains(&"source_table".to_string())); assert!(table_names.contains(&"source_table".to_string()));
assert!(table_names.contains(&"cloned_table".to_string())); assert!(table_names.contains(&"cloned_table".to_string()));
@@ -1075,6 +1159,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1133,6 +1218,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1168,6 +1254,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1207,6 +1294,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1246,6 +1334,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1300,6 +1389,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1357,6 +1447,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1442,6 +1533,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1528,6 +1620,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1621,6 +1714,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1718,6 +1812,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options: Default::default(), write_options: Default::default(),
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1771,6 +1866,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options, write_options,
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();
@@ -1844,6 +1940,7 @@ mod tests {
mode: CreateTableMode::Create, mode: CreateTableMode::Create,
write_options, write_options,
location: None, location: None,
namespace_client: None,
}) })
.await .await
.unwrap(); .unwrap();

View File

@@ -10,8 +10,10 @@ use async_trait::async_trait;
use lance_io::object_store::{LanceNamespaceStorageOptionsProvider, StorageOptionsProvider}; use lance_io::object_store::{LanceNamespaceStorageOptionsProvider, StorageOptionsProvider};
use lance_namespace::{ use lance_namespace::{
models::{ models::{
CreateEmptyTableRequest, CreateNamespaceRequest, DescribeTableRequest, CreateEmptyTableRequest, CreateNamespaceRequest, CreateNamespaceResponse,
DropNamespaceRequest, DropTableRequest, ListNamespacesRequest, ListTablesRequest, DescribeNamespaceRequest, DescribeNamespaceResponse, DescribeTableRequest,
DropNamespaceRequest, DropNamespaceResponse, DropTableRequest, ListNamespacesRequest,
ListNamespacesResponse, ListTablesRequest, ListTablesResponse,
}, },
LanceNamespace, LanceNamespace,
}; };
@@ -22,11 +24,8 @@ use crate::database::ReadConsistency;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use super::{ use super::{
listing::ListingDatabase, BaseTable, CloneTableRequest, listing::ListingDatabase, BaseTable, CloneTableRequest, CreateTableMode,
CreateNamespaceRequest as DbCreateNamespaceRequest, CreateTableMode, CreateTableRequest as DbCreateTableRequest, Database, OpenTableRequest, TableNamesRequest,
CreateTableRequest as DbCreateTableRequest, Database,
DropNamespaceRequest as DbDropNamespaceRequest,
ListNamespacesRequest as DbListNamespacesRequest, OpenTableRequest, TableNamesRequest,
}; };
/// A database implementation that uses lance-namespace for table management /// A database implementation that uses lance-namespace for table management
@@ -40,6 +39,8 @@ pub struct LanceNamespaceDatabase {
session: Option<Arc<lance::session::Session>>, session: Option<Arc<lance::session::Session>>,
// database URI // database URI
uri: String, uri: String,
// Whether to enable server-side query execution
server_side_query_enabled: bool,
} }
impl LanceNamespaceDatabase { impl LanceNamespaceDatabase {
@@ -49,6 +50,7 @@ impl LanceNamespaceDatabase {
storage_options: HashMap<String, String>, storage_options: HashMap<String, String>,
read_consistency_interval: Option<std::time::Duration>, read_consistency_interval: Option<std::time::Duration>,
session: Option<Arc<lance::session::Session>>, session: Option<Arc<lance::session::Session>>,
server_side_query_enabled: bool,
) -> Result<Self> { ) -> Result<Self> {
let mut builder = ConnectBuilder::new(ns_impl); let mut builder = ConnectBuilder::new(ns_impl);
for (key, value) in ns_properties.clone() { for (key, value) in ns_properties.clone() {
@@ -67,6 +69,7 @@ impl LanceNamespaceDatabase {
read_consistency_interval, read_consistency_interval,
session, session,
uri: format!("namespace://{}", ns_impl), uri: format!("namespace://{}", ns_impl),
server_side_query_enabled,
}) })
} }
} }
@@ -76,6 +79,7 @@ impl std::fmt::Debug for LanceNamespaceDatabase {
f.debug_struct("LanceNamespaceDatabase") f.debug_struct("LanceNamespaceDatabase")
.field("storage_options", &self.storage_options) .field("storage_options", &self.storage_options)
.field("read_consistency_interval", &self.read_consistency_interval) .field("read_consistency_interval", &self.read_consistency_interval)
.field("server_side_query_enabled", &self.server_side_query_enabled)
.finish() .finish()
} }
} }
@@ -149,92 +153,47 @@ impl Database for LanceNamespaceDatabase {
} }
} }
async fn list_namespaces(&self, request: DbListNamespacesRequest) -> Result<Vec<String>> { async fn list_namespaces(
let ns_request = ListNamespacesRequest { &self,
id: if request.namespace.is_empty() { request: ListNamespacesRequest,
None ) -> Result<ListNamespacesResponse> {
} else { Ok(self.namespace.list_namespaces(request).await?)
Some(request.namespace)
},
page_token: request.page_token,
limit: request.limit.map(|l| l as i32),
};
let response = self
.namespace
.list_namespaces(ns_request)
.await
.map_err(|e| Error::Runtime {
message: format!("Failed to list namespaces: {}", e),
})?;
Ok(response.namespaces)
} }
async fn create_namespace(&self, request: DbCreateNamespaceRequest) -> Result<()> { async fn create_namespace(
let ns_request = CreateNamespaceRequest { &self,
id: if request.namespace.is_empty() { request: CreateNamespaceRequest,
None ) -> Result<CreateNamespaceResponse> {
} else { Ok(self.namespace.create_namespace(request).await?)
Some(request.namespace)
},
mode: None,
properties: None,
};
self.namespace
.create_namespace(ns_request)
.await
.map_err(|e| Error::Runtime {
message: format!("Failed to create namespace: {}", e),
})?;
Ok(())
} }
async fn drop_namespace(&self, request: DbDropNamespaceRequest) -> Result<()> { async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<DropNamespaceResponse> {
let ns_request = DropNamespaceRequest { Ok(self.namespace.drop_namespace(request).await?)
id: if request.namespace.is_empty() { }
None
} else {
Some(request.namespace)
},
mode: None,
behavior: None,
};
self.namespace async fn describe_namespace(
.drop_namespace(ns_request) &self,
.await request: DescribeNamespaceRequest,
.map_err(|e| Error::Runtime { ) -> Result<DescribeNamespaceResponse> {
message: format!("Failed to drop namespace: {}", e), Ok(self.namespace.describe_namespace(request).await?)
})?;
Ok(())
} }
async fn table_names(&self, request: TableNamesRequest) -> Result<Vec<String>> { async fn table_names(&self, request: TableNamesRequest) -> Result<Vec<String>> {
let ns_request = ListTablesRequest { let ns_request = ListTablesRequest {
id: if request.namespace.is_empty() { id: Some(request.namespace),
None
} else {
Some(request.namespace)
},
page_token: request.start_after, page_token: request.start_after,
limit: request.limit.map(|l| l as i32), limit: request.limit.map(|l| l as i32),
}; };
let response = let response = self.namespace.list_tables(ns_request).await?;
self.namespace
.list_tables(ns_request)
.await
.map_err(|e| Error::Runtime {
message: format!("Failed to list tables: {}", e),
})?;
Ok(response.tables) Ok(response.tables)
} }
async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
Ok(self.namespace.list_tables(request).await?)
}
async fn create_table(&self, request: DbCreateTableRequest) -> Result<Arc<dyn BaseTable>> { async fn create_table(&self, request: DbCreateTableRequest) -> Result<Arc<dyn BaseTable>> {
// Extract user-provided storage options from request // Extract user-provided storage options from request
let user_storage_options = request let user_storage_options = request
@@ -290,6 +249,10 @@ impl Database for LanceNamespaceDatabase {
) )
.await?; .await?;
let namespace_client = self
.server_side_query_enabled
.then(|| self.namespace.clone());
return listing_db return listing_db
.open_table(OpenTableRequest { .open_table(OpenTableRequest {
name: request.name.clone(), name: request.name.clone(),
@@ -297,6 +260,7 @@ impl Database for LanceNamespaceDatabase {
index_cache_size: None, index_cache_size: None,
lance_read_params: None, lance_read_params: None,
location: Some(location), location: Some(location),
namespace_client,
}) })
.await; .await;
} }
@@ -333,12 +297,16 @@ impl Database for LanceNamespaceDatabase {
let listing_db = self let listing_db = self
.create_listing_database( .create_listing_database(
&location, &location,
table_id, table_id.clone(),
user_storage_options, user_storage_options,
create_empty_response.storage_options.as_ref(), create_empty_response.storage_options.as_ref(),
) )
.await?; .await?;
let namespace_client = self
.server_side_query_enabled
.then(|| self.namespace.clone());
let create_request = DbCreateTableRequest { let create_request = DbCreateTableRequest {
name: request.name, name: request.name,
namespace: request.namespace, namespace: request.namespace,
@@ -346,7 +314,9 @@ impl Database for LanceNamespaceDatabase {
mode: request.mode, mode: request.mode,
write_options: request.write_options, write_options: request.write_options,
location: Some(location), location: Some(location),
namespace_client,
}; };
listing_db.create_table(create_request).await listing_db.create_table(create_request).await
} }
@@ -380,19 +350,25 @@ impl Database for LanceNamespaceDatabase {
let listing_db = self let listing_db = self
.create_listing_database( .create_listing_database(
&location, &location,
table_id, table_id.clone(),
user_storage_options, user_storage_options,
response.storage_options.as_ref(), response.storage_options.as_ref(),
) )
.await?; .await?;
let namespace_client = self
.server_side_query_enabled
.then(|| self.namespace.clone());
let open_request = OpenTableRequest { let open_request = OpenTableRequest {
name: request.name.clone(), name: request.name.clone(),
namespace: request.namespace.clone(), namespace: request.namespace.clone(),
index_cache_size: request.index_cache_size, index_cache_size: request.index_cache_size,
lance_read_params: request.lance_read_params, lance_read_params: request.lance_read_params,
location: Some(location), location: Some(location),
namespace_client,
}; };
listing_db.open_table(open_request).await listing_db.open_table(open_request).await
} }
@@ -429,6 +405,7 @@ impl Database for LanceNamespaceDatabase {
Ok(()) Ok(())
} }
#[allow(deprecated)]
async fn drop_all_tables(&self, namespace: &[String]) -> Result<()> { async fn drop_all_tables(&self, namespace: &[String]) -> Result<()> {
let tables = self let tables = self
.table_names(TableNamesRequest { .table_names(TableNamesRequest {
@@ -455,7 +432,6 @@ impl Database for LanceNamespaceDatabase {
mod tests { mod tests {
use super::*; use super::*;
use crate::connect_namespace; use crate::connect_namespace;
use crate::database::CreateNamespaceRequest;
use crate::query::ExecutableQuery; use crate::query::ExecutableQuery;
use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator, StringArray}; use arrow_array::{Int32Array, RecordBatch, RecordBatchIterator, StringArray};
use arrow_schema::{DataType, Field, Schema}; use arrow_schema::{DataType, Field, Schema};
@@ -568,7 +544,9 @@ mod tests {
// Create a child namespace first // Create a child namespace first
conn.create_namespace(CreateNamespaceRequest { conn.create_namespace(CreateNamespaceRequest {
namespace: vec!["test_ns".into()], id: Some(vec!["test_ns".into()]),
mode: None,
properties: None,
}) })
.await .await
.expect("Failed to create namespace"); .expect("Failed to create namespace");
@@ -627,7 +605,9 @@ mod tests {
// Create a child namespace first // Create a child namespace first
conn.create_namespace(CreateNamespaceRequest { conn.create_namespace(CreateNamespaceRequest {
namespace: vec!["test_ns".into()], id: Some(vec!["test_ns".into()]),
mode: None,
properties: None,
}) })
.await .await
.expect("Failed to create namespace"); .expect("Failed to create namespace");
@@ -689,7 +669,9 @@ mod tests {
// Create a child namespace first // Create a child namespace first
conn.create_namespace(CreateNamespaceRequest { conn.create_namespace(CreateNamespaceRequest {
namespace: vec!["test_ns".into()], id: Some(vec!["test_ns".into()]),
mode: None,
properties: None,
}) })
.await .await
.expect("Failed to create namespace"); .expect("Failed to create namespace");
@@ -771,7 +753,9 @@ mod tests {
// Create a child namespace first // Create a child namespace first
conn.create_namespace(CreateNamespaceRequest { conn.create_namespace(CreateNamespaceRequest {
namespace: vec!["test_ns".into()], id: Some(vec!["test_ns".into()]),
mode: None,
properties: None,
}) })
.await .await
.expect("Failed to create namespace"); .expect("Failed to create namespace");
@@ -825,7 +809,9 @@ mod tests {
// Create a child namespace first // Create a child namespace first
conn.create_namespace(CreateNamespaceRequest { conn.create_namespace(CreateNamespaceRequest {
namespace: vec!["test_ns".into()], id: Some(vec!["test_ns".into()]),
mode: None,
properties: None,
}) })
.await .await
.expect("Failed to create namespace"); .expect("Failed to create namespace");
@@ -904,7 +890,9 @@ mod tests {
// Create a child namespace first // Create a child namespace first
conn.create_namespace(CreateNamespaceRequest { conn.create_namespace(CreateNamespaceRequest {
namespace: vec!["test_ns".into()], id: Some(vec!["test_ns".into()]),
mode: None,
properties: None,
}) })
.await .await
.expect("Failed to create namespace"); .expect("Failed to create namespace");
@@ -936,7 +924,9 @@ mod tests {
// Create a child namespace first // Create a child namespace first
conn.create_namespace(CreateNamespaceRequest { conn.create_namespace(CreateNamespaceRequest {
namespace: vec!["test_ns".into()], id: Some(vec!["test_ns".into()]),
mode: None,
properties: None,
}) })
.await .await
.expect("Failed to create namespace"); .expect("Failed to create namespace");
@@ -977,4 +967,46 @@ mod tests {
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());
} }
#[tokio::test]
async fn test_table_names_at_root() {
// Test that table_names at root (empty namespace) works correctly
// This is a regression test for a bug where empty namespace was converted to None
let tmp_dir = tempdir().unwrap();
let root_path = tmp_dir.path().to_str().unwrap().to_string();
let mut properties = HashMap::new();
properties.insert("root".to_string(), root_path);
let conn = connect_namespace("dir", properties)
.execute()
.await
.expect("Failed to connect to namespace");
// Create multiple tables at root namespace
let test_data1 = create_test_data();
let _table1 = conn
.create_table("table1", test_data1)
.execute()
.await
.expect("Failed to create table1 at root");
let test_data2 = create_test_data();
let _table2 = conn
.create_table("table2", test_data2)
.execute()
.await
.expect("Failed to create table2 at root");
// List tables at root using table_names (empty namespace means root)
let table_names = conn
.table_names()
.execute()
.await
.expect("Failed to list tables at root");
assert!(table_names.contains(&"table1".to_string()));
assert!(table_names.contains(&"table2".to_string()));
assert_eq!(table_names.len(), 2);
}
} }

View File

@@ -13,7 +13,7 @@ use crate::{table::BaseTable, DistanceType, Error, Result};
use self::{ use self::{
scalar::{BTreeIndexBuilder, BitmapIndexBuilder, LabelListIndexBuilder}, scalar::{BTreeIndexBuilder, BitmapIndexBuilder, LabelListIndexBuilder},
vector::{IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder}, vector::{IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder, IvfSqIndexBuilder},
}; };
pub mod scalar; pub mod scalar;
@@ -54,6 +54,9 @@ pub enum Index {
/// IVF index with Product Quantization /// IVF index with Product Quantization
IvfPq(IvfPqIndexBuilder), IvfPq(IvfPqIndexBuilder),
/// IVF index with Scalar Quantization
IvfSq(IvfSqIndexBuilder),
/// IVF index with RabitQ Quantization /// IVF index with RabitQ Quantization
IvfRq(IvfRqIndexBuilder), IvfRq(IvfRqIndexBuilder),
@@ -277,6 +280,8 @@ pub enum IndexType {
// Vector // Vector
#[serde(alias = "IVF_FLAT")] #[serde(alias = "IVF_FLAT")]
IvfFlat, IvfFlat,
#[serde(alias = "IVF_SQ")]
IvfSq,
#[serde(alias = "IVF_PQ")] #[serde(alias = "IVF_PQ")]
IvfPq, IvfPq,
#[serde(alias = "IVF_RQ")] #[serde(alias = "IVF_RQ")]
@@ -301,6 +306,7 @@ impl std::fmt::Display for IndexType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self { match self {
Self::IvfFlat => write!(f, "IVF_FLAT"), Self::IvfFlat => write!(f, "IVF_FLAT"),
Self::IvfSq => write!(f, "IVF_SQ"),
Self::IvfPq => write!(f, "IVF_PQ"), Self::IvfPq => write!(f, "IVF_PQ"),
Self::IvfRq => write!(f, "IVF_RQ"), Self::IvfRq => write!(f, "IVF_RQ"),
Self::IvfHnswPq => write!(f, "IVF_HNSW_PQ"), Self::IvfHnswPq => write!(f, "IVF_HNSW_PQ"),
@@ -323,6 +329,7 @@ impl std::str::FromStr for IndexType {
"LABEL_LIST" | "LABELLIST" => Ok(Self::LabelList), "LABEL_LIST" | "LABELLIST" => Ok(Self::LabelList),
"FTS" | "INVERTED" => Ok(Self::FTS), "FTS" | "INVERTED" => Ok(Self::FTS),
"IVF_FLAT" => Ok(Self::IvfFlat), "IVF_FLAT" => Ok(Self::IvfFlat),
"IVF_SQ" => Ok(Self::IvfSq),
"IVF_PQ" => Ok(Self::IvfPq), "IVF_PQ" => Ok(Self::IvfPq),
"IVF_RQ" => Ok(Self::IvfRq), "IVF_RQ" => Ok(Self::IvfRq),
"IVF_HNSW_PQ" => Ok(Self::IvfHnswPq), "IVF_HNSW_PQ" => Ok(Self::IvfHnswPq),

View File

@@ -209,6 +209,38 @@ impl IvfFlatIndexBuilder {
impl_ivf_params_setter!(); impl_ivf_params_setter!();
} }
/// Builder for an IVF SQ index.
///
/// This index compresses vectors using scalar quantization and groups them into IVF partitions.
/// It offers a balance between search performance and storage footprint.
#[derive(Debug, Clone)]
pub struct IvfSqIndexBuilder {
pub(crate) distance_type: DistanceType,
// IVF
pub(crate) num_partitions: Option<u32>,
pub(crate) sample_rate: u32,
pub(crate) max_iterations: u32,
pub(crate) target_partition_size: Option<u32>,
}
impl Default for IvfSqIndexBuilder {
fn default() -> Self {
Self {
distance_type: DistanceType::L2,
num_partitions: None,
sample_rate: 256,
max_iterations: 50,
target_partition_size: None,
}
}
}
impl IvfSqIndexBuilder {
impl_distance_type_setter!();
impl_ivf_params_setter!();
}
/// Builder for an IVF PQ index. /// Builder for an IVF PQ index.
/// ///
/// This index stores a compressed (quantized) copy of every vector. These vectors /// This index stores a compressed (quantized) copy of every vector. These vectors

View File

@@ -10,13 +10,17 @@ use http::StatusCode;
use lance_io::object_store::StorageOptions; use lance_io::object_store::StorageOptions;
use moka::future::Cache; use moka::future::Cache;
use reqwest::header::CONTENT_TYPE; use reqwest::header::CONTENT_TYPE;
use serde::Deserialize;
use tokio::task::spawn_blocking; use tokio::task::spawn_blocking;
use lance_namespace::models::{
CreateNamespaceRequest, CreateNamespaceResponse, DescribeNamespaceRequest,
DescribeNamespaceResponse, DropNamespaceRequest, DropNamespaceResponse, ListNamespacesRequest,
ListNamespacesResponse, ListTablesRequest, ListTablesResponse,
};
use crate::database::{ use crate::database::{
CloneTableRequest, CreateNamespaceRequest, CreateTableData, CreateTableMode, CloneTableRequest, CreateTableData, CreateTableMode, CreateTableRequest, Database,
CreateTableRequest, Database, DatabaseOptions, DropNamespaceRequest, ListNamespacesRequest, DatabaseOptions, OpenTableRequest, ReadConsistency, TableNamesRequest,
OpenTableRequest, ReadConsistency, TableNamesRequest,
}; };
use crate::error::Result; use crate::error::Result;
use crate::table::BaseTable; use crate::table::BaseTable;
@@ -180,11 +184,6 @@ impl RemoteDatabaseOptionsBuilder {
} }
} }
#[derive(Deserialize)]
struct ListTablesResponse {
tables: Vec<String>,
}
#[derive(Debug)] #[derive(Debug)]
pub struct RemoteDatabase<S: HttpSend = Sender> { pub struct RemoteDatabase<S: HttpSend = Sender> {
client: RestfulLanceDbClient<S>, client: RestfulLanceDbClient<S>,
@@ -337,7 +336,6 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
self.client self.client
.get(&format!("/v1/namespace/{}/table/list", namespace_id)) .get(&format!("/v1/namespace/{}/table/list", namespace_id))
} else { } else {
// TODO: use new API for all listing operations once stable
self.client.get("/v1/table/") self.client.get("/v1/table/")
}; };
@@ -371,6 +369,44 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
Ok(tables) Ok(tables)
} }
async fn list_tables(&self, request: ListTablesRequest) -> Result<ListTablesResponse> {
let namespace_parts = request.id.as_deref().unwrap_or(&[]);
let namespace_id = build_namespace_identifier(namespace_parts, &self.client.id_delimiter);
let mut req = self
.client
.get(&format!("/v1/namespace/{}/table/list", namespace_id));
if let Some(limit) = request.limit {
req = req.query(&[("limit", limit)]);
}
if let Some(ref page_token) = request.page_token {
req = req.query(&[("page_token", page_token)]);
}
let (request_id, rsp) = self.client.send_with_retry(req, None, true).await?;
let rsp = self.client.check_response(&request_id, rsp).await?;
let version = parse_server_version(&request_id, &rsp)?;
let response: ListTablesResponse = rsp.json().await.err_to_http(request_id)?;
// Cache the tables for future use
let namespace_vec = namespace_parts.to_vec();
for table in &response.tables {
let table_identifier =
build_table_identifier(table, &namespace_vec, &self.client.id_delimiter);
let cache_key = build_cache_key(table, &namespace_vec);
let remote_table = Arc::new(RemoteTable::new(
self.client.clone(),
table.clone(),
namespace_vec.clone(),
table_identifier.clone(),
version.clone(),
));
self.table_cache.insert(cache_key, remote_table).await;
}
Ok(response)
}
async fn create_table(&self, request: CreateTableRequest) -> Result<Arc<dyn BaseTable>> { async fn create_table(&self, request: CreateTableRequest) -> Result<Arc<dyn BaseTable>> {
let data = match request.data { let data = match request.data {
CreateTableData::Data(data) => data, CreateTableData::Data(data) => data,
@@ -417,6 +453,7 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
index_cache_size: None, index_cache_size: None,
lance_read_params: None, lance_read_params: None,
location: None, location: None,
namespace_client: None,
}; };
let req = (callback)(req); let req = (callback)(req);
self.open_table(req).await self.open_table(req).await
@@ -590,53 +627,101 @@ impl<S: HttpSend> Database for RemoteDatabase<S> {
}) })
} }
async fn list_namespaces(&self, request: ListNamespacesRequest) -> Result<Vec<String>> { async fn list_namespaces(
let namespace_id = &self,
build_namespace_identifier(request.namespace.as_slice(), &self.client.id_delimiter); request: ListNamespacesRequest,
) -> Result<ListNamespacesResponse> {
let namespace_parts = request.id.as_deref().unwrap_or(&[]);
let namespace_id = build_namespace_identifier(namespace_parts, &self.client.id_delimiter);
let mut req = self let mut req = self
.client .client
.get(&format!("/v1/namespace/{}/list", namespace_id)); .get(&format!("/v1/namespace/{}/list", namespace_id));
if let Some(limit) = request.limit { if let Some(limit) = request.limit {
req = req.query(&[("limit", limit)]); req = req.query(&[("limit", limit)]);
} }
if let Some(page_token) = request.page_token { if let Some(ref page_token) = request.page_token {
req = req.query(&[("page_token", page_token)]); req = req.query(&[("page_token", page_token)]);
} }
let (request_id, resp) = self.client.send(req).await?; let (request_id, resp) = self.client.send(req).await?;
let resp = self.client.check_response(&request_id, resp).await?; let resp = self.client.check_response(&request_id, resp).await?;
#[derive(Deserialize)] resp.json().await.err_to_http(request_id)
struct ListNamespacesResponse {
namespaces: Vec<String>,
} }
let parsed: ListNamespacesResponse = resp.json().await.map_err(|e| Error::Runtime { async fn create_namespace(
message: format!("Failed to parse namespace response: {}", e), &self,
})?; request: CreateNamespaceRequest,
Ok(parsed.namespaces) ) -> Result<CreateNamespaceResponse> {
} let namespace_parts = request.id.as_deref().unwrap_or(&[]);
let namespace_id = build_namespace_identifier(namespace_parts, &self.client.id_delimiter);
async fn create_namespace(&self, request: CreateNamespaceRequest) -> Result<()> { let mut req = self
let namespace_id =
build_namespace_identifier(request.namespace.as_slice(), &self.client.id_delimiter);
let req = self
.client .client
.post(&format!("/v1/namespace/{}/create", namespace_id)); .post(&format!("/v1/namespace/{}/create", namespace_id));
let (request_id, resp) = self.client.send(req).await?;
self.client.check_response(&request_id, resp).await?; // Build request body with mode and properties if present
Ok(()) #[derive(serde::Serialize)]
struct CreateNamespaceRequestBody {
#[serde(skip_serializing_if = "Option::is_none")]
mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
properties: Option<HashMap<String, String>>,
} }
async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<()> { let body = CreateNamespaceRequestBody {
let namespace_id = mode: request.mode.as_ref().map(|m| format!("{:?}", m)),
build_namespace_identifier(request.namespace.as_slice(), &self.client.id_delimiter); properties: request.properties,
let req = self };
req = req.json(&body);
let (request_id, resp) = self.client.send(req).await?;
let resp = self.client.check_response(&request_id, resp).await?;
resp.json().await.err_to_http(request_id)
}
async fn drop_namespace(&self, request: DropNamespaceRequest) -> Result<DropNamespaceResponse> {
let namespace_parts = request.id.as_deref().unwrap_or(&[]);
let namespace_id = build_namespace_identifier(namespace_parts, &self.client.id_delimiter);
let mut req = self
.client .client
.post(&format!("/v1/namespace/{}/drop", namespace_id)); .post(&format!("/v1/namespace/{}/drop", namespace_id));
// Build request body with mode and behavior if present
#[derive(serde::Serialize)]
struct DropNamespaceRequestBody {
#[serde(skip_serializing_if = "Option::is_none")]
mode: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
behavior: Option<String>,
}
let body = DropNamespaceRequestBody {
mode: request.mode.as_ref().map(|m| format!("{:?}", m)),
behavior: request.behavior.as_ref().map(|b| format!("{:?}", b)),
};
req = req.json(&body);
let (request_id, resp) = self.client.send(req).await?; let (request_id, resp) = self.client.send(req).await?;
self.client.check_response(&request_id, resp).await?; let resp = self.client.check_response(&request_id, resp).await?;
Ok(())
resp.json().await.err_to_http(request_id)
}
async fn describe_namespace(
&self,
request: DescribeNamespaceRequest,
) -> Result<DescribeNamespaceResponse> {
let namespace_parts = request.id.as_deref().unwrap_or(&[]);
let namespace_id = build_namespace_identifier(namespace_parts, &self.client.id_delimiter);
let req = self
.client
.get(&format!("/v1/namespace/{}/describe", namespace_id));
let (request_id, resp) = self.client.send(req).await?;
let resp = self.client.check_response(&request_id, resp).await?;
resp.json().await.err_to_http(request_id)
} }
fn as_any(&self) -> &dyn std::any::Any { fn as_any(&self) -> &dyn std::any::Any {

View File

@@ -1072,6 +1072,14 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
body["num_bits"] = serde_json::Value::Number(num_bits.into()); body["num_bits"] = serde_json::Value::Number(num_bits.into());
} }
} }
Index::IvfSq(index) => {
body[INDEX_TYPE_KEY] = serde_json::Value::String("IVF_SQ".to_string());
body[METRIC_TYPE_KEY] =
serde_json::Value::String(index.distance_type.to_string().to_lowercase());
if let Some(num_partitions) = index.num_partitions {
body["num_partitions"] = serde_json::Value::Number(num_partitions.into());
}
}
Index::IvfHnswSq(index) => { Index::IvfHnswSq(index) => {
body[INDEX_TYPE_KEY] = serde_json::Value::String("IVF_HNSW_SQ".to_string()); body[INDEX_TYPE_KEY] = serde_json::Value::String("IVF_HNSW_SQ".to_string());
body[METRIC_TYPE_KEY] = body[METRIC_TYPE_KEY] =

View File

@@ -40,6 +40,11 @@ use lance_index::vector::pq::PQBuildParams;
use lance_index::vector::sq::builder::SQBuildParams; use lance_index::vector::sq::builder::SQBuildParams;
use lance_index::DatasetIndexExt; use lance_index::DatasetIndexExt;
use lance_index::IndexType; use lance_index::IndexType;
use lance_namespace::models::{
QueryTableRequest as NsQueryTableRequest, QueryTableRequestFullTextQuery,
QueryTableRequestVector, StringFtsQuery,
};
use lance_namespace::LanceNamespace;
use lance_table::format::Manifest; use lance_table::format::Manifest;
use lance_table::io::commit::ManifestNamingScheme; use lance_table::io::commit::ManifestNamingScheme;
use log::info; use log::info;
@@ -1480,7 +1485,7 @@ impl NativeTableExt for Arc<dyn BaseTable> {
} }
/// A table in a LanceDB database. /// A table in a LanceDB database.
#[derive(Debug, Clone)] #[derive(Clone)]
pub struct NativeTable { pub struct NativeTable {
name: String, name: String,
namespace: Vec<String>, namespace: Vec<String>,
@@ -1490,6 +1495,22 @@ pub struct NativeTable {
// This comes from the connection options. We store here so we can pass down // This comes from the connection options. We store here so we can pass down
// to the dataset when we recreate it (for example, in checkout_latest). // to the dataset when we recreate it (for example, in checkout_latest).
read_consistency_interval: Option<std::time::Duration>, read_consistency_interval: Option<std::time::Duration>,
// Optional namespace client for server-side query execution.
// When set, queries will be executed on the namespace server instead of locally.
namespace_client: Option<Arc<dyn LanceNamespace>>,
}
impl std::fmt::Debug for NativeTable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NativeTable")
.field("name", &self.name)
.field("namespace", &self.namespace)
.field("id", &self.id)
.field("uri", &self.uri)
.field("read_consistency_interval", &self.read_consistency_interval)
.field("namespace_client", &self.namespace_client)
.finish()
}
} }
impl std::fmt::Display for NativeTable { impl std::fmt::Display for NativeTable {
@@ -1524,7 +1545,7 @@ impl NativeTable {
/// * A [NativeTable] object. /// * A [NativeTable] object.
pub async fn open(uri: &str) -> Result<Self> { pub async fn open(uri: &str) -> Result<Self> {
let name = Self::get_table_name(uri)?; let name = Self::get_table_name(uri)?;
Self::open_with_params(uri, &name, vec![], None, None, None).await Self::open_with_params(uri, &name, vec![], None, None, None, None).await
} }
/// Opens an existing Table /// Opens an existing Table
@@ -1534,10 +1555,12 @@ impl NativeTable {
/// * `base_path` - The base path where the table is located /// * `base_path` - The base path where the table is located
/// * `name` The Table name /// * `name` The Table name
/// * `params` The [ReadParams] to use when opening the table /// * `params` The [ReadParams] to use when opening the table
/// * `namespace_client` - Optional namespace client for server-side query execution
/// ///
/// # Returns /// # Returns
/// ///
/// * A [NativeTable] object. /// * A [NativeTable] object.
#[allow(clippy::too_many_arguments)]
pub async fn open_with_params( pub async fn open_with_params(
uri: &str, uri: &str,
name: &str, name: &str,
@@ -1545,6 +1568,7 @@ impl NativeTable {
write_store_wrapper: Option<Arc<dyn WrappingObjectStore>>, write_store_wrapper: Option<Arc<dyn WrappingObjectStore>>,
params: Option<ReadParams>, params: Option<ReadParams>,
read_consistency_interval: Option<std::time::Duration>, read_consistency_interval: Option<std::time::Duration>,
namespace_client: Option<Arc<dyn LanceNamespace>>,
) -> Result<Self> { ) -> Result<Self> {
let params = params.unwrap_or_default(); let params = params.unwrap_or_default();
// patch the params if we have a write store wrapper // patch the params if we have a write store wrapper
@@ -1575,9 +1599,18 @@ impl NativeTable {
uri: uri.to_string(), uri: uri.to_string(),
dataset, dataset,
read_consistency_interval, read_consistency_interval,
namespace_client,
}) })
} }
/// Set the namespace client for server-side query execution.
///
/// When set, queries will be executed on the namespace server instead of locally.
pub fn with_namespace_client(mut self, namespace_client: Arc<dyn LanceNamespace>) -> Self {
self.namespace_client = Some(namespace_client);
self
}
fn get_table_name(uri: &str) -> Result<String> { fn get_table_name(uri: &str) -> Result<String> {
let path = Path::new(uri); let path = Path::new(uri);
let name = path let name = path
@@ -1614,10 +1647,12 @@ impl NativeTable {
/// * `namespace` - The namespace path. When non-empty, an explicit URI must be provided. /// * `namespace` - The namespace path. When non-empty, an explicit URI must be provided.
/// * `batches` RecordBatch to be saved in the database. /// * `batches` RecordBatch to be saved in the database.
/// * `params` - Write parameters. /// * `params` - Write parameters.
/// * `namespace_client` - Optional namespace client for server-side query execution
/// ///
/// # Returns /// # Returns
/// ///
/// * A [TableImpl] object. /// * A [TableImpl] object.
#[allow(clippy::too_many_arguments)]
pub async fn create( pub async fn create(
uri: &str, uri: &str,
name: &str, name: &str,
@@ -1626,6 +1661,7 @@ impl NativeTable {
write_store_wrapper: Option<Arc<dyn WrappingObjectStore>>, write_store_wrapper: Option<Arc<dyn WrappingObjectStore>>,
params: Option<WriteParams>, params: Option<WriteParams>,
read_consistency_interval: Option<std::time::Duration>, read_consistency_interval: Option<std::time::Duration>,
namespace_client: Option<Arc<dyn LanceNamespace>>,
) -> Result<Self> { ) -> Result<Self> {
// Default params uses format v1. // Default params uses format v1.
let params = params.unwrap_or(WriteParams { let params = params.unwrap_or(WriteParams {
@@ -1657,9 +1693,11 @@ impl NativeTable {
uri: uri.to_string(), uri: uri.to_string(),
dataset: DatasetConsistencyWrapper::new_latest(dataset, read_consistency_interval), dataset: DatasetConsistencyWrapper::new_latest(dataset, read_consistency_interval),
read_consistency_interval, read_consistency_interval,
namespace_client,
}) })
} }
#[allow(clippy::too_many_arguments)]
pub async fn create_empty( pub async fn create_empty(
uri: &str, uri: &str,
name: &str, name: &str,
@@ -1668,6 +1706,7 @@ impl NativeTable {
write_store_wrapper: Option<Arc<dyn WrappingObjectStore>>, write_store_wrapper: Option<Arc<dyn WrappingObjectStore>>,
params: Option<WriteParams>, params: Option<WriteParams>,
read_consistency_interval: Option<std::time::Duration>, read_consistency_interval: Option<std::time::Duration>,
namespace_client: Option<Arc<dyn LanceNamespace>>,
) -> Result<Self> { ) -> Result<Self> {
let batches = RecordBatchIterator::new(vec![], schema); let batches = RecordBatchIterator::new(vec![], schema);
Self::create( Self::create(
@@ -1678,6 +1717,7 @@ impl NativeTable {
write_store_wrapper, write_store_wrapper,
params, params,
read_consistency_interval, read_consistency_interval,
namespace_client,
) )
.await .await
} }
@@ -1906,6 +1946,25 @@ impl NativeTable {
VectorIndexParams::with_ivf_flat_params(index.distance_type.into(), ivf_params); VectorIndexParams::with_ivf_flat_params(index.distance_type.into(), ivf_params);
Ok(Box::new(lance_idx_params)) Ok(Box::new(lance_idx_params))
} }
Index::IvfSq(index) => {
Self::validate_index_type(field, "IVF SQ", supported_vector_data_type)?;
let ivf_params = Self::build_ivf_params(
index.num_partitions,
index.target_partition_size,
index.sample_rate,
index.max_iterations,
);
let sq_params = SQBuildParams {
sample_rate: index.sample_rate as usize,
..Default::default()
};
let lance_idx_params = VectorIndexParams::with_ivf_sq_params(
index.distance_type.into(),
ivf_params,
sq_params,
);
Ok(Box::new(lance_idx_params))
}
Index::IvfPq(index) => { Index::IvfPq(index) => {
Self::validate_index_type(field, "IVF PQ", supported_vector_data_type)?; Self::validate_index_type(field, "IVF PQ", supported_vector_data_type)?;
let dim = Self::get_vector_dimension(field)?; let dim = Self::get_vector_dimension(field)?;
@@ -2013,6 +2072,7 @@ impl NativeTable {
Index::LabelList(_) => IndexType::LabelList, Index::LabelList(_) => IndexType::LabelList,
Index::FTS(_) => IndexType::Inverted, Index::FTS(_) => IndexType::Inverted,
Index::IvfFlat(_) Index::IvfFlat(_)
| Index::IvfSq(_)
| Index::IvfPq(_) | Index::IvfPq(_)
| Index::IvfRq(_) | Index::IvfRq(_)
| Index::IvfHnswPq(_) | Index::IvfHnswPq(_)
@@ -2035,6 +2095,278 @@ impl NativeTable {
Ok(DatasetRecordBatchStream::new(inner)) Ok(DatasetRecordBatchStream::new(inner))
} }
/// Execute a query on the namespace server instead of locally.
async fn namespace_query(
&self,
namespace_client: Arc<dyn LanceNamespace>,
query: &AnyQuery,
_options: QueryExecutionOptions,
) -> Result<DatasetRecordBatchStream> {
// Build table_id from namespace + table name
let mut table_id = self.namespace.clone();
table_id.push(self.name.clone());
// Convert AnyQuery to namespace QueryTableRequest
let mut ns_request = self.convert_to_namespace_query(query)?;
// Set the table ID on the request
ns_request.id = Some(table_id);
// Call the namespace query_table API
let response_bytes = namespace_client
.query_table(ns_request)
.await
.map_err(|e| Error::Runtime {
message: format!("Failed to execute server-side query: {}", e),
})?;
// Parse the Arrow IPC response into a RecordBatchStream
self.parse_arrow_ipc_response(response_bytes).await
}
/// Convert a QueryFilter to a SQL string for the namespace API.
fn filter_to_sql(&self, filter: &QueryFilter) -> Result<String> {
match filter {
QueryFilter::Sql(sql) => Ok(sql.clone()),
QueryFilter::Substrait(_) => Err(Error::NotSupported {
message: "Substrait filters are not supported for server-side queries".to_string(),
}),
QueryFilter::Datafusion(_) => Err(Error::NotSupported {
message: "Datafusion expression filters are not supported for server-side queries. Use SQL filter instead.".to_string(),
}),
}
}
/// Convert an AnyQuery to the namespace QueryTableRequest format.
fn convert_to_namespace_query(&self, query: &AnyQuery) -> Result<NsQueryTableRequest> {
match query {
AnyQuery::VectorQuery(vq) => {
// Extract the query vector(s)
let vector = self.extract_query_vector(&vq.query_vector)?;
// Convert filter to SQL string
let filter = match &vq.base.filter {
Some(f) => Some(self.filter_to_sql(f)?),
None => None,
};
// Convert select to columns list
let columns = match &vq.base.select {
Select::All => None,
Select::Columns(cols) => Some(cols.clone()),
Select::Dynamic(_) => {
return Err(Error::NotSupported {
message:
"Dynamic column selection is not supported for server-side queries"
.to_string(),
});
}
};
// Check for unsupported features
if vq.base.reranker.is_some() {
return Err(Error::NotSupported {
message: "Reranker is not supported for server-side queries".to_string(),
});
}
// Convert FTS query if present
let full_text_query = vq.base.full_text_search.as_ref().map(|fts| {
let columns = fts.columns();
let columns_vec = if columns.is_empty() {
None
} else {
Some(columns.into_iter().collect())
};
Box::new(QueryTableRequestFullTextQuery {
string_query: Some(Box::new(StringFtsQuery {
query: fts.query.to_string(),
columns: columns_vec,
})),
structured_query: None,
})
});
Ok(NsQueryTableRequest {
id: None, // Will be set in namespace_query
k: vq.base.limit.unwrap_or(10) as i32,
vector: Box::new(vector),
vector_column: vq.column.clone(),
filter,
columns,
offset: vq.base.offset.map(|o| o as i32),
distance_type: vq.distance_type.map(|dt| dt.to_string()),
nprobes: Some(vq.minimum_nprobes as i32),
ef: vq.ef.map(|e| e as i32),
refine_factor: vq.refine_factor.map(|r| r as i32),
lower_bound: vq.lower_bound,
upper_bound: vq.upper_bound,
prefilter: Some(vq.base.prefilter),
fast_search: Some(vq.base.fast_search),
with_row_id: Some(vq.base.with_row_id),
bypass_vector_index: Some(!vq.use_index),
full_text_query,
version: None,
})
}
AnyQuery::Query(q) => {
// For non-vector queries, pass an empty vector (similar to remote table implementation)
if q.reranker.is_some() {
return Err(Error::NotSupported {
message: "Reranker is not supported for server-side query execution"
.to_string(),
});
}
let filter = q
.filter
.as_ref()
.map(|f| self.filter_to_sql(f))
.transpose()?;
let columns = match &q.select {
Select::All => None,
Select::Columns(cols) => Some(cols.clone()),
Select::Dynamic(_) => {
return Err(Error::NotSupported {
message: "Dynamic columns are not supported for server-side query"
.to_string(),
});
}
};
// Handle full text search if present
let full_text_query = q.full_text_search.as_ref().map(|fts| {
let columns_vec = if fts.columns().is_empty() {
None
} else {
Some(fts.columns().iter().cloned().collect())
};
Box::new(QueryTableRequestFullTextQuery {
string_query: Some(Box::new(StringFtsQuery {
query: fts.query.to_string(),
columns: columns_vec,
})),
structured_query: None,
})
});
// Empty vector for non-vector queries
let vector = Box::new(QueryTableRequestVector {
single_vector: Some(vec![]),
multi_vector: None,
});
Ok(NsQueryTableRequest {
id: None, // Will be set by caller
vector,
k: q.limit.unwrap_or(10) as i32,
filter,
columns,
prefilter: Some(q.prefilter),
offset: q.offset.map(|o| o as i32),
ef: None,
refine_factor: None,
distance_type: None,
nprobes: None,
vector_column: None, // No vector column for plain queries
with_row_id: Some(q.with_row_id),
bypass_vector_index: Some(true), // No vector index for plain queries
full_text_query,
version: None,
fast_search: None,
lower_bound: None,
upper_bound: None,
})
}
}
}
/// Extract query vector(s) from Arrow arrays into the namespace format.
fn extract_query_vector(
&self,
query_vectors: &[Arc<dyn arrow_array::Array>],
) -> Result<QueryTableRequestVector> {
if query_vectors.is_empty() {
return Err(Error::InvalidInput {
message: "Query vector is required for vector search".to_string(),
});
}
// Handle single vector case
if query_vectors.len() == 1 {
let arr = &query_vectors[0];
let single_vector = self.array_to_f32_vec(arr)?;
Ok(QueryTableRequestVector {
single_vector: Some(single_vector),
multi_vector: None,
})
} else {
// Handle multi-vector case
let multi_vector: Result<Vec<Vec<f32>>> = query_vectors
.iter()
.map(|arr| self.array_to_f32_vec(arr))
.collect();
Ok(QueryTableRequestVector {
single_vector: None,
multi_vector: Some(multi_vector?),
})
}
}
/// Convert an Arrow array to a Vec<f32>.
fn array_to_f32_vec(&self, arr: &Arc<dyn arrow_array::Array>) -> Result<Vec<f32>> {
// Handle FixedSizeList (common for vectors)
if let Some(fsl) = arr
.as_any()
.downcast_ref::<arrow_array::FixedSizeListArray>()
{
let values = fsl.values();
if let Some(f32_arr) = values.as_any().downcast_ref::<arrow_array::Float32Array>() {
return Ok(f32_arr.values().to_vec());
}
}
// Handle direct Float32Array
if let Some(f32_arr) = arr.as_any().downcast_ref::<arrow_array::Float32Array>() {
return Ok(f32_arr.values().to_vec());
}
Err(Error::InvalidInput {
message: "Query vector must be Float32 type".to_string(),
})
}
/// Parse Arrow IPC response from the namespace server.
async fn parse_arrow_ipc_response(
&self,
bytes: bytes::Bytes,
) -> Result<DatasetRecordBatchStream> {
use arrow_ipc::reader::StreamReader;
use std::io::Cursor;
let cursor = Cursor::new(bytes);
let reader = StreamReader::try_new(cursor, None).map_err(|e| Error::Runtime {
message: format!("Failed to parse Arrow IPC response: {}", e),
})?;
// Collect all record batches
let schema = reader.schema();
let batches: Vec<_> = reader
.into_iter()
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(|e| Error::Runtime {
message: format!("Failed to read Arrow IPC batches: {}", e),
})?;
// Create a stream from the batches
let stream = futures::stream::iter(batches.into_iter().map(Ok));
let record_batch_stream = Box::pin(
datafusion_physical_plan::stream::RecordBatchStreamAdapter::new(schema, stream),
);
Ok(DatasetRecordBatchStream::new(record_batch_stream))
}
/// Check whether the table uses V2 manifest paths. /// Check whether the table uses V2 manifest paths.
/// ///
/// See [Self::migrate_manifest_paths_v2] and [ManifestNamingScheme] for /// See [Self::migrate_manifest_paths_v2] and [ManifestNamingScheme] for
@@ -2466,6 +2798,12 @@ impl BaseTable for NativeTable {
query: &AnyQuery, query: &AnyQuery,
options: QueryExecutionOptions, options: QueryExecutionOptions,
) -> Result<DatasetRecordBatchStream> { ) -> Result<DatasetRecordBatchStream> {
// If namespace client is configured, use server-side query execution
if let Some(ref namespace_client) = self.namespace_client {
return self
.namespace_query(namespace_client.clone(), query, options)
.await;
}
self.generic_query(query, options).await self.generic_query(query, options).await
} }
@@ -2934,7 +3272,7 @@ mod tests {
let batches = make_test_batches(); let batches = make_test_batches();
let batches = Box::new(batches) as Box<dyn RecordBatchReader + Send>; let batches = Box::new(batches) as Box<dyn RecordBatchReader + Send>;
let table = NativeTable::create(uri, "test", vec![], batches, None, None, None) let table = NativeTable::create(uri, "test", vec![], batches, None, None, None, None)
.await .await
.unwrap(); .unwrap();
@@ -4574,4 +4912,91 @@ mod tests {
assert_eq!(result.len(), 1); assert_eq!(result.len(), 1);
assert_eq!(result[0].index_type, crate::index::IndexType::Bitmap); assert_eq!(result[0].index_type, crate::index::IndexType::Bitmap);
} }
#[tokio::test]
async fn test_convert_to_namespace_query_vector() {
let tmp_dir = tempdir().unwrap();
let dataset_path = tmp_dir.path().join("test_ns_query.lance");
let batches = make_test_batches();
Dataset::write(batches, dataset_path.to_str().unwrap(), None)
.await
.unwrap();
let table = NativeTable::open(dataset_path.to_str().unwrap())
.await
.unwrap();
// Create a vector query
let query_vector = Arc::new(Float32Array::from(vec![1.0, 2.0, 3.0, 4.0]));
let vq = VectorQueryRequest {
base: QueryRequest {
limit: Some(10),
offset: Some(5),
filter: Some(QueryFilter::Sql("id > 0".to_string())),
select: Select::Columns(vec!["id".to_string()]),
..Default::default()
},
column: Some("vector".to_string()),
query_vector: vec![query_vector as Arc<dyn Array>],
minimum_nprobes: 20,
distance_type: Some(crate::DistanceType::L2),
..Default::default()
};
let any_query = AnyQuery::VectorQuery(vq);
let ns_request = table.convert_to_namespace_query(&any_query).unwrap();
assert_eq!(ns_request.k, 10);
assert_eq!(ns_request.offset, Some(5));
assert_eq!(ns_request.filter, Some("id > 0".to_string()));
assert_eq!(ns_request.columns, Some(vec!["id".to_string()]));
assert_eq!(ns_request.vector_column, Some("vector".to_string()));
assert_eq!(ns_request.distance_type, Some("l2".to_string()));
assert!(ns_request.vector.single_vector.is_some());
assert_eq!(
ns_request.vector.single_vector.as_ref().unwrap(),
&vec![1.0, 2.0, 3.0, 4.0]
);
}
#[tokio::test]
async fn test_convert_to_namespace_query_plain_query() {
let tmp_dir = tempdir().unwrap();
let dataset_path = tmp_dir.path().join("test_ns_plain.lance");
let batches = make_test_batches();
Dataset::write(batches, dataset_path.to_str().unwrap(), None)
.await
.unwrap();
let table = NativeTable::open(dataset_path.to_str().unwrap())
.await
.unwrap();
// Create a plain (non-vector) query with filter and select
let q = QueryRequest {
limit: Some(20),
offset: Some(5),
filter: Some(QueryFilter::Sql("id > 5".to_string())),
select: Select::Columns(vec!["id".to_string()]),
with_row_id: true,
..Default::default()
};
let any_query = AnyQuery::Query(q);
let ns_request = table.convert_to_namespace_query(&any_query).unwrap();
// Plain queries should pass an empty vector
assert_eq!(ns_request.k, 20);
assert_eq!(ns_request.offset, Some(5));
assert_eq!(ns_request.filter, Some("id > 5".to_string()));
assert_eq!(ns_request.columns, Some(vec!["id".to_string()]));
assert_eq!(ns_request.with_row_id, Some(true));
assert_eq!(ns_request.bypass_vector_index, Some(true));
assert!(ns_request.vector_column.is_none()); // No vector column for plain queries
// Should have an empty vector
assert!(ns_request.vector.single_vector.as_ref().unwrap().is_empty());
}
} }