Compare commits

..

7 Commits

73 changed files with 1073 additions and 1597 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.7"
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
+248 -264
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" = "=6.0.0-beta.1", default-features = false, "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", default-features = false, "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", default-features = false, "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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" = "=6.0.0-beta.1", "tag" = "v6.0.0-beta.1", "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.7</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.7</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.7</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>6.0.0-beta.1</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.7"
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.7",
"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.7",
"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.7",
"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.7",
"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.7",
"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.7",
"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.7",
"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.7",
"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.7",
"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.7",
"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.7"
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.7"
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 -9
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
), ),
+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
]
)
)
+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 -71
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
+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
+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)
+2
View File
@@ -1948,6 +1948,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 +2019,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
+1 -9
View File
@@ -525,7 +525,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 +537,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 +556,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());
+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()),
+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.7"
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",
-175
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 {
@@ -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 -143
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,
}; };
@@ -2335,7 +2202,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 -232
View File
@@ -24,13 +24,8 @@ use lance_table::io::commit::external_manifest::ExternalManifestCommitHandler;
use crate::connection::NamespaceClientPushdownOperation; use crate::connection::NamespaceClientPushdownOperation;
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,
@@ -54,8 +49,6 @@ pub struct LanceNamespaceDatabase {
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 {
@@ -77,15 +70,9 @@ impl LanceNamespaceDatabase {
pushdown_operations: namespace_client_pushdown_operations, pushdown_operations: namespace_client_pushdown_operations,
ns_impl: namespace_client_impl, ns_impl: namespace_client_impl,
ns_properties: namespace_client_properties, 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>,
@@ -93,27 +80,6 @@ impl LanceNamespaceDatabase {
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<NamespaceClientPushdownOperation>,
) -> 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 +101,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,7 +184,6 @@ impl Database for LanceNamespaceDatabase {
async fn create_table(&self, request: DbCreateTableRequest) -> Result<Arc<dyn BaseTable>> { async fn create_table(&self, request: DbCreateTableRequest) -> Result<Arc<dyn BaseTable>> {
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;
match request.mode { match request.mode {
CreateTableMode::Create => {} CreateTableMode::Create => {}
@@ -298,7 +192,20 @@ impl Database for LanceNamespaceDatabase {
id: Some(table_id.clone()), id: Some(table_id.clone()),
..Default::default() ..Default::default()
}; };
existing_table = self.namespace.describe_table(describe_request).await.ok(); let describe_result = self.namespace.describe_table(describe_request).await;
if describe_result.is_ok() {
// Drop the existing table - must succeed
let drop_request = DropTableRequest {
id: Some(table_id.clone()),
..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 { let describe_request = DescribeTableRequest {
@@ -333,86 +240,39 @@ 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
let loc = response.location.ok_or_else(|| Error::Runtime { .namespace
message: "Table location is missing from describe_table response".to_string(), .declare_table(declare_request)
})?; .await
let opts = response .map_err(|e| {
.storage_options let err_str = e.to_string();
.or_else(|| Some(self.storage_options.clone())) if matches!(request.mode, CreateTableMode::Create)
.filter(|o| !o.is_empty()); && (err_str.contains("already exists")
(loc, opts, response.managed_versioning) || err_str.contains("TableAlreadyExists")
} else { || err_str.contains("table already exists"))
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 Error::TableAlreadyExists {
.namespace name: request.name.clone(),
.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(),
});
} }
} else {
let loc = response.location.ok_or_else(|| Error::Runtime { 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), message: format!("Failed to declare table: {}", e),
}); }
} }
} })?;
} let loc = response.location.ok_or_else(|| Error::Runtime {
message: "Table location is missing from declare_table response".to_string(),
})?;
// Use storage options from response, fall back to self.storage_options
let opts = response
.storage_options
.or_else(|| Some(self.storage_options.clone()))
.filter(|o| !o.is_empty());
(loc, opts, response.managed_versioning)
}; };
// 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 +730,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
} }
+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
} }