Compare commits

..

4 Commits

Author SHA1 Message Date
Jack Ye a3e3c1ad5c feat(rust): add namespace client properties 2026-04-15 22:58:05 -07:00
Jack Ye c6e43b545d refactor(rust): eagerly init listing namespace db 2026-04-15 22:28:52 -07:00
Jack Ye 5807ad464b refactor(rust): cache listing namespace db 2026-04-15 17:23:50 -07:00
Jack Ye 513c2215c5 feat(rust): support nested namespace ops in listing db 2026-04-15 17:15:26 -07:00
80 changed files with 1739 additions and 1957 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "0.28.0-beta.10" current_version = "0.28.0-beta.5"
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*)\\.
-18
View File
@@ -1,18 +0,0 @@
version: 2
# Scope: the root Cargo workspace, which produces the Rust binaries we
# ship to users (the Node.js and Python native extensions). The
# `rust/lancedb` library crate shares the same lockfile; its consumers
# pick their own dependency versions, but bumping transitive deps here
# keeps the binaries we ship current.
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 10
groups:
rust-minor-patch:
update-types:
- minor
- patch
-3
View File
@@ -8,9 +8,6 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions:
contents: read
jobs: jobs:
labeler: labeler:
permissions: permissions:
-3
View File
@@ -19,9 +19,6 @@ on:
paths: paths:
- .github/workflows/java-publish.yml - .github/workflows/java-publish.yml
permissions:
contents: read
jobs: jobs:
publish: publish:
name: Build and Publish name: Build and Publish
-3
View File
@@ -24,9 +24,6 @@ on:
- java/** - java/**
- .github/workflows/java.yml - .github/workflows/java.yml
permissions:
contents: read
jobs: jobs:
build-java: build-java:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -10,10 +10,6 @@ on:
- nodejs/** - nodejs/**
- java/** - java/**
- .github/workflows/license-header-check.yml - .github/workflows/license-header-check.yml
permissions:
contents: read
jobs: jobs:
check-licenses: check-licenses:
runs-on: ubuntu-latest runs-on: ubuntu-latest
-3
View File
@@ -15,9 +15,6 @@ on:
- .github/workflows/nodejs.yml - .github/workflows/nodejs.yml
- docker-compose.yml - docker-compose.yml
permissions:
contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
+3 -12
View File
@@ -14,16 +14,10 @@ on:
env: env:
PIP_EXTRA_INDEX_URL: "https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/" PIP_EXTRA_INDEX_URL: "https://pypi.fury.io/lance-format/ https://pypi.fury.io/lancedb/"
permissions:
contents: read
jobs: jobs:
linux: linux:
name: Python ${{ matrix.config.platform }} manylinux${{ matrix.config.manylinux }} name: Python ${{ matrix.config.platform }} manylinux${{ matrix.config.manylinux }}
timeout-minutes: 60 timeout-minutes: 60
permissions:
id-token: write
contents: read
strategy: strategy:
matrix: matrix:
config: config:
@@ -63,12 +57,10 @@ jobs:
- uses: ./.github/workflows/upload_wheel - uses: ./.github/workflows/upload_wheel
if: startsWith(github.ref, 'refs/tags/python-v') if: startsWith(github.ref, 'refs/tags/python-v')
with: with:
pypi_token: ${{ secrets.LANCEDB_PYPI_API_TOKEN }}
fury_token: ${{ secrets.FURY_TOKEN }} fury_token: ${{ secrets.FURY_TOKEN }}
mac: mac:
timeout-minutes: 90 timeout-minutes: 90
permissions:
id-token: write
contents: read
runs-on: ${{ matrix.config.runner }} runs-on: ${{ matrix.config.runner }}
strategy: strategy:
matrix: matrix:
@@ -93,12 +85,10 @@ jobs:
- uses: ./.github/workflows/upload_wheel - uses: ./.github/workflows/upload_wheel
if: startsWith(github.ref, 'refs/tags/python-v') if: startsWith(github.ref, 'refs/tags/python-v')
with: with:
pypi_token: ${{ secrets.LANCEDB_PYPI_API_TOKEN }}
fury_token: ${{ secrets.FURY_TOKEN }} fury_token: ${{ secrets.FURY_TOKEN }}
windows: windows:
timeout-minutes: 60 timeout-minutes: 60
permissions:
id-token: write
contents: read
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -117,6 +107,7 @@ jobs:
- uses: ./.github/workflows/upload_wheel - uses: ./.github/workflows/upload_wheel
if: startsWith(github.ref, 'refs/tags/python-v') if: startsWith(github.ref, 'refs/tags/python-v')
with: with:
pypi_token: ${{ secrets.LANCEDB_PYPI_API_TOKEN }}
fury_token: ${{ secrets.FURY_TOKEN }} fury_token: ${{ secrets.FURY_TOKEN }}
gh-release: gh-release:
if: startsWith(github.ref, 'refs/tags/python-v') if: startsWith(github.ref, 'refs/tags/python-v')
+2 -3
View File
@@ -17,9 +17,6 @@ on:
- .github/workflows/build_windows_wheel/** - .github/workflows/build_windows_wheel/**
- .github/workflows/run_tests/** - .github/workflows/run_tests/**
permissions:
contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
@@ -111,6 +108,7 @@ jobs:
- name: Install - name: Install
run: | run: |
pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .[tests,dev,embeddings] pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .[tests,dev,embeddings]
pip install tantivy
pip install mlx pip install mlx
- name: Doctest - name: Doctest
run: pytest --doctest-modules python/lancedb run: pytest --doctest-modules python/lancedb
@@ -229,5 +227,6 @@ jobs:
pip install "pydantic<2" pip install "pydantic<2"
pip install pyarrow==16 pip install pyarrow==16
pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .[tests] pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .[tests]
pip install tantivy
- name: Run tests - name: Run tests
run: pytest -m "not slow and not s3_test" -x -v --durations=30 python/tests run: pytest -m "not slow and not s3_test" -x -v --durations=30 python/tests
-17
View File
@@ -9,15 +9,9 @@ on:
- Cargo.toml - Cargo.toml
- Cargo.lock - Cargo.lock
- rust-toolchain.toml - rust-toolchain.toml
- deny.toml
- rust/** - rust/**
- nodejs/Cargo.toml
- python/Cargo.toml
- .github/workflows/rust.yml - .github/workflows/rust.yml
permissions:
contents: read
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true cancel-in-progress: true
@@ -59,17 +53,6 @@ jobs:
- name: Run clippy (without remote feature) - name: Run clippy (without remote feature)
run: cargo clippy --profile ci --workspace --tests -- -D warnings run: cargo clippy --profile ci --workspace --tests -- -D warnings
deny:
# Supply-chain checks: advisories, licenses, banned crates, and source
# restrictions. Configuration lives in `deny.toml` at the workspace root.
timeout-minutes: 10
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check advisories bans licenses sources
build-no-lock: build-no-lock:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
timeout-minutes: 30 timeout-minutes: 30
@@ -3,9 +3,6 @@ name: Update package-lock.json
on: on:
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -3,9 +3,6 @@ name: Update NodeJs package-lock.json
on: on:
workflow_dispatch: workflow_dispatch:
permissions:
contents: read
jobs: jobs:
publish: publish:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+21 -10
View File
@@ -2,6 +2,9 @@ name: upload-wheel
description: "Upload wheels to Pypi" description: "Upload wheels to Pypi"
inputs: inputs:
pypi_token:
required: true
description: "release token for the repo"
fury_token: fury_token:
required: true required: true
description: "release token for the fury repo" description: "release token for the fury repo"
@@ -9,6 +12,12 @@ inputs:
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Install dependencies
shell: bash
run: |
python -m pip install --upgrade pip
pip install twine
python3 -m pip install --upgrade pkginfo
- name: Choose repo - name: Choose repo
shell: bash shell: bash
id: choose_repo id: choose_repo
@@ -18,17 +27,19 @@ runs:
else else
echo "repo=pypi" >> $GITHUB_OUTPUT echo "repo=pypi" >> $GITHUB_OUTPUT
fi fi
- name: Publish to Fury - name: Publish to PyPI
if: steps.choose_repo.outputs.repo == 'fury'
shell: bash shell: bash
env: env:
FURY_TOKEN: ${{ inputs.fury_token }} FURY_TOKEN: ${{ inputs.fury_token }}
PYPI_TOKEN: ${{ inputs.pypi_token }}
run: | run: |
WHEEL=$(ls target/wheels/lancedb-*.whl 2> /dev/null | head -n 1) if [[ ${{ steps.choose_repo.outputs.repo }} == fury ]]; then
echo "Uploading $WHEEL to Fury" WHEEL=$(ls target/wheels/lancedb-*.whl 2> /dev/null | head -n 1)
curl -f -F package=@$WHEEL https://$FURY_TOKEN@push.fury.io/lancedb/ echo "Uploading $WHEEL to Fury"
- name: Publish to PyPI curl -f -F package=@$WHEEL https://$FURY_TOKEN@push.fury.io/lancedb/
if: steps.choose_repo.outputs.repo == 'pypi' else
uses: pypa/gh-action-pypi-publish@release/v1 twine upload --repository ${{ steps.choose_repo.outputs.repo }} \
with: --username __token__ \
packages-dir: target/wheels/ --password $PYPI_TOKEN \
target/wheels/lancedb-*.whl
fi
Generated
+536 -288
View File
File diff suppressed because it is too large Load Diff
+33 -31
View File
@@ -1,5 +1,7 @@
[workspace] [workspace]
members = ["rust/lancedb", "nodejs", "python"] members = ["rust/lancedb", "nodejs", "python"]
# Python package needs to be built by maturin.
exclude = ["python"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
@@ -13,40 +15,40 @@ categories = ["database-implementations"]
rust-version = "1.91.0" rust-version = "1.91.0"
[workspace.dependencies] [workspace.dependencies]
lance = { "version" = "=6.0.0-beta.7", default-features = false, "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance = { "version" = "=5.1.0-beta.3", default-features = false, "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-core = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-core = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-datagen = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-datagen = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-file = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-file = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-io = { "version" = "=6.0.0-beta.7", default-features = false, "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-io = { "version" = "=5.1.0-beta.3", default-features = false, "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-index = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-index = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-linalg = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-linalg = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-namespace = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-namespace-impls = { "version" = "=6.0.0-beta.7", default-features = false, "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-namespace-impls = { "version" = "=5.1.0-beta.3", default-features = false, "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-table = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-table = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-testing = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-testing = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-datafusion = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-datafusion = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-encoding = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-encoding = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "git" = "https://github.com/lance-format/lance.git" }
lance-arrow = { "version" = "=6.0.0-beta.7", "tag" = "v6.0.0-beta.7", "git" = "https://github.com/lance-format/lance.git" } lance-arrow = { "version" = "=5.1.0-beta.3", "tag" = "v5.1.0-beta.3", "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 = "58.0.0", optional = false } arrow = { version = "57.2", optional = false }
arrow-array = "58.0.0" arrow-array = "57.2"
arrow-data = "58.0.0" arrow-data = "57.2"
arrow-ipc = "58.0.0" arrow-ipc = "57.2"
arrow-ord = "58.0.0" arrow-ord = "57.2"
arrow-schema = "58.0.0" arrow-schema = "57.2"
arrow-select = "58.0.0" arrow-select = "57.2"
arrow-cast = "58.0.0" arrow-cast = "57.2"
async-trait = "0" async-trait = "0"
datafusion = { version = "53.0.0", default-features = false } datafusion = { version = "52.1", default-features = false }
datafusion-catalog = "53.0.0" datafusion-catalog = "52.1"
datafusion-common = { version = "53.0.0", default-features = false } datafusion-common = { version = "52.1", default-features = false }
datafusion-execution = "53.0.0" datafusion-execution = "52.1"
datafusion-expr = "53.0.0" datafusion-expr = "52.1"
datafusion-functions = "53.0.0" datafusion-functions = "52.1"
datafusion-physical-plan = "53.0.0" datafusion-physical-plan = "52.1"
datafusion-physical-expr = "53.0.0" datafusion-physical-expr = "52.1"
datafusion-sql = "53.0.0" datafusion-sql = "52.1"
env_logger = "0.11" env_logger = "0.11"
half = { "version" = "2.7.1", default-features = false, features = [ half = { "version" = "2.7.1", default-features = false, features = [
"num-traits", "num-traits",
-172
View File
@@ -1,172 +0,0 @@
# cargo-deny configuration for LanceDB.
#
# Run locally with `cargo deny check`. See
# https://embarkstudios.github.io/cargo-deny/ for the full reference.
# The set of target triples we care about. cargo-deny will only consider
# dependencies that are used on at least one of these targets. Keeping this
# explicit avoids noise from platform-specific crates (e.g. wasm, android,
# ios) that we never actually ship.
[graph]
targets = [
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
"aarch64-pc-windows-msvc",
]
all-features = true
[output]
feature-depth = 1
# ---------------------------------------------------------------------------
# Advisories: security vulnerabilities and yanked crates.
# ---------------------------------------------------------------------------
[advisories]
version = 2
# Fail the check if any crate in the lockfile has been yanked from crates.io.
# Yanked crates are a signal the author retracted the release (often due to
# bugs or security issues) and should not be depended on.
yanked = "deny"
# Advisory IDs we have explicitly reviewed and chosen to accept. Every
# entry must include a rationale and, where possible, an upstream issue
# pointing to a fix. Revisit this list whenever dependencies are updated.
ignore = [
# rsa: Marvin Attack timing side-channel in PKCS#1 v1.5 decryption.
# Reached only through opendal → reqsign → rsa. We do not use RSA
# decryption in LanceDB ourselves; this is dormant in the signing path.
# No fixed release exists upstream as of this writing.
# https://rustsec.org/advisories/RUSTSEC-2023-0071
{ id = "RUSTSEC-2023-0071", reason = "rsa crate via opendal/reqsign; no fixed upstream release" },
# instant: unmaintained. Pulled in via backoff → instant. Upstream
# recommends switching to `web-time`; fix has to come from backoff.
# https://rustsec.org/advisories/RUSTSEC-2024-0384
{ id = "RUSTSEC-2024-0384", reason = "transitive via backoff; waiting on backoff replacement" },
# paste: unmaintained (author archived the repo). Used transitively by
# datafusion and the arrow ecosystem; widespread, no drop-in replacement.
# https://rustsec.org/advisories/RUSTSEC-2024-0436
{ id = "RUSTSEC-2024-0436", reason = "transitive via datafusion; awaiting ecosystem migration" },
# tantivy: segfault on malformed input due to missing bounds check.
# Pulled in via lance for full-text search. We only feed tantivy
# documents we construct ourselves, not attacker-controlled bytes.
# Tracked for a lance dependency bump.
# https://rustsec.org/advisories/RUSTSEC-2025-0003
{ id = "RUSTSEC-2025-0003", reason = "tantivy via lance; inputs are internally produced, not user-supplied bytes" },
# backoff: unmaintained. Reached only via async-openai. Replacement
# requires async-openai to migrate (or us to drop async-openai).
# https://rustsec.org/advisories/RUSTSEC-2025-0012
{ id = "RUSTSEC-2025-0012", reason = "transitive via async-openai; waiting on upstream migration" },
# number_prefix: unmaintained. Transitive via indicatif → hf-hub.
# No security impact, just maintenance status.
# https://rustsec.org/advisories/RUSTSEC-2025-0119
{ id = "RUSTSEC-2025-0119", reason = "transitive via hf-hub/indicatif; cosmetic formatting crate" },
# rustls-pemfile: unmaintained. Reached from two separate chains:
# rustls-native-certs 0.6 (via hyper-rustls 0.24) and object_store 0.12.
# Both upstream dependencies need to move before we can drop it.
# https://rustsec.org/advisories/RUSTSEC-2025-0134
{ id = "RUSTSEC-2025-0134", reason = "transitive via rustls-native-certs/object_store; waiting on upstream migration" },
# rustls-webpki 0.101.7 (old major line): name-constraint checks for
# URI / wildcard names. Pulled in only via the legacy rustls 0.21 chain
# from aws-smithy-http-client. The 0.103 line we actively use is patched.
# Clearing the 0.101 copy requires the aws-sdk chain to migrate off
# rustls 0.21.
# https://rustsec.org/advisories/RUSTSEC-2026-0098
# https://rustsec.org/advisories/RUSTSEC-2026-0099
{ id = "RUSTSEC-2026-0098", reason = "only affects rustls-webpki 0.101 from legacy aws-smithy/rustls 0.21 chain" },
{ id = "RUSTSEC-2026-0099", reason = "only affects rustls-webpki 0.101 from legacy aws-smithy/rustls 0.21 chain" },
# rustls-webpki 0.101.7: reachable panic in CRL parsing. Same legacy
# rustls 0.21 chain from aws-smithy-http-client as above. The 0.103 line
# we actively use is upgraded to 0.103.13 which contains the fix.
# https://rustsec.org/advisories/RUSTSEC-2026-0104
{ id = "RUSTSEC-2026-0104", reason = "only affects rustls-webpki 0.101 from legacy aws-smithy/rustls 0.21 chain" },
]
# ---------------------------------------------------------------------------
# Licenses: only allow licenses we've reviewed as compatible with Apache-2.0.
# ---------------------------------------------------------------------------
[licenses]
version = 2
# SPDX identifiers for licenses that are compatible with our Apache-2.0
# distribution. Additions require legal review.
allow = [
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"MIT",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unicode-3.0",
"Unicode-DFS-2016",
"Zlib",
"CC0-1.0",
"MPL-2.0",
"BSL-1.0",
"OpenSSL",
# 0BSD ("BSD Zero Clause") is effectively public domain — no attribution
# required. Pulled in by `mock_instant`.
"0BSD",
# bzip2-1.0.6 is the permissive upstream bzip2 license (BSD-like). Pulled
# in by `libbz2-rs-sys`, the pure-Rust bzip2 implementation.
"bzip2-1.0.6",
# CDLA-Permissive-2.0 is a permissive data license used by `webpki-roots`
# for the Mozilla CA root bundle. Data-only, distribution-compatible.
"CDLA-Permissive-2.0",
]
confidence-threshold = 0.8
# Crates whose license cannot be determined from Cargo metadata but whose
# license we've manually confirmed from upstream. Keep this list minimal.
[[licenses.clarify]]
# polars-arrow-format omits the `license` field in its Cargo.toml, but the
# upstream repo (pola-rs/polars-arrow-format) is dual-licensed Apache-2.0 OR
# MIT. See https://github.com/pola-rs/polars-arrow-format/blob/main/LICENSE
crate = "polars-arrow-format"
expression = "Apache-2.0 OR MIT"
license-files = []
# ---------------------------------------------------------------------------
# Bans: disallow specific crates and flag dependency hygiene issues.
# ---------------------------------------------------------------------------
[bans]
# Warn (not deny) on duplicate versions of the same crate. In a large
# workspace like this one, duplicates are common and often unavoidable
# transitively. We surface them to discourage growth, but don't fail CI.
multiple-versions = "warn"
# Wildcard version requirements (`foo = "*"`) are a footgun — they let any
# future release in without review. Ban them outright.
wildcards = "deny"
# Internal workspace crates reference each other via `path = "..."`, which
# cargo-deny sees as a wildcard version. That's fine for private workspace
# members (not published to crates.io), so allow it specifically for paths.
allow-wildcard-paths = true
# Features that, if enabled, should cause the check to fail.
deny = []
# Crates to skip when checking for duplicate versions.
skip = []
# Similar to `skip`, but also skips the entire transitive subtree.
skip-tree = []
# ---------------------------------------------------------------------------
# Sources: restrict where crates can come from.
# ---------------------------------------------------------------------------
[sources]
# Deny any registry other than the ones explicitly listed below.
unknown-registry = "deny"
# Deny any git dependency whose host isn't in the allow-list below. This
# prevents accidental pulls from arbitrary forks.
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# Lance is developed in a sibling repo and pulled as a git dependency until
# releases are cut to crates.io. Allow that specific host.
allow-git = [
"https://github.com/lance-format/lance",
]
+1 -1
View File
@@ -24,4 +24,4 @@ RUN python --version && \
rustc --version && \ rustc --version && \
protoc --version protoc --version
RUN pip install --no-cache-dir lancedb RUN pip install --no-cache-dir tantivy lancedb
+1 -1
View File
@@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`:
<dependency> <dependency>
<groupId>com.lancedb</groupId> <groupId>com.lancedb</groupId>
<artifactId>lancedb-core</artifactId> <artifactId>lancedb-core</artifactId>
<version>0.28.0-beta.10</version> <version>0.28.0-beta.5</version>
</dependency> </dependency>
``` ```
@@ -41,29 +41,6 @@ for testing purposes.
*** ***
### manifestEnabled?
```ts
optional manifestEnabled: boolean;
```
(For LanceDB OSS only): use directory namespace manifests as the source
of truth for table metadata. Existing directory-listed root tables are
migrated into the manifest on access.
***
### namespaceClientProperties?
```ts
optional namespaceClientProperties: Record<string, string>;
```
(For LanceDB OSS only): extra properties for the backing namespace
client used by manifest-enabled native connections.
***
### readConsistencyInterval? ### readConsistencyInterval?
```ts ```ts
+1 -1
View File
@@ -8,7 +8,7 @@
<parent> <parent>
<groupId>com.lancedb</groupId> <groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId> <artifactId>lancedb-parent</artifactId>
<version>0.28.0-beta.10</version> <version>0.28.0-beta.5</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>
+2 -2
View File
@@ -6,7 +6,7 @@
<groupId>com.lancedb</groupId> <groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId> <artifactId>lancedb-parent</artifactId>
<version>0.28.0-beta.10</version> <version>0.28.0-beta.5</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>${project.artifactId}</name> <name>${project.artifactId}</name>
<description>LanceDB Java SDK Parent POM</description> <description>LanceDB Java SDK Parent POM</description>
@@ -28,7 +28,7 @@
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<arrow.version>15.0.0</arrow.version> <arrow.version>15.0.0</arrow.version>
<lance-core.version>6.0.0-beta.7</lance-core.version> <lance-core.version>5.1.0-beta.3</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>
+4 -5
View File
@@ -1,8 +1,7 @@
[package] [package]
name = "lancedb-nodejs" name = "lancedb-nodejs"
edition.workspace = true edition.workspace = true
version = "0.28.0-beta.10" version = "0.28.0-beta.5"
publish = false
license.workspace = true license.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true
@@ -16,7 +15,7 @@ crate-type = ["cdylib"]
async-trait.workspace = true async-trait.workspace = true
arrow-ipc.workspace = true arrow-ipc.workspace = true
arrow-array.workspace = true arrow-array.workspace = true
arrow-buffer = "58.0.0" arrow-buffer = "57.2"
half.workspace = true half.workspace = true
arrow-schema.workspace = true arrow-schema.workspace = true
env_logger.workspace = true env_logger.workspace = true
@@ -32,8 +31,8 @@ lzma-sys = { version = "0.1", features = ["static"] }
log.workspace = true log.workspace = true
# Pin to resolve build failures; update periodically for security patches. # Pin to resolve build failures; update periodically for security patches.
aws-lc-sys = "=0.40.0" aws-lc-sys = "=0.38.0"
aws-lc-rs = "=1.16.3" aws-lc-rs = "=1.16.1"
[build-dependencies] [build-dependencies]
napi-build = "2.3.1" napi-build = "2.3.1"
-89
View File
@@ -1,8 +1,6 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors // SPDX-FileCopyrightText: Copyright The LanceDB Authors
import { spawn } from "node:child_process";
import * as path from "node:path";
import { RecordBatch } from "apache-arrow"; import { RecordBatch } from "apache-arrow";
import * as tmp from "tmp"; import * as tmp from "tmp";
import { Connection, Index, Table, connect, makeArrowTable } from "../lancedb"; import { Connection, Index, Table, connect, makeArrowTable } from "../lancedb";
@@ -78,91 +76,4 @@ describe("rerankers", function () {
expect(result).toHaveLength(2); expect(result).toHaveLength(2);
}); });
it("does not keep process alive after rerank query", async function () {
const script = `
import * as lancedb from "./dist/index.js";
import * as os from "node:os";
import * as path from "node:path";
import * as fs from "node:fs/promises";
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "lancedb-rerank-exit-"));
const db = await lancedb.connect(dir);
const table = await db.createTable("test", [{ text: "hello", vector: [1, 2, 3] }], {
mode: "overwrite",
});
await table.createIndex("text", { config: lancedb.Index.fts() });
await table.waitForIndex(["text_idx"], 30);
const reranker = await lancedb.rerankers.RRFReranker.create();
await table
.query()
.nearestTo([1, 2, 3])
.fullTextSearch("hello")
.rerank(reranker)
.toArray();
table.close();
db.close();
`;
await new Promise<void>((resolve, reject) => {
const child = spawn(
process.execPath,
["--input-type=module", "-e", script],
{
cwd: path.resolve(__dirname, ".."),
stdio: ["ignore", "pipe", "pipe"],
},
);
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
const timeout = setTimeout(() => {
child.kill();
reject(
new Error(
`child process did not exit in time\nstdout:\n${stdout}\nstderr:\n${stderr}`,
),
);
}, 20_000);
child.on("error", (err) => {
clearTimeout(timeout);
reject(err);
});
child.on("exit", (code, signal) => {
clearTimeout(timeout);
if (signal !== null) {
reject(
new Error(
`child process exited with signal ${signal}\nstdout:\n${stdout}\nstderr:\n${stderr}`,
),
);
return;
}
if (code !== 0) {
reject(
new Error(
`child process exited with code ${code}\nstdout:\n${stdout}\nstderr:\n${stderr}`,
),
);
return;
}
resolve();
});
});
});
}); });
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-darwin-arm64", "name": "@lancedb/lancedb-darwin-arm64",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"os": ["darwin"], "os": ["darwin"],
"cpu": ["arm64"], "cpu": ["arm64"],
"main": "lancedb.darwin-arm64.node", "main": "lancedb.darwin-arm64.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-linux-arm64-gnu", "name": "@lancedb/lancedb-linux-arm64-gnu",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"os": ["linux"], "os": ["linux"],
"cpu": ["arm64"], "cpu": ["arm64"],
"main": "lancedb.linux-arm64-gnu.node", "main": "lancedb.linux-arm64-gnu.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-linux-arm64-musl", "name": "@lancedb/lancedb-linux-arm64-musl",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"os": ["linux"], "os": ["linux"],
"cpu": ["arm64"], "cpu": ["arm64"],
"main": "lancedb.linux-arm64-musl.node", "main": "lancedb.linux-arm64-musl.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-linux-x64-gnu", "name": "@lancedb/lancedb-linux-x64-gnu",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"os": ["linux"], "os": ["linux"],
"cpu": ["x64"], "cpu": ["x64"],
"main": "lancedb.linux-x64-gnu.node", "main": "lancedb.linux-x64-gnu.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-linux-x64-musl", "name": "@lancedb/lancedb-linux-x64-musl",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"os": ["linux"], "os": ["linux"],
"cpu": ["x64"], "cpu": ["x64"],
"main": "lancedb.linux-x64-musl.node", "main": "lancedb.linux-x64-musl.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-win32-arm64-msvc", "name": "@lancedb/lancedb-win32-arm64-msvc",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"os": [ "os": [
"win32" "win32"
], ],
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@lancedb/lancedb-win32-x64-msvc", "name": "@lancedb/lancedb-win32-x64-msvc",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"os": ["win32"], "os": ["win32"],
"cpu": ["x64"], "cpu": ["x64"],
"main": "lancedb.win32-x64-msvc.node", "main": "lancedb.win32-x64-msvc.node",
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "@lancedb/lancedb", "name": "@lancedb/lancedb",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@lancedb/lancedb", "name": "@lancedb/lancedb",
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"cpu": [ "cpu": [
"x64", "x64",
"arm64" "arm64"
+1 -1
View File
@@ -11,7 +11,7 @@
"ann" "ann"
], ],
"private": false, "private": false,
"version": "0.28.0-beta.10", "version": "0.28.0-beta.5",
"main": "dist/index.js", "main": "dist/index.js",
"exports": { "exports": {
".": "./dist/index.js", ".": "./dist/index.js",
-6
View File
@@ -67,12 +67,6 @@ impl Connection {
builder = builder.storage_option(key, value); builder = builder.storage_option(key, value);
} }
} }
if let Some(manifest_enabled) = options.manifest_enabled {
builder = builder.manifest_enabled(manifest_enabled);
}
if let Some(namespace_client_properties) = options.namespace_client_properties {
builder = builder.namespace_client_properties(namespace_client_properties);
}
// Create client config, optionally with header provider // Create client config, optionally with header provider
let client_config = options.client_config.unwrap_or_default(); let client_config = options.client_config.unwrap_or_default();
-7
View File
@@ -37,13 +37,6 @@ pub struct ConnectionOptions {
/// ///
/// The available options are described at https://docs.lancedb.com/storage/ /// The available options are described at https://docs.lancedb.com/storage/
pub storage_options: Option<HashMap<String, String>>, pub storage_options: Option<HashMap<String, String>>,
/// (For LanceDB OSS only): use directory namespace manifests as the source
/// of truth for table metadata. Existing directory-listed root tables are
/// migrated into the manifest on access.
pub manifest_enabled: Option<bool>,
/// (For LanceDB OSS only): extra properties for the backing namespace
/// client used by manifest-enabled native connections.
pub namespace_client_properties: Option<HashMap<String, String>>,
/// (For LanceDB OSS only): the session to use for this connection. Holds /// (For LanceDB OSS only): the session to use for this connection. Holds
/// shared caches and other session-specific state. /// shared caches and other session-specific state.
pub session: Option<session::Session>, pub session: Option<session::Session>,
+1 -5
View File
@@ -18,7 +18,6 @@ type RerankHybridFn = ThreadsafeFunction<
RerankHybridCallbackArgs, RerankHybridCallbackArgs,
Status, Status,
false, false,
true,
>; >;
/// Reranker implementation that "wraps" a NodeJS Reranker implementation. /// Reranker implementation that "wraps" a NodeJS Reranker implementation.
@@ -33,10 +32,7 @@ impl Reranker {
pub fn new( pub fn new(
rerank_hybrid: Function<RerankHybridCallbackArgs, Promise<Buffer>>, rerank_hybrid: Function<RerankHybridCallbackArgs, Promise<Buffer>>,
) -> napi::Result<Self> { ) -> napi::Result<Self> {
let rerank_hybrid = rerank_hybrid let rerank_hybrid = rerank_hybrid.build_threadsafe_function().build()?;
.build_threadsafe_function()
.weak::<true>()
.build()?;
Ok(Self { rerank_hybrid }) Ok(Self { rerank_hybrid })
} }
} }
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "0.31.0-beta.11" current_version = "0.31.0-beta.5"
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*)\\.
+5 -6
View File
@@ -1,7 +1,6 @@
[package] [package]
name = "lancedb-python" name = "lancedb-python"
version = "0.31.0-beta.11" version = "0.31.0-beta.5"
publish = false
edition.workspace = true edition.workspace = true
description = "Python bindings for LanceDB" description = "Python bindings for LanceDB"
license.workspace = true license.workspace = true
@@ -15,7 +14,7 @@ name = "_lancedb"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
arrow = { version = "58.0.0", features = ["pyarrow"] } arrow = { version = "57.2", features = ["pyarrow"] }
async-trait = "0.1" async-trait = "0.1"
bytes = "1" bytes = "1"
lancedb = { path = "../rust/lancedb", default-features = false } lancedb = { path = "../rust/lancedb", default-features = false }
@@ -25,8 +24,8 @@ lance-namespace-impls.workspace = true
lance-io.workspace = true lance-io.workspace = true
env_logger.workspace = true env_logger.workspace = true
log.workspace = true log.workspace = true
pyo3 = { version = "0.28", features = ["extension-module", "abi3-py39"] } pyo3 = { version = "0.26", features = ["extension-module", "abi3-py39"] }
pyo3-async-runtimes = { version = "0.28", features = [ pyo3-async-runtimes = { version = "0.26", features = [
"attributes", "attributes",
"tokio-runtime", "tokio-runtime",
] } ] }
@@ -38,7 +37,7 @@ snafu.workspace = true
tokio = { version = "1.40", features = ["sync"] } tokio = { version = "1.40", features = ["sync"] }
[build-dependencies] [build-dependencies]
pyo3-build-config = { version = "0.28", features = [ pyo3-build-config = { version = "0.26", features = [
"extension-module", "extension-module",
"abi3-py39", "abi3-py39",
] } ] }
+1
View File
@@ -183,6 +183,7 @@
| stack-data | 0.6.3 | MIT License | http://github.com/alexmojaki/stack_data | | stack-data | 0.6.3 | MIT License | http://github.com/alexmojaki/stack_data |
| sympy | 1.14.0 | BSD License | https://sympy.org | | sympy | 1.14.0 | BSD License | https://sympy.org |
| tabulate | 0.9.0 | MIT License | https://github.com/astanin/python-tabulate | | tabulate | 0.9.0 | MIT License | https://github.com/astanin/python-tabulate |
| tantivy | 0.25.1 | UNKNOWN | UNKNOWN |
| threadpoolctl | 3.6.0 | BSD License | https://github.com/joblib/threadpoolctl | | threadpoolctl | 3.6.0 | BSD License | https://github.com/joblib/threadpoolctl |
| timm | 1.0.24 | Apache Software License | https://github.com/huggingface/pytorch-image-models | | timm | 1.0.24 | Apache Software License | https://github.com/huggingface/pytorch-image-models |
| tinycss2 | 1.4.0 | BSD License | https://www.courtbouillon.org/tinycss2 | | tinycss2 | 1.4.0 | BSD License | https://www.courtbouillon.org/tinycss2 |
+1
View File
@@ -57,6 +57,7 @@ tests = [
"duckdb>=0.9.0", "duckdb>=0.9.0",
"pytz>=2023.3", "pytz>=2023.3",
"polars>=0.19, <=1.3.0", "polars>=0.19, <=1.3.0",
"tantivy>=0.20.0",
"pyarrow-stubs>=16.0", "pyarrow-stubs>=16.0",
"pylance>=5.0.0b5", "pylance>=5.0.0b5",
"requests>=2.31.0", "requests>=2.31.0",
+4 -30
View File
@@ -73,7 +73,6 @@ def connect(
client_config: Union[ClientConfig, Dict[str, Any], None] = None, client_config: Union[ClientConfig, Dict[str, Any], None] = None,
storage_options: Optional[Dict[str, str]] = None, storage_options: Optional[Dict[str, str]] = None,
session: Optional[Session] = None, session: Optional[Session] = None,
manifest_enabled: bool = False,
namespace_client_impl: Optional[str] = None, namespace_client_impl: Optional[str] = None,
namespace_client_properties: Optional[Dict[str, str]] = None, namespace_client_properties: Optional[Dict[str, str]] = None,
namespace_client_pushdown_operations: Optional[List[str]] = None, namespace_client_pushdown_operations: Optional[List[str]] = None,
@@ -112,10 +111,6 @@ def connect(
storage_options: dict, optional storage_options: dict, optional
Additional options for the storage backend. See available options at Additional options for the storage backend. See available options at
<https://docs.lancedb.com/storage/> <https://docs.lancedb.com/storage/>
manifest_enabled : bool, default False
When true for local/native connections, use directory namespace
manifests as the source of truth for table metadata. Existing
directory-listed root tables are migrated into the manifest on access.
session: Session, optional session: Session, optional
(For LanceDB OSS only) (For LanceDB OSS only)
A session to use for this connection. Sessions allow you to configure A session to use for this connection. Sessions allow you to configure
@@ -163,11 +158,11 @@ def connect(
conn : DBConnection conn : DBConnection
A connection to a LanceDB database. A connection to a LanceDB database.
""" """
if namespace_client_impl is not None: if namespace_client_impl is not None or namespace_client_properties is not None:
if namespace_client_properties is None: if namespace_client_impl is None or namespace_client_properties is None:
raise ValueError( raise ValueError(
"namespace_client_properties must be provided when " "Both namespace_client_impl and "
"namespace_client_impl is set" "namespace_client_properties must be provided"
) )
if kwargs: if kwargs:
raise ValueError(f"Unknown keyword arguments: {kwargs}") raise ValueError(f"Unknown keyword arguments: {kwargs}")
@@ -180,12 +175,6 @@ def connect(
namespace_client_pushdown_operations=namespace_client_pushdown_operations, namespace_client_pushdown_operations=namespace_client_pushdown_operations,
) )
if namespace_client_properties is not None and not manifest_enabled:
raise ValueError(
"namespace_client_impl must be provided when using "
"namespace_client_properties unless manifest_enabled=True"
)
if namespace_client_pushdown_operations is not None: if namespace_client_pushdown_operations is not None:
raise ValueError( raise ValueError(
"namespace_client_pushdown_operations is only valid when " "namespace_client_pushdown_operations is only valid when "
@@ -223,8 +212,6 @@ def connect(
read_consistency_interval=read_consistency_interval, read_consistency_interval=read_consistency_interval,
storage_options=storage_options, storage_options=storage_options,
session=session, session=session,
manifest_enabled=manifest_enabled,
namespace_client_properties=namespace_client_properties,
) )
@@ -302,8 +289,6 @@ def deserialize_conn(
parsed["uri"], parsed["uri"],
read_consistency_interval=rci, read_consistency_interval=rci,
storage_options=storage_options, storage_options=storage_options,
manifest_enabled=parsed.get("manifest_enabled", False),
namespace_client_properties=parsed.get("namespace_client_properties"),
) )
else: else:
raise ValueError(f"Unknown connection_type: {connection_type}") raise ValueError(f"Unknown connection_type: {connection_type}")
@@ -319,8 +304,6 @@ async def connect_async(
client_config: Optional[Union[ClientConfig, Dict[str, Any]]] = None, client_config: Optional[Union[ClientConfig, Dict[str, Any]]] = None,
storage_options: Optional[Dict[str, str]] = None, storage_options: Optional[Dict[str, str]] = None,
session: Optional[Session] = None, session: Optional[Session] = None,
manifest_enabled: bool = False,
namespace_client_properties: Optional[Dict[str, str]] = None,
) -> AsyncConnection: ) -> AsyncConnection:
"""Connect to a LanceDB database. """Connect to a LanceDB database.
@@ -360,13 +343,6 @@ async def connect_async(
cache sizes for index and metadata caches, which can significantly cache sizes for index and metadata caches, which can significantly
impact memory use and performance. They can also be re-used across impact memory use and performance. They can also be re-used across
multiple connections to share the same cache state. multiple connections to share the same cache state.
manifest_enabled : bool, default False
When true for local/native connections, use directory namespace
manifests as the source of truth for table metadata. Existing
directory-listed root tables are migrated into the manifest on access.
namespace_client_properties : dict, optional
Additional directory namespace client properties to use with
``manifest_enabled=True``.
Examples Examples
-------- --------
@@ -409,8 +385,6 @@ async def connect_async(
client_config, client_config,
storage_options, storage_options,
session, session,
manifest_enabled,
namespace_client_properties,
) )
) )
-2
View File
@@ -242,8 +242,6 @@ async def connect(
client_config: Optional[Union[ClientConfig, Dict[str, Any]]], client_config: Optional[Union[ClientConfig, Dict[str, Any]]],
storage_options: Optional[Dict[str, str]], storage_options: Optional[Dict[str, str]],
session: Optional[Session], session: Optional[Session],
manifest_enabled: bool = False,
namespace_client_properties: Optional[Dict[str, str]] = None,
) -> Connection: ... ) -> Connection: ...
class RecordBatchStream: class RecordBatchStream:
+1 -12
View File
@@ -590,13 +590,8 @@ class LanceDBConnection(DBConnection):
read_consistency_interval: Optional[timedelta] = None, read_consistency_interval: Optional[timedelta] = None,
storage_options: Optional[Dict[str, str]] = None, storage_options: Optional[Dict[str, str]] = None,
session: Optional[Session] = None, session: Optional[Session] = None,
manifest_enabled: bool = False,
namespace_client_properties: Optional[Dict[str, str]] = None,
_inner: Optional[LanceDbConnection] = None, _inner: Optional[LanceDbConnection] = None,
): ):
self.storage_options = storage_options
self._manifest_enabled = manifest_enabled
self._namespace_client_properties = namespace_client_properties
if _inner is not None: if _inner is not None:
self._conn = _inner self._conn = _inner
self._cached_namespace_client = None self._cached_namespace_client = None
@@ -638,8 +633,6 @@ class LanceDBConnection(DBConnection):
None, None,
storage_options, storage_options,
session, session,
manifest_enabled,
namespace_client_properties,
) )
# TODO: It would be nice if we didn't store self.storage_options but it is # TODO: It would be nice if we didn't store self.storage_options but it is
@@ -647,6 +640,7 @@ class LanceDBConnection(DBConnection):
# work because some paths like LanceDBConnection.from_inner will lose the # work because some paths like LanceDBConnection.from_inner will lose the
# storage_options. Also, this class really shouldn't be holding any state # storage_options. Also, this class really shouldn't be holding any state
# beyond _conn. # beyond _conn.
self.storage_options = storage_options
self._conn = AsyncConnection(LOOP.run(do_connect())) self._conn = AsyncConnection(LOOP.run(do_connect()))
self._cached_namespace_client: Optional[LanceNamespace] = None self._cached_namespace_client: Optional[LanceNamespace] = None
@@ -683,8 +677,6 @@ class LanceDBConnection(DBConnection):
"connection_type": "local", "connection_type": "local",
"uri": self.uri, "uri": self.uri,
"storage_options": self.storage_options, "storage_options": self.storage_options,
"manifest_enabled": self._manifest_enabled,
"namespace_client_properties": self._namespace_client_properties,
"read_consistency_interval_seconds": ( "read_consistency_interval_seconds": (
rci.total_seconds() if rci else None rci.total_seconds() if rci else None
), ),
@@ -1398,7 +1390,6 @@ class AsyncConnection(object):
namespace_path: Optional[List[str]] = None, namespace_path: Optional[List[str]] = None,
embedding_functions: Optional[List[EmbeddingFunctionConfig]] = None, embedding_functions: Optional[List[EmbeddingFunctionConfig]] = None,
location: Optional[str] = None, location: Optional[str] = None,
namespace_client: Optional[Any] = None,
) -> AsyncTable: ) -> AsyncTable:
"""Create an [AsyncTable][lancedb.table.AsyncTable] in the database. """Create an [AsyncTable][lancedb.table.AsyncTable] in the database.
@@ -1596,7 +1587,6 @@ class AsyncConnection(object):
namespace_path=namespace_path, namespace_path=namespace_path,
storage_options=storage_options, storage_options=storage_options,
location=location, location=location,
namespace_client=namespace_client,
) )
else: else:
data = data_to_reader(data, schema) data = data_to_reader(data, schema)
@@ -1607,7 +1597,6 @@ class AsyncConnection(object):
namespace_path=namespace_path, namespace_path=namespace_path,
storage_options=storage_options, storage_options=storage_options,
location=location, location=location,
namespace_client=namespace_client,
) )
return AsyncTable(new_table) return AsyncTable(new_table)
+201
View File
@@ -0,0 +1,201 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
"""Full text search index using tantivy-py"""
import os
from typing import List, Tuple, Optional
import pyarrow as pa
try:
import tantivy
except ImportError:
raise ImportError(
"Please install tantivy-py `pip install tantivy` to use the full text search feature." # noqa: E501
)
from .table import LanceTable
def create_index(
index_path: str,
text_fields: List[str],
ordering_fields: Optional[List[str]] = None,
tokenizer_name: str = "default",
) -> tantivy.Index:
"""
Create a new Index (not populated)
Parameters
----------
index_path : str
Path to the index directory
text_fields : List[str]
List of text fields to index
ordering_fields: List[str]
List of unsigned type fields to order by at search time
tokenizer_name : str, default "default"
The tokenizer to use
Returns
-------
index : tantivy.Index
The index object (not yet populated)
"""
if ordering_fields is None:
ordering_fields = []
# Declaring our schema.
schema_builder = tantivy.SchemaBuilder()
# special field that we'll populate with row_id
schema_builder.add_integer_field("doc_id", stored=True)
# data fields
for name in text_fields:
schema_builder.add_text_field(name, stored=True, tokenizer_name=tokenizer_name)
if ordering_fields:
for name in ordering_fields:
schema_builder.add_unsigned_field(name, fast=True)
schema = schema_builder.build()
os.makedirs(index_path, exist_ok=True)
index = tantivy.Index(schema, path=index_path)
return index
def populate_index(
index: tantivy.Index,
table: LanceTable,
fields: List[str],
writer_heap_size: Optional[int] = None,
ordering_fields: Optional[List[str]] = None,
) -> int:
"""
Populate an index with data from a LanceTable
Parameters
----------
index : tantivy.Index
The index object
table : LanceTable
The table to index
fields : List[str]
List of fields to index
writer_heap_size : int
The writer heap size in bytes, defaults to 1GB
Returns
-------
int
The number of rows indexed
"""
if ordering_fields is None:
ordering_fields = []
writer_heap_size = writer_heap_size or 1024 * 1024 * 1024
# first check the fields exist and are string or large string type
nested = []
for name in fields:
try:
f = table.schema.field(name) # raises KeyError if not found
except KeyError:
f = resolve_path(table.schema, name)
nested.append(name)
if not pa.types.is_string(f.type) and not pa.types.is_large_string(f.type):
raise TypeError(f"Field {name} is not a string type")
# create a tantivy writer
writer = index.writer(heap_size=writer_heap_size)
# write data into index
dataset = table.to_lance()
row_id = 0
max_nested_level = 0
if len(nested) > 0:
max_nested_level = max([len(name.split(".")) for name in nested])
for b in dataset.to_batches(columns=fields + ordering_fields):
if max_nested_level > 0:
b = pa.Table.from_batches([b])
for _ in range(max_nested_level - 1):
b = b.flatten()
for i in range(b.num_rows):
doc = tantivy.Document()
for name in fields:
value = b[name][i].as_py()
if value is not None:
doc.add_text(name, value)
for name in ordering_fields:
value = b[name][i].as_py()
if value is not None:
doc.add_unsigned(name, value)
if not doc.is_empty:
doc.add_integer("doc_id", row_id)
writer.add_document(doc)
row_id += 1
# commit changes
writer.commit()
return row_id
def resolve_path(schema, field_name: str) -> pa.Field:
"""
Resolve a nested field path to a list of field names
Parameters
----------
field_name : str
The field name to resolve
Returns
-------
List[str]
The resolved path
"""
path = field_name.split(".")
field = schema.field(path.pop(0))
for segment in path:
if pa.types.is_struct(field.type):
field = field.type.field(segment)
else:
raise KeyError(f"field {field_name} not found in schema {schema}")
return field
def search_index(
index: tantivy.Index, query: str, limit: int = 10, ordering_field=None
) -> Tuple[Tuple[int], Tuple[float]]:
"""
Search an index for a query
Parameters
----------
index : tantivy.Index
The index object
query : str
The query string
limit : int
The maximum number of results to return
Returns
-------
ids_and_score: list[tuple[int], tuple[float]]
A tuple of two tuples, the first containing the document ids
and the second containing the scores
"""
searcher = index.searcher()
query = index.parse_query(query)
# get top results
if ordering_field:
results = searcher.search(query, limit, order_by_field=ordering_field)
else:
results = searcher.search(query, limit)
if results.count == 0:
return tuple(), tuple()
return tuple(
zip(
*[
(searcher.doc(doc_address)["doc_id"][0], score)
for score, doc_address in results.hits
]
)
)
+357 -111
View File
@@ -10,6 +10,7 @@ through a namespace abstraction.
from __future__ import annotations from __future__ import annotations
import asyncio
import sys import sys
from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Union
@@ -24,24 +25,7 @@ if TYPE_CHECKING:
from datetime import timedelta from datetime import timedelta
import pyarrow as pa import pyarrow as pa
from lance_namespace_urllib3_client.models.json_arrow_data_type import JsonArrowDataType from lancedb.db import DBConnection, LanceDBConnection
from lance_namespace_urllib3_client.models.json_arrow_field import JsonArrowField
from lance_namespace_urllib3_client.models.json_arrow_schema import JsonArrowSchema
from lance_namespace_urllib3_client.models.query_table_request import QueryTableRequest
from lance_namespace_urllib3_client.models.query_table_request_columns import (
QueryTableRequestColumns,
)
from lance_namespace_urllib3_client.models.query_table_request_full_text_query import (
QueryTableRequestFullTextQuery,
)
from lance_namespace_urllib3_client.models.query_table_request_vector import (
QueryTableRequestVector,
)
from lance_namespace_urllib3_client.models.string_fts_query import StringFtsQuery
from lance_namespace.errors import TableNotFoundError
from lancedb._lancedb import connect_namespace_client as _connect_namespace_client
from lancedb.background_loop import LOOP
from lancedb.db import AsyncConnection, DBConnection
from lancedb.namespace_utils import ( from lancedb.namespace_utils import (
_normalize_create_namespace_mode, _normalize_create_namespace_mode,
_normalize_drop_namespace_mode, _normalize_drop_namespace_mode,
@@ -56,11 +40,14 @@ from lance_namespace import (
ListNamespacesResponse, ListNamespacesResponse,
ListTablesResponse, ListTablesResponse,
ListTablesRequest, ListTablesRequest,
DescribeTableRequest,
DescribeNamespaceRequest, DescribeNamespaceRequest,
DropTableRequest, DropTableRequest,
ListNamespacesRequest, ListNamespacesRequest,
CreateNamespaceRequest, CreateNamespaceRequest,
DropNamespaceRequest, DropNamespaceRequest,
DeclareTableRequest,
CreateTableRequest,
) )
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
@@ -69,6 +56,21 @@ 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_urllib3_client.models.json_arrow_schema import JsonArrowSchema
from lance_namespace_urllib3_client.models.json_arrow_field import JsonArrowField
from lance_namespace_urllib3_client.models.json_arrow_data_type import JsonArrowDataType
from lance_namespace_urllib3_client.models.query_table_request import QueryTableRequest
from lance_namespace_urllib3_client.models.query_table_request_vector import (
QueryTableRequestVector,
)
from lance_namespace_urllib3_client.models.query_table_request_columns import (
QueryTableRequestColumns,
)
from lance_namespace_urllib3_client.models.query_table_request_full_text_query import (
QueryTableRequestFullTextQuery,
)
from lance_namespace_urllib3_client.models.string_fts_query import StringFtsQuery
def _query_to_namespace_request( def _query_to_namespace_request(
table_id: List[str], table_id: List[str],
@@ -422,23 +424,6 @@ class LanceNamespaceDBConnection(DBConnection):
) )
self._namespace_client_impl = namespace_client_impl self._namespace_client_impl = namespace_client_impl
self._namespace_client_properties = namespace_client_properties self._namespace_client_properties = namespace_client_properties
self._inner = AsyncConnection(
_connect_namespace_client(
namespace_client,
read_consistency_interval=(
read_consistency_interval.total_seconds()
if read_consistency_interval is not None
else None
),
storage_options=self.storage_options or None,
session=session,
namespace_client_pushdown_operations=(
list(self._namespace_client_pushdown_operations)
),
namespace_client_impl=namespace_client_impl,
namespace_client_properties=namespace_client_properties,
)
)
@override @override
def serialize(self) -> str: def serialize(self) -> str:
@@ -512,10 +497,13 @@ class LanceNamespaceDBConnection(DBConnection):
if mode.lower() not in ["create", "overwrite"]: if mode.lower() not in ["create", "overwrite"]:
raise ValueError("mode must be either 'create' or 'overwrite'") raise ValueError("mode must be either 'create' or 'overwrite'")
validate_table_name(name) validate_table_name(name)
async_table = LOOP.run(
self._inner.create_table( table_id = namespace_path + [name]
name,
data, if "CreateTable" in self._namespace_client_pushdown_operations:
return self._create_table_server_side(
name=name,
data=data,
schema=schema, schema=schema,
mode=mode, mode=mode,
exist_ok=exist_ok, exist_ok=exist_ok,
@@ -525,15 +513,130 @@ class LanceNamespaceDBConnection(DBConnection):
namespace_path=namespace_path, namespace_path=namespace_path,
storage_options=storage_options, storage_options=storage_options,
) )
# Local create path: declare_table + local write
# Step 1: Get the table location and storage options from namespace
# In overwrite mode, if table exists, use describe_table to get
# existing location. Otherwise, call create_empty_table to reserve
# a new location
location = None
namespace_storage_options = None
if mode.lower() == "overwrite":
# Try to describe the table first to see if it exists
try:
describe_request = DescribeTableRequest(id=table_id)
describe_response = self._namespace_client.describe_table(
describe_request
)
location = describe_response.location
namespace_storage_options = describe_response.storage_options
except Exception:
# Table doesn't exist, will create a new one below
pass
if location is None:
# Table doesn't exist or mode is "create", reserve a new location
declare_request = DeclareTableRequest(
id=table_id,
location=None,
properties=self.storage_options if self.storage_options else None,
)
declare_response = self._namespace_client.declare_table(declare_request)
if not declare_response.location:
raise ValueError(
"Table location is missing from declare_table response"
)
location = declare_response.location
namespace_storage_options = declare_response.storage_options
# Merge storage options: self.storage_options < user options < namespace options
merged_storage_options = dict(self.storage_options)
if storage_options:
merged_storage_options.update(storage_options)
if namespace_storage_options:
merged_storage_options.update(namespace_storage_options)
# Step 2: Create table using LanceTable.create with the location
# We need a temporary connection for the LanceTable.create method
temp_conn = LanceDBConnection(
location, # Use the actual location as the connection URI
read_consistency_interval=self.read_consistency_interval,
storage_options=merged_storage_options,
session=self.session,
) )
return LanceTable( # Note: storage_options_provider is auto-created in Rust from namespace_client
self, tbl = LanceTable.create(
temp_conn,
name, name,
data,
schema,
mode=mode,
exist_ok=exist_ok,
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
embedding_functions=embedding_functions,
namespace_path=namespace_path, namespace_path=namespace_path,
storage_options=merged_storage_options,
location=location,
namespace_client=self._namespace_client, namespace_client=self._namespace_client,
pushdown_operations=self._namespace_client_pushdown_operations, pushdown_operations=self._namespace_client_pushdown_operations,
_async=async_table, )
return tbl
def _create_table_server_side(
self,
name: str,
data: Optional[DATA],
schema: Optional[Union[pa.Schema, LanceModel]],
mode: str,
exist_ok: bool,
on_bad_vectors: str,
fill_value: float,
embedding_functions: Optional[List[EmbeddingFunctionConfig]],
namespace_path: Optional[List[str]],
storage_options: Optional[Dict[str, str]],
) -> Table:
"""Create a table using server-side namespace.create_table()."""
if namespace_path is None:
namespace_path = []
table_id = namespace_path + [name]
arrow_ipc_bytes = _data_to_arrow_ipc(
data=data,
schema=schema,
embedding_functions=embedding_functions,
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
)
merged = dict(self.storage_options or {})
if storage_options:
merged.update(storage_options)
request = CreateTableRequest(
id=table_id,
mode=_normalize_create_table_mode(mode),
properties=merged or None,
)
try:
self._namespace_client.create_table(request, arrow_ipc_bytes)
except Exception as e:
if exist_ok and "already exists" in str(e).lower():
return self.open_table(
name,
namespace_path=namespace_path,
storage_options=storage_options,
)
raise
return self.open_table(
name,
namespace_path=namespace_path,
storage_options=storage_options,
) )
@override @override
@@ -547,28 +650,30 @@ class LanceNamespaceDBConnection(DBConnection):
) -> Table: ) -> Table:
if namespace_path is None: if namespace_path is None:
namespace_path = [] namespace_path = []
try: table_id = namespace_path + [name]
async_table = LOOP.run( request = DescribeTableRequest(id=table_id)
self._inner.open_table( response = self._namespace_client.describe_table(request)
name,
namespace_path=namespace_path,
storage_options=storage_options,
index_cache_size=index_cache_size,
)
)
except RuntimeError as e:
if "Table not found" in str(e):
table_id = namespace_path + [name]
raise TableNotFoundError(f"Table not found: {'$'.join(table_id)}")
raise
return LanceTable( # Merge storage options: self.storage_options < user options < namespace options
self, merged_storage_options = dict(self.storage_options)
if storage_options:
merged_storage_options.update(storage_options)
if response.storage_options:
merged_storage_options.update(response.storage_options)
# Pass managed_versioning to avoid redundant describe_table call in Rust.
# Convert None to False since we already have the answer from describe_table.
managed_versioning = response.managed_versioning is True
# Note: storage_options_provider is auto-created in Rust from namespace_client
return self._lance_table_from_uri(
name, name,
response.location,
namespace_path=namespace_path, namespace_path=namespace_path,
storage_options=merged_storage_options,
index_cache_size=index_cache_size,
namespace_client=self._namespace_client, namespace_client=self._namespace_client,
pushdown_operations=self._namespace_client_pushdown_operations, managed_versioning=managed_versioning,
_async=async_table,
) )
@override @override
@@ -792,34 +897,33 @@ class LanceNamespaceDBConnection(DBConnection):
namespace_client: Optional[Any] = None, namespace_client: Optional[Any] = None,
managed_versioning: Optional[bool] = None, managed_versioning: Optional[bool] = None,
) -> LanceTable: ) -> LanceTable:
# Open a table directly from the namespace-resolved physical location. # Open a table directly from a URI using the location parameter
# # Note: storage_options should already be merged by the caller
# Open the table through the Rust namespace-backed connection. The Rust # Note: storage_options_provider is auto-created in Rust from namespace_client
# layer keeps the logical namespace path and namespace client intact.
if namespace_path is None: if namespace_path is None:
namespace_path = [] namespace_path = []
temp_conn = LanceDBConnection(
async_table = LOOP.run( table_uri, # Use the table location as the connection URI
self._inner.open_table( read_consistency_interval=self.read_consistency_interval,
name, storage_options=storage_options if storage_options is not None else {},
namespace_path=namespace_path, session=self.session,
storage_options=storage_options,
index_cache_size=index_cache_size,
location=None,
namespace_client=namespace_client,
managed_versioning=managed_versioning,
)
) )
return LanceTable( # Open the table using the temporary connection with the location parameter
self, # Pass namespace_client to enable managed versioning support and auto-create
# storage options provider
# Pass managed_versioning to avoid redundant describe_table call
# Pass pushdown_operations if configured on this connection
return LanceTable.open(
temp_conn,
name, name,
namespace_path=namespace_path, namespace_path=namespace_path,
storage_options=storage_options,
index_cache_size=index_cache_size,
location=table_uri, location=table_uri,
namespace_client=namespace_client, namespace_client=namespace_client,
managed_versioning=managed_versioning, managed_versioning=managed_versioning,
pushdown_operations=self._namespace_client_pushdown_operations, pushdown_operations=self._namespace_client_pushdown_operations,
_async=async_table,
) )
@override @override
@@ -886,23 +990,6 @@ class AsyncLanceNamespaceDBConnection:
self._namespace_client_pushdown_operations = set( self._namespace_client_pushdown_operations = set(
namespace_client_pushdown_operations or [] namespace_client_pushdown_operations or []
) )
self._inner = AsyncConnection(
_connect_namespace_client(
namespace_client,
read_consistency_interval=(
read_consistency_interval.total_seconds()
if read_consistency_interval is not None
else None
),
storage_options=self.storage_options or None,
session=session,
namespace_client_pushdown_operations=(
list(self._namespace_client_pushdown_operations)
),
namespace_client_impl=None,
namespace_client_properties=None,
)
)
async def table_names( async def table_names(
self, self,
@@ -954,16 +1041,148 @@ class AsyncLanceNamespaceDBConnection:
if mode.lower() not in ["create", "overwrite"]: if mode.lower() not in ["create", "overwrite"]:
raise ValueError("mode must be either 'create' or 'overwrite'") raise ValueError("mode must be either 'create' or 'overwrite'")
validate_table_name(name) validate_table_name(name)
return await self._inner.create_table(
table_id = namespace_path + [name]
if "CreateTable" in self._namespace_client_pushdown_operations:
return await self._create_table_server_side(
name=name,
data=data,
schema=schema,
mode=mode,
exist_ok=exist_ok,
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
embedding_functions=embedding_functions,
namespace_path=namespace_path,
storage_options=storage_options,
)
# Local create path: declare_table + local write
# Step 1: Get the table location and storage options from namespace
location = None
namespace_storage_options = None
if mode.lower() == "overwrite":
# Try to describe the table first to see if it exists
try:
describe_request = DescribeTableRequest(id=table_id)
describe_response = self._namespace_client.describe_table(
describe_request
)
location = describe_response.location
namespace_storage_options = describe_response.storage_options
except Exception:
# Table doesn't exist, will create a new one below
pass
if location is None:
# Table doesn't exist or mode is "create", reserve a new location
declare_request = DeclareTableRequest(
id=table_id,
location=None,
properties=self.storage_options if self.storage_options else None,
)
declare_response = self._namespace_client.declare_table(declare_request)
if not declare_response.location:
raise ValueError(
"Table location is missing from declare_table response"
)
location = declare_response.location
namespace_storage_options = declare_response.storage_options
# Merge storage options: self.storage_options < user options < namespace options
merged_storage_options = dict(self.storage_options)
if storage_options:
merged_storage_options.update(storage_options)
if namespace_storage_options:
merged_storage_options.update(namespace_storage_options)
# Step 2: Create table using LanceTable.create with the location
# Run the sync operation in a thread
def _create_table():
temp_conn = LanceDBConnection(
location,
read_consistency_interval=self.read_consistency_interval,
storage_options=merged_storage_options,
session=self.session,
)
# storage_options_provider is auto-created in Rust from namespace_client
return LanceTable.create(
temp_conn,
name,
data,
schema,
mode=mode,
exist_ok=exist_ok,
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
embedding_functions=embedding_functions,
namespace_path=namespace_path,
storage_options=merged_storage_options,
location=location,
namespace_client=self._namespace_client,
pushdown_operations=self._namespace_client_pushdown_operations,
)
lance_table = await asyncio.to_thread(_create_table)
# Get the underlying async table from LanceTable
return lance_table._table
async def _create_table_server_side(
self,
name: str,
data: Optional[DATA],
schema: Optional[Union[pa.Schema, LanceModel]],
mode: str,
exist_ok: bool,
on_bad_vectors: str,
fill_value: float,
embedding_functions: Optional[List[EmbeddingFunctionConfig]],
namespace_path: Optional[List[str]],
storage_options: Optional[Dict[str, str]],
) -> AsyncTable:
"""Create a table using server-side namespace.create_table()."""
if namespace_path is None:
namespace_path = []
table_id = namespace_path + [name]
def _prepare_and_create():
arrow_ipc_bytes = _data_to_arrow_ipc(
data=data,
schema=schema,
embedding_functions=embedding_functions,
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
)
merged = dict(self.storage_options or {})
if storage_options:
merged.update(storage_options)
request = CreateTableRequest(
id=table_id,
mode=_normalize_create_table_mode(mode),
properties=merged or None,
)
self._namespace_client.create_table(request, arrow_ipc_bytes)
try:
await asyncio.to_thread(_prepare_and_create)
except Exception as e:
if exist_ok and "already exists" in str(e).lower():
return await self.open_table(
name,
namespace_path=namespace_path,
storage_options=storage_options,
)
raise
return await self.open_table(
name, name,
data,
schema=schema,
mode=mode,
exist_ok=exist_ok,
on_bad_vectors=on_bad_vectors,
fill_value=fill_value,
namespace_path=namespace_path, namespace_path=namespace_path,
embedding_functions=embedding_functions,
storage_options=storage_options, storage_options=storage_options,
) )
@@ -978,18 +1197,45 @@ class AsyncLanceNamespaceDBConnection:
"""Open an existing table from the namespace.""" """Open an existing table from the namespace."""
if namespace_path is None: if namespace_path is None:
namespace_path = [] namespace_path = []
try: table_id = namespace_path + [name]
return await self._inner.open_table( request = DescribeTableRequest(id=table_id)
response = self._namespace_client.describe_table(request)
# Merge storage options: self.storage_options < user options < namespace options
merged_storage_options = dict(self.storage_options)
if storage_options:
merged_storage_options.update(storage_options)
if response.storage_options:
merged_storage_options.update(response.storage_options)
# Capture managed_versioning from describe response.
# Convert None to False since we already have the answer from describe_table.
managed_versioning = response.managed_versioning is True
# Open table in a thread
# Note: storage_options_provider is auto-created in Rust from namespace_client
def _open_table():
temp_conn = LanceDBConnection(
response.location,
read_consistency_interval=self.read_consistency_interval,
storage_options=merged_storage_options,
session=self.session,
)
return LanceTable.open(
temp_conn,
name, name,
namespace_path=namespace_path, namespace_path=namespace_path,
storage_options=storage_options, storage_options=merged_storage_options,
index_cache_size=index_cache_size, index_cache_size=index_cache_size,
location=response.location,
namespace_client=self._namespace_client,
managed_versioning=managed_versioning,
pushdown_operations=self._namespace_client_pushdown_operations,
) )
except RuntimeError as e:
if "Table not found" in str(e): lance_table = await asyncio.to_thread(_open_table)
table_id = namespace_path + [name] return lance_table._table
raise TableNotFoundError(f"Table not found: {'$'.join(table_id)}")
raise
async def drop_table(self, name: str, namespace_path: Optional[List[str]] = None): async def drop_table(self, name: str, namespace_path: Optional[List[str]] = None):
"""Drop a table from the namespace.""" """Drop a table from the namespace."""
+88 -1
View File
@@ -25,6 +25,7 @@ import deprecation
import numpy as np import numpy as np
import pyarrow as pa import pyarrow as pa
import pyarrow.compute as pc import pyarrow.compute as pc
import pyarrow.fs as pa_fs
import pydantic import pydantic
from lancedb.pydantic import PYDANTIC_VERSION from lancedb.pydantic import PYDANTIC_VERSION
@@ -1525,7 +1526,9 @@ class LanceFtsQueryBuilder(LanceQueryBuilder):
return self._table._output_schema(self.to_query_object()) return self._table._output_schema(self.to_query_object())
def to_arrow(self, *, timeout: Optional[timedelta] = None) -> pa.Table: def to_arrow(self, *, timeout: Optional[timedelta] = None) -> pa.Table:
self._table._ensure_no_legacy_fts_index() path, fs, exist = self._table._get_fts_index_path()
if exist:
return self.tantivy_to_arrow()
query = self._query query = self._query
if self._phrase_query: if self._phrase_query:
@@ -1549,6 +1552,90 @@ class LanceFtsQueryBuilder(LanceQueryBuilder):
): ):
raise NotImplementedError("to_batches on an FTS query") raise NotImplementedError("to_batches on an FTS query")
def tantivy_to_arrow(self) -> pa.Table:
try:
import tantivy
except ImportError:
raise ImportError(
"Please install tantivy-py `pip install tantivy` to use the full text search feature." # noqa: E501
)
from .fts import search_index
# get the index path
path, fs, exist = self._table._get_fts_index_path()
# check if the index exist
if not exist:
raise FileNotFoundError(
"Fts index does not exist. "
"Please first call table.create_fts_index(['<field_names>']) to "
"create the fts index."
)
# Check that we are on local filesystem
if not isinstance(fs, pa_fs.LocalFileSystem):
raise NotImplementedError(
"Tantivy-based full text search "
"is only supported on the local filesystem"
)
# open the index
index = tantivy.Index.open(path)
# get the scores and doc ids
query = self._query
if self._phrase_query:
query = query.replace('"', "'")
query = f'"{query}"'
limit = self._limit if self._limit is not None else 10
row_ids, scores = search_index(
index, query, limit, ordering_field=self.ordering_field_name
)
if len(row_ids) == 0:
empty_schema = pa.schema([pa.field("_score", pa.float32())])
return pa.Table.from_batches([], schema=empty_schema)
scores = pa.array(scores)
output_tbl = self._table.to_lance().take(row_ids, columns=self._columns)
output_tbl = output_tbl.append_column("_score", scores)
# this needs to match vector search results which are uint64
row_ids = pa.array(row_ids, type=pa.uint64())
if self._where is not None:
tmp_name = "__lancedb__duckdb__indexer__"
output_tbl = output_tbl.append_column(
tmp_name, pa.array(range(len(output_tbl)))
)
try:
# TODO would be great to have Substrait generate pyarrow compute
# expressions or conversely have pyarrow support SQL expressions
# using Substrait
import duckdb
indexer = duckdb.sql(
f"SELECT {tmp_name} FROM output_tbl WHERE {self._where}"
).to_arrow_table()[tmp_name]
output_tbl = output_tbl.take(indexer).drop([tmp_name])
row_ids = row_ids.take(indexer)
except ImportError:
import tempfile
import lance
# TODO Use "memory://" instead once that's supported
with tempfile.TemporaryDirectory() as tmp:
ds = lance.write_dataset(output_tbl, tmp)
output_tbl = ds.to_table(filter=self._where)
indexer = output_tbl[tmp_name]
row_ids = row_ids.take(indexer)
output_tbl = output_tbl.drop([tmp_name])
if self._with_row_id:
output_tbl = output_tbl.append_column("_rowid", row_ids)
if self._reranker is not None:
output_tbl = self._reranker.rerank_fts(self._query, output_tbl)
return output_tbl
def rerank(self, reranker: Reranker) -> LanceFtsQueryBuilder: def rerank(self, reranker: Reranker) -> LanceFtsQueryBuilder:
"""Rerank the results using the specified reranker. """Rerank the results using the specified reranker.
+85 -72
View File
@@ -943,26 +943,29 @@ class Table(ABC):
Parameters Parameters
---------- ----------
field_names: str or list of str field_names: str or list of str
The name of the field to index. Native FTS indexes can only be The name(s) of the field to index.
created on a single field at a time. To search over multiple text If ``use_tantivy`` is False (default), only a single field name
fields, create a separate FTS index for each field. (str) is supported. To index multiple fields, create a separate
FTS index for each field.
replace: bool, default False replace: bool, default False
If True, replace the existing index if it exists. Note that this is If True, replace the existing index if it exists. Note that this is
not yet an atomic operation; the index will be temporarily not yet an atomic operation; the index will be temporarily
unavailable while the new index is being created. unavailable while the new index is being created.
writer_heap_size: int, default 1GB writer_heap_size: int, default 1GB
Deprecated legacy Tantivy parameter. Any value other than the Only available with use_tantivy=True
default raises an error.
ordering_field_names: ordering_field_names:
Deprecated legacy Tantivy parameter. Setting this raises an error. A list of unsigned type fields to index to optionally order
results on at search time.
only available with use_tantivy=True
tokenizer_name: str, default "default" tokenizer_name: str, default "default"
A compatibility alias for native tokenizer configs. Can be "raw", The tokenizer to use for the index. Can be "raw", "default" or the 2 letter
"default" or the 2 letter language code followed by "_stem". So language code followed by "_stem". So for english it would be "en_stem".
for english it would be "en_stem". For available languages see: https://docs.rs/tantivy/latest/tantivy/tokenizer/enum.Language.html
use_tantivy: bool, default False use_tantivy: bool, default False
Deprecated legacy Tantivy parameter. Setting this to True raises an If True, use the legacy full-text search implementation based on tantivy.
error. If False, use the new full-text search implementation based on lance-index.
with_position: bool, default False with_position: bool, default False
Only available with use_tantivy=False
If False, do not store the positions of the terms in the text. If False, do not store the positions of the terms in the text.
This can reduce the size of the index and improve indexing speed. This can reduce the size of the index and improve indexing speed.
But it will raise an exception for phrase queries. But it will raise an exception for phrase queries.
@@ -1743,16 +1746,6 @@ class Table(ABC):
index_exists = fs.get_file_info(path).type != pa_fs.FileType.NotFound index_exists = fs.get_file_info(path).type != pa_fs.FileType.NotFound
return (path, fs, index_exists) return (path, fs, index_exists)
def _ensure_no_legacy_fts_index(self):
path, _, exists = self._get_fts_index_path()
if exists:
raise ValueError(
"Legacy Tantivy FTS index detected at "
f"{path}. Tantivy-based FTS has been removed. "
"Delete the legacy index and recreate it with "
"table.create_fts_index(...)."
)
@abstractmethod @abstractmethod
def uses_v2_manifest_paths(self) -> bool: def uses_v2_manifest_paths(self) -> bool:
""" """
@@ -2412,63 +2405,84 @@ class LanceTable(Table):
prefix_only: bool = False, prefix_only: bool = False,
name: Optional[str] = None, name: Optional[str] = None,
): ):
self._ensure_no_legacy_fts_index() if not use_tantivy:
if not isinstance(field_names, str):
raise ValueError(
"Native FTS indexes can only be created on a single field "
"at a time. To search over multiple text fields, create a "
"separate FTS index for each field."
)
if use_tantivy: if tokenizer_name is None:
raise ValueError( tokenizer_configs = {
"Tantivy-based FTS has been removed. " "base_tokenizer": base_tokenizer,
"Remove use_tantivy and recreate the index with native FTS." "language": language,
"with_position": with_position,
"max_token_length": max_token_length,
"lower_case": lower_case,
"stem": stem,
"remove_stop_words": remove_stop_words,
"ascii_folding": ascii_folding,
"ngram_min_length": ngram_min_length,
"ngram_max_length": ngram_max_length,
"prefix_only": prefix_only,
}
else:
tokenizer_configs = self.infer_tokenizer_configs(tokenizer_name)
config = FTS(
**tokenizer_configs,
) )
if ordering_field_names is not None:
raise ValueError( # delete the existing legacy index if it exists
"ordering_field_names was only supported by the removed " if replace:
"Tantivy-based FTS implementation." path, fs, exist = self._get_fts_index_path()
if exist:
fs.delete_dir(path)
LOOP.run(
self._table.create_index(
field_names,
replace=replace,
config=config,
name=name,
)
) )
if writer_heap_size != 1024 * 1024 * 1024: return
raise ValueError(
"writer_heap_size was only supported by the removed " from .fts import create_index, populate_index
"Tantivy-based FTS implementation."
) if isinstance(field_names, str):
if not isinstance(field_names, str): field_names = [field_names]
raise ValueError(
"Native FTS indexes can only be created on a single field " if isinstance(ordering_field_names, str):
"at a time. To search over multiple text fields, create a " ordering_field_names = [ordering_field_names]
"separate FTS index for each field."
) path, fs, exist = self._get_fts_index_path()
if "." in field_names: if exist:
raise ValueError( if not replace:
"Native FTS indexes can only be created on top-level fields. " raise ValueError("Index already exists. Use replace=True to overwrite.")
f"Received nested field path: {field_names!r}." fs.delete_dir(path)
if not isinstance(fs, pa_fs.LocalFileSystem):
raise NotImplementedError(
"Full-text search is only supported on the local filesystem"
) )
if tokenizer_name is None: if tokenizer_name is None:
tokenizer_configs = { tokenizer_name = "default"
"base_tokenizer": base_tokenizer, index = create_index(
"language": language, path,
"with_position": with_position, field_names,
"max_token_length": max_token_length, ordering_fields=ordering_field_names,
"lower_case": lower_case, tokenizer_name=tokenizer_name,
"stem": stem,
"remove_stop_words": remove_stop_words,
"ascii_folding": ascii_folding,
"ngram_min_length": ngram_min_length,
"ngram_max_length": ngram_max_length,
"prefix_only": prefix_only,
}
else:
tokenizer_configs = self.infer_tokenizer_configs(tokenizer_name)
config = FTS(
**tokenizer_configs,
) )
populate_index(
LOOP.run( index,
self._table.create_index( self,
field_names, field_names,
replace=replace, ordering_fields=ordering_field_names,
config=config, writer_heap_size=writer_heap_size,
name=name,
)
) )
@staticmethod @staticmethod
@@ -2915,7 +2929,6 @@ class LanceTable(Table):
namespace_path=namespace_path, namespace_path=namespace_path,
storage_options=storage_options, storage_options=storage_options,
location=location, location=location,
namespace_client=namespace_client,
) )
) )
return self return self
+8 -5
View File
@@ -180,7 +180,7 @@ def test_fts_fuzzy_query():
), ),
mode="overwrite", mode="overwrite",
) )
table.create_fts_index("text", replace=True) table.create_fts_index("text", use_tantivy=False, replace=True)
results = table.search(MatchQuery("foo", "text", fuzziness=1)).to_pandas() results = table.search(MatchQuery("foo", "text", fuzziness=1)).to_pandas()
assert len(results) == 4 assert len(results) == 4
@@ -230,7 +230,7 @@ def test_fts_boost_query():
), ),
mode="overwrite", mode="overwrite",
) )
table.create_fts_index("desc", replace=True) table.create_fts_index("desc", use_tantivy=False, replace=True)
results = table.search( results = table.search(
BoostQuery( BoostQuery(
@@ -265,7 +265,7 @@ def test_fts_boolean_query(tmp_path):
], ],
mode="overwrite", mode="overwrite",
) )
table.create_fts_index("text", replace=True) table.create_fts_index("text", use_tantivy=False, replace=True)
# SHOULD # SHOULD
results = table.search( results = table.search(
@@ -319,7 +319,9 @@ def test_fts_native():
], ],
) )
table.create_fts_index("text") # passing `use_tantivy=False` to use lance FTS index
# `use_tantivy=True` by default
table.create_fts_index("text", use_tantivy=False)
table.search("puppy").limit(10).select(["text"]).to_list() table.search("puppy").limit(10).select(["text"]).to_list()
# [{'text': 'Frodo was a happy puppy', '_score': 0.6931471824645996}] # [{'text': 'Frodo was a happy puppy', '_score': 0.6931471824645996}]
# ... # ...
@@ -330,6 +332,7 @@ def test_fts_native():
# --8<-- [start:fts_config_folding] # --8<-- [start:fts_config_folding]
table.create_fts_index( table.create_fts_index(
"text", "text",
use_tantivy=False,
language="French", language="French",
stem=True, stem=True,
ascii_folding=True, ascii_folding=True,
@@ -343,7 +346,7 @@ def test_fts_native():
table.search("puppy").limit(10).where("text='foo'", prefilter=False).to_list() table.search("puppy").limit(10).where("text='foo'", prefilter=False).to_list()
# --8<-- [end:fts_postfiltering] # --8<-- [end:fts_postfiltering]
# --8<-- [start:fts_with_position] # --8<-- [start:fts_with_position]
table.create_fts_index("text", with_position=True, replace=True) table.create_fts_index("text", use_tantivy=False, with_position=True, replace=True)
# --8<-- [end:fts_with_position] # --8<-- [end:fts_with_position]
# --8<-- [start:fts_incremental_index] # --8<-- [start:fts_incremental_index]
table.add([{"vector": [3.1, 4.1], "text": "Frodo was a happy puppy"}]) table.add([{"vector": [3.1, 4.1], "text": "Frodo was a happy puppy"}])
+3 -2
View File
@@ -15,7 +15,8 @@ import pytest
from lancedb.pydantic import LanceModel, Vector from lancedb.pydantic import LanceModel, Vector
def test_basic(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_basic(tmp_path, use_tantivy):
db = lancedb.connect(tmp_path) db = lancedb.connect(tmp_path)
assert db.uri == str(tmp_path) assert db.uri == str(tmp_path)
@@ -48,7 +49,7 @@ def test_basic(tmp_path):
assert len(rs) == 1 assert len(rs) == 1
assert rs["item"].iloc[0] == "foo" assert rs["item"].iloc[0] == "foo"
table.create_fts_index("item") table.create_fts_index("item", use_tantivy=use_tantivy)
rs = table.search("bar", query_type="fts").to_pandas() rs = table.search("bar", query_type="fts").to_pandas()
assert len(rs) == 1 assert len(rs) == 1
assert rs["item"].iloc[0] == "bar" assert rs["item"].iloc[0] == "bar"
+158 -100
View File
@@ -36,6 +36,9 @@ import pytest
import pytest_asyncio import pytest_asyncio
from utils import exception_output from utils import exception_output
pytest.importorskip("lancedb.fts")
tantivy = pytest.importorskip("tantivy")
@pytest.fixture @pytest.fixture
def table(tmp_path) -> ldb.table.LanceTable: def table(tmp_path) -> ldb.table.LanceTable:
@@ -141,53 +144,58 @@ async def async_table(tmp_path) -> ldb.table.AsyncTable:
return table return table
@pytest.mark.parametrize( def test_create_index(tmp_path):
("kwargs", "match"), index = ldb.fts.create_index(str(tmp_path / "index"), ["text"])
[ assert isinstance(index, tantivy.Index)
( assert os.path.exists(str(tmp_path / "index"))
{"use_tantivy": True},
"Tantivy-based FTS has been removed",
),
(
{"ordering_field_names": ["count"]},
"ordering_field_names was only supported",
),
(
{"writer_heap_size": 128},
"writer_heap_size was only supported",
),
],
)
def test_reject_removed_tantivy_parameters(table, kwargs, match):
with pytest.raises(ValueError, match=match):
table.create_fts_index("text", **kwargs)
def test_reject_legacy_tantivy_index(table): def test_create_index_with_stemming(tmp_path, table):
path, _, _ = table._get_fts_index_path() index = ldb.fts.create_index(
os.makedirs(path, exist_ok=True) str(tmp_path / "index"), ["text"], tokenizer_name="en_stem"
)
assert isinstance(index, tantivy.Index)
assert os.path.exists(str(tmp_path / "index"))
with pytest.raises(ValueError, match="Legacy Tantivy FTS index detected"): # Check stemming by running tokenizer on non empty table
table.search("puppy").limit(5).to_list() table.create_fts_index("text", tokenizer_name="en_stem", use_tantivy=True)
with pytest.raises(ValueError, match="Legacy Tantivy FTS index detected"):
table.create_fts_index("text")
@pytest.mark.parametrize("use_tantivy", [True, False])
@pytest.mark.parametrize("with_position", [True, False]) @pytest.mark.parametrize("with_position", [True, False])
def test_create_inverted_index(table, with_position): def test_create_inverted_index(table, use_tantivy, with_position):
if use_tantivy and not with_position:
pytest.skip("we don't support building a tantivy index without position")
table.create_fts_index( table.create_fts_index(
"text", "text",
use_tantivy=use_tantivy,
with_position=with_position, with_position=with_position,
name="custom_fts_index", name="custom_fts_index",
) )
indices = table.list_indices() if not use_tantivy:
fts_indices = [i for i in indices if i.index_type == "FTS"] indices = table.list_indices()
assert any(i.name == "custom_fts_index" for i in fts_indices) fts_indices = [i for i in indices if i.index_type == "FTS"]
assert any(i.name == "custom_fts_index" for i in fts_indices)
def test_search_fts(table): def test_populate_index(tmp_path, table):
table.create_fts_index("text") index = ldb.fts.create_index(str(tmp_path / "index"), ["text"])
assert ldb.fts.populate_index(index, table, ["text"]) == len(table)
def test_search_index(tmp_path, table):
index = ldb.fts.create_index(str(tmp_path / "index"), ["text"])
ldb.fts.populate_index(index, table, ["text"])
index.reload()
results = ldb.fts.search_index(index, query="puppy", limit=5)
assert len(results) == 2
assert len(results[0]) == 5 # row_ids
assert len(results[1]) == 5 # _score
@pytest.mark.parametrize("use_tantivy", [True, False])
def test_search_fts(table, use_tantivy):
table.create_fts_index("text", use_tantivy=use_tantivy)
results = table.search("puppy").select(["id", "text"]).limit(5).to_list() results = table.search("puppy").select(["id", "text"]).limit(5).to_list()
assert len(results) == 5 assert len(results) == 5
assert len(results[0]) == 3 # id, text, _score assert len(results[0]) == 3 # id, text, _score
@@ -196,52 +204,53 @@ def test_search_fts(table):
results = table.search("puppy").select(["id", "text"]).to_list() results = table.search("puppy").select(["id", "text"]).to_list()
assert len(results) == 10 assert len(results) == 10
# Test with a query if not use_tantivy:
results = ( # Test with a query
table.search(MatchQuery("puppy", "text")) results = (
.select(["id", "text"]) table.search(MatchQuery("puppy", "text"))
.limit(5) .select(["id", "text"])
.to_list() .limit(5)
) .to_list()
assert len(results) == 5
# Test boost query
results = (
table.search(
BoostQuery(
MatchQuery("puppy", "text"),
MatchQuery("runs", "text"),
)
) )
.select(["id", "text"]) assert len(results) == 5
.limit(5)
.to_list()
)
assert len(results) == 5
# Test multi match query # Test boost query
table.create_fts_index("text2") results = (
results = ( table.search(
table.search(MultiMatchQuery("puppy", ["text", "text2"])) BoostQuery(
.select(["id", "text"]) MatchQuery("puppy", "text"),
.limit(5) MatchQuery("runs", "text"),
.to_list() )
) )
assert len(results) == 5 .select(["id", "text"])
assert len(results[0]) == 3 # id, text, _score .limit(5)
.to_list()
)
assert len(results) == 5
# Test boolean query # Test multi match query
results = ( table.create_fts_index("text2", use_tantivy=use_tantivy)
table.search(MatchQuery("puppy", "text") & MatchQuery("runs", "text")) results = (
.select(["id", "text"]) table.search(MultiMatchQuery("puppy", ["text", "text2"]))
.limit(5) .select(["id", "text"])
.to_list() .limit(5)
) .to_list()
assert len(results) == 5 )
assert len(results[0]) == 3 # id, text, _score assert len(results) == 5
for r in results: assert len(results[0]) == 3 # id, text, _score
assert "puppy" in r["text"]
assert "runs" in r["text"] # Test boolean query
results = (
table.search(MatchQuery("puppy", "text") & MatchQuery("runs", "text"))
.select(["id", "text"])
.limit(5)
.to_list()
)
assert len(results) == 5
assert len(results[0]) == 3 # id, text, _score
for r in results:
assert "puppy" in r["text"]
assert "runs" in r["text"]
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -309,13 +318,13 @@ async def test_fts_select_async(async_table):
def test_search_fts_phrase_query(table): def test_search_fts_phrase_query(table):
table.create_fts_index("text", with_position=False) table.create_fts_index("text", use_tantivy=False, with_position=False)
try: try:
phrase_results = table.search('"puppy runs"').limit(100).to_list() phrase_results = table.search('"puppy runs"').limit(100).to_list()
assert False assert False
except Exception: except Exception:
pass pass
table.create_fts_index("text", with_position=True, replace=True) table.create_fts_index("text", use_tantivy=False, with_position=True, replace=True)
results = table.search("puppy").limit(100).to_list() results = table.search("puppy").limit(100).to_list()
# Test with quotation marks # Test with quotation marks
@@ -366,8 +375,8 @@ async def test_search_fts_phrase_query_async(async_table):
def test_search_fts_specify_column(table): def test_search_fts_specify_column(table):
table.create_fts_index("text") table.create_fts_index("text", use_tantivy=False)
table.create_fts_index("text2") table.create_fts_index("text2", use_tantivy=False)
results = table.search("puppy", fts_columns="text").limit(5).to_list() results = table.search("puppy", fts_columns="text").limit(5).to_list()
assert len(results) == 5 assert len(results) == 5
@@ -461,8 +470,42 @@ async def test_search_fts_specify_column_async(async_table):
pass pass
def test_create_index_from_table(tmp_path, table): def test_search_ordering_field_index_table(tmp_path, table):
table.create_fts_index("text") table.create_fts_index("text", ordering_field_names=["count"], use_tantivy=True)
rows = (
table.search("puppy", ordering_field_name="count")
.limit(20)
.select(["text", "count"])
.to_list()
)
for r in rows:
assert "puppy" in r["text"]
assert sorted(rows, key=lambda x: x["count"], reverse=True) == rows
def test_search_ordering_field_index(tmp_path, table):
index = ldb.fts.create_index(
str(tmp_path / "index"), ["text"], ordering_fields=["count"]
)
ldb.fts.populate_index(index, table, ["text"], ordering_fields=["count"])
index.reload()
results = ldb.fts.search_index(
index, query="puppy", limit=5, ordering_field="count"
)
assert len(results) == 2
assert len(results[0]) == 5 # row_ids
assert len(results[1]) == 5 # _distance
rows = table.to_lance().take(results[0]).to_pylist()
for r in rows:
assert "puppy" in r["text"]
assert sorted(rows, key=lambda x: x["count"], reverse=True) == rows
@pytest.mark.parametrize("use_tantivy", [True, False])
def test_create_index_from_table(tmp_path, table, use_tantivy):
table.create_fts_index("text", use_tantivy=use_tantivy)
df = table.search("puppy").limit(5).select(["text"]).to_pandas() df = table.search("puppy").limit(5).select(["text"]).to_pandas()
assert len(df) <= 5 assert len(df) <= 5
assert "text" in df.columns assert "text" in df.columns
@@ -482,24 +525,36 @@ def test_create_index_from_table(tmp_path, table):
) )
with pytest.raises(Exception, match="already exists"): with pytest.raises(Exception, match="already exists"):
table.create_fts_index("text") table.create_fts_index("text", use_tantivy=use_tantivy)
table.create_fts_index("text", replace=True) table.create_fts_index("text", replace=True, use_tantivy=use_tantivy)
assert len(table.search("gorilla").limit(1).to_pandas()) == 1 assert len(table.search("gorilla").limit(1).to_pandas()) == 1
def test_create_index_multiple_columns(tmp_path, table): def test_create_index_multiple_columns(tmp_path, table):
with pytest.raises(ValueError, match="Native FTS indexes can only be created"): table.create_fts_index(["text", "text2"], use_tantivy=True)
table.create_fts_index(["text", "text2"]) df = table.search("puppy").limit(5).to_pandas()
assert len(df) == 5
assert "text" in df.columns
assert "text2" in df.columns
def test_empty_rs(tmp_path, table, mocker):
table.create_fts_index(["text", "text2"], use_tantivy=True)
mocker.patch("lancedb.fts.search_index", return_value=([], []))
df = table.search("puppy").limit(5).to_pandas()
assert len(df) == 0
def test_nested_schema(tmp_path, table): def test_nested_schema(tmp_path, table):
with pytest.raises(ValueError, match="top-level fields"): table.create_fts_index("nested.text", use_tantivy=True)
table.create_fts_index("nested.text") rs = table.search("puppy").limit(5).to_list()
assert len(rs) == 5
def test_search_index_with_filter(table): @pytest.mark.parametrize("use_tantivy", [True, False])
table.create_fts_index("text") def test_search_index_with_filter(table, use_tantivy):
table.create_fts_index("text", use_tantivy=use_tantivy)
orig_import = __import__ orig_import = __import__
def import_mock(name, *args): def import_mock(name, *args):
@@ -529,7 +584,8 @@ def test_search_index_with_filter(table):
assert r["_rowid"] is not None assert r["_rowid"] is not None
def test_null_input(table): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_null_input(table, use_tantivy):
table.add( table.add(
[ [
{ {
@@ -542,13 +598,14 @@ def test_null_input(table):
} }
] ]
) )
table.create_fts_index("text") table.create_fts_index("text", use_tantivy=use_tantivy)
def test_syntax(table): def test_syntax(table):
# https://github.com/lancedb/lancedb/issues/769 # https://github.com/lancedb/lancedb/issues/769
table.create_fts_index("text") table.create_fts_index("text", use_tantivy=True)
table.search("they could have been dogs OR").limit(10).to_list() with pytest.raises(ValueError, match="Syntax Error"):
table.search("they could have been dogs OR").limit(10).to_list()
# these should work # these should work
@@ -559,7 +616,6 @@ def test_syntax(table):
).to_list() ).to_list()
# phrase queries # phrase queries
table.create_fts_index("text", with_position=True, replace=True)
table.search("they could have been dogs OR cats").phrase_query().limit(10).to_list() table.search("they could have been dogs OR cats").phrase_query().limit(10).to_list()
table.search('"they could have been dogs OR cats"').limit(10).to_list() table.search('"they could have been dogs OR cats"').limit(10).to_list()
table.search('''"the cats OR dogs were not really 'pets' at all"''').limit( table.search('''"the cats OR dogs were not really 'pets' at all"''').limit(
@@ -583,7 +639,7 @@ def test_language(mem_db: DBConnection):
table = mem_db.create_table("test", data=data) table = mem_db.create_table("test", data=data)
with pytest.raises(ValueError) as e: with pytest.raises(ValueError) as e:
table.create_fts_index("text", language="klingon") table.create_fts_index("text", use_tantivy=False, language="klingon")
assert exception_output(e) == ( assert exception_output(e) == (
"ValueError: LanceDB does not support the requested language: 'klingon'\n" "ValueError: LanceDB does not support the requested language: 'klingon'\n"
@@ -594,6 +650,7 @@ def test_language(mem_db: DBConnection):
table.create_fts_index( table.create_fts_index(
"text", "text",
use_tantivy=False,
language="French", language="French",
stem=True, stem=True,
ascii_folding=True, ascii_folding=True,
@@ -633,7 +690,7 @@ def test_fts_on_list(mem_db: DBConnection):
} }
) )
table = mem_db.create_table("test", data=data) table = mem_db.create_table("test", data=data)
table.create_fts_index("text", with_position=True) table.create_fts_index("text", use_tantivy=False, with_position=True)
res = table.search("lance").limit(5).to_list() res = table.search("lance").limit(5).to_list()
assert len(res) == 3 assert len(res) == 3
@@ -645,7 +702,7 @@ def test_fts_on_list(mem_db: DBConnection):
def test_fts_ngram(mem_db: DBConnection): def test_fts_ngram(mem_db: DBConnection):
data = pa.table({"text": ["hello world", "lance database", "lance is cool"]}) data = pa.table({"text": ["hello world", "lance database", "lance is cool"]})
table = mem_db.create_table("test", data=data) table = mem_db.create_table("test", data=data)
table.create_fts_index("text", base_tokenizer="ngram") table.create_fts_index("text", use_tantivy=False, base_tokenizer="ngram")
results = table.search("lan", query_type="fts").limit(10).to_list() results = table.search("lan", query_type="fts").limit(10).to_list()
assert len(results) == 2 assert len(results) == 2
@@ -664,6 +721,7 @@ def test_fts_ngram(mem_db: DBConnection):
# test setting min_ngram_length and prefix_only # test setting min_ngram_length and prefix_only
table.create_fts_index( table.create_fts_index(
"text", "text",
use_tantivy=False,
base_tokenizer="ngram", base_tokenizer="ngram",
replace=True, replace=True,
ngram_min_length=2, ngram_min_length=2,
@@ -828,7 +886,7 @@ def test_fts_query_to_json():
def test_fts_fast_search(table): def test_fts_fast_search(table):
table.create_fts_index("text") table.create_fts_index("text", use_tantivy=False)
# Insert some unindexed data # Insert some unindexed data
table.add( table.add(
+2 -2
View File
@@ -28,7 +28,7 @@ def sync_table(tmpdir_factory) -> Table:
} }
) )
table = db.create_table("test", data) table = db.create_table("test", data)
table.create_fts_index("text", with_position=False) table.create_fts_index("text", with_position=False, use_tantivy=False)
return table return table
@@ -192,7 +192,7 @@ def table_with_id(tmpdir_factory) -> Table:
} }
) )
table = db.create_table("test_with_id", data) table = db.create_table("test_with_id", data)
table.create_fts_index("text", with_position=False) table.create_fts_index("text", with_position=False, use_tantivy=False)
return table return table
@@ -18,9 +18,6 @@ Tests verify:
""" """
import copy import copy
import shutil
import sys
import tempfile
import time import time
import uuid import uuid
from typing import Dict, Optional from typing import Dict, Optional
@@ -390,66 +387,6 @@ def test_namespace_open_table_with_provider(s3_bucket: str, use_custom: bool):
assert get_describe_call_count(inner_ns_client) == describe_count_after_open assert get_describe_call_count(inner_ns_client) == describe_count_after_open
@pytest.mark.skipif(
sys.platform == "win32",
reason="TODO: fix schema-only namespace metrics test on Windows",
)
@pytest.mark.parametrize("use_custom", [False, True], ids=["DirectoryNS", "CustomNS"])
def test_namespace_create_schema_only_with_provider(use_custom: bool):
"""
Test creating a schema-only table through namespace with storage options provider.
Verifies:
- declare_table is called once to reserve the location
- describe_table is not needed during create in create mode
- the table can be reopened successfully afterward
- opening the table triggers exactly one describe_table call
"""
temp_dir = tempfile.mkdtemp()
try:
ns_client, inner_ns_client = create_tracking_namespace(
bucket_name=temp_dir,
storage_options={},
credential_expires_in_seconds=3600,
use_custom=use_custom,
)
db = LanceNamespaceDBConnection(ns_client)
namespace_name = f"test_ns_{uuid.uuid4().hex[:8]}"
db.create_namespace([namespace_name])
table_name = f"test_table_{uuid.uuid4().hex}"
namespace_path = [namespace_name]
schema = pa.schema(
[
pa.field("id", pa.int64()),
pa.field("vector", pa.list_(pa.float32(), 2)),
pa.field("text", pa.string()),
]
)
assert get_declare_call_count(inner_ns_client) == 0
assert get_describe_call_count(inner_ns_client) == 0
table = db.create_table(
table_name, schema=schema, namespace_path=namespace_path
)
assert table.name == table_name
assert table.namespace == namespace_path
assert get_declare_call_count(inner_ns_client) == 1
assert get_describe_call_count(inner_ns_client) == 0
reopened_table = db.open_table(table_name, namespace_path=namespace_path)
assert reopened_table.schema == schema
assert get_declare_call_count(inner_ns_client) == 1
assert get_describe_call_count(inner_ns_client) == 1
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
@pytest.mark.s3_test @pytest.mark.s3_test
@pytest.mark.parametrize("use_custom", [False, True], ids=["DirectoryNS", "CustomNS"]) @pytest.mark.parametrize("use_custom", [False, True], ids=["DirectoryNS", "CustomNS"])
def test_namespace_credential_refresh_on_read(s3_bucket: str, use_custom: bool): def test_namespace_credential_refresh_on_read(s3_bucket: str, use_custom: bool):
+1 -1
View File
@@ -1385,7 +1385,7 @@ def test_query_timeout(tmp_path):
} }
) )
table = db.create_table("test", data) table = db.create_table("test", data)
table.create_fts_index("text") table.create_fts_index("text", use_tantivy=False)
with pytest.raises(Exception, match="Query timeout"): with pytest.raises(Exception, match="Query timeout"):
table.search().where("text = 'a'").to_list(timeout=timedelta(0)) table.search().where("text = 'a'").to_list(timeout=timedelta(0))
+43 -29
View File
@@ -26,8 +26,11 @@ from lancedb.rerankers import (
) )
from lancedb.table import LanceTable from lancedb.table import LanceTable
# Tests rely on FTS index
pytest.importorskip("lancedb.fts")
def get_test_table(tmp_path):
def get_test_table(tmp_path, use_tantivy):
db = lancedb.connect(tmp_path) db = lancedb.connect(tmp_path)
# Create a LanceDB table schema with a vector and a text column # Create a LanceDB table schema with a vector and a text column
emb = EmbeddingFunctionRegistry.get_instance().get("test").create() emb = EmbeddingFunctionRegistry.get_instance().get("test").create()
@@ -95,7 +98,7 @@ def get_test_table(tmp_path):
) )
# Create a fts index # Create a fts index
table.create_fts_index("text", replace=True) table.create_fts_index("text", use_tantivy=use_tantivy, replace=True)
return table, MyTable return table, MyTable
@@ -205,8 +208,8 @@ def _run_test_reranker(reranker, table, query, query_vector, schema):
assert len(result) == 20 and result == result_arrow assert len(result) == 20 and result == result_arrow
def _run_test_hybrid_reranker(reranker, tmp_path): def _run_test_hybrid_reranker(reranker, tmp_path, use_tantivy):
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
# The default reranker # The default reranker
result1 = ( result1 = (
table.search( table.search(
@@ -282,7 +285,8 @@ def _run_test_hybrid_reranker(reranker, tmp_path):
) )
def test_linear_combination(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_linear_combination(tmp_path, use_tantivy):
reranker = LinearCombinationReranker() reranker = LinearCombinationReranker()
vector_results = pa.Table.from_pydict( vector_results = pa.Table.from_pydict(
@@ -309,20 +313,22 @@ def test_linear_combination(tmp_path):
assert "_score" not in combined_results.column_names assert "_score" not in combined_results.column_names
assert "_relevance_score" in combined_results.column_names assert "_relevance_score" in combined_results.column_names
_run_test_hybrid_reranker(reranker, tmp_path) _run_test_hybrid_reranker(reranker, tmp_path, use_tantivy)
def test_rrf_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_rrf_reranker(tmp_path, use_tantivy):
reranker = RRFReranker() reranker = RRFReranker()
_run_test_hybrid_reranker(reranker, tmp_path) _run_test_hybrid_reranker(reranker, tmp_path, use_tantivy)
def test_mrr_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_mrr_reranker(tmp_path, use_tantivy):
reranker = MRRReranker() reranker = MRRReranker()
_run_test_hybrid_reranker(reranker, tmp_path) _run_test_hybrid_reranker(reranker, tmp_path, use_tantivy)
# Test multi-vector part # Test multi-vector part
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
query = "single player experience" query = "single player experience"
rs1 = table.search(query, vector_column_name="vector").limit(10).with_row_id(True) rs1 = table.search(query, vector_column_name="vector").limit(10).with_row_id(True)
rs2 = ( rs2 = (
@@ -357,7 +363,7 @@ def test_rrf_reranker_distance():
table = db.create_table("test", data) table = db.create_table("test", data)
table.create_index(num_partitions=1, num_sub_vectors=2) table.create_index(num_partitions=1, num_sub_vectors=2)
table.create_fts_index("text") table.create_fts_index("text", use_tantivy=False)
reranker = RRFReranker(return_score="all") reranker = RRFReranker(return_score="all")
@@ -416,31 +422,35 @@ def test_rrf_reranker_distance():
@pytest.mark.skipif( @pytest.mark.skipif(
os.environ.get("COHERE_API_KEY") is None, reason="COHERE_API_KEY not set" os.environ.get("COHERE_API_KEY") is None, reason="COHERE_API_KEY not set"
) )
def test_cohere_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_cohere_reranker(tmp_path, use_tantivy):
pytest.importorskip("cohere") pytest.importorskip("cohere")
reranker = CohereReranker() reranker = CohereReranker()
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
_run_test_reranker(reranker, table, "single player experience", None, schema) _run_test_reranker(reranker, table, "single player experience", None, schema)
def test_cross_encoder_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_cross_encoder_reranker(tmp_path, use_tantivy):
pytest.importorskip("sentence_transformers") pytest.importorskip("sentence_transformers")
reranker = CrossEncoderReranker() reranker = CrossEncoderReranker()
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
_run_test_reranker(reranker, table, "single player experience", None, schema) _run_test_reranker(reranker, table, "single player experience", None, schema)
def test_colbert_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_colbert_reranker(tmp_path, use_tantivy):
pytest.importorskip("rerankers") pytest.importorskip("rerankers")
reranker = ColbertReranker() reranker = ColbertReranker()
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
_run_test_reranker(reranker, table, "single player experience", None, schema) _run_test_reranker(reranker, table, "single player experience", None, schema)
def test_answerdotai_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_answerdotai_reranker(tmp_path, use_tantivy):
pytest.importorskip("rerankers") pytest.importorskip("rerankers")
reranker = AnswerdotaiRerankers() reranker = AnswerdotaiRerankers()
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
_run_test_reranker(reranker, table, "single player experience", None, schema) _run_test_reranker(reranker, table, "single player experience", None, schema)
@@ -449,9 +459,10 @@ def test_answerdotai_reranker(tmp_path):
or os.environ.get("OPENAI_BASE_URL") is not None, or os.environ.get("OPENAI_BASE_URL") is not None,
reason="OPENAI_API_KEY not set", reason="OPENAI_API_KEY not set",
) )
def test_openai_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_openai_reranker(tmp_path, use_tantivy):
pytest.importorskip("openai") pytest.importorskip("openai")
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
reranker = OpenaiReranker() reranker = OpenaiReranker()
_run_test_reranker(reranker, table, "single player experience", None, schema) _run_test_reranker(reranker, table, "single player experience", None, schema)
@@ -459,9 +470,10 @@ def test_openai_reranker(tmp_path):
@pytest.mark.skipif( @pytest.mark.skipif(
os.environ.get("JINA_API_KEY") is None, reason="JINA_API_KEY not set" os.environ.get("JINA_API_KEY") is None, reason="JINA_API_KEY not set"
) )
def test_jina_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_jina_reranker(tmp_path, use_tantivy):
pytest.importorskip("jina") pytest.importorskip("jina")
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
reranker = JinaReranker() reranker = JinaReranker()
_run_test_reranker(reranker, table, "single player experience", None, schema) _run_test_reranker(reranker, table, "single player experience", None, schema)
@@ -469,10 +481,11 @@ def test_jina_reranker(tmp_path):
@pytest.mark.skipif( @pytest.mark.skipif(
os.environ.get("VOYAGE_API_KEY") is None, reason="VOYAGE_API_KEY not set" os.environ.get("VOYAGE_API_KEY") is None, reason="VOYAGE_API_KEY not set"
) )
def test_voyageai_reranker(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_voyageai_reranker(tmp_path, use_tantivy):
pytest.importorskip("voyageai") pytest.importorskip("voyageai")
reranker = VoyageAIReranker(model_name="rerank-2.5") reranker = VoyageAIReranker(model_name="rerank-2.5")
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
_run_test_reranker(reranker, table, "single player experience", None, schema) _run_test_reranker(reranker, table, "single player experience", None, schema)
@@ -491,7 +504,7 @@ def test_empty_result_reranker():
# Create empty table with schema # Create empty table with schema
empty_table = db.create_table("empty_table", schema=schema, mode="overwrite") empty_table = db.create_table("empty_table", schema=schema, mode="overwrite")
empty_table.create_fts_index("text", replace=True) empty_table.create_fts_index("text", use_tantivy=False, replace=True)
for reranker in [ for reranker in [
CrossEncoderReranker(), CrossEncoderReranker(),
# ColbertReranker(), # ColbertReranker(),
@@ -590,10 +603,11 @@ def test_empty_hybrid_result_reranker():
assert "_rowid" in result.column_names assert "_rowid" in result.column_names
def test_cross_encoder_reranker_return_all(tmp_path): @pytest.mark.parametrize("use_tantivy", [True, False])
def test_cross_encoder_reranker_return_all(tmp_path, use_tantivy):
pytest.importorskip("sentence_transformers") pytest.importorskip("sentence_transformers")
reranker = CrossEncoderReranker(return_score="all") reranker = CrossEncoderReranker(return_score="all")
table, schema = get_test_table(tmp_path) table, schema = get_test_table(tmp_path, use_tantivy)
query = "single player experience" query = "single player experience"
result = ( result = (
table.search(query, query_type="hybrid", vector_column_name="vector") table.search(query, query_type="hybrid", vector_column_name="vector")
+2 -2
View File
@@ -242,8 +242,8 @@ def test_s3_dynamodb_sync(s3_bucket: str, commit_table: str, monkeypatch):
# FTS indices should error since they are not supported yet. # FTS indices should error since they are not supported yet.
with pytest.raises( with pytest.raises(
ValueError, NotImplementedError,
match="Tantivy-based FTS has been removed", match="Full-text search is only supported on the local filesystem",
): ):
table.create_fts_index("x", use_tantivy=True) table.create_fts_index("x", use_tantivy=True)
+3 -8
View File
@@ -3,7 +3,6 @@
import os import os
import sys
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from time import sleep from time import sleep
from typing import List from typing import List
@@ -1948,6 +1947,7 @@ def setup_hybrid_search_table(db: DBConnection, embedding_func):
def test_hybrid_search(tmp_db: DBConnection): def test_hybrid_search(tmp_db: DBConnection):
# This test uses an FTS index # This test uses an FTS index
pytest.importorskip("lancedb.fts")
pytest.importorskip("lance") pytest.importorskip("lance")
table, MyTable, emb = setup_hybrid_search_table(tmp_db, "test") table, MyTable, emb = setup_hybrid_search_table(tmp_db, "test")
@@ -2018,6 +2018,7 @@ def test_hybrid_search(tmp_db: DBConnection):
def test_hybrid_search_metric_type(tmp_db: DBConnection): def test_hybrid_search_metric_type(tmp_db: DBConnection):
# This test uses an FTS index # This test uses an FTS index
pytest.importorskip("lancedb.fts")
pytest.importorskip("lance") pytest.importorskip("lance")
# Need to use nonnorm as the embedding function so l2 and dot results # Need to use nonnorm as the embedding function so l2 and dot results
@@ -2039,13 +2040,6 @@ def test_hybrid_search_metric_type(tmp_db: DBConnection):
@pytest.mark.parametrize( @pytest.mark.parametrize(
"consistency_interval", [None, timedelta(seconds=0), timedelta(seconds=0.1)] "consistency_interval", [None, timedelta(seconds=0), timedelta(seconds=0.1)]
) )
@pytest.mark.skipif(
sys.platform == "win32",
reason=(
"TODO: directory namespace is not supported on Windows yet; "
"re-enable after that is fixed."
),
)
def test_consistency(tmp_path, consistency_interval): def test_consistency(tmp_path, consistency_interval):
db = lancedb.connect(tmp_path) db = lancedb.connect(tmp_path)
table = db.create_table("my_table", data=[{"id": 0}]) table = db.create_table("my_table", data=[{"id": 0}])
@@ -2066,6 +2060,7 @@ def test_consistency(tmp_path, consistency_interval):
elif consistency_interval == timedelta(seconds=0): elif consistency_interval == timedelta(seconds=0):
assert table2.version == table.version assert table2.version == table.version
else: else:
# (consistency_interval == timedelta(seconds=0.1)
assert table2.version == table.version - 1 assert table2.version == table.version - 1
sleep(0.1) sleep(0.1)
assert table2.version == table.version assert table2.version == table.version
+2 -85
View File
@@ -1,17 +1,11 @@
// 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 std::{ use std::{collections::HashMap, sync::Arc, time::Duration};
collections::{HashMap, HashSet},
sync::Arc,
time::Duration,
};
use arrow::{datatypes::Schema, ffi_stream::ArrowArrayStreamReader, pyarrow::FromPyArrow}; use arrow::{datatypes::Schema, ffi_stream::ArrowArrayStreamReader, pyarrow::FromPyArrow};
use lancedb::{ use lancedb::{
connection::Connection as LanceConnection, connection::Connection as LanceConnection,
connection::NamespaceClientPushdownOperation,
database::namespace::LanceNamespaceDatabase,
database::{CreateTableMode, Database, ReadConsistency}, database::{CreateTableMode, Database, ReadConsistency},
}; };
use pyo3::{ use pyo3::{
@@ -45,29 +39,6 @@ impl Connection {
} }
} }
fn parse_namespace_client_pushdown_operations(
operations: Option<Vec<String>>,
) -> PyResult<HashSet<NamespaceClientPushdownOperation>> {
let mut parsed = HashSet::new();
for operation in operations.unwrap_or_default() {
match operation.as_str() {
"QueryTable" => {
parsed.insert(NamespaceClientPushdownOperation::QueryTable);
}
"CreateTable" => {
parsed.insert(NamespaceClientPushdownOperation::CreateTable);
}
_ => {
return Err(PyValueError::new_err(format!(
"Invalid pushdown operation: {}",
operation
)));
}
}
}
Ok(parsed)
}
impl Connection { impl Connection {
fn parse_create_mode_str(mode: &str) -> PyResult<CreateTableMode> { fn parse_create_mode_str(mode: &str) -> PyResult<CreateTableMode> {
match mode { match mode {
@@ -525,7 +496,7 @@ impl Connection {
} }
#[pyfunction] #[pyfunction]
#[pyo3(signature = (uri, api_key=None, region=None, host_override=None, read_consistency_interval=None, client_config=None, storage_options=None, session=None, manifest_enabled=false, namespace_client_properties=None))] #[pyo3(signature = (uri, api_key=None, region=None, host_override=None, read_consistency_interval=None, client_config=None, storage_options=None, session=None))]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn connect( pub fn connect(
py: Python<'_>, py: Python<'_>,
@@ -537,8 +508,6 @@ pub fn connect(
client_config: Option<PyClientConfig>, client_config: Option<PyClientConfig>,
storage_options: Option<HashMap<String, String>>, storage_options: Option<HashMap<String, String>>,
session: Option<crate::session::Session>, session: Option<crate::session::Session>,
manifest_enabled: bool,
namespace_client_properties: Option<HashMap<String, String>>,
) -> PyResult<Bound<'_, PyAny>> { ) -> PyResult<Bound<'_, PyAny>> {
future_into_py(py, async move { future_into_py(py, async move {
let mut builder = lancedb::connect(&uri); let mut builder = lancedb::connect(&uri);
@@ -558,12 +527,6 @@ pub fn connect(
if let Some(storage_options) = storage_options { if let Some(storage_options) = storage_options {
builder = builder.storage_options(storage_options); builder = builder.storage_options(storage_options);
} }
if manifest_enabled {
builder = builder.manifest_enabled(true);
}
if let Some(namespace_client_properties) = namespace_client_properties {
builder = builder.namespace_client_properties(namespace_client_properties);
}
#[cfg(feature = "remote")] #[cfg(feature = "remote")]
if let Some(client_config) = client_config { if let Some(client_config) = client_config {
builder = builder.client_config(client_config.into()); builder = builder.client_config(client_config.into());
@@ -575,52 +538,6 @@ pub fn connect(
}) })
} }
#[pyfunction]
#[pyo3(signature = (
namespace_client,
read_consistency_interval=None,
storage_options=None,
session=None,
namespace_client_pushdown_operations=None,
namespace_client_impl=None,
namespace_client_properties=None,
))]
#[allow(clippy::too_many_arguments)]
pub fn connect_namespace_client(
py: Python<'_>,
namespace_client: Py<PyAny>,
read_consistency_interval: Option<f64>,
storage_options: Option<HashMap<String, String>>,
session: Option<crate::session::Session>,
namespace_client_pushdown_operations: Option<Vec<String>>,
namespace_client_impl: Option<String>,
namespace_client_properties: Option<HashMap<String, String>>,
) -> PyResult<Connection> {
let namespace_client = extract_namespace_arc(py, namespace_client)?;
let read_consistency_interval = read_consistency_interval.map(Duration::from_secs_f64);
let namespace_client_pushdown_operations =
parse_namespace_client_pushdown_operations(namespace_client_pushdown_operations)?;
let ns_impl = namespace_client_impl.unwrap_or_else(|| "python".to_string());
let ns_properties = namespace_client_properties.unwrap_or_default();
let storage_options = storage_options.unwrap_or_default();
let session = session.map(|s| s.inner.clone());
let database = LanceNamespaceDatabase::from_namespace_client(
namespace_client,
ns_impl,
ns_properties,
storage_options,
read_consistency_interval,
session,
namespace_client_pushdown_operations,
);
Ok(Connection::new(LanceConnection::new(
Arc::new(database),
Arc::new(lancedb::embeddings::MemoryRegistry::new()),
)))
}
#[derive(FromPyObject)] #[derive(FromPyObject)]
pub struct PyClientConfig { pub struct PyClientConfig {
user_agent: String, user_agent: String,
+1 -1
View File
@@ -17,7 +17,7 @@ use pyo3::{Bound, PyAny, PyResult, exceptions::PyValueError, prelude::*, pyfunct
/// [`expr_lit`] and combined with the methods on this struct. On the Python /// [`expr_lit`] and combined with the methods on this struct. On the Python
/// side a thin wrapper class (`lancedb.expr.Expr`) delegates to these methods /// side a thin wrapper class (`lancedb.expr.Expr`) delegates to these methods
/// and adds Python operator overloads. /// and adds Python operator overloads.
#[pyclass(name = "PyExpr", from_py_object)] #[pyclass(name = "PyExpr")]
#[derive(Clone)] #[derive(Clone)]
pub struct PyExpr(pub DfExpr); pub struct PyExpr(pub DfExpr);
+1 -1
View File
@@ -33,7 +33,7 @@ impl PyHeaderProvider {
Ok(headers_py) => { Ok(headers_py) => {
// Convert Python dict to Rust HashMap // Convert Python dict to Rust HashMap
let bound_headers = headers_py.bind(py); let bound_headers = headers_py.bind(py);
let dict: &Bound<PyDict> = bound_headers.cast().map_err(|e| { let dict: &Bound<PyDict> = bound_headers.downcast().map_err(|e| {
format!("HeaderProvider.get_headers must return a dict: {}", e) format!("HeaderProvider.get_headers must return a dict: {}", e)
})?; })?;
+2 -2
View File
@@ -13,7 +13,7 @@ use pyo3::{
Bound, FromPyObject, PyAny, PyResult, Python, Bound, FromPyObject, PyAny, PyResult, Python,
exceptions::{PyKeyError, PyValueError}, exceptions::{PyKeyError, PyValueError},
intern, pyclass, pymethods, intern, pyclass, pymethods,
types::{PyAnyMethods, PyString}, types::PyAnyMethods,
}; };
use crate::util::parse_distance_type; use crate::util::parse_distance_type;
@@ -22,7 +22,7 @@ pub fn class_name(ob: &'_ Bound<'_, PyAny>) -> PyResult<String> {
let full_name = ob let full_name = ob
.getattr(intern!(ob.py(), "__class__"))? .getattr(intern!(ob.py(), "__class__"))?
.getattr(intern!(ob.py(), "__name__"))?; .getattr(intern!(ob.py(), "__name__"))?;
let full_name = full_name.cast::<PyString>()?.to_string_lossy(); let full_name = full_name.downcast()?.to_string_lossy();
match full_name.rsplit_once('.') { match full_name.rsplit_once('.') {
Some((_, name)) => Ok(name.to_string()), Some((_, name)) => Ok(name.to_string()),
+1 -2
View File
@@ -2,7 +2,7 @@
// SPDX-FileCopyrightText: Copyright The LanceDB Authors // SPDX-FileCopyrightText: Copyright The LanceDB Authors
use arrow::RecordBatchStream; use arrow::RecordBatchStream;
use connection::{Connection, connect, connect_namespace_client}; use connection::{Connection, connect};
use env_logger::Env; use env_logger::Env;
use expr::{PyExpr, expr_col, expr_func, expr_lit}; use expr::{PyExpr, expr_col, expr_func, expr_lit};
use index::IndexConfig; use index::IndexConfig;
@@ -58,7 +58,6 @@ pub fn _lancedb(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<PyPermutationReader>()?; m.add_class::<PyPermutationReader>()?;
m.add_class::<PyExpr>()?; m.add_class::<PyExpr>()?;
m.add_function(wrap_pyfunction!(connect, m)?)?; m.add_function(wrap_pyfunction!(connect, m)?)?;
m.add_function(wrap_pyfunction!(connect_namespace_client, m)?)?;
m.add_function(wrap_pyfunction!(permutation::async_permutation_builder, m)?)?; m.add_function(wrap_pyfunction!(permutation::async_permutation_builder, m)?)?;
m.add_function(wrap_pyfunction!(util::validate_table_name, m)?)?; m.add_function(wrap_pyfunction!(util::validate_table_name, m)?)?;
m.add_function(wrap_pyfunction!(query::fts_query_to_json, m)?)?; m.add_function(wrap_pyfunction!(query::fts_query_to_json, m)?)?;
+2 -2
View File
@@ -183,7 +183,7 @@ async fn call_py_method_primitive<Req, Resp>(
) -> lance_core::Result<Resp> ) -> lance_core::Result<Resp>
where where
Req: serde::Serialize + Send + 'static, Req: serde::Serialize + Send + 'static,
Resp: for<'a, 'py> pyo3::FromPyObject<'a, 'py> + Send + 'static, Resp: for<'py> pyo3::FromPyObject<'py> + Send + 'static,
{ {
let request_json = serde_json::to_string(&request).map_err(|e| { let request_json = serde_json::to_string(&request).map_err(|e| {
lance_core::Error::io(format!( lance_core::Error::io(format!(
@@ -203,7 +203,7 @@ where
// Call the Python method // Call the Python method
let result = py_namespace.call_method1(py, method_name, (request_arg,))?; let result = py_namespace.call_method1(py, method_name, (request_arg,))?;
let value: Resp = result.extract(py).map_err(Into::into)?; let value: Resp = result.extract(py)?;
Ok::<_, PyErr>(value) Ok::<_, PyErr>(value)
}) })
}) })
+5 -5
View File
@@ -25,12 +25,12 @@ use pyo3_async_runtimes::tokio::future_into_py;
fn table_from_py<'a>(table: Bound<'a, PyAny>) -> PyResult<Bound<'a, Table>> { fn table_from_py<'a>(table: Bound<'a, PyAny>) -> PyResult<Bound<'a, Table>> {
if table.hasattr("_inner")? { if table.hasattr("_inner")? {
Ok(table.getattr("_inner")?.cast_into::<Table>()?) Ok(table.getattr("_inner")?.downcast_into::<Table>()?)
} else if table.hasattr("_table")? { } else if table.hasattr("_table")? {
Ok(table Ok(table
.getattr("_table")? .getattr("_table")?
.getattr("_inner")? .getattr("_inner")?
.cast_into::<Table>()?) .downcast_into::<Table>()?)
} else { } else {
Err(PyRuntimeError::new_err( Err(PyRuntimeError::new_err(
"Provided table does not appear to be a Table or RemoteTable instance", "Provided table does not appear to be a Table or RemoteTable instance",
@@ -90,9 +90,9 @@ impl PyAsyncPermutationBuilder {
database database
.getattr("_conn")? .getattr("_conn")?
.getattr("_inner")? .getattr("_inner")?
.cast_into::<Connection>()? .downcast_into::<Connection>()?
} else { } else {
database.getattr("_inner")?.cast_into::<Connection>()? database.getattr("_inner")?.downcast_into::<Connection>()?
}; };
let database = conn.borrow().database()?; let database = conn.borrow().database()?;
slf.modify(|builder| builder.persist(database, table_name)) slf.modify(|builder| builder.persist(database, table_name))
@@ -243,7 +243,7 @@ impl PyPermutationReader {
let Some(selection) = selection else { let Some(selection) = selection else {
return Ok(Select::All); return Ok(Select::All);
}; };
let selection = selection.cast_into::<PyDict>()?; let selection = selection.downcast_into::<PyDict>()?;
let selection = selection let selection = selection
.iter() .iter()
.map(|(key, value)| { .map(|(key, value)| {
+7 -10
View File
@@ -33,7 +33,7 @@ use pyo3::pyfunction;
use pyo3::pymethods; use pyo3::pymethods;
use pyo3::types::PyList; use pyo3::types::PyList;
use pyo3::types::{PyDict, PyString}; use pyo3::types::{PyDict, PyString};
use pyo3::{Borrowed, FromPyObject, exceptions::PyRuntimeError}; use pyo3::{FromPyObject, exceptions::PyRuntimeError};
use pyo3::{PyErr, pyclass}; use pyo3::{PyErr, pyclass};
use pyo3::{exceptions::PyValueError, intern}; use pyo3::{exceptions::PyValueError, intern};
use pyo3_async_runtimes::tokio::future_into_py; use pyo3_async_runtimes::tokio::future_into_py;
@@ -43,12 +43,9 @@ use crate::util::parse_distance_type;
use crate::{arrow::RecordBatchStream, util::PyLanceDB}; use crate::{arrow::RecordBatchStream, util::PyLanceDB};
use crate::{error::PythonErrorExt, index::class_name}; use crate::{error::PythonErrorExt, index::class_name};
impl<'a, 'py> FromPyObject<'a, 'py> for PyLanceDB<FtsQuery> { impl FromPyObject<'_> for PyLanceDB<FtsQuery> {
type Error = PyErr; fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<Self> {
match class_name(ob)?.as_str() {
fn extract(ob: Borrowed<'a, 'py, PyAny>) -> PyResult<Self> {
let ob = ob.to_owned();
match class_name(&ob)?.as_str() {
"MatchQuery" => { "MatchQuery" => {
let query = ob.getattr("query")?.extract()?; let query = ob.getattr("query")?.extract()?;
let column = ob.getattr("column")?.extract()?; let column = ob.getattr("column")?.extract()?;
@@ -427,7 +424,7 @@ impl Query {
"Query text is required for nearest_to_text", "Query text is required for nearest_to_text",
))?; ))?;
let query = if let Ok(query_text) = fts_query.cast::<PyString>() { let query = if let Ok(query_text) = fts_query.downcast::<PyString>() {
let mut query_text = query_text.to_string(); let mut query_text = query_text.to_string();
let columns = query let columns = query
.get_item("columns")? .get_item("columns")?
@@ -609,7 +606,7 @@ impl TakeQuery {
} }
} }
#[pyclass(from_py_object)] #[pyclass]
#[derive(Clone)] #[derive(Clone)]
pub struct FTSQuery { pub struct FTSQuery {
inner: LanceDbQuery, inner: LanceDbQuery,
@@ -738,7 +735,7 @@ impl FTSQuery {
} }
} }
#[pyclass(from_py_object)] #[pyclass]
#[derive(Clone)] #[derive(Clone)]
pub struct VectorQuery { pub struct VectorQuery {
inner: LanceDbVectorQuery, inner: LanceDbVectorQuery,
+1 -1
View File
@@ -11,7 +11,7 @@ use pyo3::{PyResult, pyclass, pymethods};
/// Sessions allow you to configure cache sizes for index and metadata caches, /// Sessions allow you to configure cache sizes for index and metadata caches,
/// which can significantly impact memory use and performance. They can /// which can significantly impact memory use and performance. They can
/// also be re-used across multiple connections to share the same cache state. /// also be re-used across multiple connections to share the same cache state.
#[pyclass(from_py_object)] #[pyclass]
#[derive(Clone)] #[derive(Clone)]
pub struct Session { pub struct Session {
pub(crate) inner: Arc<LanceSession>, pub(crate) inner: Arc<LanceSession>,
+10 -10
View File
@@ -29,7 +29,7 @@ use pyo3_async_runtimes::tokio::future_into_py;
mod scannable; mod scannable;
/// Statistics about a compaction operation. /// Statistics about a compaction operation.
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CompactionStats { pub struct CompactionStats {
/// The number of fragments removed /// The number of fragments removed
@@ -43,7 +43,7 @@ pub struct CompactionStats {
} }
/// Statistics about a cleanup operation /// Statistics about a cleanup operation
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct RemovalStats { pub struct RemovalStats {
/// The number of bytes removed /// The number of bytes removed
@@ -53,7 +53,7 @@ pub struct RemovalStats {
} }
/// Statistics about an optimize operation /// Statistics about an optimize operation
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct OptimizeStats { pub struct OptimizeStats {
/// Statistics about the compaction operation /// Statistics about the compaction operation
@@ -62,7 +62,7 @@ pub struct OptimizeStats {
pub prune: RemovalStats, pub prune: RemovalStats,
} }
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct UpdateResult { pub struct UpdateResult {
pub rows_updated: u64, pub rows_updated: u64,
@@ -88,7 +88,7 @@ impl From<lancedb::table::UpdateResult> for UpdateResult {
} }
} }
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AddResult { pub struct AddResult {
pub version: u64, pub version: u64,
@@ -109,7 +109,7 @@ impl From<lancedb::table::AddResult> for AddResult {
} }
} }
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DeleteResult { pub struct DeleteResult {
pub num_deleted_rows: u64, pub num_deleted_rows: u64,
@@ -135,7 +135,7 @@ impl From<lancedb::table::DeleteResult> for DeleteResult {
} }
} }
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MergeResult { pub struct MergeResult {
pub version: u64, pub version: u64,
@@ -171,7 +171,7 @@ impl From<lancedb::table::MergeResult> for MergeResult {
} }
} }
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AddColumnsResult { pub struct AddColumnsResult {
pub version: u64, pub version: u64,
@@ -192,7 +192,7 @@ impl From<lancedb::table::AddColumnsResult> for AddColumnsResult {
} }
} }
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AlterColumnsResult { pub struct AlterColumnsResult {
pub version: u64, pub version: u64,
@@ -213,7 +213,7 @@ impl From<lancedb::table::AlterColumnsResult> for AlterColumnsResult {
} }
} }
#[pyclass(get_all, from_py_object)] #[pyclass(get_all)]
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct DropColumnsResult { pub struct DropColumnsResult {
pub version: u64, pub version: u64,
+2 -5
View File
@@ -126,11 +126,8 @@ impl Scannable for PyScannable {
} }
} }
impl<'a, 'py> FromPyObject<'a, 'py> for PyScannable { impl<'py> FromPyObject<'py> for PyScannable {
type Error = pyo3::PyErr; fn extract_bound(ob: &pyo3::Bound<'py, PyAny>) -> pyo3::PyResult<Self> {
fn extract(ob: pyo3::Borrowed<'a, 'py, PyAny>) -> pyo3::PyResult<Self> {
let ob = ob.to_owned();
// Convert from Scannable dataclass. // Convert from Scannable dataclass.
let schema: PyArrowType<Schema> = ob.getattr("schema")?.extract()?; let schema: PyArrowType<Schema> = ob.getattr("schema")?.extract()?;
let schema = Arc::new(schema.0); let schema = Arc::new(schema.0);
+40
View File
@@ -1996,6 +1996,7 @@ tests = [
{ name = "pytest-mock" }, { name = "pytest-mock" },
{ name = "pytz" }, { name = "pytz" },
{ name = "requests" }, { name = "requests" },
{ name = "tantivy" },
] ]
[package.metadata] [package.metadata]
@@ -2049,6 +2050,7 @@ requires-dist = [
{ name = "sentence-transformers", marker = "extra == 'embeddings'", specifier = ">=2.2.0" }, { name = "sentence-transformers", marker = "extra == 'embeddings'", specifier = ">=2.2.0" },
{ name = "sentencepiece", marker = "extra == 'embeddings'", specifier = ">=0.1.99" }, { name = "sentencepiece", marker = "extra == 'embeddings'", specifier = ">=0.1.99" },
{ name = "sentencepiece", marker = "extra == 'siglip'" }, { name = "sentencepiece", marker = "extra == 'siglip'" },
{ name = "tantivy", marker = "extra == 'tests'", specifier = ">=0.20.0" },
{ name = "torch", marker = "extra == 'clip'" }, { name = "torch", marker = "extra == 'clip'" },
{ name = "torch", marker = "extra == 'embeddings'", specifier = ">=2.0.0" }, { name = "torch", marker = "extra == 'embeddings'", specifier = ">=2.0.0" },
{ name = "torch", marker = "extra == 'siglip'" }, { name = "torch", marker = "extra == 'siglip'" },
@@ -4777,6 +4779,44 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
] ]
[[package]]
name = "tantivy"
version = "0.25.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/f9/0cd3955d155d3e3ef74b864769514dd191e5dacba9f0beb7af2d914942ce/tantivy-0.25.1.tar.gz", hash = "sha256:68a3314699a7d18fcf338b52bae8ce46a97dde1128a3e47e33fa4db7f71f265e", size = 75120, upload-time = "2025-12-02T11:57:12.997Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/f7/2276bed3bed983ce2970dc70e3571f372587fe4f5f2bac1d6d617df08fa3/tantivy-0.25.1-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7aa587a3dc9470584cacf5e3640fee93d12ec5f10109669c1f47c4e90820b958", size = 7638510, upload-time = "2025-12-02T11:56:08.754Z" },
{ url = "https://files.pythonhosted.org/packages/20/8c/078dc50570e243414356b05633f52fe544b85179281ffa9f1fe05d76bbd8/tantivy-0.25.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:56d77fe667595693d9fa5f0b4545776d84da9526bab0273b3fc6c7536dc0d8a2", size = 3932659, upload-time = "2025-12-02T11:56:10.621Z" },
{ url = "https://files.pythonhosted.org/packages/bd/dc/281c48436a1e3178b58fe463af314434fe0f3a4ec0c7588a362900e0c69e/tantivy-0.25.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ba8c347cd48595fcaeabb28a909ebce92cf9c5e5c84ab5ba1136a280a307b5c", size = 4197430, upload-time = "2025-12-02T11:56:12.65Z" },
{ url = "https://files.pythonhosted.org/packages/7b/6c/61e6e0b0a350007d10a9b66a35703361d3345e14e7a7cc83494776b2a054/tantivy-0.25.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa7c4932e8fde1f09f2d46226060e827e197c2749abdc6129d73a752773adc38", size = 4184055, upload-time = "2025-12-02T11:56:14.647Z" },
{ url = "https://files.pythonhosted.org/packages/5f/fd/0eb059b12f0b6f91623a54a46448a83b7f716d08f3bca68c095d697b85da/tantivy-0.25.1-cp310-cp310-win_amd64.whl", hash = "sha256:afcfc5dbb0bcd5d24531f4471737ae0896f33528426ab0b1dad3e427c19120f6", size = 3424134, upload-time = "2025-12-02T11:56:16.242Z" },
{ url = "https://files.pythonhosted.org/packages/4e/7a/8a277f377e8a151fc0e71d4ffc1114aefb6e5e1c7dd609fed0955cf34ed8/tantivy-0.25.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d363d7b4207d3a5aa7f0d212420df35bed18bdb6bae26a2a8bd57428388b7c29", size = 7637033, upload-time = "2025-12-02T11:56:18.104Z" },
{ url = "https://files.pythonhosted.org/packages/71/31/8b4acdedfc9f9a2d04b1340d07eef5213d6f151d1e18da0cb423e5f090d2/tantivy-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8f4389cf1d889a1df7c5a3195806b4b56c37cee10d8a26faaa0dea35a867b5ff", size = 3932180, upload-time = "2025-12-02T11:56:19.833Z" },
{ url = "https://files.pythonhosted.org/packages/2f/dc/3e8499c21b4b9795e8f2fc54c68ce5b92905aaeadadaa56ecfa9180b11b1/tantivy-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99864c09fc54652c3c2486cdf13f86cdc8200f4b481569cb291e095ca5d496e5", size = 4197620, upload-time = "2025-12-02T11:56:21.496Z" },
{ url = "https://files.pythonhosted.org/packages/f8/8e/f2ce62fffc811eb62bead92c7b23c2e218f817cbd54c4f3b802e03ba1438/tantivy-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05abf37ddbc5063c575548be0d62931629c086bff7a5a1b67cf5a8f5ebf4cd8c", size = 4183794, upload-time = "2025-12-02T11:56:23.215Z" },
{ url = "https://files.pythonhosted.org/packages/de/64/24e2891b0ba3fd9853e10c296095a33b89bf3efd65e29da1ee5dae736040/tantivy-0.25.1-cp311-cp311-win_amd64.whl", hash = "sha256:f307ee8ad21597b0be23af83008fd66cfd5f958cdfa24ec0aaa08a38e86bbef4", size = 3424235, upload-time = "2025-12-02T11:56:25.172Z" },
{ url = "https://files.pythonhosted.org/packages/41/e7/6849c713ed0996c7628324c60512c4882006f0a62145e56c624a93407f90/tantivy-0.25.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:90fd919e5f611809f746560ecf36eb9be824dec62e21ae17a27243759edb9aa1", size = 7621494, upload-time = "2025-12-02T11:56:27.069Z" },
{ url = "https://files.pythonhosted.org/packages/c5/22/c3d8294600dc6e7fa350daef9ff337d3c06e132b81df727de9f7a50c692a/tantivy-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4613c7cf6c23f3a97989819690a0f956d799354957de7a204abcc60083cebe02", size = 3925219, upload-time = "2025-12-02T11:56:29.403Z" },
{ url = "https://files.pythonhosted.org/packages/41/fc/cbb1df71dd44c9110eff4eaaeda9d44f2d06182fe0452193be20ddfba93f/tantivy-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c477bd20b4df804d57dfc5033431bef27cde605695ae141b03abbf6ebc069129", size = 4198699, upload-time = "2025-12-02T11:56:31.359Z" },
{ url = "https://files.pythonhosted.org/packages/47/4d/71abb78b774073c3ce12a4faa4351a9d910a71ffa3659526affba163873d/tantivy-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9b1a1ba1113c523c7ff7b10f282d6c4074006f7ef8d71e1d973d51bf7291ddb", size = 4183585, upload-time = "2025-12-02T11:56:33.317Z" },
{ url = "https://files.pythonhosted.org/packages/be/16/3f00cd7ec458b92a0e977960af9ddfbeb762127d9acc68da9094a1fda556/tantivy-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:9de0bafd3bd7ac9f8f82d53e17562e9db11a5af308fe5185c4bd86feaddbe4a6", size = 3424622, upload-time = "2025-12-02T11:56:34.788Z" },
{ url = "https://files.pythonhosted.org/packages/3d/25/73cfbcf1a8ea49be6c42817431cac46b70a119fe64da903fcc2d92b5b511/tantivy-0.25.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f51ff7196c6f31719202080ed8372d5e3d51e92c749c032fb8234f012e99744c", size = 7622530, upload-time = "2025-12-02T11:56:36.839Z" },
{ url = "https://files.pythonhosted.org/packages/12/c8/c0d7591cdf4f7e7a9fc4da786d1ca8cd1aacffaa2be16ea6d401a8e4a566/tantivy-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:550e63321bfcacc003859f2fa29c1e8e56450807b3c9a501c1add27cfb9236d9", size = 3925637, upload-time = "2025-12-02T11:56:38.425Z" },
{ url = "https://files.pythonhosted.org/packages/3a/09/bedfc223bffec7641b417dd7ab071134b2ef8f8550e9b1fb6014657ef52e/tantivy-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde31cc8d6e122faf7902aeea32bc008a429a6e8904e34d3468126a3ec01b016", size = 4197322, upload-time = "2025-12-02T11:56:40.411Z" },
{ url = "https://files.pythonhosted.org/packages/f5/f1/1fa5183500c8042200c9f2b840d34f5bbcfb434a1ee750e7132262d2a5c9/tantivy-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b11bd5a518b0be645320b47af8493f6a40c4f3234313e37adcf4534a564d27dd", size = 4183143, upload-time = "2025-12-02T11:56:42.048Z" },
{ url = "https://files.pythonhosted.org/packages/d5/74/a4c4f4eb95888ccb784da3b017aa0625ab1ac411bf5d022a9a797d9a2334/tantivy-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:cc7fe88853e06b3251ee4fa42b7a2038727f850c8765bcc8167cfc73585dd24e", size = 3423491, upload-time = "2025-12-02T11:56:43.858Z" },
{ url = "https://files.pythonhosted.org/packages/8b/2f/581519492226f97d23bd0adc95dad991ebeaa73ea6abc8bff389a3096d9a/tantivy-0.25.1-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dae99e75b7eaa9bf5bd16ab106b416370f08c135aed0e117d62a3201cd1ffe36", size = 7610316, upload-time = "2025-12-02T11:56:45.927Z" },
{ url = "https://files.pythonhosted.org/packages/91/40/5d7bc315ab9e6a22c5572656e8ada1c836cfa96dccf533377504fbc3c9d9/tantivy-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:506e9533c5ef4d3df43bad64ffecc0aa97c76e361ea610815dc3a20a9d6b30b3", size = 3919882, upload-time = "2025-12-02T11:56:48.469Z" },
{ url = "https://files.pythonhosted.org/packages/02/b9/e0ef2f57a6a72444cb66c2ffbc310ab33ffaace275f1c4b0319d84ea3f18/tantivy-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbd4f8f264dacbcc9dee542832da2173fd53deaaea03f082d95214f8b5ed6bc", size = 4196031, upload-time = "2025-12-02T11:56:50.151Z" },
{ url = "https://files.pythonhosted.org/packages/1e/02/bf3f8cacfd08642e14a73f7956a3fb95d58119132c98c121b9065a1f8615/tantivy-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:824c643ccb640dd9e35e00c5d5054ddf3323f56fe4219d57d428a9eeea13d22c", size = 4183437, upload-time = "2025-12-02T11:56:51.818Z" },
{ url = "https://files.pythonhosted.org/packages/9c/83/afa90e570198e2d1139dd567bec3c9cf44d8c54f63a649f16d711ede02f5/tantivy-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:09c987b840afcebac817836ac08407eff17272d8aa60ce6e291f89c81830221d", size = 3419409, upload-time = "2025-12-02T11:56:53.451Z" },
{ url = "https://files.pythonhosted.org/packages/ff/44/9f1d67aa5030f7eebc966c863d1316a510a971dd8bb45651df4acdfae9ed/tantivy-0.25.1-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7f5d29ae85dd0f23df8d15b3e7b341d4f9eb5a446bbb9640df48ac1f6d9e0c6c", size = 7623723, upload-time = "2025-12-02T11:56:55.066Z" },
{ url = "https://files.pythonhosted.org/packages/db/30/6e085bd3ed9d12da3c91c185854abd70f9dfd35fb36a75ea98428d42c30b/tantivy-0.25.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f2d2938fb69a74fc1bb36edfaf7f0d1596fa1264db0f377bda2195c58bcb6245", size = 3926243, upload-time = "2025-12-02T11:56:57.058Z" },
{ url = "https://files.pythonhosted.org/packages/32/f5/a00d65433430f51718e5cc6938df571765d7c4e03aedec5aef4ab567aa9b/tantivy-0.25.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f5ff124c4802558e627091e780b362ca944169736caba5a372eef39a79d0ae0", size = 4207186, upload-time = "2025-12-02T11:56:58.803Z" },
{ url = "https://files.pythonhosted.org/packages/19/63/61bdb12fc95f2a7f77bd419a5149bfa9f28caa76cb569bf2b6b06e1d033e/tantivy-0.25.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b80ef62a340416139c93d19264e5f808da48e04f9305f1092b8ed22be0a5be", size = 4187312, upload-time = "2025-12-02T11:57:00.595Z" },
{ url = "https://files.pythonhosted.org/packages/b7/de/e39c0b01d59019bf5c38face8b81defbc4a68cebf5e0c53bcb2cd715a449/tantivy-0.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:286b654f40c70c1e6b64b9bc7031ed0bf5c440f5bffeaeeee21a0ee6cc39f0e2", size = 3436535, upload-time = "2025-12-02T11:57:02.267Z" },
]
[[package]] [[package]]
name = "threadpoolctl" name = "threadpoolctl"
version = "3.6.0" version = "3.6.0"
+2 -7
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "lancedb" name = "lancedb"
version = "0.28.0-beta.10" version = "0.28.0-beta.5"
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
@@ -111,12 +111,7 @@ default = []
aws = ["lance/aws", "lance-io/aws", "lance-namespace-impls/dir-aws"] aws = ["lance/aws", "lance-io/aws", "lance-namespace-impls/dir-aws"]
oss = ["lance/oss", "lance-io/oss", "lance-namespace-impls/dir-oss"] oss = ["lance/oss", "lance-io/oss", "lance-namespace-impls/dir-oss"]
gcs = ["lance/gcp", "lance-io/gcp", "lance-namespace-impls/dir-gcp"] gcs = ["lance/gcp", "lance-io/gcp", "lance-namespace-impls/dir-gcp"]
azure = [ azure = ["lance/azure", "lance-io/azure", "lance-namespace-impls/dir-azure"]
"lance/azure",
"lance-io/azure",
"lance-namespace-impls/dir-azure",
"lance-namespace-impls/credential-vendor-azure",
]
huggingface = [ huggingface = [
"lance/huggingface", "lance/huggingface",
"lance-io/huggingface", "lance-io/huggingface",
+6 -181
View File
@@ -590,15 +590,6 @@ pub struct ConnectRequest {
/// storage options. /// storage options.
pub namespace_client_properties: HashMap<String, String>, pub namespace_client_properties: HashMap<String, String>,
/// Use directory namespace manifests as the source of truth for native
/// LanceDB table metadata.
///
/// When enabled for a local/native connection, LanceDB returns a
/// namespace-backed database directly. Directory listing fallback remains
/// enabled for migration, and directory-listing-to-manifest migration is
/// forced on.
pub manifest_enabled: bool,
/// The interval at which to check for updates from other processes. /// The interval at which to check for updates from other processes.
/// ///
/// If None, then consistency is not checked. For performance /// If None, then consistency is not checked. For performance
@@ -639,7 +630,6 @@ impl ConnectBuilder {
read_consistency_interval: None, read_consistency_interval: None,
options: HashMap::new(), options: HashMap::new(),
namespace_client_properties: HashMap::new(), namespace_client_properties: HashMap::new(),
manifest_enabled: false,
session: None, session: None,
}, },
embedding_registry: None, embedding_registry: None,
@@ -801,17 +791,6 @@ impl ConnectBuilder {
self self
} }
/// Enable or disable manifest-backed directory namespace mode for local
/// native connections.
///
/// When enabled, the connection uses the directory namespace database
/// directly for all table operations and forces
/// `dir_listing_to_manifest_migration_enabled=true`.
pub fn manifest_enabled(mut self, enabled: bool) -> Self {
self.request.manifest_enabled = enabled;
self
}
/// The interval at which to check for updates from other processes. This /// The interval at which to check for updates from other processes. This
/// only affects LanceDB OSS. /// only affects LanceDB OSS.
/// ///
@@ -907,16 +886,6 @@ impl ConnectBuilder {
pub async fn execute(self) -> Result<Connection> { pub async fn execute(self) -> Result<Connection> {
if self.request.uri.starts_with("db") { if self.request.uri.starts_with("db") {
self.execute_remote() self.execute_remote()
} else if self.request.manifest_enabled {
let internal = Arc::new(
ListingDatabase::connect_manifest_enabled_namespace_database(&self.request).await?,
);
Ok(Connection {
internal,
embedding_registry: self
.embedding_registry
.unwrap_or_else(|| Arc::new(MemoryRegistry::new())),
})
} else { } else {
let internal = Arc::new(ListingDatabase::connect_with_options(&self.request).await?); let internal = Arc::new(ListingDatabase::connect_with_options(&self.request).await?);
Ok(Connection { Ok(Connection {
@@ -946,7 +915,7 @@ use std::collections::HashSet;
/// These operations will be executed on the namespace server instead of locally /// These operations will be executed on the namespace server instead of locally
/// when enabled via [`ConnectNamespaceBuilder::pushdown_operations`]. /// when enabled via [`ConnectNamespaceBuilder::pushdown_operations`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NamespaceClientPushdownOperation { pub enum PushdownOperation {
/// Execute queries on the namespace server via `query_table()` instead of locally. /// Execute queries on the namespace server via `query_table()` instead of locally.
QueryTable, QueryTable,
/// Execute table creation on the namespace server via `create_table()` /// Execute table creation on the namespace server via `create_table()`
@@ -962,7 +931,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>>,
pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pushdown_operations: HashSet<PushdownOperation>,
} }
impl ConnectNamespaceBuilder { impl ConnectNamespaceBuilder {
@@ -1060,11 +1029,11 @@ impl ConnectNamespaceBuilder {
/// and leveraging server-side compute resources. /// and leveraging server-side compute resources.
/// ///
/// Available operations: /// Available operations:
/// - [`NamespaceClientPushdownOperation::QueryTable`]: Execute queries via `namespace.query_table()` /// - [`PushdownOperation::QueryTable`]: Execute queries via `namespace.query_table()`
/// - [`NamespaceClientPushdownOperation::CreateTable`]: Execute table creation via `namespace.create_table()` /// - [`PushdownOperation::CreateTable`]: Execute table creation via `namespace.create_table()`
/// ///
/// By default, no operations are pushed down (all executed locally). /// By default, no operations are pushed down (all executed locally).
pub fn pushdown_operation(mut self, operation: NamespaceClientPushdownOperation) -> Self { pub fn pushdown_operation(mut self, operation: PushdownOperation) -> Self {
self.pushdown_operations.insert(operation); self.pushdown_operations.insert(operation);
self self
} }
@@ -1074,7 +1043,7 @@ impl ConnectNamespaceBuilder {
/// See [`Self::pushdown_operation`] for details. /// See [`Self::pushdown_operation`] for details.
pub fn pushdown_operations( pub fn pushdown_operations(
mut self, mut self,
operations: impl IntoIterator<Item = NamespaceClientPushdownOperation>, operations: impl IntoIterator<Item = PushdownOperation>,
) -> Self { ) -> Self {
self.pushdown_operations.extend(operations); self.pushdown_operations.extend(operations);
self self
@@ -1163,9 +1132,6 @@ mod tests {
use lance_testing::datagen::{BatchGenerator, IncrementingInt32}; use lance_testing::datagen::{BatchGenerator, IncrementingInt32};
use tempfile::tempdir; use tempfile::tempdir;
use crate::database::listing::{ListingDatabaseOptions, OPT_NEW_TABLE_V2_MANIFEST_PATHS};
use crate::database::namespace::LanceNamespaceDatabase;
use crate::table::NativeTable;
use crate::test_utils::connection::new_test_connection; use crate::test_utils::connection::new_test_connection;
use super::*; use super::*;
@@ -1238,147 +1204,6 @@ mod tests {
); );
} }
#[tokio::test]
async fn test_connect_with_manifest_enabled_uses_directory_namespace() {
let tmp_dir = tempdir().unwrap();
let uri = tmp_dir.path().to_str().unwrap();
let db = connect(uri)
.manifest_enabled(true)
.storage_option("timeout", "30s")
.namespace_client_property("manifest_enabled", "false")
.namespace_client_property("dir_listing_to_manifest_migration_enabled", "false")
.execute()
.await
.unwrap();
assert!(
db.database()
.as_any()
.downcast_ref::<LanceNamespaceDatabase>()
.is_some()
);
assert_eq!(db.uri(), uri);
let (ns_impl, properties) = db.namespace_client_config().await.unwrap();
assert_eq!(ns_impl, "dir");
assert_eq!(properties.get("root"), Some(&uri.to_string()));
assert_eq!(
properties.get("manifest_enabled"),
Some(&"true".to_string())
);
assert_eq!(
properties.get("dir_listing_to_manifest_migration_enabled"),
Some(&"true".to_string())
);
assert_eq!(properties.get("storage.timeout"), Some(&"30s".to_string()));
}
#[tokio::test]
async fn test_manifest_enabled_rejects_commit_engine_uri() {
let Err(err) = connect("s3+ddb://bucket/db?ddbTableName=manifest")
.manifest_enabled(true)
.execute()
.await
else {
panic!("expected manifest-enabled s3+ddb connection to fail");
};
assert!(
matches!(err, Error::NotSupported { message } if message.contains("commit engine URI schemes"))
);
let Err(err) = connect("s3://bucket/db?engine=ddb&ddbTableName=manifest")
.manifest_enabled(true)
.execute()
.await
else {
panic!("expected manifest-enabled engine query connection to fail");
};
assert!(
matches!(err, Error::NotSupported { message } if message.contains("commit engine"))
);
}
#[tokio::test]
async fn test_manifest_enabled_connection_migrates_root_listing_table() {
let tmp_dir = tempdir().unwrap();
let uri = tmp_dir.path().to_str().unwrap();
let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int32, false)]));
connect(uri)
.execute()
.await
.unwrap()
.create_empty_table("legacy", schema)
.execute()
.await
.unwrap();
let db = connect(uri).manifest_enabled(true).execute().await.unwrap();
let tables = db.table_names().execute().await.unwrap();
assert_eq!(tables, vec!["legacy".to_string()]);
db.open_table("legacy").execute().await.unwrap();
}
#[tokio::test]
async fn test_manifest_enabled_preserves_new_table_options() {
let tmp_dir = tempdir().unwrap();
let uri = tmp_dir.path().to_str().unwrap();
let options = ListingDatabaseOptions::builder()
.enable_v2_manifest_paths(true)
.build();
let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int32, false)]));
let table = connect(uri)
.manifest_enabled(true)
.database_options(&options)
.execute()
.await
.unwrap()
.create_empty_table("v1_manifest", schema)
.storage_option(OPT_NEW_TABLE_V2_MANIFEST_PATHS, "false")
.execute()
.await
.unwrap();
let native_table = table
.base_table()
.as_any()
.downcast_ref::<NativeTable>()
.unwrap();
assert!(!native_table.uses_v2_manifest_paths().await.unwrap());
}
#[tokio::test]
async fn test_manifest_enabled_vend_input_storage_options() {
let tmp_dir = tempdir().unwrap();
let uri = tmp_dir.path().to_str().unwrap();
let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int32, false)]));
let table = connect(uri)
.manifest_enabled(true)
.storage_option("test_storage_option", "test_value")
.namespace_client_property("vend_input_storage_options", "true")
.namespace_client_property(
"vend_input_storage_options_refresh_interval_millis",
"60000",
)
.execute()
.await
.unwrap()
.create_empty_table("vended", schema)
.execute()
.await
.unwrap();
let storage_options = table.latest_storage_options().await.unwrap().unwrap();
assert_eq!(
storage_options.get("test_storage_option"),
Some(&"test_value".to_string())
);
assert!(storage_options.contains_key("expires_at_millis"));
}
#[tokio::test] #[tokio::test]
async fn test_table_names() { async fn test_table_names() {
let tc = new_test_connection().await.unwrap(); let tc = new_test_connection().await.unwrap();
+9 -144
View File
@@ -285,7 +285,7 @@ const MIRRORED_STORE: &str = "mirroredStore";
/// A connection to LanceDB /// A connection to LanceDB
impl ListingDatabase { impl ListingDatabase {
pub(crate) fn build_namespace_client_properties( fn build_namespace_client_properties(
uri: &str, uri: &str,
storage_options: &HashMap<String, String>, storage_options: &HashMap<String, String>,
namespace_client_properties: HashMap<String, String>, namespace_client_properties: HashMap<String, String>,
@@ -298,24 +298,6 @@ impl ListingDatabase {
properties properties
} }
pub(crate) fn build_manifest_enabled_namespace_client_properties(
uri: &str,
storage_options: &HashMap<String, String>,
namespace_client_properties: HashMap<String, String>,
) -> HashMap<String, String> {
let mut properties = Self::build_namespace_client_properties(
uri,
storage_options,
namespace_client_properties,
);
properties.insert("manifest_enabled".to_string(), "true".to_string());
properties.insert(
"dir_listing_to_manifest_migration_enabled".to_string(),
"true".to_string(),
);
properties
}
async fn connect_namespace_database( async fn connect_namespace_database(
uri: &str, uri: &str,
storage_options: HashMap<String, String>, storage_options: HashMap<String, String>,
@@ -341,119 +323,6 @@ impl ListingDatabase {
)) ))
} }
async fn prepare_namespace_root(
uri: &str,
storage_options: &HashMap<String, String>,
session: Arc<lance::session::Session>,
) -> Result<String> {
match url::Url::parse(uri) {
Ok(url) if url.scheme().len() == 1 && cfg!(windows) => {
let (object_store, _) = ObjectStore::from_uri_and_params(
session.store_registry(),
uri,
&ObjectStoreParams::default(),
)
.await?;
if object_store.is_local() {
Self::try_create_dir(uri).context(CreateDirSnafu { path: uri })?;
}
Ok(uri.to_string())
}
Ok(mut url) => {
if url.scheme().contains('+') {
return Err(Error::NotSupported {
message: "commit engine URI schemes are not supported for manifest-enabled namespace connections".to_string(),
});
}
for (key, value) in url.query_pairs() {
if key == ENGINE {
return Err(Error::NotSupported {
message: format!(
"commit engine '{}' is not supported for manifest-enabled namespace connections",
value
),
});
} else if key == MIRRORED_STORE {
return Err(Error::NotSupported {
message: "mirrored store is not supported for manifest-enabled namespace connections"
.to_string(),
});
}
}
url.set_query(None);
let plain_uri = url.to_string();
let os_params = ObjectStoreParams {
storage_options_accessor: if storage_options.is_empty() {
None
} else {
Some(Arc::new(StorageOptionsAccessor::with_static_options(
storage_options.clone(),
)))
},
..Default::default()
};
let (object_store, _) = ObjectStore::from_uri_and_params(
session.store_registry(),
&plain_uri,
&os_params,
)
.await?;
if object_store.is_local() {
Self::try_create_dir(&plain_uri).context(CreateDirSnafu {
path: plain_uri.clone(),
})?;
}
Ok(plain_uri)
}
Err(_) => {
let (object_store, _) = ObjectStore::from_uri_and_params(
session.store_registry(),
uri,
&ObjectStoreParams::default(),
)
.await?;
if object_store.is_local() {
Self::try_create_dir(uri).context(CreateDirSnafu { path: uri })?;
}
Ok(uri.to_string())
}
}
}
pub(crate) async fn connect_manifest_enabled_namespace_database(
request: &ConnectRequest,
) -> Result<LanceNamespaceDatabase> {
let options = ListingDatabaseOptions::parse_from_map(&request.options)?;
let session = request
.session
.clone()
.unwrap_or_else(|| Arc::new(lance::session::Session::default()));
let namespace_root =
Self::prepare_namespace_root(&request.uri, &options.storage_options, session.clone())
.await?;
let ns_properties = Self::build_manifest_enabled_namespace_client_properties(
&namespace_root,
&options.storage_options,
request.namespace_client_properties.clone(),
);
LanceNamespaceDatabase::connect_with_new_table_config(
"dir",
ns_properties,
options.storage_options,
request.read_consistency_interval,
Some(session),
HashSet::new(),
options.new_table_config,
)
.await
.map(|db| db.with_uri(request.uri.clone()))
}
/// Connect to a listing database /// Connect to a listing database
/// ///
/// The URI should be a path to a directory where the tables are stored. /// The URI should be a path to a directory where the tables are stored.
@@ -821,12 +690,15 @@ impl ListingDatabase {
store_params.storage_options_accessor = Some(Arc::new(accessor)); store_params.storage_options_accessor = Some(Arc::new(accessor));
} }
write_params.data_storage_version = storage_version_override write_params.data_storage_version = self
.or(write_params.data_storage_version) .new_table_config
.or(self.new_table_config.data_storage_version); .data_storage_version
.or(storage_version_override);
if let Some(enable_v2_manifest_paths) = if let Some(enable_v2_manifest_paths) = self
v2_manifest_override.or(self.new_table_config.enable_v2_manifest_paths) .new_table_config
.enable_v2_manifest_paths
.or(v2_manifest_override)
{ {
write_params.enable_v2_manifest_paths = enable_v2_manifest_paths; write_params.enable_v2_manifest_paths = enable_v2_manifest_paths;
} }
@@ -1286,7 +1158,6 @@ mod tests {
client_config: Default::default(), client_config: Default::default(),
options: Default::default(), options: Default::default(),
namespace_client_properties: Default::default(), namespace_client_properties: Default::default(),
manifest_enabled: false,
read_consistency_interval: None, read_consistency_interval: None,
session: None, session: None,
}; };
@@ -1421,7 +1292,6 @@ mod tests {
client_config: Default::default(), client_config: Default::default(),
options: options.clone(), options: options.clone(),
namespace_client_properties: Default::default(), namespace_client_properties: Default::default(),
manifest_enabled: false,
read_consistency_interval: None, read_consistency_interval: None,
session: None, session: None,
}; };
@@ -1957,7 +1827,6 @@ mod tests {
client_config: Default::default(), client_config: Default::default(),
options, options,
namespace_client_properties: Default::default(), namespace_client_properties: Default::default(),
manifest_enabled: false,
read_consistency_interval: None, read_consistency_interval: None,
session: None, session: None,
}; };
@@ -2064,7 +1933,6 @@ mod tests {
client_config: Default::default(), client_config: Default::default(),
options, options,
namespace_client_properties: Default::default(), namespace_client_properties: Default::default(),
manifest_enabled: false,
read_consistency_interval: None, read_consistency_interval: None,
session: None, session: None,
}; };
@@ -2137,7 +2005,6 @@ mod tests {
client_config: Default::default(), client_config: Default::default(),
options, options,
namespace_client_properties: Default::default(), namespace_client_properties: Default::default(),
manifest_enabled: false,
read_consistency_interval: None, read_consistency_interval: None,
session: None, session: None,
}; };
@@ -2317,7 +2184,6 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
#[cfg(not(windows))] // TODO: support Windows once directory namespace-backed listing DB tests are supported.
async fn test_listing_database_with_namespace_client_properties() { async fn test_listing_database_with_namespace_client_properties() {
let tempdir = tempdir().unwrap(); let tempdir = tempdir().unwrap();
let uri = tempdir.path().to_str().unwrap(); let uri = tempdir.path().to_str().unwrap();
@@ -2335,7 +2201,6 @@ mod tests {
client_config: Default::default(), client_config: Default::default(),
options: Default::default(), options: Default::default(),
namespace_client_properties, namespace_client_properties,
manifest_enabled: false,
read_consistency_interval: None, read_consistency_interval: None,
session: None, session: None,
}; };
+40 -270
View File
@@ -22,15 +22,10 @@ use lance_namespace_impls::ConnectBuilder;
use lance_table::io::commit::CommitHandler; use lance_table::io::commit::CommitHandler;
use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler; use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler;
use crate::connection::NamespaceClientPushdownOperation; use crate::connection::PushdownOperation;
use crate::database::ReadConsistency; use crate::database::ReadConsistency;
use crate::database::listing::{
NewTableConfig, OPT_NEW_TABLE_ENABLE_STABLE_ROW_IDS, OPT_NEW_TABLE_STORAGE_VERSION,
OPT_NEW_TABLE_V2_MANIFEST_PATHS,
};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::table::NativeTable; use crate::table::NativeTable;
use lance::dataset::WriteMode;
use super::{ use super::{
BaseTable, CloneTableRequest, CreateTableMode, CreateTableRequest as DbCreateTableRequest, BaseTable, CloneTableRequest, CreateTableMode, CreateTableRequest as DbCreateTableRequest,
@@ -49,71 +44,21 @@ pub struct LanceNamespaceDatabase {
// database URI // database URI
uri: String, uri: String,
// Operations to push down to the namespace server // Operations to push down to the namespace server
pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pushdown_operations: HashSet<PushdownOperation>,
// Namespace implementation type (e.g., "dir", "rest") // Namespace implementation type (e.g., "dir", "rest")
ns_impl: String, ns_impl: String,
// Namespace properties used to construct the namespace client // Namespace properties used to construct the namespace client
ns_properties: HashMap<String, String>, ns_properties: HashMap<String, String>,
// Options for tables created by this connection
new_table_config: NewTableConfig,
} }
impl LanceNamespaceDatabase { impl LanceNamespaceDatabase {
pub fn from_namespace_client(
namespace_client: Arc<dyn LanceNamespace>,
namespace_client_impl: String,
namespace_client_properties: HashMap<String, String>,
storage_options: HashMap<String, String>,
read_consistency_interval: Option<std::time::Duration>,
session: Option<Arc<lance::session::Session>>,
namespace_client_pushdown_operations: HashSet<NamespaceClientPushdownOperation>,
) -> Self {
Self {
namespace: namespace_client,
storage_options,
read_consistency_interval,
session,
uri: format!("namespace://{}", namespace_client_impl),
pushdown_operations: namespace_client_pushdown_operations,
ns_impl: namespace_client_impl,
ns_properties: namespace_client_properties,
new_table_config: NewTableConfig::default(),
}
}
pub(crate) fn with_uri(mut self, uri: impl Into<String>) -> Self {
self.uri = uri.into();
self
}
pub async fn connect( pub async fn connect(
ns_impl: &str, ns_impl: &str,
ns_properties: HashMap<String, String>, ns_properties: HashMap<String, String>,
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>>,
pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pushdown_operations: HashSet<PushdownOperation>,
) -> Result<Self> {
Self::connect_with_new_table_config(
ns_impl,
ns_properties,
storage_options,
read_consistency_interval,
session,
pushdown_operations,
NewTableConfig::default(),
)
.await
}
pub(crate) async fn connect_with_new_table_config(
ns_impl: &str,
ns_properties: HashMap<String, String>,
storage_options: HashMap<String, String>,
read_consistency_interval: Option<std::time::Duration>,
session: Option<Arc<lance::session::Session>>,
pushdown_operations: HashSet<NamespaceClientPushdownOperation>,
new_table_config: NewTableConfig,
) -> 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() {
@@ -135,79 +80,8 @@ impl LanceNamespaceDatabase {
pushdown_operations, pushdown_operations,
ns_impl: ns_impl.to_string(), ns_impl: ns_impl.to_string(),
ns_properties, ns_properties,
new_table_config,
}) })
} }
fn extract_storage_overrides(
&self,
request: &DbCreateTableRequest,
) -> Result<(
Option<lance_encoding::version::LanceFileVersion>,
Option<bool>,
Option<bool>,
)> {
let storage_options = request
.write_options
.lance_write_params
.as_ref()
.and_then(|p| p.store_params.as_ref())
.and_then(|sp| sp.storage_options());
let storage_version_override = storage_options
.and_then(|opts| opts.get(OPT_NEW_TABLE_STORAGE_VERSION))
.map(|s| s.parse::<lance_encoding::version::LanceFileVersion>())
.transpose()?;
let v2_manifest_override = storage_options
.and_then(|opts| opts.get(OPT_NEW_TABLE_V2_MANIFEST_PATHS))
.map(|s| s.parse::<bool>())
.transpose()
.map_err(|_| Error::InvalidInput {
message: "enable_v2_manifest_paths must be a boolean".to_string(),
})?;
let stable_row_ids_override = storage_options
.and_then(|opts| opts.get(OPT_NEW_TABLE_ENABLE_STABLE_ROW_IDS))
.map(|s| s.parse::<bool>())
.transpose()
.map_err(|_| Error::InvalidInput {
message: "enable_stable_row_ids must be a boolean".to_string(),
})?;
Ok((
storage_version_override,
v2_manifest_override,
stable_row_ids_override,
))
}
fn apply_new_table_config(
&self,
params: &mut lance::dataset::WriteParams,
request: &DbCreateTableRequest,
) -> Result<()> {
let (storage_version_override, v2_manifest_override, stable_row_ids_override) =
self.extract_storage_overrides(request)?;
params.data_storage_version = storage_version_override
.or(params.data_storage_version)
.or(self.new_table_config.data_storage_version);
if let Some(enable_v2_manifest_paths) =
v2_manifest_override.or(self.new_table_config.enable_v2_manifest_paths)
{
params.enable_v2_manifest_paths = enable_v2_manifest_paths;
}
if let Some(enable_stable_row_ids) =
stable_row_ids_override.or(self.new_table_config.enable_stable_row_ids)
{
params.enable_stable_row_ids = enable_stable_row_ids;
}
Ok(())
}
} }
impl std::fmt::Debug for LanceNamespaceDatabase { impl std::fmt::Debug for LanceNamespaceDatabase {
@@ -289,23 +163,37 @@ impl Database for LanceNamespaceDatabase {
async fn create_table(&self, request: DbCreateTableRequest) -> Result<Arc<dyn BaseTable>> { async fn create_table(&self, request: DbCreateTableRequest) -> Result<Arc<dyn BaseTable>> {
let mut table_id = request.namespace_path.clone(); let mut table_id = request.namespace_path.clone();
table_id.push(request.name.clone()); table_id.push(request.name.clone());
let mut existing_table = None; let describe_request = DescribeTableRequest {
id: Some(table_id.clone()),
..Default::default()
};
let describe_result = self.namespace.describe_table(describe_request).await;
match request.mode { match request.mode {
CreateTableMode::Create => {} CreateTableMode::Create => {
if describe_result.is_ok() {
return Err(Error::TableAlreadyExists {
name: request.name.clone(),
});
}
}
CreateTableMode::Overwrite => { CreateTableMode::Overwrite => {
let describe_request = DescribeTableRequest { if describe_result.is_ok() {
id: Some(table_id.clone()), // Drop the existing table - must succeed
..Default::default() let drop_request = DropTableRequest {
}; id: Some(table_id.clone()),
existing_table = self.namespace.describe_table(describe_request).await.ok(); ..Default::default()
};
self.namespace
.drop_table(drop_request)
.await
.map_err(|e| Error::Runtime {
message: format!("Failed to drop existing table for overwrite: {}", e),
})?;
}
} }
CreateTableMode::ExistOk(_) => { CreateTableMode::ExistOk(_) => {
let describe_request = DescribeTableRequest {
id: Some(table_id.clone()),
..Default::default()
};
let describe_result = self.namespace.describe_table(describe_request).await;
if describe_result.is_ok() { if describe_result.is_ok() {
let native_table = NativeTable::open_from_namespace( let native_table = NativeTable::open_from_namespace(
self.namespace.clone(), self.namespace.clone(),
@@ -333,86 +221,20 @@ impl Database for LanceNamespaceDatabase {
}; };
let (location, initial_storage_options, managed_versioning) = { let (location, initial_storage_options, managed_versioning) = {
if let Some(response) = existing_table { let response = self.namespace.declare_table(declare_request).await?;
let loc = response.location.ok_or_else(|| Error::Runtime { let loc = response.location.ok_or_else(|| Error::Runtime {
message: "Table location is missing from describe_table response".to_string(), message: "Table location is missing from declare_table response".to_string(),
})?; })?;
let opts = response // Use storage options from response, fall back to self.storage_options
.storage_options let opts = response
.or_else(|| Some(self.storage_options.clone())) .storage_options
.filter(|o| !o.is_empty()); .or_else(|| Some(self.storage_options.clone()))
(loc, opts, response.managed_versioning) .filter(|o| !o.is_empty());
} else { (loc, opts, response.managed_versioning)
match self.namespace.declare_table(declare_request).await {
Ok(response) => {
let loc = response.location.ok_or_else(|| Error::Runtime {
message: "Table location is missing from declare_table response"
.to_string(),
})?;
let opts = response
.storage_options
.or_else(|| Some(self.storage_options.clone()))
.filter(|o: &HashMap<String, String>| !o.is_empty());
(loc, opts, response.managed_versioning)
}
Err(e)
if matches!(request.mode, CreateTableMode::Create) && {
let err_str = e.to_string();
err_str.contains("already exists")
|| err_str.contains("TableAlreadyExists")
|| err_str.contains("table already exists")
} =>
{
let response = self
.namespace
.describe_table(DescribeTableRequest {
id: Some(table_id.clone()),
..Default::default()
})
.await
.map_err(|describe_err| Error::Runtime {
message: format!(
"Failed to describe existing declared table after declare conflict: {}",
describe_err
),
})?;
if response.version.is_some() && response.schema.is_some() {
return Err(Error::TableAlreadyExists {
name: request.name.clone(),
});
}
let loc = response.location.ok_or_else(|| Error::Runtime {
message: "Table location is missing from describe_table response"
.to_string(),
})?;
let opts = response
.storage_options
.or_else(|| Some(self.storage_options.clone()))
.filter(|o: &HashMap<String, String>| !o.is_empty());
(loc, opts, response.managed_versioning)
}
Err(e) => {
return Err(Error::Runtime {
message: format!("Failed to declare table: {}", e),
});
}
}
}
}; };
// Build write params with storage options and commit handler // Build write params with storage options and commit handler
let mut params = request let mut params = request.write_options.lance_write_params.unwrap_or_default();
.write_options
.lance_write_params
.clone()
.unwrap_or_default();
self.apply_new_table_config(&mut params, &request)?;
if matches!(request.mode, CreateTableMode::Overwrite) {
params.mode = WriteMode::Overwrite;
}
// Set up storage options if provided // Set up storage options if provided
if let Some(storage_opts) = initial_storage_options { if let Some(storage_opts) = initial_storage_options {
@@ -870,58 +692,6 @@ mod tests {
assert_eq!(id_col.value(2), 30); assert_eq!(id_col.value(2), 30);
} }
#[tokio::test]
async fn test_namespace_create_table_after_declare_conflict() {
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");
conn.create_namespace(CreateNamespaceRequest {
id: Some(vec!["test_ns".into()]),
..Default::default()
})
.await
.expect("Failed to create namespace");
let namespace_client = conn.namespace_client().await.unwrap();
namespace_client
.declare_table(DeclareTableRequest {
id: Some(vec!["test_ns".into(), "declared_test".into()]),
..Default::default()
})
.await
.expect("Failed to declare table");
let test_data = create_test_data();
let table = conn
.create_table("declared_test", test_data)
.namespace(vec!["test_ns".into()])
.execute()
.await
.expect("Failed to create table after declare conflict");
let results = table
.query()
.execute()
.await
.expect("Failed to query table")
.try_collect::<Vec<_>>()
.await
.expect("Failed to collect results");
assert_eq!(results.len(), 1);
assert_eq!(results[0].num_rows(), 5);
assert_eq!(table.namespace(), &["test_ns"]);
assert_eq!(table.id(), "test_ns$declared_test");
}
#[tokio::test] #[tokio::test]
async fn test_namespace_create_table_exist_ok_mode() { async fn test_namespace_create_table_exist_ok_mode() {
// Setup: Create a temporary directory for the namespace // Setup: Create a temporary directory for the namespace
+3 -15
View File
@@ -16,7 +16,7 @@ use crate::remote::retry::{ResolvedRetryConfig, RetryCounter};
const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id"); const REQUEST_ID_HEADER: HeaderName = HeaderName::from_static("x-request-id");
/// Configuration for TLS/mTLS settings. /// Configuration for TLS/mTLS settings.
#[derive(Clone, Debug)] #[derive(Clone, Debug, Default)]
pub struct TlsConfig { pub struct TlsConfig {
/// Path to the client certificate file (PEM format) /// Path to the client certificate file (PEM format)
pub cert_file: Option<String>, pub cert_file: Option<String>,
@@ -24,22 +24,10 @@ pub struct TlsConfig {
pub key_file: Option<String>, pub key_file: Option<String>,
/// Path to the CA certificate file for server verification (PEM format) /// Path to the CA certificate file for server verification (PEM format)
pub ssl_ca_cert: Option<String>, pub ssl_ca_cert: Option<String>,
/// Whether to verify the hostname in the server's certificate. /// Whether to verify the hostname in the server's certificate
/// Defaults to `true`.
pub assert_hostname: bool, pub assert_hostname: bool,
} }
impl Default for TlsConfig {
fn default() -> Self {
Self {
cert_file: None,
key_file: None,
ssl_ca_cert: None,
assert_hostname: true,
}
}
}
/// Trait for providing custom headers for each request /// Trait for providing custom headers for each request
#[async_trait::async_trait] #[async_trait::async_trait]
pub trait HeaderProvider: Send + Sync + std::fmt::Debug { pub trait HeaderProvider: Send + Sync + std::fmt::Debug {
@@ -938,7 +926,7 @@ mod tests {
assert!(config.cert_file.is_none()); assert!(config.cert_file.is_none());
assert!(config.key_file.is_none()); assert!(config.key_file.is_none());
assert!(config.ssl_ca_cert.is_none()); assert!(config.ssl_ca_cert.is_none());
assert!(config.assert_hostname); assert!(!config.assert_hostname);
} }
#[test] #[test]
+3 -3
View File
@@ -43,7 +43,7 @@ pub struct RemoteInsertExec<S: HttpSend = Sender> {
client: RestfulLanceDbClient<S>, client: RestfulLanceDbClient<S>,
input: Arc<dyn ExecutionPlan>, input: Arc<dyn ExecutionPlan>,
overwrite: bool, overwrite: bool,
properties: Arc<PlanProperties>, properties: PlanProperties,
add_result: Arc<Mutex<Option<AddResult>>>, add_result: Arc<Mutex<Option<AddResult>>>,
metrics: ExecutionPlanMetricsSet, metrics: ExecutionPlanMetricsSet,
upload_id: Option<String>, upload_id: Option<String>,
@@ -118,7 +118,7 @@ impl<S: HttpSend + 'static> RemoteInsertExec<S> {
client, client,
input, input,
overwrite, overwrite,
properties: Arc::new(properties), properties,
add_result: Arc::new(Mutex::new(None)), add_result: Arc::new(Mutex::new(None)),
metrics: ExecutionPlanMetricsSet::new(), metrics: ExecutionPlanMetricsSet::new(),
upload_id, upload_id,
@@ -232,7 +232,7 @@ impl<S: HttpSend + 'static> ExecutionPlan for RemoteInsertExec<S> {
self self
} }
fn properties(&self) -> &Arc<PlanProperties> { fn properties(&self) -> &PlanProperties {
&self.properties &self.properties
} }
+9 -9
View File
@@ -47,7 +47,7 @@ use std::format;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use crate::connection::NamespaceClientPushdownOperation; use crate::connection::PushdownOperation;
use crate::data::scannable::{PeekedScannable, Scannable, estimate_write_partitions}; use crate::data::scannable::{PeekedScannable, Scannable, estimate_write_partitions};
use crate::database::Database; use crate::database::Database;
@@ -1272,7 +1272,7 @@ pub struct NativeTable {
pub(crate) namespace_client: Option<Arc<dyn LanceNamespace>>, pub(crate) namespace_client: Option<Arc<dyn LanceNamespace>>,
// Operations to push down to the namespace server. // Operations to push down to the namespace server.
// pub(crate) so query.rs can access the field for server-side query execution. // pub(crate) so query.rs can access the field for server-side query execution.
pub(crate) pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pub(crate) pushdown_operations: HashSet<PushdownOperation>,
} }
impl std::fmt::Debug for NativeTable { impl std::fmt::Debug for NativeTable {
@@ -1359,7 +1359,7 @@ impl NativeTable {
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>>, namespace_client: Option<Arc<dyn LanceNamespace>>,
pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pushdown_operations: HashSet<PushdownOperation>,
managed_versioning: Option<bool>, managed_versioning: Option<bool>,
) -> Result<Self> { ) -> Result<Self> {
let params = params.unwrap_or_default(); let params = params.unwrap_or_default();
@@ -1470,7 +1470,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>,
pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pushdown_operations: HashSet<PushdownOperation>,
session: Option<Arc<lance::session::Session>>, session: Option<Arc<lance::session::Session>>,
) -> Result<Self> { ) -> Result<Self> {
let mut params = params.unwrap_or_default(); let mut params = params.unwrap_or_default();
@@ -1518,7 +1518,7 @@ impl NativeTable {
let id = Self::build_id(&namespace, name); let id = Self::build_id(&namespace, name);
let stored_namespace_client = let stored_namespace_client =
if pushdown_operations.contains(&NamespaceClientPushdownOperation::QueryTable) { if pushdown_operations.contains(&PushdownOperation::QueryTable) {
Some(namespace_client) Some(namespace_client)
} else { } else {
None None
@@ -1588,7 +1588,7 @@ impl NativeTable {
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>>, namespace_client: Option<Arc<dyn LanceNamespace>>,
pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pushdown_operations: HashSet<PushdownOperation>,
) -> 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 {
@@ -1635,7 +1635,7 @@ impl NativeTable {
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>>, namespace_client: Option<Arc<dyn LanceNamespace>>,
pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pushdown_operations: HashSet<PushdownOperation>,
) -> Result<Self> { ) -> Result<Self> {
let data: Box<dyn Scannable> = Box::new(RecordBatch::new_empty(schema)); let data: Box<dyn Scannable> = Box::new(RecordBatch::new_empty(schema));
Self::create( Self::create(
@@ -1685,7 +1685,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>,
pushdown_operations: HashSet<NamespaceClientPushdownOperation>, pushdown_operations: HashSet<PushdownOperation>,
session: Option<Arc<lance::session::Session>>, session: Option<Arc<lance::session::Session>>,
) -> Result<Self> { ) -> Result<Self> {
// Build table_id from namespace + name for the storage options provider // Build table_id from namespace + name for the storage options provider
@@ -1738,7 +1738,7 @@ impl NativeTable {
let id = Self::build_id(&namespace, name); let id = Self::build_id(&namespace, name);
let stored_namespace_client = let stored_namespace_client =
if pushdown_operations.contains(&NamespaceClientPushdownOperation::QueryTable) { if pushdown_operations.contains(&PushdownOperation::QueryTable) {
Some(namespace_client) Some(namespace_client)
} else { } else {
None None
+4 -9
View File
@@ -39,26 +39,21 @@ use lance_index::scalar::FullTextSearchQuery;
struct MetadataEraserExec { struct MetadataEraserExec {
input: Arc<dyn ExecutionPlan>, input: Arc<dyn ExecutionPlan>,
schema: Arc<ArrowSchema>, schema: Arc<ArrowSchema>,
properties: Arc<PlanProperties>, properties: PlanProperties,
} }
impl MetadataEraserExec { impl MetadataEraserExec {
fn compute_properties_from_input( fn compute_properties_from_input(
input: &Arc<dyn ExecutionPlan>, input: &Arc<dyn ExecutionPlan>,
schema: &Arc<ArrowSchema>, schema: &Arc<ArrowSchema>,
) -> Arc<PlanProperties> { ) -> PlanProperties {
let input_properties = input.properties(); let input_properties = input.properties();
let eq_properties = input_properties let eq_properties = input_properties
.eq_properties .eq_properties
.clone() .clone()
.with_new_schema(schema.clone()) .with_new_schema(schema.clone())
.unwrap(); .unwrap();
Arc::new( input_properties.clone().with_eq_properties(eq_properties)
input_properties
.as_ref()
.clone()
.with_eq_properties(eq_properties),
)
} }
fn new(input: Arc<dyn ExecutionPlan>) -> Self { fn new(input: Arc<dyn ExecutionPlan>) -> Self {
@@ -92,7 +87,7 @@ impl ExecutionPlan for MetadataEraserExec {
self self
} }
fn properties(&self) -> &Arc<PlanProperties> { fn properties(&self) -> &PlanProperties {
&self.properties &self.properties
} }
+3 -3
View File
@@ -81,7 +81,7 @@ pub struct InsertExec {
dataset: Arc<Dataset>, dataset: Arc<Dataset>,
input: Arc<dyn ExecutionPlan>, input: Arc<dyn ExecutionPlan>,
write_params: WriteParams, write_params: WriteParams,
properties: Arc<PlanProperties>, properties: PlanProperties,
partial_transactions: Arc<Mutex<Vec<Transaction>>>, partial_transactions: Arc<Mutex<Vec<Transaction>>>,
metrics: ExecutionPlanMetricsSet, metrics: ExecutionPlanMetricsSet,
} }
@@ -107,7 +107,7 @@ impl InsertExec {
dataset, dataset,
input, input,
write_params, write_params,
properties: Arc::new(properties), properties,
partial_transactions: Arc::new(Mutex::new(Vec::with_capacity(num_partitions))), partial_transactions: Arc::new(Mutex::new(Vec::with_capacity(num_partitions))),
metrics: ExecutionPlanMetricsSet::new(), metrics: ExecutionPlanMetricsSet::new(),
} }
@@ -136,7 +136,7 @@ impl ExecutionPlan for InsertExec {
self self
} }
fn properties(&self) -> &Arc<PlanProperties> { fn properties(&self) -> &PlanProperties {
&self.properties &self.properties
} }
@@ -20,7 +20,7 @@ pub(crate) struct ScannableExec {
// We don't require Scannable to be Sync, so we wrap it in a Mutex to allow safe concurrent access. // We don't require Scannable to be Sync, so we wrap it in a Mutex to allow safe concurrent access.
source: Mutex<Box<dyn Scannable>>, source: Mutex<Box<dyn Scannable>>,
num_rows: Option<usize>, num_rows: Option<usize>,
properties: Arc<PlanProperties>, properties: PlanProperties,
tracker: Option<Arc<WriteProgressTracker>>, tracker: Option<Arc<WriteProgressTracker>>,
} }
@@ -49,7 +49,7 @@ impl ScannableExec {
Self { Self {
source, source,
num_rows, num_rows,
properties: Arc::new(properties), properties,
tracker, tracker,
} }
} }
@@ -70,7 +70,7 @@ impl ExecutionPlan for ScannableExec {
self self
} }
fn properties(&self) -> &Arc<PlanProperties> { fn properties(&self) -> &PlanProperties {
&self.properties &self.properties
} }
+1 -8
View File
@@ -285,10 +285,7 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{connect, io::object_store::io_tracking::IoStatsHolder, table::WriteOptions};
connect, io::object_store::io_tracking::IoStatsHolder, table::WriteOptions,
utils::background_cache::clock,
};
async fn create_test_dataset(uri: &str) -> Dataset { async fn create_test_dataset(uri: &str) -> Dataset {
let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)])); let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]));
@@ -466,10 +463,6 @@ mod tests {
let uri = dir.path().to_str().unwrap(); let uri = dir.path().to_str().unwrap();
let ds = create_test_dataset(uri).await; let ds = create_test_dataset(uri).await;
// Other tests use a thread-local mock clock. Simulate leaked state from a
// previous test to ensure this wrapper starts from real time.
clock::advance_by(Duration::from_secs(60));
let wrapper = DatasetConsistencyWrapper::new_latest(ds, Some(Duration::from_millis(200))); let wrapper = DatasetConsistencyWrapper::new_latest(ds, Some(Duration::from_millis(200)));
// Populate the cache // Populate the cache
+2 -2
View File
@@ -4,7 +4,7 @@
use std::sync::Arc; use std::sync::Arc;
use super::NativeTable; use super::NativeTable;
use crate::connection::NamespaceClientPushdownOperation; use crate::connection::PushdownOperation;
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::expr::expr_to_sql_string; use crate::expr::expr_to_sql_string;
use crate::query::{ use crate::query::{
@@ -44,7 +44,7 @@ pub async fn execute_query(
// If QueryTable pushdown is enabled and namespace client is configured, use server-side query execution // If QueryTable pushdown is enabled and namespace client is configured, use server-side query execution
if table if table
.pushdown_operations .pushdown_operations
.contains(&NamespaceClientPushdownOperation::QueryTable) .contains(&PushdownOperation::QueryTable)
&& let Some(ref namespace_client) = table.namespace_client && let Some(ref namespace_client) = table.namespace_client
{ {
return execute_namespace_query(table, namespace_client.clone(), query, options).await; return execute_namespace_query(table, namespace_client.clone(), query, options).await;
@@ -107,14 +107,6 @@ where
refresh_window < ttl, refresh_window < ttl,
"refresh_window ({refresh_window:?}) must be less than ttl ({ttl:?})" "refresh_window ({refresh_window:?}) must be less than ttl ({ttl:?})"
); );
#[cfg(test)]
{
// Tests may advance the thread-local mock clock and leave it behind for
// the next test that happens to run on the same worker thread. Each new
// cache should start from a clean clock state instead of inheriting
// unrelated mock time from a previous test.
clock::clear_mock();
}
Self { Self {
inner: Arc::new(Mutex::new(CacheInner { inner: Arc::new(Mutex::new(CacheInner {
state: State::Empty, state: State::Empty,