Compare commits

..

2 Commits

Author SHA1 Message Date
David Myriel
9e278fc5a6 fix small details 2025-05-05 23:03:17 +02:00
David Myriel
09fed1f286 add quickstart doc 2025-05-05 22:02:11 +02:00
87 changed files with 1674 additions and 3618 deletions

View File

@@ -1,5 +1,5 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "0.20.0-beta.2" current_version = "0.19.1-beta.1"
parse = """(?x) parse = """(?x)
(?P<major>0|[1-9]\\d*)\\. (?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\. (?P<minor>0|[1-9]\\d*)\\.

View File

@@ -35,9 +35,6 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
workspaces: java/core/lancedb-jni workspaces: java/core/lancedb-jni
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt
- name: Run cargo fmt - name: Run cargo fmt
run: cargo fmt --check run: cargo fmt --check
working-directory: ./java/core/lancedb-jni working-directory: ./java/core/lancedb-jni
@@ -71,9 +68,6 @@ jobs:
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
workspaces: java/core/lancedb-jni workspaces: java/core/lancedb-jni
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt
- name: Run cargo fmt - name: Run cargo fmt
run: cargo fmt --check run: cargo fmt --check
working-directory: ./java/core/lancedb-jni working-directory: ./java/core/lancedb-jni
@@ -116,3 +110,4 @@ jobs:
-Djdk.reflect.useDirectMethodHandle=false \ -Djdk.reflect.useDirectMethodHandle=false \
-Dio.netty.tryReflectionSetAccessible=true" -Dio.netty.tryReflectionSetAccessible=true"
JAVA_HOME=$JAVA_17 mvn clean test JAVA_HOME=$JAVA_17 mvn clean test

View File

@@ -84,7 +84,6 @@ jobs:
run: | run: |
pip install bump-my-version PyGithub packaging pip install bump-my-version PyGithub packaging
bash ci/bump_version.sh ${{ inputs.type }} ${{ inputs.bump-minor }} v $COMMIT_BEFORE_BUMP bash ci/bump_version.sh ${{ inputs.type }} ${{ inputs.bump-minor }} v $COMMIT_BEFORE_BUMP
bash ci/update_lockfiles.sh --amend
- name: Push new version tag - name: Push new version tag
if: ${{ !inputs.dry_run }} if: ${{ !inputs.dry_run }}
uses: ad-m/github-push-action@master uses: ad-m/github-push-action@master
@@ -93,3 +92,11 @@ jobs:
github_token: ${{ secrets.LANCEDB_RELEASE_TOKEN }} github_token: ${{ secrets.LANCEDB_RELEASE_TOKEN }}
branch: ${{ github.ref }} branch: ${{ github.ref }}
tags: true tags: true
- uses: ./.github/workflows/update_package_lock
if: ${{ !inputs.dry_run && inputs.other }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- uses: ./.github/workflows/update_package_lock_nodejs
if: ${{ !inputs.dry_run && inputs.other }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -47,9 +47,6 @@ jobs:
run: | run: |
sudo apt update sudo apt update
sudo apt install -y protobuf-compiler libssl-dev sudo apt install -y protobuf-compiler libssl-dev
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt, clippy
- name: Lint - name: Lint
run: | run: |
cargo fmt --all -- --check cargo fmt --all -- --check
@@ -116,7 +113,7 @@ jobs:
set -e set -e
npm ci npm ci
npm run docs npm run docs
if ! git diff --exit-code -- . ':(exclude)Cargo.lock'; then if ! git diff --exit-code; then
echo "Docs need to be updated" echo "Docs need to be updated"
echo "Run 'npm run docs', fix any warnings, and commit the changes." echo "Run 'npm run docs', fix any warnings, and commit the changes."
exit 1 exit 1

View File

@@ -505,8 +505,6 @@ jobs:
name: vectordb NPM Publish name: vectordb NPM Publish
needs: [node, node-macos, node-linux-gnu, node-windows] needs: [node, node-macos, node-linux-gnu, node-windows]
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: write
# Only runs on tags that matches the make-release action # Only runs on tags that matches the make-release action
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
steps: steps:
@@ -539,10 +537,6 @@ jobs:
# We need to deprecate the old package to avoid confusion. # We need to deprecate the old package to avoid confusion.
# Each time we publish a new version, it gets undeprecated. # Each time we publish a new version, it gets undeprecated.
run: npm deprecate vectordb "Use @lancedb/lancedb instead." run: npm deprecate vectordb "Use @lancedb/lancedb instead."
- name: Update package-lock.json
run: bash ci/update_lockfiles.sh
- name: Push new commit
uses: ad-m/github-push-action@master
- name: Notify Slack Action - name: Notify Slack Action
uses: ravsamhq/notify-slack-action@2.3.0 uses: ravsamhq/notify-slack-action@2.3.0
if: ${{ always() }} if: ${{ always() }}
@@ -552,3 +546,21 @@ jobs:
notification_title: "{workflow} is failing" notification_title: "{workflow} is failing"
env: env:
SLACK_WEBHOOK_URL: ${{ secrets.ACTION_MONITORING_SLACK }} SLACK_WEBHOOK_URL: ${{ secrets.ACTION_MONITORING_SLACK }}
update-package-lock:
if: startsWith(github.ref, 'refs/tags/v')
needs: [release]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.LANCEDB_RELEASE_TOKEN }}
fetch-depth: 0
lfs: true
- uses: ./.github/workflows/update_package_lock
with:
github_token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -24,8 +24,8 @@ runs:
- name: pytest (with integration) - name: pytest (with integration)
shell: bash shell: bash
if: ${{ inputs.integration == 'true' }} if: ${{ inputs.integration == 'true' }}
run: pytest -m "not slow" -vv --durations=30 python/python/tests run: pytest -m "not slow" -x -v --durations=30 python/python/tests
- name: pytest (no integration tests) - name: pytest (no integration tests)
shell: bash shell: bash
if: ${{ inputs.integration != 'true' }} if: ${{ inputs.integration != 'true' }}
run: pytest -m "not slow and not s3_test" -vv --durations=30 python/python/tests run: pytest -m "not slow and not s3_test" -x -v --durations=30 python/python/tests

View File

@@ -40,9 +40,6 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
lfs: true lfs: true
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
workspaces: rust workspaces: rust
@@ -163,8 +160,8 @@ jobs:
strategy: strategy:
matrix: matrix:
target: target:
- x86_64-pc-windows-msvc - x86_64-pc-windows-msvc
- aarch64-pc-windows-msvc - aarch64-pc-windows-msvc
defaults: defaults:
run: run:
working-directory: rust/lancedb working-directory: rust/lancedb

View File

@@ -0,0 +1,33 @@
name: update_package_lock
description: "Update node's package.lock"
inputs:
github_token:
required: true
description: "github token for the repo"
runs:
using: "composite"
steps:
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Set git configs
shell: bash
run: |
git config user.name 'Lance Release'
git config user.email 'lance-dev@lancedb.com'
- name: Update package-lock.json file
working-directory: ./node
run: |
npm install
git add package-lock.json
git commit -m "Updating package-lock.json"
shell: bash
- name: Push changes
if: ${{ inputs.dry_run }} == "false"
uses: ad-m/github-push-action@master
with:
github_token: ${{ inputs.github_token }}
branch: main
tags: true

View File

@@ -0,0 +1,33 @@
name: update_package_lock_nodejs
description: "Update nodejs's package.lock"
inputs:
github_token:
required: true
description: "github token for the repo"
runs:
using: "composite"
steps:
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Set git configs
shell: bash
run: |
git config user.name 'Lance Release'
git config user.email 'lance-dev@lancedb.com'
- name: Update package-lock.json file
working-directory: ./nodejs
run: |
npm install
git add package-lock.json
git commit -m "Updating package-lock.json"
shell: bash
- name: Push changes
if: ${{ inputs.dry_run }} == "false"
uses: ad-m/github-push-action@master
with:
github_token: ${{ inputs.github_token }}
branch: main
tags: true

1471
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,32 +21,32 @@ categories = ["database-implementations"]
rust-version = "1.78.0" rust-version = "1.78.0"
[workspace.dependencies] [workspace.dependencies]
lance = { "version" = "=0.29.0", "features" = ["dynamodb"] } lance = { "version" = "=0.27.0", "features" = ["dynamodb"], tag = "v0.27.0-beta.2", git="https://github.com/lancedb/lance.git" }
lance-io = "=0.29.0" lance-io = { version = "=0.27.0", tag = "v0.27.0-beta.2", git="https://github.com/lancedb/lance.git" }
lance-index = "=0.29.0" lance-index = { version = "=0.27.0", tag = "v0.27.0-beta.2", git="https://github.com/lancedb/lance.git" }
lance-linalg = "=0.29.0" lance-linalg = { version = "=0.27.0", tag = "v0.27.0-beta.2", git="https://github.com/lancedb/lance.git" }
lance-table = "=0.29.0" lance-table = { version = "=0.27.0", tag = "v0.27.0-beta.2", git="https://github.com/lancedb/lance.git" }
lance-testing = "=0.29.0" lance-testing = { version = "=0.27.0", tag = "v0.27.0-beta.2", git="https://github.com/lancedb/lance.git" }
lance-datafusion = "=0.29.0" lance-datafusion = { version = "=0.27.0", tag = "v0.27.0-beta.2", git="https://github.com/lancedb/lance.git" }
lance-encoding = "=0.29.0" lance-encoding = { version = "=0.27.0", tag = "v0.27.0-beta.2", git="https://github.com/lancedb/lance.git" }
# Note that this one does not include pyarrow # Note that this one does not include pyarrow
arrow = { version = "55.1", optional = false } arrow = { version = "54.1", optional = false }
arrow-array = "55.1" arrow-array = "54.1"
arrow-data = "55.1" arrow-data = "54.1"
arrow-ipc = "55.1" arrow-ipc = "54.1"
arrow-ord = "55.1" arrow-ord = "54.1"
arrow-schema = "55.1" arrow-schema = "54.1"
arrow-arith = "55.1" arrow-arith = "54.1"
arrow-cast = "55.1" arrow-cast = "54.1"
async-trait = "0" async-trait = "0"
datafusion = { version = "47.0", default-features = false } datafusion = { version = "46.0", default-features = false }
datafusion-catalog = "47.0" datafusion-catalog = "46.0"
datafusion-common = { version = "47.0", default-features = false } datafusion-common = { version = "46.0", default-features = false }
datafusion-execution = "47.0" datafusion-execution = "46.0"
datafusion-expr = "47.0" datafusion-expr = "46.0"
datafusion-physical-plan = "47.0" datafusion-physical-plan = "46.0"
env_logger = "0.11" env_logger = "0.11"
half = { "version" = "=2.5.0", default-features = false, features = [ half = { "version" = "=2.4.1", default-features = false, features = [
"num-traits", "num-traits",
] } ] }
futures = "0" futures = "0"
@@ -57,16 +57,19 @@ pin-project = "1.0.7"
snafu = "0.8" snafu = "0.8"
url = "2" url = "2"
num-traits = "0.2" num-traits = "0.2"
rand = "0.9" rand = "0.8"
regex = "1.10" regex = "1.10"
lazy_static = "1" lazy_static = "1"
semver = "1.0.25" semver = "1.0.25"
# Temporary pins to work around downstream issues # Temporary pins to work around downstream issues
# https://github.com/apache/arrow-rs/commit/2fddf85afcd20110ce783ed5b4cdeb82293da30b # https://github.com/apache/arrow-rs/commit/2fddf85afcd20110ce783ed5b4cdeb82293da30b
chrono = "=0.4.41" chrono = "=0.4.39"
# https://github.com/RustCrypto/formats/issues/1684 # https://github.com/RustCrypto/formats/issues/1684
base64ct = "=1.6.0" base64ct = "=1.6.0"
# Workaround for: https://github.com/eira-fransham/crunchy/issues/13 # Workaround for: https://github.com/eira-fransham/crunchy/issues/13
crunchy = "=0.2.2" crunchy = "=0.2.2"
# Workaround for: https://github.com/Lokathor/bytemuck/issues/306 # Workaround for: https://github.com/Lokathor/bytemuck/issues/306
bytemuck_derive = ">=1.8.1, <1.9.0" bytemuck_derive = ">=1.8.1, <1.9.0"

167
README.md
View File

@@ -1,97 +1,94 @@
<a href="https://cloud.lancedb.com" target="_blank"> <a href="https://cloud.lancedb.com" target="_blank">
<img src="https://github.com/user-attachments/assets/92dad0a2-2a37-4ce1-b783-0d1b4f30a00c" alt="LanceDB Cloud Public Beta" width="100%" style="max-width: 100%;"> <img src="https://github.com/user-attachments/assets/92dad0a2-2a37-4ce1-b783-0d1b4f30a00c" alt="LanceDB Cloud Public Beta" width="100%" style="max-width: 100%;">
</a> </a>
<div align="center"> <div align="center">
<p align="center">
[![LanceDB](docs/src/assets/hero-header.png)](https://lancedb.com) <picture>
[![Website](https://img.shields.io/badge/-Website-100000?style=for-the-badge&labelColor=645cfb&color=645cfb)](https://lancedb.com/) <source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/ac270358-333e-4bea-a132-acefaa94040e">
[![Blog](https://img.shields.io/badge/Blog-100000?style=for-the-badge&labelColor=645cfb&color=645cfb)](https://blog.lancedb.com/) <source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/b864d814-0d29-4784-8fd9-807297c758c0">
[![Discord](https://img.shields.io/badge/-Discord-100000?style=for-the-badge&logo=discord&logoColor=white&labelColor=645cfb&color=645cfb)](https://discord.gg/zMM32dvNtd) <img alt="LanceDB Logo" src="https://github.com/user-attachments/assets/b864d814-0d29-4784-8fd9-807297c758c0" width=300>
[![Twitter](https://img.shields.io/badge/-Twitter-100000?style=for-the-badge&logo=x&logoColor=white&labelColor=645cfb&color=645cfb)](https://twitter.com/lancedb) </picture>
[![LinkedIn](https://img.shields.io/badge/-LinkedIn-100000?style=for-the-badge&logo=linkedin&logoColor=white&labelColor=645cfb&color=645cfb)](https://www.linkedin.com/company/lancedb/)
**Search More, Manage Less**
<img src="docs/src/assets/lancedb.png" alt="LanceDB" width="50%"> <a href='https://github.com/lancedb/vectordb-recipes/tree/main' target="_blank"><img alt='LanceDB' src='https://img.shields.io/badge/VectorDB_Recipes-100000?style=for-the-badge&logo=LanceDB&logoColor=white&labelColor=645cfb&color=645cfb'/></a>
<a href='https://lancedb.github.io/lancedb/' target="_blank"><img alt='lancdb' src='https://img.shields.io/badge/DOCS-100000?style=for-the-badge&logo=lancdb&logoColor=white&labelColor=645cfb&color=645cfb'/></a>
[![Blog](https://img.shields.io/badge/Blog-12100E?style=for-the-badge&logoColor=white)](https://blog.lancedb.com/)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/zMM32dvNtd)
[![Twitter](https://img.shields.io/badge/Twitter-%231DA1F2.svg?style=for-the-badge&logo=Twitter&logoColor=white)](https://twitter.com/lancedb)
[![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20LanceDB%20Guru-006BFF?style=for-the-badge)](https://gurubase.io/g/lancedb)
# **The Multimodal AI Lakehouse** </p>
[**How to Install** ](#how-to-install) ✦ [**Detailed Documentation**](https://lancedb.github.io/lancedb/) ✦ [**Tutorials and Recipes**](https://github.com/lancedb/vectordb-recipes/tree/main) ✦ [**Contributors**](#contributors)
**The ultimate multimodal data platform for AI/ML applications.**
LanceDB is designed for fast, scalable, and production-ready vector search. It is built on top of the Lance columnar format. You can store, index, and search over petabytes of multimodal data and vectors with ease.
LanceDB is a central location where developers can build, train and analyze their AI workloads.
</div>
<br>
## **Demo: Multimodal Search by Keyword, Vector or with SQL**
<img max-width="750px" alt="LanceDB Multimodal Search" src="https://github.com/lancedb/lancedb/assets/917119/09c5afc5-7816-4687-bae4-f2ca194426ec"> <img max-width="750px" alt="LanceDB Multimodal Search" src="https://github.com/lancedb/lancedb/assets/917119/09c5afc5-7816-4687-bae4-f2ca194426ec">
## **Star LanceDB to get updates!** </p>
<details>
<summary>⭐ Click here ⭐ to see how fast we're growing!</summary>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=lancedb/lancedb&theme=dark&type=Date">
<img width="100%" src="https://api.star-history.com/svg?repos=lancedb/lancedb&theme=dark&type=Date">
</picture>
</details>
## **Key Features**:
- **Fast Vector Search**: Search billions of vectors in milliseconds with state-of-the-art indexing.
- **Comprehensive Search**: Support for vector similarity search, full-text search and SQL.
- **Multimodal Support**: Store, query and filter vectors, metadata and multimodal data (text, images, videos, point clouds, and more).
- **Advanced Features**: Zero-copy, automatic versioning, manage versions of your data without needing extra infrastructure. GPU support in building vector index.
### **Products**:
- **Open Source & Local**: 100% open source, runs locally or in your cloud. No vendor lock-in.
- **Cloud and Enterprise**: Production-scale vector search with no servers to manage. Complete data sovereignty and security.
### **Ecosystem**:
- **Columnar Storage**: Built on the Lance columnar format for efficient storage and analytics.
- **Seamless Integration**: Python, Node.js, Rust, and REST APIs for easy integration. Native Python and Javascript/Typescript support.
- **Rich Ecosystem**: Integrations with [**LangChain** 🦜️🔗](https://python.langchain.com/docs/integrations/vectorstores/lancedb/), [**LlamaIndex** 🦙](https://gpt-index.readthedocs.io/en/latest/examples/vector_stores/LanceDBIndexDemo.html), Apache-Arrow, Pandas, Polars, DuckDB and more on the way.
## **How to Install**:
Follow the [Quickstart](https://lancedb.github.io/lancedb/basic/) doc to set up LanceDB locally.
**API & SDK:** We also support Python, Typescript and Rust SDKs
| Interface | Documentation |
|-----------|---------------|
| Python SDK | https://lancedb.github.io/lancedb/python/python/ |
| Typescript SDK | https://lancedb.github.io/lancedb/js/globals/ |
| Rust SDK | https://docs.rs/lancedb/latest/lancedb/index.html |
| REST API | https://docs.lancedb.com/api-reference/introduction |
## **Join Us and Contribute**
We welcome contributions from everyone! Whether you're a developer, researcher, or just someone who wants to help out.
If you have any suggestions or feature requests, please feel free to open an issue on GitHub or discuss it on our [**Discord**](https://discord.gg/G5DcmnZWKB) server.
[**Check out the GitHub Issues**](https://github.com/lancedb/lancedb/issues) if you would like to work on the features that are planned for the future. If you have any suggestions or feature requests, please feel free to open an issue on GitHub.
## **Contributors**
<a href="https://github.com/lancedb/lancedb/graphs/contributors">
<img src="https://contrib.rocks/image?repo=lancedb/lancedb" />
</a>
## **Stay in Touch With Us**
<div align="center">
</br>
[![Website](https://img.shields.io/badge/-Website-100000?style=for-the-badge&labelColor=645cfb&color=645cfb)](https://lancedb.com/)
[![Blog](https://img.shields.io/badge/Blog-100000?style=for-the-badge&labelColor=645cfb&color=645cfb)](https://blog.lancedb.com/)
[![Discord](https://img.shields.io/badge/-Discord-100000?style=for-the-badge&logo=discord&logoColor=white&labelColor=645cfb&color=645cfb)](https://discord.gg/zMM32dvNtd)
[![Twitter](https://img.shields.io/badge/-Twitter-100000?style=for-the-badge&logo=x&logoColor=white&labelColor=645cfb&color=645cfb)](https://twitter.com/lancedb)
[![LinkedIn](https://img.shields.io/badge/-LinkedIn-100000?style=for-the-badge&logo=linkedin&logoColor=white&labelColor=645cfb&color=645cfb)](https://www.linkedin.com/company/lancedb/)
</div> </div>
<hr />
LanceDB is an open-source database for vector-search built with persistent storage, which greatly simplifies retrieval, filtering and management of embeddings.
The key features of LanceDB include:
* Production-scale vector search with no servers to manage.
* Store, query and filter vectors, metadata and multi-modal data (text, images, videos, point clouds, and more).
* Support for vector similarity search, full-text search and SQL.
* Native Python and Javascript/Typescript support.
* Zero-copy, automatic versioning, manage versions of your data without needing extra infrastructure.
* GPU support in building vector index(*).
* Ecosystem integrations with [LangChain 🦜️🔗](https://python.langchain.com/docs/integrations/vectorstores/lancedb/), [LlamaIndex 🦙](https://gpt-index.readthedocs.io/en/latest/examples/vector_stores/LanceDBIndexDemo.html), Apache-Arrow, Pandas, Polars, DuckDB and more on the way.
LanceDB's core is written in Rust 🦀 and is built using <a href="https://github.com/lancedb/lance">Lance</a>, an open-source columnar format designed for performant ML workloads.
## Quick Start
**Javascript**
```shell
npm install @lancedb/lancedb
```
```javascript
import * as lancedb from "@lancedb/lancedb";
const db = await lancedb.connect("data/sample-lancedb");
const table = await db.createTable("vectors", [
{ id: 1, vector: [0.1, 0.2], item: "foo", price: 10 },
{ id: 2, vector: [1.1, 1.2], item: "bar", price: 50 },
], {mode: 'overwrite'});
const query = table.vectorSearch([0.1, 0.3]).limit(2);
const results = await query.toArray();
// You can also search for rows by specific criteria without involving a vector search.
const rowsByCriteria = await table.query().where("price >= 10").toArray();
```
**Python**
```shell
pip install lancedb
```
```python
import lancedb
uri = "data/sample-lancedb"
db = lancedb.connect(uri)
table = db.create_table("my_table",
data=[{"vector": [3.1, 4.1], "item": "foo", "price": 10.0},
{"vector": [5.9, 26.5], "item": "bar", "price": 20.0}])
result = table.search([100, 100]).limit(2).to_pandas()
```
## Blogs, Tutorials & Videos
* 📈 <a href="https://blog.lancedb.com/benchmarking-random-access-in-lance/">2000x better performance with Lance over Parquet</a>
* 🤖 <a href="https://github.com/lancedb/vectordb-recipes/tree/main/examples/Youtube-Search-QA-Bot">Build a question and answer bot with LanceDB</a>

View File

@@ -1,174 +0,0 @@
import argparse
import sys
import json
def run_command(command: str) -> str:
"""
Run a shell command and return stdout as a string.
If exit code is not 0, raise an exception with the stderr output.
"""
import subprocess
result = subprocess.run(command, shell=True, capture_output=True, text=True)
if result.returncode != 0:
raise Exception(f"Command failed with error: {result.stderr.strip()}")
return result.stdout.strip()
def get_latest_stable_version() -> str:
version_line = run_command("cargo info lance | grep '^version:'")
version = version_line.split(" ")[1].strip()
return version
def get_latest_preview_version() -> str:
lance_tags = run_command(
"git ls-remote --tags https://github.com/lancedb/lance.git | grep 'refs/tags/v[0-9beta.-]\\+$'"
).splitlines()
lance_tags = (
tag.split("refs/tags/")[1]
for tag in lance_tags
if "refs/tags/" in tag and "beta" in tag
)
from packaging.version import Version
latest = max(
(tag[1:] for tag in lance_tags if tag.startswith("v")), key=lambda t: Version(t)
)
return str(latest)
def extract_features(line: str) -> list:
"""
Extracts the features from a line in Cargo.toml.
Example: 'lance = { "version" = "=0.29.0", "features" = ["dynamodb"] }'
Returns: ['dynamodb']
"""
import re
match = re.search(r'"features"\s*=\s*\[(.*?)\]', line)
if match:
features_str = match.group(1)
return [f.strip('"') for f in features_str.split(",")]
return []
def update_cargo_toml(line_updater):
"""
Updates the Cargo.toml file by applying the line_updater function to each line.
The line_updater function should take a line as input and return the updated line.
"""
with open("Cargo.toml", "r") as f:
lines = f.readlines()
new_lines = []
for line in lines:
if line.startswith("lance"):
# Update the line using the provided function
new_lines.append(line_updater(line))
else:
# Keep the line unchanged
new_lines.append(line)
with open("Cargo.toml", "w") as f:
f.writelines(new_lines)
def set_stable_version(version: str):
"""
Sets lines to
lance = { "version" = "=0.29.0", "features" = ["dynamodb"] }
lance-io = "=0.29.0"
...
"""
def line_updater(line: str) -> str:
package_name = line.split("=", maxsplit=1)[0].strip()
features = extract_features(line)
if features:
return f'{package_name} = {{ "version" = "={version}", "features" = {json.dumps(features)} }}\n'
else:
return f'{package_name} = "={version}"\n'
update_cargo_toml(line_updater)
def set_preview_version(version: str):
"""
Sets lines to
lance = { "version" = "=0.29.0", "features" = ["dynamodb"], tag = "v0.29.0-beta.2", git="https://github.com/lancedb/lance.git" }
lance-io = { version = "=0.29.0", tag = "v0.29.0-beta.2", git="https://github.com/lancedb/lance.git" }
...
"""
def line_updater(line: str) -> str:
package_name = line.split("=", maxsplit=1)[0].strip()
features = extract_features(line)
base_version = version.split("-")[0] # Get the base version without beta suffix
if features:
return f'{package_name} = {{ "version" = "={base_version}", "features" = {json.dumps(features)}, "tag" = "v{version}", "git" = "https://github.com/lancedb/lance.git" }}\n'
else:
return f'{package_name} = {{ "version" = "={base_version}", "tag" = "v{version}", "git" = "https://github.com/lancedb/lance.git" }}\n'
update_cargo_toml(line_updater)
def set_local_version():
"""
Sets lines to
lance = { path = "../lance/rust/lance", features = ["dynamodb"] }
lance-io = { path = "../lance/rust/lance-io" }
...
"""
def line_updater(line: str) -> str:
package_name = line.split("=", maxsplit=1)[0].strip()
features = extract_features(line)
if features:
return f'{package_name} = {{ "path" = "../lance/rust/{package_name}", "features" = {json.dumps(features)} }}\n'
else:
return f'{package_name} = {{ "path" = "../lance/rust/{package_name}" }}\n'
update_cargo_toml(line_updater)
parser = argparse.ArgumentParser(description="Set the version of the Lance package.")
parser.add_argument(
"version",
type=str,
help="The version to set for the Lance package. Use 'stable' for the latest stable version, 'preview' for latest preview version, or a specific version number (e.g., '0.1.0'). You can also specify 'local' to use a local path.",
)
args = parser.parse_args()
if args.version == "stable":
latest_stable_version = get_latest_stable_version()
print(
f"Found latest stable version: \033[1mv{latest_stable_version}\033[0m",
file=sys.stderr,
)
set_stable_version(latest_stable_version)
elif args.version == "preview":
latest_preview_version = get_latest_preview_version()
print(
f"Found latest preview version: \033[1mv{latest_preview_version}\033[0m",
file=sys.stderr,
)
set_preview_version(latest_preview_version)
elif args.version == "local":
set_local_version()
else:
# Parse the version number.
version = args.version
# Ignore initial v if present.
if version.startswith("v"):
version = version[1:]
if "beta" in version:
set_preview_version(version)
else:
set_stable_version(version)
print("Updating lockfiles...", file=sys.stderr, end="")
run_command("cargo metadata > /dev/null")
print(" done.", file=sys.stderr)

View File

@@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
AMEND=false
for arg in "$@"; do
if [[ "$arg" == "--amend" ]]; then
AMEND=true
fi
done
# This updates the lockfile without building
cargo metadata --quiet > /dev/null
pushd nodejs || exit 1
npm install --package-lock-only --silent
popd
pushd node || exit 1
npm install --package-lock-only --silent
popd
if git diff --quiet --exit-code; then
echo "No lockfile changes to commit; skipping amend."
elif $AMEND; then
git add Cargo.lock nodejs/package-lock.json node/package-lock.json
git commit --amend --no-edit
else
git add Cargo.lock nodejs/package-lock.json node/package-lock.json
git commit -m "Update lockfiles"
fi

View File

@@ -105,7 +105,8 @@ markdown_extensions:
nav: nav:
- Home: - Home:
- LanceDB: index.md - LanceDB: index.md
- 🏃🏼‍♂️ Quick start: basic.md - 👉 Quickstart: quickstart.md
- 🏃🏼‍♂️ Basic Usage: basic.md
- 📚 Concepts: - 📚 Concepts:
- Vector search: concepts/vector_search.md - Vector search: concepts/vector_search.md
- Indexing: - Indexing:
@@ -193,7 +194,6 @@ nav:
- Pandas and PyArrow: python/pandas_and_pyarrow.md - Pandas and PyArrow: python/pandas_and_pyarrow.md
- Polars: python/polars_arrow.md - Polars: python/polars_arrow.md
- DuckDB: python/duckdb.md - DuckDB: python/duckdb.md
- Datafusion: python/datafusion.md
- LangChain: - LangChain:
- LangChain 🔗: integrations/langchain.md - LangChain 🔗: integrations/langchain.md
- LangChain demo: notebooks/langchain_demo.ipynb - LangChain demo: notebooks/langchain_demo.ipynb
@@ -206,7 +206,6 @@ nav:
- PromptTools: integrations/prompttools.md - PromptTools: integrations/prompttools.md
- dlt: integrations/dlt.md - dlt: integrations/dlt.md
- phidata: integrations/phidata.md - phidata: integrations/phidata.md
- Genkit: integrations/genkit.md
- 🎯 Examples: - 🎯 Examples:
- Overview: examples/index.md - Overview: examples/index.md
- 🐍 Python: - 🐍 Python:
@@ -239,7 +238,9 @@ nav:
- 👾 JavaScript (lancedb): js/globals.md - 👾 JavaScript (lancedb): js/globals.md
- 🦀 Rust: https://docs.rs/lancedb/latest/lancedb/ - 🦀 Rust: https://docs.rs/lancedb/latest/lancedb/
- Quick start: basic.md - Getting Started:
- Quickstart: quickstart.md
- Basic Usage: basic.md
- Concepts: - Concepts:
- Vector search: concepts/vector_search.md - Vector search: concepts/vector_search.md
- Indexing: - Indexing:
@@ -249,7 +250,6 @@ nav:
- Data management: concepts/data_management.md - Data management: concepts/data_management.md
- Guides: - Guides:
- Working with tables: guides/tables.md - Working with tables: guides/tables.md
- Working with SQL: guides/sql_querying.md
- Building an ANN index: ann_indexes.md - Building an ANN index: ann_indexes.md
- Vector Search: search.md - Vector Search: search.md
- Full-text search (native): fts.md - Full-text search (native): fts.md
@@ -326,7 +326,6 @@ nav:
- Pandas and PyArrow: python/pandas_and_pyarrow.md - Pandas and PyArrow: python/pandas_and_pyarrow.md
- Polars: python/polars_arrow.md - Polars: python/polars_arrow.md
- DuckDB: python/duckdb.md - DuckDB: python/duckdb.md
- Datafusion: python/datafusion.md
- LangChain 🦜️🔗↗: integrations/langchain.md - LangChain 🦜️🔗↗: integrations/langchain.md
- LangChain.js 🦜️🔗↗: https://js.langchain.com/docs/integrations/vectorstores/lancedb - LangChain.js 🦜️🔗↗: https://js.langchain.com/docs/integrations/vectorstores/lancedb
- LlamaIndex 🦙↗: integrations/llamaIndex.md - LlamaIndex 🦙↗: integrations/llamaIndex.md
@@ -335,7 +334,6 @@ nav:
- PromptTools: integrations/prompttools.md - PromptTools: integrations/prompttools.md
- dlt: integrations/dlt.md - dlt: integrations/dlt.md
- phidata: integrations/phidata.md - phidata: integrations/phidata.md
- Genkit: integrations/genkit.md
- Examples: - Examples:
- examples/index.md - examples/index.md
- 🐍 Python: - 🐍 Python:

View File

@@ -1,5 +0,0 @@
{% extends "base.html" %}
{% block announce %}
📚 Starting June 1st, 2025, please use <a href="https://lancedb.github.io/documentation" target="_blank" rel="noopener noreferrer">lancedb.github.io/documentation</a> for the latest docs.
{% endblock %}

View File

@@ -291,7 +291,7 @@ Product quantization can lead to approximately `16 * sizeof(float32) / 1 = 64` t
`num_partitions` is used to decide how many partitions the first level `IVF` index uses. `num_partitions` is used to decide how many partitions the first level `IVF` index uses.
Higher number of partitions could lead to more efficient I/O during queries and better accuracy, but it takes much more time to train. Higher number of partitions could lead to more efficient I/O during queries and better accuracy, but it takes much more time to train.
On `SIFT-1M` dataset, our benchmark shows that keeping each partition 4K-8K rows lead to a good latency / recall. On `SIFT-1M` dataset, our benchmark shows that keeping each partition 1K-4K rows lead to a good latency / recall.
`num_sub_vectors` specifies how many Product Quantization (PQ) short codes to generate on each vector. The number should be a factor of the vector dimension. Because `num_sub_vectors` specifies how many Product Quantization (PQ) short codes to generate on each vector. The number should be a factor of the vector dimension. Because
PQ is a lossy compression of the original vector, a higher `num_sub_vectors` usually results in PQ is a lossy compression of the original vector, a higher `num_sub_vectors` usually results in

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -1,4 +1,4 @@
# Quick start # Basic Usage
!!! info "LanceDB can be run in a number of ways:" !!! info "LanceDB can be run in a number of ways:"

View File

@@ -1,66 +0,0 @@
You can use DuckDB and Apache Datafusion to query your LanceDB tables using SQL.
This guide will show how to query Lance tables them using both.
We will re-use the dataset [created previously](./pandas_and_pyarrow.md):
```python
import lancedb
db = lancedb.connect("data/sample-lancedb")
data = [
{"vector": [3.1, 4.1], "item": "foo", "price": 10.0},
{"vector": [5.9, 26.5], "item": "bar", "price": 20.0}
]
table = db.create_table("pd_table", data=data)
```
## Querying a LanceDB Table with DuckDb
The `to_lance` method converts the LanceDB table to a `LanceDataset`, which is accessible to DuckDB through the Arrow compatibility layer.
To query the resulting Lance dataset in DuckDB, all you need to do is reference the dataset by the same name in your SQL query.
```python
import duckdb
arrow_table = table.to_lance()
duckdb.query("SELECT * FROM arrow_table")
```
```
┌─────────────┬─────────┬────────┐
│ vector │ item │ price │
│ float[] │ varchar │ double │
├─────────────┼─────────┼────────┤
│ [3.1, 4.1] │ foo │ 10.0 │
│ [5.9, 26.5] │ bar │ 20.0 │
└─────────────┴─────────┴────────┘
```
## Querying a LanceDB Table with Apache Datafusion
Have the required imports before doing any querying.
=== "Python"
```python
--8<-- "python/python/tests/docs/test_guide_tables.py:import-lancedb"
--8<-- "python/python/tests/docs/test_guide_tables.py:import-session-context"
--8<-- "python/python/tests/docs/test_guide_tables.py:import-ffi-dataset"
```
Register the table created with the Datafusion session context.
=== "Python"
```python
--8<-- "python/python/tests/docs/test_guide_tables.py:lance_sql_basic"
```
```
┌─────────────┬─────────┬────────┐
│ vector │ item │ price │
│ float[] │ varchar │ double │
├─────────────┼─────────┼────────┤
│ [3.1, 4.1] │ foo │ 10.0 │
│ [5.9, 26.5] │ bar │ 20.0 │
└─────────────┴─────────┴────────┘
```

View File

@@ -765,7 +765,7 @@ This can be used to update zero to all rows depending on how many rows match the
]; ];
const tbl = await db.createTable("my_table", data) const tbl = await db.createTable("my_table", data)
await tbl.update({ await tbl.update({
values: { vector: [10, 10] }, values: { vector: [10, 10] },
where: "x = 2" where: "x = 2"
}); });
@@ -787,9 +787,9 @@ This can be used to update zero to all rows depending on how many rows match the
]; ];
const tbl = await db.createTable("my_table", data) const tbl = await db.createTable("my_table", data)
await tbl.update({ await tbl.update({
where: "x = 2", where: "x = 2",
values: { vector: [10, 10] } values: { vector: [10, 10] }
}); });
``` ```

View File

@@ -1,183 +0,0 @@
### genkitx-lancedb
This is a lancedb plugin for genkit framework. It allows you to use LanceDB for ingesting and rereiving data using genkit framework.
![integration-banner-genkit](https://github.com/user-attachments/assets/a6cc28af-98e9-4425-b87c-7ab139bd7893)
### Installation
```bash
pnpm install genkitx-lancedb
```
### Usage
Adding LanceDB plugin to your genkit instance.
```ts
import { lancedbIndexerRef, lancedb, lancedbRetrieverRef, WriteMode } from 'genkitx-lancedb';
import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai';
import { gemini } from '@genkit-ai/vertexai';
import { z, genkit } from 'genkit';
import { Document } from 'genkit/retriever';
import { chunk } from 'llm-chunk';
import { readFile } from 'fs/promises';
import path from 'path';
import pdf from 'pdf-parse/lib/pdf-parse';
const ai = genkit({
plugins: [
// vertexAI provides the textEmbedding004 embedder
vertexAI(),
// the local vector store requires an embedder to translate from text to vector
lancedb([
{
dbUri: '.db', // optional lancedb uri, default to .db
tableName: 'table', // optional table name, default to table
embedder: textEmbedding004,
},
]),
],
});
```
You can run this app with the following command:
```bash
genkit start -- tsx --watch src/index.ts
```
This'll add LanceDB as a retriever and indexer to the genkit instance. You can see it in the GUI view
<img width="1710" alt="Screenshot 2025-05-11 at 7 21 05PM" src="https://github.com/user-attachments/assets/e752f7f4-785b-4797-a11e-72ab06a531b7" />
**Testing retrieval on a sample table**
Let's see the raw retrieval results
<img width="1710" alt="Screenshot 2025-05-11 at 7 21 05PM" src="https://github.com/user-attachments/assets/b8d356ed-8421-4790-8fc0-d6af563b9657" />
On running this query, you'll 5 results fetched from the lancedb table, where each result looks something like this:
<img width="1417" alt="Screenshot 2025-05-11 at 7 21 18PM" src="https://github.com/user-attachments/assets/77429525-36e2-4da6-a694-e58c1cf9eb83" />
## Creating a custom RAG flow
Now that we've seen how you can use LanceDB for in a genkit pipeline, let's refine the flow and create a RAG. A RAG flow will consist of an index and a retreiver with its outputs postprocessed an fed into an LLM for final response
### Creating custom indexer flows
You can also create custom indexer flows, utilizing more options and features provided by LanceDB.
```ts
export const menuPdfIndexer = lancedbIndexerRef({
// Using all defaults, for dbUri, tableName, and embedder, etc
});
const chunkingConfig = {
minLength: 1000,
maxLength: 2000,
splitter: 'sentence',
overlap: 100,
delimiters: '',
} as any;
async function extractTextFromPdf(filePath: string) {
const pdfFile = path.resolve(filePath);
const dataBuffer = await readFile(pdfFile);
const data = await pdf(dataBuffer);
return data.text;
}
export const indexMenu = ai.defineFlow(
{
name: 'indexMenu',
inputSchema: z.string().describe('PDF file path'),
outputSchema: z.void(),
},
async (filePath: string) => {
filePath = path.resolve(filePath);
// Read the pdf.
const pdfTxt = await ai.run('extract-text', () =>
extractTextFromPdf(filePath)
);
// Divide the pdf text into segments.
const chunks = await ai.run('chunk-it', async () =>
chunk(pdfTxt, chunkingConfig)
);
// Convert chunks of text into documents to store in the index.
const documents = chunks.map((text) => {
return Document.fromText(text, { filePath });
});
// Add documents to the index.
await ai.index({
indexer: menuPdfIndexer,
documents,
options: {
writeMode: WriteMode.Overwrite,
} as any
});
}
);
```
<img width="1316" alt="Screenshot 2025-05-11 at 8 35 56PM" src="https://github.com/user-attachments/assets/e2a20ce4-d1d0-4fa2-9a84-f2cc26e3a29f" />
In your console, you can see the logs
<img width="511" alt="Screenshot 2025-05-11 at 7 19 14PM" src="https://github.com/user-attachments/assets/243f26c5-ed38-40b6-b661-002f40f0423a" />
### Creating custom retriever flows
You can also create custom retriever flows, utilizing more options and features provided by LanceDB.
```ts
export const menuRetriever = lancedbRetrieverRef({
tableName: "table", // Use the same table name as the indexer.
displayName: "Menu", // Use a custom display name.
export const menuQAFlow = ai.defineFlow(
{ name: "Menu", inputSchema: z.string(), outputSchema: z.string() },
async (input: string) => {
// retrieve relevant documents
const docs = await ai.retrieve({
retriever: menuRetriever,
query: input,
options: {
k: 3,
},
});
const extractedContent = docs.map(doc => {
if (doc.content && Array.isArray(doc.content) && doc.content.length > 0) {
if (doc.content[0].media && doc.content[0].media.url) {
return doc.content[0].media.url;
}
}
return "No content found";
});
console.log("Extracted content:", extractedContent);
const { text } = await ai.generate({
model: gemini('gemini-2.0-flash'),
prompt: `
You are acting as a helpful AI assistant that can answer
questions about the food available on the menu at Genkit Grub Pub.
Use only the context provided to answer the question.
If you don't know, do not make up an answer.
Do not add or change items on the menu.
Context:
${extractedContent.join('\n\n')}
Question: ${input}`,
docs,
});
return text;
}
);
```
Now using our retrieval flow, we can ask question about the ingsted PDF
<img width="1306" alt="Screenshot 2025-05-11 at 7 18 45PM" src="https://github.com/user-attachments/assets/86c66b13-7c12-4d5f-9d81-ae36bfb1c346" />

View File

@@ -33,7 +33,7 @@ Construct a MergeInsertBuilder. __Internal use only.__
### execute() ### execute()
```ts ```ts
execute(data, execOptions?): Promise<MergeResult> execute(data): Promise<MergeStats>
``` ```
Executes the merge insert operation Executes the merge insert operation
@@ -42,13 +42,11 @@ Executes the merge insert operation
* **data**: [`Data`](../type-aliases/Data.md) * **data**: [`Data`](../type-aliases/Data.md)
* **execOptions?**: `Partial`&lt;[`WriteExecutionOptions`](../interfaces/WriteExecutionOptions.md)&gt;
#### Returns #### Returns
`Promise`&lt;[`MergeResult`](../interfaces/MergeResult.md)&gt; `Promise`&lt;[`MergeStats`](../interfaces/MergeStats.md)&gt;
the merge result Statistics about the merge operation: counts of inserted, updated, and deleted rows
*** ***

View File

@@ -40,7 +40,7 @@ Returns the name of the table
### add() ### add()
```ts ```ts
abstract add(data, options?): Promise<AddResult> abstract add(data, options?): Promise<void>
``` ```
Insert records into this Table. Insert records into this Table.
@@ -54,17 +54,14 @@ Insert records into this Table.
#### Returns #### Returns
`Promise`&lt;[`AddResult`](../interfaces/AddResult.md)&gt; `Promise`&lt;`void`&gt;
A promise that resolves to an object
containing the new version number of the table
*** ***
### addColumns() ### addColumns()
```ts ```ts
abstract addColumns(newColumnTransforms): Promise<AddColumnsResult> abstract addColumns(newColumnTransforms): Promise<void>
``` ```
Add new columns with defined values. Add new columns with defined values.
@@ -79,17 +76,14 @@ Add new columns with defined values.
#### Returns #### Returns
`Promise`&lt;[`AddColumnsResult`](../interfaces/AddColumnsResult.md)&gt; `Promise`&lt;`void`&gt;
A promise that resolves to an object
containing the new version number of the table after adding the columns.
*** ***
### alterColumns() ### alterColumns()
```ts ```ts
abstract alterColumns(columnAlterations): Promise<AlterColumnsResult> abstract alterColumns(columnAlterations): Promise<void>
``` ```
Alter the name or nullability of columns. Alter the name or nullability of columns.
@@ -102,10 +96,7 @@ Alter the name or nullability of columns.
#### Returns #### Returns
`Promise`&lt;[`AlterColumnsResult`](../interfaces/AlterColumnsResult.md)&gt; `Promise`&lt;`void`&gt;
A promise that resolves to an object
containing the new version number of the table after altering the columns.
*** ***
@@ -261,7 +252,7 @@ await table.createIndex("my_float_col");
### delete() ### delete()
```ts ```ts
abstract delete(predicate): Promise<DeleteResult> abstract delete(predicate): Promise<void>
``` ```
Delete the rows that satisfy the predicate. Delete the rows that satisfy the predicate.
@@ -272,10 +263,7 @@ Delete the rows that satisfy the predicate.
#### Returns #### Returns
`Promise`&lt;[`DeleteResult`](../interfaces/DeleteResult.md)&gt; `Promise`&lt;`void`&gt;
A promise that resolves to an object
containing the new version number of the table
*** ***
@@ -296,7 +284,7 @@ Return a brief description of the table
### dropColumns() ### dropColumns()
```ts ```ts
abstract dropColumns(columnNames): Promise<DropColumnsResult> abstract dropColumns(columnNames): Promise<void>
``` ```
Drop one or more columns from the dataset Drop one or more columns from the dataset
@@ -315,10 +303,7 @@ then call ``cleanup_files`` to remove the old files.
#### Returns #### Returns
`Promise`&lt;[`DropColumnsResult`](../interfaces/DropColumnsResult.md)&gt; `Promise`&lt;`void`&gt;
A promise that resolves to an object
containing the new version number of the table after dropping the columns.
*** ***
@@ -693,7 +678,7 @@ Return the table as an arrow table
#### update(opts) #### update(opts)
```ts ```ts
abstract update(opts): Promise<UpdateResult> abstract update(opts): Promise<void>
``` ```
Update existing records in the Table Update existing records in the Table
@@ -704,10 +689,7 @@ Update existing records in the Table
##### Returns ##### Returns
`Promise`&lt;[`UpdateResult`](../interfaces/UpdateResult.md)&gt; `Promise`&lt;`void`&gt;
A promise that resolves to an object containing
the number of rows updated and the new version number
##### Example ##### Example
@@ -718,7 +700,7 @@ table.update({where:"x = 2", values:{"vector": [10, 10]}})
#### update(opts) #### update(opts)
```ts ```ts
abstract update(opts): Promise<UpdateResult> abstract update(opts): Promise<void>
``` ```
Update existing records in the Table Update existing records in the Table
@@ -729,10 +711,7 @@ Update existing records in the Table
##### Returns ##### Returns
`Promise`&lt;[`UpdateResult`](../interfaces/UpdateResult.md)&gt; `Promise`&lt;`void`&gt;
A promise that resolves to an object containing
the number of rows updated and the new version number
##### Example ##### Example
@@ -743,7 +722,7 @@ table.update({where:"x = 2", valuesSql:{"x": "x + 1"}})
#### update(updates, options) #### update(updates, options)
```ts ```ts
abstract update(updates, options?): Promise<UpdateResult> abstract update(updates, options?): Promise<void>
``` ```
Update existing records in the Table Update existing records in the Table
@@ -766,6 +745,10 @@ repeatedly calilng this method.
* **updates**: `Record`&lt;`string`, `string`&gt; \| `Map`&lt;`string`, `string`&gt; * **updates**: `Record`&lt;`string`, `string`&gt; \| `Map`&lt;`string`, `string`&gt;
the the
columns to update columns to update
Keys in the map should specify the name of the column to update.
Values in the map provide the new value of the column. These can
be SQL literal strings (e.g. "7" or "'foo'") or they can be expressions
based on the row being updated (e.g. "my_col + 1")
* **options?**: `Partial`&lt;[`UpdateOptions`](../interfaces/UpdateOptions.md)&gt; * **options?**: `Partial`&lt;[`UpdateOptions`](../interfaces/UpdateOptions.md)&gt;
additional options to control additional options to control
@@ -773,15 +756,7 @@ repeatedly calilng this method.
##### Returns ##### Returns
`Promise`&lt;[`UpdateResult`](../interfaces/UpdateResult.md)&gt; `Promise`&lt;`void`&gt;
A promise that resolves to an object
containing the number of rows updated and the new version number
Keys in the map should specify the name of the column to update.
Values in the map provide the new value of the column. These can
be SQL literal strings (e.g. "7" or "'foo'") or they can be expressions
based on the row being updated (e.g. "my_col + 1")
*** ***

View File

@@ -34,18 +34,13 @@
## Interfaces ## Interfaces
- [AddColumnsResult](interfaces/AddColumnsResult.md)
- [AddColumnsSql](interfaces/AddColumnsSql.md) - [AddColumnsSql](interfaces/AddColumnsSql.md)
- [AddDataOptions](interfaces/AddDataOptions.md) - [AddDataOptions](interfaces/AddDataOptions.md)
- [AddResult](interfaces/AddResult.md)
- [AlterColumnsResult](interfaces/AlterColumnsResult.md)
- [ClientConfig](interfaces/ClientConfig.md) - [ClientConfig](interfaces/ClientConfig.md)
- [ColumnAlteration](interfaces/ColumnAlteration.md) - [ColumnAlteration](interfaces/ColumnAlteration.md)
- [CompactionStats](interfaces/CompactionStats.md) - [CompactionStats](interfaces/CompactionStats.md)
- [ConnectionOptions](interfaces/ConnectionOptions.md) - [ConnectionOptions](interfaces/ConnectionOptions.md)
- [CreateTableOptions](interfaces/CreateTableOptions.md) - [CreateTableOptions](interfaces/CreateTableOptions.md)
- [DeleteResult](interfaces/DeleteResult.md)
- [DropColumnsResult](interfaces/DropColumnsResult.md)
- [ExecutableQuery](interfaces/ExecutableQuery.md) - [ExecutableQuery](interfaces/ExecutableQuery.md)
- [FragmentStatistics](interfaces/FragmentStatistics.md) - [FragmentStatistics](interfaces/FragmentStatistics.md)
- [FragmentSummaryStats](interfaces/FragmentSummaryStats.md) - [FragmentSummaryStats](interfaces/FragmentSummaryStats.md)
@@ -59,7 +54,7 @@
- [IndexStatistics](interfaces/IndexStatistics.md) - [IndexStatistics](interfaces/IndexStatistics.md)
- [IvfFlatOptions](interfaces/IvfFlatOptions.md) - [IvfFlatOptions](interfaces/IvfFlatOptions.md)
- [IvfPqOptions](interfaces/IvfPqOptions.md) - [IvfPqOptions](interfaces/IvfPqOptions.md)
- [MergeResult](interfaces/MergeResult.md) - [MergeStats](interfaces/MergeStats.md)
- [OpenTableOptions](interfaces/OpenTableOptions.md) - [OpenTableOptions](interfaces/OpenTableOptions.md)
- [OptimizeOptions](interfaces/OptimizeOptions.md) - [OptimizeOptions](interfaces/OptimizeOptions.md)
- [OptimizeStats](interfaces/OptimizeStats.md) - [OptimizeStats](interfaces/OptimizeStats.md)
@@ -70,9 +65,7 @@
- [TableStatistics](interfaces/TableStatistics.md) - [TableStatistics](interfaces/TableStatistics.md)
- [TimeoutConfig](interfaces/TimeoutConfig.md) - [TimeoutConfig](interfaces/TimeoutConfig.md)
- [UpdateOptions](interfaces/UpdateOptions.md) - [UpdateOptions](interfaces/UpdateOptions.md)
- [UpdateResult](interfaces/UpdateResult.md)
- [Version](interfaces/Version.md) - [Version](interfaces/Version.md)
- [WriteExecutionOptions](interfaces/WriteExecutionOptions.md)
## Type Aliases ## Type Aliases

View File

@@ -1,15 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / AddColumnsResult
# Interface: AddColumnsResult
## Properties
### version
```ts
version: number;
```

View File

@@ -1,15 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / AddResult
# Interface: AddResult
## Properties
### version
```ts
version: number;
```

View File

@@ -1,15 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / AlterColumnsResult
# Interface: AlterColumnsResult
## Properties
### version
```ts
version: number;
```

View File

@@ -1,15 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / DeleteResult
# Interface: DeleteResult
## Properties
### version
```ts
version: number;
```

View File

@@ -1,15 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / DropColumnsResult
# Interface: DropColumnsResult
## Properties
### version
```ts
version: number;
```

View File

@@ -1,39 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MergeResult
# Interface: MergeResult
## Properties
### numDeletedRows
```ts
numDeletedRows: number;
```
***
### numInsertedRows
```ts
numInsertedRows: number;
```
***
### numUpdatedRows
```ts
numUpdatedRows: number;
```
***
### version
```ts
version: number;
```

View File

@@ -0,0 +1,31 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / MergeStats
# Interface: MergeStats
## Properties
### numDeletedRows
```ts
numDeletedRows: bigint;
```
***
### numInsertedRows
```ts
numInsertedRows: bigint;
```
***
### numUpdatedRows
```ts
numUpdatedRows: bigint;
```

View File

@@ -1,23 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / UpdateResult
# Interface: UpdateResult
## Properties
### rowsUpdated
```ts
rowsUpdated: number;
```
***
### version
```ts
version: number;
```

View File

@@ -1,26 +0,0 @@
[**@lancedb/lancedb**](../README.md) • **Docs**
***
[@lancedb/lancedb](../globals.md) / WriteExecutionOptions
# Interface: WriteExecutionOptions
## Properties
### timeoutMs?
```ts
optional timeoutMs: number;
```
Maximum time to run the operation before cancelling it.
By default, there is a 30-second timeout that is only enforced after the
first attempt. This is to prevent spending too long retrying to resolve
conflicts. For example, if a write attempt takes 20 seconds and fails,
the second attempt will be cancelled after 10 seconds, hitting the
30-second timeout. However, a write that takes one hour and succeeds on the
first attempt will not be cancelled.
When this is set, the timeout is enforced on all attempts, including the first.

View File

@@ -1,53 +0,0 @@
# Apache Datafusion
In Python, LanceDB tables can also be queried with [Apache Datafusion](https://datafusion.apache.org/), an extensible query engine written in Rust that uses Apache Arrow as its in-memory format. This means you can write complex SQL queries to analyze your data in LanceDB.
This integration is done via [Datafusion FFI](https://docs.rs/datafusion-ffi/latest/datafusion_ffi/), which provides a native integration between LanceDB and Datafusion.
The Datafusion FFI allows to pass down column selections and basic filters to LanceDB, reducing the amount of scanned data when executing your query. Additionally, the integration allows streaming data from LanceDB tables which allows to do aggregation larger-than-memory.
We can demonstrate this by first installing `datafusion` and `lancedb`.
```shell
pip install datafusion lancedb
```
We will re-use the dataset [created previously](./pandas_and_pyarrow.md):
```python
import lancedb
from datafusion import SessionContext
from lance import FFILanceTableProvider
db = lancedb.connect("data/sample-lancedb")
data = [
{"vector": [3.1, 4.1], "item": "foo", "price": 10.0},
{"vector": [5.9, 26.5], "item": "bar", "price": 20.0}
]
lance_table = db.create_table("lance_table", data)
ctx = SessionContext()
ffi_lance_table = FFILanceTableProvider(
lance_table.to_lance(), with_row_id=True, with_row_addr=True
)
ctx.register_table_provider("ffi_lance_table", ffi_lance_table)
```
The `to_lance` method converts the LanceDB table to a `LanceDataset`, which is accessible to Datafusion through the Datafusion FFI integration layer.
To query the resulting Lance dataset in Datafusion, you first need to register the dataset with Datafusion and then just reference it by the same name in your SQL query.
```python
ctx.table("ffi_lance_table")
ctx.sql("SELECT * FROM ffi_lance_table")
```
```
┌─────────────┬─────────┬────────┬─────────────────┬─────────────────┐
│ vector │ item │ price │ _rowid │ _rowaddr │
│ float[] │ varchar │ double │ bigint unsigned │ bigint unsigned │
├─────────────┼─────────┼────────┼─────────────────┼─────────────────┤
│ [3.1, 4.1] │ foo │ 10.0 │ 0 │ 0 │
│ [5.9, 26.5] │ bar │ 20.0 │ 1 │ 1 │
└─────────────┴─────────┴────────┴─────────────────┴─────────────────┘
```

101
docs/src/quickstart.md Normal file
View File

@@ -0,0 +1,101 @@
# Getting Started with LanceDB: A Minimal Vector Search Tutorial
Let's set up a LanceDB database, insert vector data, and perform a simple vector search. We'll use simple character classes like "knight" and "rogue" to illustrate semantic relevance.
## 1. Install Dependencies
Before starting, make sure you have the necessary packages:
```bash
pip install lancedb pandas numpy
```
## 2. Import Required Libraries
```python
import lancedb
import pandas as pd
import numpy as np
```
## 3. Connect to LanceDB
You can use a local directory to store your database:
```python
db = lancedb.connect("./lancedb")
```
## 4. Create Sample Data
Add sample text data and corresponding 4D vectors:
```python
data = pd.DataFrame([
{"id": "1", "vector": [1.0, 0.0, 0.0, 0.0], "text": "knight"},
{"id": "2", "vector": [0.9, 0.1, 0.0, 0.0], "text": "warrior"},
{"id": "3", "vector": [0.0, 1.0, 0.0, 0.0], "text": "rogue"},
{"id": "4", "vector": [0.0, 0.9, 0.1, 0.0], "text": "thief"},
{"id": "5", "vector": [0.5, 0.5, 0.0, 0.0], "text": "ranger"},
])
```
## 5. Create a Table in LanceDB
```python
table = db.create_table("rpg_classes", data=data, mode="overwrite")
```
Let's see how the table looks:
```python
print(data)
```
| id | vector | text |
|----|--------|------|
| 1 | [1.0, 0.0, 0.0, 0.0] | knight |
| 2 | [0.9, 0.1, 0.0, 0.0] | warrior |
| 3 | [0.0, 1.0, 0.0, 0.0] | rogue |
| 4 | [0.0, 0.9, 0.1, 0.0] | thief |
| 5 | [0.5, 0.5, 0.0, 0.0] | ranger |
## 6. Perform a Vector Search
Search for the most similar character classes to our query vector:
```python
# Query as if we are searching for "rogue"
results = table.search([0.95, 0.05, 0.0, 0.0]).limit(3).to_df()
print(results)
```
This will return the top 3 closest classes to the vector, effectively showing how LanceDB can be used for semantic search.
| id | vector | text | _distance |
|------|------------------------|----------|-----------|
| 3 | [0.0, 1.0, 0.0, 0.0] | rogue | 0.00 |
| 4 | [0.0, 0.9, 0.1, 0.0] | thief | 0.02 |
| 5 | [0.5, 0.5, 0.0, 0.0] | ranger | 0.50 |
Let's try searching for "knight"
```python
query_vector = [1.0, 0.0, 0.0, 0.0]
results = table.search(query_vector).limit(3).to_pandas()
print(results)
```
| id | vector | text | _distance |
|------|------------------------|----------|-----------|
| 1 | [1.0, 0.0, 0.0, 0.0] | knight | 0.00 |
| 2 | [0.9, 0.1, 0.0, 0.0] | warrior | 0.02 |
| 5 | [0.5, 0.5, 0.0, 0.0] | ranger | 0.50 |
## Next Steps
That's it - you just conducted vector search!
For more beginner tips, check out the [Basic Usage](basic.md) guide.

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.20.0-beta.2</version> <version>0.19.1-beta.1</version>
<relativePath>../pom.xml</relativePath> <relativePath>../pom.xml</relativePath>
</parent> </parent>

View File

@@ -6,7 +6,7 @@
<groupId>com.lancedb</groupId> <groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId> <artifactId>lancedb-parent</artifactId>
<version>0.20.0-beta.2</version> <version>0.19.1-beta.1</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>LanceDB Parent</name> <name>LanceDB Parent</name>

49
node/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "vectordb", "name": "vectordb",
"version": "0.20.0-beta.2", "version": "0.19.1-beta.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "vectordb", "name": "vectordb",
"version": "0.20.0-beta.2", "version": "0.19.1-beta.1",
"cpu": [ "cpu": [
"x64", "x64",
"arm64" "arm64"
@@ -52,11 +52,11 @@
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@lancedb/vectordb-darwin-arm64": "0.20.0-beta.2", "@lancedb/vectordb-darwin-arm64": "0.19.1-beta.1",
"@lancedb/vectordb-darwin-x64": "0.20.0-beta.2", "@lancedb/vectordb-darwin-x64": "0.19.1-beta.1",
"@lancedb/vectordb-linux-arm64-gnu": "0.20.0-beta.2", "@lancedb/vectordb-linux-arm64-gnu": "0.19.1-beta.1",
"@lancedb/vectordb-linux-x64-gnu": "0.20.0-beta.2", "@lancedb/vectordb-linux-x64-gnu": "0.19.1-beta.1",
"@lancedb/vectordb-win32-x64-msvc": "0.20.0-beta.2" "@lancedb/vectordb-win32-x64-msvc": "0.19.1-beta.1"
}, },
"peerDependencies": { "peerDependencies": {
"@apache-arrow/ts": "^14.0.2", "@apache-arrow/ts": "^14.0.2",
@@ -327,60 +327,65 @@
} }
}, },
"node_modules/@lancedb/vectordb-darwin-arm64": { "node_modules/@lancedb/vectordb-darwin-arm64": {
"version": "0.20.0-beta.2", "version": "0.19.1-beta.1",
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-arm64/-/vectordb-darwin-arm64-0.20.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-arm64/-/vectordb-darwin-arm64-0.19.1-beta.1.tgz",
"integrity": "sha512-H9PmJ/5KSvstVzR8Q7T22+eHRjJZ2ef3aA3gdFxXvoMi3xQ0MGIxz23HuKHGTRT4tfl1nNnpOPb2W7Na8etK9w==", "integrity": "sha512-Epvel0pF5TM6MtIWQ2KhqezqSSHTL3Wr7a2rGAwz6X/XY23i6DbMPpPs0HyeIDzDrhxNfE3cz3S+SiCA6xpR0g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ]
}, },
"node_modules/@lancedb/vectordb-darwin-x64": { "node_modules/@lancedb/vectordb-darwin-x64": {
"version": "0.20.0-beta.2", "version": "0.19.1-beta.1",
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.20.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-darwin-x64/-/vectordb-darwin-x64-0.19.1-beta.1.tgz",
"integrity": "sha512-9AQkv4tIys+vg0cplZtSE48o61jd7EnmuMkUht+vLORL5/HAma84eAoU9lXHT7zAtPAQmL+98Bfvcsx7fJ6mVw==", "integrity": "sha512-hOiUSlIoISbiXytp46hToi/r6sF5pImAsfbzCsIq8ExDV4TPa8fjbhcIT80vxxOwc2mpSSK4HsVJYod95RSbEQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
] ]
}, },
"node_modules/@lancedb/vectordb-linux-arm64-gnu": { "node_modules/@lancedb/vectordb-linux-arm64-gnu": {
"version": "0.20.0-beta.2", "version": "0.19.1-beta.1",
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.20.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-arm64-gnu/-/vectordb-linux-arm64-gnu-0.19.1-beta.1.tgz",
"integrity": "sha512-eQWoJz2ePml7NyEInTBeakWx56+5c6r2p3F+iHC5tsLuznn6eFX90koXJunRxH1WXHDN48ECUlEmKypgfEmn4w==", "integrity": "sha512-/1JhGVDEngwrlM8o2TNW8G6nJ9U/VgHKAORmj/cTA7O30helJIoo9jfvUAUy+vZ4VoEwRXQbMI+gaYTg0l3MTg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@lancedb/vectordb-linux-x64-gnu": { "node_modules/@lancedb/vectordb-linux-x64-gnu": {
"version": "0.20.0-beta.2", "version": "0.19.1-beta.1",
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.20.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-linux-x64-gnu/-/vectordb-linux-x64-gnu-0.19.1-beta.1.tgz",
"integrity": "sha512-/+84U+Dt07m8Jk0b8h+SvOzlrynITPP3SDBOlB+OonwmGSxirXhc8gkfNZctgXOJYKMyRIRSsMHP/QNjOp2ajA==", "integrity": "sha512-zNRGSSUt8nTJMmll4NdxhQjwxR8Rezq3T4dsRoiDts5ienMam5HFjYiZ3FkDZQo16rgq2BcbFuH1G8u1chywlg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
] ]
}, },
"node_modules/@lancedb/vectordb-win32-x64-msvc": { "node_modules/@lancedb/vectordb-win32-x64-msvc": {
"version": "0.20.0-beta.2", "version": "0.19.1-beta.1",
"resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.20.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/@lancedb/vectordb-win32-x64-msvc/-/vectordb-win32-x64-msvc-0.19.1-beta.1.tgz",
"integrity": "sha512-bgdunAPnknBh/5oO+vr6RXMr6wb3hHugNPXcIidxYMQvgFa8uhaAKtgYkAKuoyUReOYo8DGtVkZxNUUpZbF7/A==", "integrity": "sha512-yV550AJGlsIFdm1KoHQPJ1TZx121ZXCIdebBtBZj3wOObIhyB/i0kZAtGvwjkmr7EYyfzt1EHZzbjSGVdehIAA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "Apache-2.0",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"

View File

@@ -1,6 +1,6 @@
{ {
"name": "vectordb", "name": "vectordb",
"version": "0.20.0-beta.2", "version": "0.19.1-beta.1",
"description": " Serverless, low-latency vector database for AI applications", "description": " Serverless, low-latency vector database for AI applications",
"private": false, "private": false,
"main": "dist/index.js", "main": "dist/index.js",
@@ -89,10 +89,10 @@
} }
}, },
"optionalDependencies": { "optionalDependencies": {
"@lancedb/vectordb-darwin-x64": "0.20.0-beta.2", "@lancedb/vectordb-darwin-x64": "0.19.1-beta.1",
"@lancedb/vectordb-darwin-arm64": "0.20.0-beta.2", "@lancedb/vectordb-darwin-arm64": "0.19.1-beta.1",
"@lancedb/vectordb-linux-x64-gnu": "0.20.0-beta.2", "@lancedb/vectordb-linux-x64-gnu": "0.19.1-beta.1",
"@lancedb/vectordb-linux-arm64-gnu": "0.20.0-beta.2", "@lancedb/vectordb-linux-arm64-gnu": "0.19.1-beta.1",
"@lancedb/vectordb-win32-x64-msvc": "0.20.0-beta.2" "@lancedb/vectordb-win32-x64-msvc": "0.19.1-beta.1"
} }
} }

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "lancedb-nodejs" name = "lancedb-nodejs"
edition.workspace = true edition.workspace = true
version = "0.20.0-beta.2" version = "0.19.1-beta.1"
license.workspace = true license.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true
@@ -30,7 +30,6 @@ log.workspace = true
# Workaround for build failure until we can fix it. # Workaround for build failure until we can fix it.
aws-lc-sys = "=0.28.0" aws-lc-sys = "=0.28.0"
aws-lc-rs = "=1.13.0"
[build-dependencies] [build-dependencies]
napi-build = "2.1" napi-build = "2.1"

View File

@@ -34,7 +34,6 @@ import {
} from "../lancedb/embedding"; } from "../lancedb/embedding";
import { Index } from "../lancedb/indices"; import { Index } from "../lancedb/indices";
import { instanceOfFullTextQuery } from "../lancedb/query"; import { instanceOfFullTextQuery } from "../lancedb/query";
import exp = require("constants");
describe.each([arrow15, arrow16, arrow17, arrow18])( describe.each([arrow15, arrow16, arrow17, arrow18])(
"Given a table", "Given a table",
@@ -96,9 +95,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
}); });
it("should overwrite data if asked", async () => { it("should overwrite data if asked", async () => {
const addRes = await table.add([{ id: 1 }, { id: 2 }]); await table.add([{ id: 1 }, { id: 2 }]);
expect(addRes).toHaveProperty("version");
expect(addRes.version).toBe(2);
await table.add([{ id: 1 }], { mode: "overwrite" }); await table.add([{ id: 1 }], { mode: "overwrite" });
await expect(table.countRows()).resolves.toBe(1); await expect(table.countRows()).resolves.toBe(1);
}); });
@@ -114,11 +111,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
await table.add([{ id: 1 }]); await table.add([{ id: 1 }]);
expect(await table.countRows("id == 1")).toBe(1); expect(await table.countRows("id == 1")).toBe(1);
expect(await table.countRows("id == 7")).toBe(0); expect(await table.countRows("id == 7")).toBe(0);
const updateRes = await table.update({ id: "7" }); await table.update({ id: "7" });
expect(updateRes).toHaveProperty("version");
expect(updateRes.version).toBe(3);
expect(updateRes).toHaveProperty("rowsUpdated");
expect(updateRes.rowsUpdated).toBe(1);
expect(await table.countRows("id == 1")).toBe(0); expect(await table.countRows("id == 1")).toBe(0);
expect(await table.countRows("id == 7")).toBe(1); expect(await table.countRows("id == 7")).toBe(1);
await table.add([{ id: 2 }]); await table.add([{ id: 2 }]);
@@ -345,16 +338,15 @@ describe("merge insert", () => {
{ a: 3, b: "y" }, { a: 3, b: "y" },
{ a: 4, b: "z" }, { a: 4, b: "z" },
]; ];
const mergeInsertRes = await table const stats = await table
.mergeInsert("a") .mergeInsert("a")
.whenMatchedUpdateAll() .whenMatchedUpdateAll()
.whenNotMatchedInsertAll() .whenNotMatchedInsertAll()
.execute(newData, { timeoutMs: 10_000 }); .execute(newData);
expect(mergeInsertRes).toHaveProperty("version");
expect(mergeInsertRes.version).toBe(2); expect(stats.numInsertedRows).toBe(1n);
expect(mergeInsertRes.numInsertedRows).toBe(1); expect(stats.numUpdatedRows).toBe(2n);
expect(mergeInsertRes.numUpdatedRows).toBe(2); expect(stats.numDeletedRows).toBe(0n);
expect(mergeInsertRes.numDeletedRows).toBe(0);
const expected = [ const expected = [
{ a: 1, b: "a" }, { a: 1, b: "a" },
@@ -373,12 +365,10 @@ describe("merge insert", () => {
{ a: 3, b: "y" }, { a: 3, b: "y" },
{ a: 4, b: "z" }, { a: 4, b: "z" },
]; ];
const mergeInsertRes = await table await table
.mergeInsert("a") .mergeInsert("a")
.whenMatchedUpdateAll({ where: "target.b = 'b'" }) .whenMatchedUpdateAll({ where: "target.b = 'b'" })
.execute(newData); .execute(newData);
expect(mergeInsertRes).toHaveProperty("version");
expect(mergeInsertRes.version).toBe(2);
const expected = [ const expected = [
{ a: 1, b: "a" }, { a: 1, b: "a" },
@@ -463,20 +453,6 @@ describe("merge insert", () => {
res = res.sort((a, b) => a.a - b.a); res = res.sort((a, b) => a.a - b.a);
expect(res).toEqual(expected); expect(res).toEqual(expected);
}); });
test("timeout", async () => {
const newData = [
{ a: 2, b: "x" },
{ a: 4, b: "z" },
];
await expect(
table
.mergeInsert("a")
.whenMatchedUpdateAll()
.whenNotMatchedInsertAll()
.execute(newData, { timeoutMs: 0 }),
).rejects.toThrow("merge insert timed out");
});
}); });
describe("When creating an index", () => { describe("When creating an index", () => {
@@ -1052,19 +1028,15 @@ describe("schema evolution", function () {
{ id: 1n, vector: [0.1, 0.2] }, { id: 1n, vector: [0.1, 0.2] },
]); ]);
// Can create a non-nullable column only through addColumns at the moment. // Can create a non-nullable column only through addColumns at the moment.
const addColumnsRes = await table.addColumns([ await table.addColumns([
{ name: "price", valueSql: "cast(10.0 as double)" }, { name: "price", valueSql: "cast(10.0 as double)" },
]); ]);
expect(addColumnsRes).toHaveProperty("version");
expect(addColumnsRes.version).toBe(2);
expect(await table.schema()).toEqual(schema); expect(await table.schema()).toEqual(schema);
const alterColumnsRes = await table.alterColumns([ await table.alterColumns([
{ path: "id", rename: "new_id" }, { path: "id", rename: "new_id" },
{ path: "price", nullable: true }, { path: "price", nullable: true },
]); ]);
expect(alterColumnsRes).toHaveProperty("version");
expect(alterColumnsRes.version).toBe(3);
const expectedSchema = new Schema([ const expectedSchema = new Schema([
new Field("new_id", new Int64(), true), new Field("new_id", new Int64(), true),
@@ -1182,9 +1154,7 @@ describe("schema evolution", function () {
const table = await con.createTable("vectors", [ const table = await con.createTable("vectors", [
{ id: 1n, vector: [0.1, 0.2] }, { id: 1n, vector: [0.1, 0.2] },
]); ]);
const dropColumnsRes = await table.dropColumns(["vector"]); await table.dropColumns(["vector"]);
expect(dropColumnsRes).toHaveProperty("version");
expect(dropColumnsRes.version).toBe(2);
const expectedSchema = new Schema([new Field("id", new Int64(), true)]); const expectedSchema = new Schema([new Field("id", new Int64(), true)]);
expect(await table.schema()).toEqual(expectedSchema); expect(await table.schema()).toEqual(expectedSchema);
@@ -1301,32 +1271,6 @@ describe("when dealing with tags", () => {
await table.checkoutLatest(); await table.checkoutLatest();
expect(await table.version()).toBe(4); expect(await table.version()).toBe(4);
}); });
it("can checkout and restore tags", async () => {
const conn = await connect(tmpDir.name, {
readConsistencyInterval: 0,
});
const table = await conn.createTable("my_table", [
{ id: 1n, vector: [0.1, 0.2] },
]);
expect(await table.version()).toBe(1);
expect(await table.countRows()).toBe(1);
const tagsManager = await table.tags();
const tag1 = "tag1";
await tagsManager.create(tag1, 1);
await table.add([{ id: 2n, vector: [0.3, 0.4] }]);
const tag2 = "tag2";
await tagsManager.create(tag2, 2);
expect(await table.version()).toBe(2);
await table.checkout(tag1);
expect(await table.version()).toBe(1);
await table.restore();
expect(await table.version()).toBe(3);
expect(await table.countRows()).toBe(1);
await table.add([{ id: 3n, vector: [0.5, 0.6] }]);
expect(await table.countRows()).toBe(2);
});
}); });
describe("when optimizing a dataset", () => { describe("when optimizing a dataset", () => {
@@ -1506,9 +1450,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
]; ];
const table = await db.createTable("test", data); const table = await db.createTable("test", data);
await table.createIndex("text", { await table.createIndex("text", {
config: Index.fts({ config: Index.fts(),
withPosition: true,
}),
}); });
const results = await table.search("lance").toArray(); const results = await table.search("lance").toArray();
@@ -1561,9 +1503,7 @@ describe.each([arrow15, arrow16, arrow17, arrow18])(
]; ];
const table = await db.createTable("test", data); const table = await db.createTable("test", data);
await table.createIndex("text", { await table.createIndex("text", {
config: Index.fts({ config: Index.fts(),
withPosition: true,
}),
}); });
const results = await table.search("world").toArray(); const results = await table.search("world").toArray();

View File

@@ -28,13 +28,7 @@ export {
FragmentSummaryStats, FragmentSummaryStats,
Tags, Tags,
TagContents, TagContents,
MergeResult, MergeStats,
AddResult,
AddColumnsResult,
AlterColumnsResult,
DeleteResult,
DropColumnsResult,
UpdateResult,
} from "./native.js"; } from "./native.js";
export { export {
@@ -86,7 +80,7 @@ export {
ColumnAlteration, ColumnAlteration,
} from "./table"; } from "./table";
export { MergeInsertBuilder, WriteExecutionOptions } from "./merge"; export { MergeInsertBuilder } from "./merge";
export * as embedding from "./embedding"; export * as embedding from "./embedding";
export * as rerankers from "./rerankers"; export * as rerankers from "./rerankers";

View File

@@ -1,7 +1,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors // SPDX-FileCopyrightText: Copyright The LanceDB Authors
import { Data, Schema, fromDataToBuffer } from "./arrow"; import { Data, Schema, fromDataToBuffer } from "./arrow";
import { MergeResult, NativeMergeInsertBuilder } from "./native"; import { MergeStats, NativeMergeInsertBuilder } from "./native";
/** A builder used to create and run a merge insert operation */ /** A builder used to create and run a merge insert operation */
export class MergeInsertBuilder { export class MergeInsertBuilder {
@@ -73,12 +73,9 @@ export class MergeInsertBuilder {
/** /**
* Executes the merge insert operation * Executes the merge insert operation
* *
* @returns {Promise<MergeResult>} the merge result * @returns Statistics about the merge operation: counts of inserted, updated, and deleted rows
*/ */
async execute( async execute(data: Data): Promise<MergeStats> {
data: Data,
execOptions?: Partial<WriteExecutionOptions>,
): Promise<MergeResult> {
let schema: Schema; let schema: Schema;
if (this.#schema instanceof Promise) { if (this.#schema instanceof Promise) {
schema = await this.#schema; schema = await this.#schema;
@@ -86,28 +83,7 @@ export class MergeInsertBuilder {
} else { } else {
schema = this.#schema; schema = this.#schema;
} }
if (execOptions?.timeoutMs !== undefined) {
this.#native.setTimeout(execOptions.timeoutMs);
}
const buffer = await fromDataToBuffer(data, undefined, schema); const buffer = await fromDataToBuffer(data, undefined, schema);
return await this.#native.execute(buffer); return await this.#native.execute(buffer);
} }
} }
export interface WriteExecutionOptions {
/**
* Maximum time to run the operation before cancelling it.
*
* By default, there is a 30-second timeout that is only enforced after the
* first attempt. This is to prevent spending too long retrying to resolve
* conflicts. For example, if a write attempt takes 20 seconds and fails,
* the second attempt will be cancelled after 10 seconds, hitting the
* 30-second timeout. However, a write that takes one hour and succeeds on the
* first attempt will not be cancelled.
*
* When this is set, the timeout is enforced on all attempts, including the first.
*/
timeoutMs?: number;
}

View File

@@ -16,18 +16,12 @@ import { EmbeddingFunctionConfig, getRegistry } from "./embedding/registry";
import { IndexOptions } from "./indices"; import { IndexOptions } from "./indices";
import { MergeInsertBuilder } from "./merge"; import { MergeInsertBuilder } from "./merge";
import { import {
AddColumnsResult,
AddColumnsSql, AddColumnsSql,
AddResult,
AlterColumnsResult,
DeleteResult,
DropColumnsResult,
IndexConfig, IndexConfig,
IndexStatistics, IndexStatistics,
OptimizeStats, OptimizeStats,
TableStatistics, TableStatistics,
Tags, Tags,
UpdateResult,
Table as _NativeTable, Table as _NativeTable,
} from "./native"; } from "./native";
import { import {
@@ -132,19 +126,12 @@ export abstract class Table {
/** /**
* Insert records into this Table. * Insert records into this Table.
* @param {Data} data Records to be inserted into the Table * @param {Data} data Records to be inserted into the Table
* @returns {Promise<AddResult>} A promise that resolves to an object
* containing the new version number of the table
*/ */
abstract add( abstract add(data: Data, options?: Partial<AddDataOptions>): Promise<void>;
data: Data,
options?: Partial<AddDataOptions>,
): Promise<AddResult>;
/** /**
* Update existing records in the Table * Update existing records in the Table
* @param opts.values The values to update. The keys are the column names and the values * @param opts.values The values to update. The keys are the column names and the values
* are the values to set. * are the values to set.
* @returns {Promise<UpdateResult>} A promise that resolves to an object containing
* the number of rows updated and the new version number
* @example * @example
* ```ts * ```ts
* table.update({where:"x = 2", values:{"vector": [10, 10]}}) * table.update({where:"x = 2", values:{"vector": [10, 10]}})
@@ -154,13 +141,11 @@ export abstract class Table {
opts: { opts: {
values: Map<string, IntoSql> | Record<string, IntoSql>; values: Map<string, IntoSql> | Record<string, IntoSql>;
} & Partial<UpdateOptions>, } & Partial<UpdateOptions>,
): Promise<UpdateResult>; ): Promise<void>;
/** /**
* Update existing records in the Table * Update existing records in the Table
* @param opts.valuesSql The values to update. The keys are the column names and the values * @param opts.valuesSql The values to update. The keys are the column names and the values
* are the values to set. The values are SQL expressions. * are the values to set. The values are SQL expressions.
* @returns {Promise<UpdateResult>} A promise that resolves to an object containing
* the number of rows updated and the new version number
* @example * @example
* ```ts * ```ts
* table.update({where:"x = 2", valuesSql:{"x": "x + 1"}}) * table.update({where:"x = 2", valuesSql:{"x": "x + 1"}})
@@ -170,7 +155,7 @@ export abstract class Table {
opts: { opts: {
valuesSql: Map<string, string> | Record<string, string>; valuesSql: Map<string, string> | Record<string, string>;
} & Partial<UpdateOptions>, } & Partial<UpdateOptions>,
): Promise<UpdateResult>; ): Promise<void>;
/** /**
* Update existing records in the Table * Update existing records in the Table
* *
@@ -188,8 +173,6 @@ export abstract class Table {
* repeatedly calilng this method. * repeatedly calilng this method.
* @param {Map<string, string> | Record<string, string>} updates - the * @param {Map<string, string> | Record<string, string>} updates - the
* columns to update * columns to update
* @returns {Promise<UpdateResult>} A promise that resolves to an object
* containing the number of rows updated and the new version number
* *
* Keys in the map should specify the name of the column to update. * Keys in the map should specify the name of the column to update.
* Values in the map provide the new value of the column. These can * Values in the map provide the new value of the column. These can
@@ -201,16 +184,12 @@ export abstract class Table {
abstract update( abstract update(
updates: Map<string, string> | Record<string, string>, updates: Map<string, string> | Record<string, string>,
options?: Partial<UpdateOptions>, options?: Partial<UpdateOptions>,
): Promise<UpdateResult>; ): Promise<void>;
/** Count the total number of rows in the dataset. */ /** Count the total number of rows in the dataset. */
abstract countRows(filter?: string): Promise<number>; abstract countRows(filter?: string): Promise<number>;
/** /** Delete the rows that satisfy the predicate. */
* Delete the rows that satisfy the predicate. abstract delete(predicate: string): Promise<void>;
* @returns {Promise<DeleteResult>} A promise that resolves to an object
* containing the new version number of the table
*/
abstract delete(predicate: string): Promise<DeleteResult>;
/** /**
* Create an index to speed up queries. * Create an index to speed up queries.
* *
@@ -364,23 +343,15 @@ export abstract class Table {
* the SQL expression to use to calculate the value of the new column. These * the SQL expression to use to calculate the value of the new column. These
* expressions will be evaluated for each row in the table, and can * expressions will be evaluated for each row in the table, and can
* reference existing columns in the table. * reference existing columns in the table.
* @returns {Promise<AddColumnsResult>} A promise that resolves to an object
* containing the new version number of the table after adding the columns.
*/ */
abstract addColumns( abstract addColumns(newColumnTransforms: AddColumnsSql[]): Promise<void>;
newColumnTransforms: AddColumnsSql[],
): Promise<AddColumnsResult>;
/** /**
* Alter the name or nullability of columns. * Alter the name or nullability of columns.
* @param {ColumnAlteration[]} columnAlterations One or more alterations to * @param {ColumnAlteration[]} columnAlterations One or more alterations to
* apply to columns. * apply to columns.
* @returns {Promise<AlterColumnsResult>} A promise that resolves to an object
* containing the new version number of the table after altering the columns.
*/ */
abstract alterColumns( abstract alterColumns(columnAlterations: ColumnAlteration[]): Promise<void>;
columnAlterations: ColumnAlteration[],
): Promise<AlterColumnsResult>;
/** /**
* Drop one or more columns from the dataset * Drop one or more columns from the dataset
* *
@@ -391,10 +362,8 @@ export abstract class Table {
* @param {string[]} columnNames The names of the columns to drop. These can * @param {string[]} columnNames The names of the columns to drop. These can
* be nested column references (e.g. "a.b.c") or top-level column names * be nested column references (e.g. "a.b.c") or top-level column names
* (e.g. "a"). * (e.g. "a").
* @returns {Promise<DropColumnsResult>} A promise that resolves to an object
* containing the new version number of the table after dropping the columns.
*/ */
abstract dropColumns(columnNames: string[]): Promise<DropColumnsResult>; abstract dropColumns(columnNames: string[]): Promise<void>;
/** Retrieve the version of the table */ /** Retrieve the version of the table */
abstract version(): Promise<number>; abstract version(): Promise<number>;
@@ -560,12 +529,12 @@ export class LocalTable extends Table {
return tbl.schema; return tbl.schema;
} }
async add(data: Data, options?: Partial<AddDataOptions>): Promise<AddResult> { async add(data: Data, options?: Partial<AddDataOptions>): Promise<void> {
const mode = options?.mode ?? "append"; const mode = options?.mode ?? "append";
const schema = await this.schema(); const schema = await this.schema();
const buffer = await fromDataToBuffer(data, undefined, schema); const buffer = await fromDataToBuffer(data, undefined, schema);
return await this.inner.add(buffer, mode); await this.inner.add(buffer, mode);
} }
async update( async update(
@@ -578,7 +547,7 @@ export class LocalTable extends Table {
valuesSql: Map<string, string> | Record<string, string>; valuesSql: Map<string, string> | Record<string, string>;
} & Partial<UpdateOptions>), } & Partial<UpdateOptions>),
options?: Partial<UpdateOptions>, options?: Partial<UpdateOptions>,
): Promise<UpdateResult> { ) {
const isValues = const isValues =
"values" in optsOrUpdates && typeof optsOrUpdates.values !== "string"; "values" in optsOrUpdates && typeof optsOrUpdates.values !== "string";
const isValuesSql = const isValuesSql =
@@ -625,15 +594,15 @@ export class LocalTable extends Table {
columns = Object.entries(optsOrUpdates as Record<string, string>); columns = Object.entries(optsOrUpdates as Record<string, string>);
predicate = options?.where; predicate = options?.where;
} }
return await this.inner.update(predicate, columns); await this.inner.update(predicate, columns);
} }
async countRows(filter?: string): Promise<number> { async countRows(filter?: string): Promise<number> {
return await this.inner.countRows(filter); return await this.inner.countRows(filter);
} }
async delete(predicate: string): Promise<DeleteResult> { async delete(predicate: string): Promise<void> {
return await this.inner.delete(predicate); await this.inner.delete(predicate);
} }
async createIndex(column: string, options?: Partial<IndexOptions>) { async createIndex(column: string, options?: Partial<IndexOptions>) {
@@ -721,15 +690,11 @@ export class LocalTable extends Table {
// TODO: Support BatchUDF // TODO: Support BatchUDF
async addColumns( async addColumns(newColumnTransforms: AddColumnsSql[]): Promise<void> {
newColumnTransforms: AddColumnsSql[], await this.inner.addColumns(newColumnTransforms);
): Promise<AddColumnsResult> {
return await this.inner.addColumns(newColumnTransforms);
} }
async alterColumns( async alterColumns(columnAlterations: ColumnAlteration[]): Promise<void> {
columnAlterations: ColumnAlteration[],
): Promise<AlterColumnsResult> {
const processedAlterations = columnAlterations.map((alteration) => { const processedAlterations = columnAlterations.map((alteration) => {
if (typeof alteration.dataType === "string") { if (typeof alteration.dataType === "string") {
return { return {
@@ -750,11 +715,11 @@ export class LocalTable extends Table {
} }
}); });
return await this.inner.alterColumns(processedAlterations); await this.inner.alterColumns(processedAlterations);
} }
async dropColumns(columnNames: string[]): Promise<DropColumnsResult> { async dropColumns(columnNames: string[]): Promise<void> {
return await this.inner.dropColumns(columnNames); await this.inner.dropColumns(columnNames);
} }
async version(): Promise<number> { async version(): Promise<number> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -125,30 +125,32 @@ impl Index {
ascii_folding: Option<bool>, ascii_folding: Option<bool>,
) -> Self { ) -> Self {
let mut opts = FtsIndexBuilder::default(); let mut opts = FtsIndexBuilder::default();
let mut tokenizer_configs = opts.tokenizer_configs.clone();
if let Some(with_position) = with_position { if let Some(with_position) = with_position {
opts = opts.with_position(with_position); opts = opts.with_position(with_position);
} }
if let Some(base_tokenizer) = base_tokenizer { if let Some(base_tokenizer) = base_tokenizer {
opts = opts.base_tokenizer(base_tokenizer); tokenizer_configs = tokenizer_configs.base_tokenizer(base_tokenizer);
} }
if let Some(language) = language { if let Some(language) = language {
opts = opts.language(&language).unwrap(); tokenizer_configs = tokenizer_configs.language(&language).unwrap();
} }
if let Some(max_token_length) = max_token_length { if let Some(max_token_length) = max_token_length {
opts = opts.max_token_length(Some(max_token_length as usize)); tokenizer_configs = tokenizer_configs.max_token_length(Some(max_token_length as usize));
} }
if let Some(lower_case) = lower_case { if let Some(lower_case) = lower_case {
opts = opts.lower_case(lower_case); tokenizer_configs = tokenizer_configs.lower_case(lower_case);
} }
if let Some(stem) = stem { if let Some(stem) = stem {
opts = opts.stem(stem); tokenizer_configs = tokenizer_configs.stem(stem);
} }
if let Some(remove_stop_words) = remove_stop_words { if let Some(remove_stop_words) = remove_stop_words {
opts = opts.remove_stop_words(remove_stop_words); tokenizer_configs = tokenizer_configs.remove_stop_words(remove_stop_words);
} }
if let Some(ascii_folding) = ascii_folding { if let Some(ascii_folding) = ascii_folding {
opts = opts.ascii_folding(ascii_folding); tokenizer_configs = tokenizer_configs.ascii_folding(ascii_folding);
} }
opts.tokenizer_configs = tokenizer_configs;
Self { Self {
inner: Mutex::new(Some(LanceDbIndex::FTS(opts))), inner: Mutex::new(Some(LanceDbIndex::FTS(opts))),

View File

@@ -1,13 +1,11 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors // SPDX-FileCopyrightText: Copyright The LanceDB Authors
use std::time::Duration;
use lancedb::{arrow::IntoArrow, ipc::ipc_file_to_batches, table::merge::MergeInsertBuilder}; use lancedb::{arrow::IntoArrow, ipc::ipc_file_to_batches, table::merge::MergeInsertBuilder};
use napi::bindgen_prelude::*; use napi::bindgen_prelude::*;
use napi_derive::napi; use napi_derive::napi;
use crate::{error::convert_error, table::MergeResult}; use crate::error::convert_error;
#[napi] #[napi]
#[derive(Clone)] #[derive(Clone)]
@@ -38,13 +36,8 @@ impl NativeMergeInsertBuilder {
this this
} }
#[napi]
pub fn set_timeout(&mut self, timeout: u32) {
self.inner.timeout(Duration::from_millis(timeout as u64));
}
#[napi(catch_unwind)] #[napi(catch_unwind)]
pub async fn execute(&self, buf: Buffer) -> napi::Result<MergeResult> { pub async fn execute(&self, buf: Buffer) -> napi::Result<MergeStats> {
let data = ipc_file_to_batches(buf.to_vec()) let data = ipc_file_to_batches(buf.to_vec())
.and_then(IntoArrow::into_arrow) .and_then(IntoArrow::into_arrow)
.map_err(|e| { .map_err(|e| {
@@ -53,13 +46,14 @@ impl NativeMergeInsertBuilder {
let this = self.clone(); let this = self.clone();
let res = this.inner.execute(data).await.map_err(|e| { let stats = this.inner.execute(data).await.map_err(|e| {
napi::Error::from_reason(format!( napi::Error::from_reason(format!(
"Failed to execute merge insert: {}", "Failed to execute merge insert: {}",
convert_error(&e) convert_error(&e)
)) ))
})?; })?;
Ok(res.into())
Ok(stats.into())
} }
} }
@@ -68,3 +62,20 @@ impl From<MergeInsertBuilder> for NativeMergeInsertBuilder {
Self { inner } Self { inner }
} }
} }
#[napi(object)]
pub struct MergeStats {
pub num_inserted_rows: BigInt,
pub num_updated_rows: BigInt,
pub num_deleted_rows: BigInt,
}
impl From<lancedb::table::MergeStats> for MergeStats {
fn from(stats: lancedb::table::MergeStats) -> Self {
Self {
num_inserted_rows: stats.num_inserted_rows.into(),
num_updated_rows: stats.num_updated_rows.into(),
num_deleted_rows: stats.num_deleted_rows.into(),
}
}
}

View File

@@ -75,7 +75,7 @@ impl Table {
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
pub async fn add(&self, buf: Buffer, mode: String) -> napi::Result<AddResult> { pub async fn add(&self, buf: Buffer, mode: String) -> napi::Result<()> {
let batches = ipc_file_to_batches(buf.to_vec()) let batches = ipc_file_to_batches(buf.to_vec())
.map_err(|e| napi::Error::from_reason(format!("Failed to read IPC file: {}", e)))?; .map_err(|e| napi::Error::from_reason(format!("Failed to read IPC file: {}", e)))?;
let mut op = self.inner_ref()?.add(batches); let mut op = self.inner_ref()?.add(batches);
@@ -88,8 +88,7 @@ impl Table {
return Err(napi::Error::from_reason(format!("Invalid mode: {}", mode))); return Err(napi::Error::from_reason(format!("Invalid mode: {}", mode)));
}; };
let res = op.execute().await.default_error()?; op.execute().await.default_error()
Ok(res.into())
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
@@ -102,9 +101,8 @@ impl Table {
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
pub async fn delete(&self, predicate: String) -> napi::Result<DeleteResult> { pub async fn delete(&self, predicate: String) -> napi::Result<()> {
let res = self.inner_ref()?.delete(&predicate).await.default_error()?; self.inner_ref()?.delete(&predicate).await.default_error()
Ok(res.into())
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
@@ -170,7 +168,7 @@ impl Table {
&self, &self,
only_if: Option<String>, only_if: Option<String>,
columns: Vec<(String, String)>, columns: Vec<(String, String)>,
) -> napi::Result<UpdateResult> { ) -> napi::Result<u64> {
let mut op = self.inner_ref()?.update(); let mut op = self.inner_ref()?.update();
if let Some(only_if) = only_if { if let Some(only_if) = only_if {
op = op.only_if(only_if); op = op.only_if(only_if);
@@ -178,8 +176,7 @@ impl Table {
for (column_name, value) in columns { for (column_name, value) in columns {
op = op.column(column_name, value); op = op.column(column_name, value);
} }
let res = op.execute().await.default_error()?; op.execute().await.default_error()
Ok(res.into())
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
@@ -193,28 +190,21 @@ impl Table {
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
pub async fn add_columns( pub async fn add_columns(&self, transforms: Vec<AddColumnsSql>) -> napi::Result<()> {
&self,
transforms: Vec<AddColumnsSql>,
) -> napi::Result<AddColumnsResult> {
let transforms = transforms let transforms = transforms
.into_iter() .into_iter()
.map(|sql| (sql.name, sql.value_sql)) .map(|sql| (sql.name, sql.value_sql))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let transforms = NewColumnTransform::SqlExpressions(transforms); let transforms = NewColumnTransform::SqlExpressions(transforms);
let res = self self.inner_ref()?
.inner_ref()?
.add_columns(transforms, None) .add_columns(transforms, None)
.await .await
.default_error()?; .default_error()?;
Ok(res.into()) Ok(())
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
pub async fn alter_columns( pub async fn alter_columns(&self, alterations: Vec<ColumnAlteration>) -> napi::Result<()> {
&self,
alterations: Vec<ColumnAlteration>,
) -> napi::Result<AlterColumnsResult> {
for alteration in &alterations { for alteration in &alterations {
if alteration.rename.is_none() if alteration.rename.is_none()
&& alteration.nullable.is_none() && alteration.nullable.is_none()
@@ -231,23 +221,21 @@ impl Table {
.collect::<std::result::Result<Vec<_>, String>>() .collect::<std::result::Result<Vec<_>, String>>()
.map_err(napi::Error::from_reason)?; .map_err(napi::Error::from_reason)?;
let res = self self.inner_ref()?
.inner_ref()?
.alter_columns(&alterations) .alter_columns(&alterations)
.await .await
.default_error()?; .default_error()?;
Ok(res.into()) Ok(())
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
pub async fn drop_columns(&self, columns: Vec<String>) -> napi::Result<DropColumnsResult> { pub async fn drop_columns(&self, columns: Vec<String>) -> napi::Result<()> {
let col_refs = columns.iter().map(String::as_str).collect::<Vec<_>>(); let col_refs = columns.iter().map(String::as_str).collect::<Vec<_>>();
let res = self self.inner_ref()?
.inner_ref()?
.drop_columns(&col_refs) .drop_columns(&col_refs)
.await .await
.default_error()?; .default_error()?;
Ok(res.into()) Ok(())
} }
#[napi(catch_unwind)] #[napi(catch_unwind)]
@@ -654,105 +642,6 @@ pub struct Version {
pub metadata: HashMap<String, String>, pub metadata: HashMap<String, String>,
} }
#[napi(object)]
pub struct UpdateResult {
pub rows_updated: i64,
pub version: i64,
}
impl From<lancedb::table::UpdateResult> for UpdateResult {
fn from(value: lancedb::table::UpdateResult) -> Self {
Self {
rows_updated: value.rows_updated as i64,
version: value.version as i64,
}
}
}
#[napi(object)]
pub struct AddResult {
pub version: i64,
}
impl From<lancedb::table::AddResult> for AddResult {
fn from(value: lancedb::table::AddResult) -> Self {
Self {
version: value.version as i64,
}
}
}
#[napi(object)]
pub struct DeleteResult {
pub version: i64,
}
impl From<lancedb::table::DeleteResult> for DeleteResult {
fn from(value: lancedb::table::DeleteResult) -> Self {
Self {
version: value.version as i64,
}
}
}
#[napi(object)]
pub struct MergeResult {
pub version: i64,
pub num_inserted_rows: i64,
pub num_updated_rows: i64,
pub num_deleted_rows: i64,
}
impl From<lancedb::table::MergeResult> for MergeResult {
fn from(value: lancedb::table::MergeResult) -> Self {
Self {
version: value.version as i64,
num_inserted_rows: value.num_inserted_rows as i64,
num_updated_rows: value.num_updated_rows as i64,
num_deleted_rows: value.num_deleted_rows as i64,
}
}
}
#[napi(object)]
pub struct AddColumnsResult {
pub version: i64,
}
impl From<lancedb::table::AddColumnsResult> for AddColumnsResult {
fn from(value: lancedb::table::AddColumnsResult) -> Self {
Self {
version: value.version as i64,
}
}
}
#[napi(object)]
pub struct AlterColumnsResult {
pub version: i64,
}
impl From<lancedb::table::AlterColumnsResult> for AlterColumnsResult {
fn from(value: lancedb::table::AlterColumnsResult) -> Self {
Self {
version: value.version as i64,
}
}
}
#[napi(object)]
pub struct DropColumnsResult {
pub version: i64,
}
impl From<lancedb::table::DropColumnsResult> for DropColumnsResult {
fn from(value: lancedb::table::DropColumnsResult) -> Self {
Self {
version: value.version as i64,
}
}
}
#[napi] #[napi]
pub struct TagContents { pub struct TagContents {
pub version: i64, pub version: i64,

View File

@@ -1,5 +1,5 @@
[tool.bumpversion] [tool.bumpversion]
current_version = "0.23.0" current_version = "0.22.1-beta.1"
parse = """(?x) parse = """(?x)
(?P<major>0|[1-9]\\d*)\\. (?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\. (?P<minor>0|[1-9]\\d*)\\.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lancedb-python" name = "lancedb-python"
version = "0.23.0" version = "0.22.1-beta.1"
edition.workspace = true edition.workspace = true
description = "Python bindings for LanceDB" description = "Python bindings for LanceDB"
license.workspace = true license.workspace = true
@@ -14,11 +14,11 @@ name = "_lancedb"
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
arrow = { version = "55.1", features = ["pyarrow"] } arrow = { version = "54.1", features = ["pyarrow"] }
lancedb = { path = "../rust/lancedb", default-features = false } lancedb = { path = "../rust/lancedb", default-features = false }
env_logger.workspace = true env_logger.workspace = true
pyo3 = { version = "0.24", features = ["extension-module", "abi3-py39"] } pyo3 = { version = "0.23", features = ["extension-module", "abi3-py39"] }
pyo3-async-runtimes = { version = "0.24", features = [ pyo3-async-runtimes = { version = "0.23", features = [
"attributes", "attributes",
"tokio-runtime", "tokio-runtime",
] } ] }
@@ -27,7 +27,7 @@ futures.workspace = true
tokio = { version = "1.40", features = ["sync"] } tokio = { version = "1.40", features = ["sync"] }
[build-dependencies] [build-dependencies]
pyo3-build-config = { version = "0.24", features = [ pyo3-build-config = { version = "0.23", features = [
"extension-module", "extension-module",
"abi3-py39", "abi3-py39",
] } ] }

View File

@@ -60,7 +60,6 @@ tests = [
"pyarrow-stubs", "pyarrow-stubs",
"pylance>=0.25", "pylance>=0.25",
"requests", "requests",
"datafusion",
] ]
dev = [ dev = [
"ruff", "ruff",

View File

@@ -36,10 +36,8 @@ class Table:
async def schema(self) -> pa.Schema: ... async def schema(self) -> pa.Schema: ...
async def add( async def add(
self, data: pa.RecordBatchReader, mode: Literal["append", "overwrite"] self, data: pa.RecordBatchReader, mode: Literal["append", "overwrite"]
) -> AddResult: ... ) -> None: ...
async def update( async def update(self, updates: Dict[str, str], where: Optional[str]) -> None: ...
self, updates: Dict[str, str], where: Optional[str]
) -> UpdateResult: ...
async def count_rows(self, filter: Optional[str]) -> int: ... async def count_rows(self, filter: Optional[str]) -> int: ...
async def create_index( async def create_index(
self, self,
@@ -51,14 +49,12 @@ class Table:
async def version(self) -> int: ... async def version(self) -> int: ...
async def checkout(self, version: Union[int, str]): ... async def checkout(self, version: Union[int, str]): ...
async def checkout_latest(self): ... async def checkout_latest(self): ...
async def restore(self, version: Optional[Union[int, str]] = None): ... async def restore(self, version: Optional[int] = None): ...
async def list_indices(self) -> list[IndexConfig]: ... async def list_indices(self) -> list[IndexConfig]: ...
async def delete(self, filter: str) -> DeleteResult: ... async def delete(self, filter: str): ...
async def add_columns(self, columns: list[tuple[str, str]]) -> AddColumnsResult: ... async def add_columns(self, columns: list[tuple[str, str]]) -> None: ...
async def add_columns_with_schema(self, schema: pa.Schema) -> AddColumnsResult: ... async def add_columns_with_schema(self, schema: pa.Schema) -> None: ...
async def alter_columns( async def alter_columns(self, columns: list[dict[str, Any]]) -> None: ...
self, columns: list[dict[str, Any]]
) -> AlterColumnsResult: ...
async def optimize( async def optimize(
self, self,
*, *,
@@ -212,28 +208,3 @@ class OptimizeStats:
class Tag(TypedDict): class Tag(TypedDict):
version: int version: int
manifest_size: int manifest_size: int
class AddResult:
version: int
class DeleteResult:
version: int
class UpdateResult:
rows_updated: int
version: int
class MergeResult:
version: int
num_updated_rows: int
num_inserted_rows: int
num_deleted_rows: int
class AddColumnsResult:
version: int
class AlterColumnsResult:
version: int
class DropColumnsResult:
version: int

View File

@@ -102,7 +102,7 @@ class FTS:
Attributes Attributes
---------- ----------
with_position : bool, default False with_position : bool, default True
Whether to store the position of the token in the document. Setting this Whether to store the position of the token in the document. Setting this
to False can reduce the size of the index and improve indexing speed, to False can reduce the size of the index and improve indexing speed,
but it will disable support for phrase queries. but it will disable support for phrase queries.
@@ -118,25 +118,25 @@ class FTS:
ignored. ignored.
lower_case : bool, default True lower_case : bool, default True
Whether to convert the token to lower case. This makes queries case-insensitive. Whether to convert the token to lower case. This makes queries case-insensitive.
stem : bool, default True stem : bool, default False
Whether to stem the token. Stemming reduces words to their root form. Whether to stem the token. Stemming reduces words to their root form.
For example, in English "running" and "runs" would both be reduced to "run". For example, in English "running" and "runs" would both be reduced to "run".
remove_stop_words : bool, default True remove_stop_words : bool, default False
Whether to remove stop words. Stop words are common words that are often Whether to remove stop words. Stop words are common words that are often
removed from text before indexing. For example, in English "the" and "and". removed from text before indexing. For example, in English "the" and "and".
ascii_folding : bool, default True ascii_folding : bool, default False
Whether to fold ASCII characters. This converts accented characters to Whether to fold ASCII characters. This converts accented characters to
their ASCII equivalent. For example, "café" would be converted to "cafe". their ASCII equivalent. For example, "café" would be converted to "cafe".
""" """
with_position: bool = False with_position: bool = True
base_tokenizer: Literal["simple", "raw", "whitespace"] = "simple" base_tokenizer: Literal["simple", "raw", "whitespace"] = "simple"
language: str = "English" language: str = "English"
max_token_length: Optional[int] = 40 max_token_length: Optional[int] = 40
lower_case: bool = True lower_case: bool = True
stem: bool = True stem: bool = False
remove_stop_words: bool = True remove_stop_words: bool = False
ascii_folding: bool = True ascii_folding: bool = False
@dataclass @dataclass

View File

@@ -4,14 +4,10 @@
from __future__ import annotations from __future__ import annotations
from datetime import timedelta
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, List, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from .common import DATA from .common import DATA
from ._lancedb import (
MergeInsertResult,
)
class LanceMergeInsertBuilder(object): class LanceMergeInsertBuilder(object):
@@ -32,7 +28,6 @@ class LanceMergeInsertBuilder(object):
self._when_not_matched_insert_all = False self._when_not_matched_insert_all = False
self._when_not_matched_by_source_delete = False self._when_not_matched_by_source_delete = False
self._when_not_matched_by_source_condition = None self._when_not_matched_by_source_condition = None
self._timeout = None
def when_matched_update_all( def when_matched_update_all(
self, *, where: Optional[str] = None self, *, where: Optional[str] = None
@@ -83,8 +78,7 @@ class LanceMergeInsertBuilder(object):
new_data: DATA, new_data: DATA,
on_bad_vectors: str = "error", on_bad_vectors: str = "error",
fill_value: float = 0.0, fill_value: float = 0.0,
timeout: Optional[timedelta] = None, ):
) -> MergeInsertResult:
""" """
Executes the merge insert operation Executes the merge insert operation
@@ -101,24 +95,5 @@ class LanceMergeInsertBuilder(object):
One of "error", "drop", "fill". One of "error", "drop", "fill".
fill_value: float, default 0. fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill". The value to use when filling vectors. Only used if on_bad_vectors="fill".
timeout: Optional[timedelta], default None
Maximum time to run the operation before cancelling it.
By default, there is a 30-second timeout that is only enforced after the
first attempt. This is to prevent spending too long retrying to resolve
conflicts. For example, if a write attempt takes 20 seconds and fails,
the second attempt will be cancelled after 10 seconds, hitting the
30-second timeout. However, a write that takes one hour and succeeds on the
first attempt will not be cancelled.
When this is set, the timeout is enforced on all attempts, including
the first.
Returns
-------
MergeInsertResult
version: the new version number of the table after doing merge insert.
""" """
if timeout is not None:
self._timeout = timeout
return self._table._do_merge(self, new_data, on_bad_vectors, fill_value) return self._table._do_merge(self, new_data, on_bad_vectors, fill_value)

View File

@@ -415,7 +415,6 @@ class LanceModel(pydantic.BaseModel):
>>> table.add([ >>> table.add([
... TestModel(name="test", vector=[1.0, 2.0]) ... TestModel(name="test", vector=[1.0, 2.0])
... ]) ... ])
AddResult(version=2)
>>> table.search([0., 0.]).limit(1).to_pydantic(TestModel) >>> table.search([0., 0.]).limit(1).to_pydantic(TestModel)
[TestModel(name='test', vector=FixedSizeList(dim=2))] [TestModel(name='test', vector=FixedSizeList(dim=2))]
""" """

View File

@@ -7,16 +7,7 @@ from functools import cached_property
from typing import Dict, Iterable, List, Optional, Union, Literal from typing import Dict, Iterable, List, Optional, Union, Literal
import warnings import warnings
from lancedb._lancedb import ( from lancedb._lancedb import IndexConfig
AddColumnsResult,
AddResult,
AlterColumnsResult,
DeleteResult,
DropColumnsResult,
IndexConfig,
MergeResult,
UpdateResult,
)
from lancedb.embeddings.base import EmbeddingFunctionConfig from lancedb.embeddings.base import EmbeddingFunctionConfig
from lancedb.index import FTS, BTree, Bitmap, HnswPq, HnswSq, IvfFlat, IvfPq, LabelList from lancedb.index import FTS, BTree, Bitmap, HnswPq, HnswSq, IvfFlat, IvfPq, LabelList
from lancedb.remote.db import LOOP from lancedb.remote.db import LOOP
@@ -47,6 +38,9 @@ class RemoteTable(Table):
def __repr__(self) -> str: def __repr__(self) -> str:
return f"RemoteTable({self.db_name}.{self.name})" return f"RemoteTable({self.db_name}.{self.name})"
def __len__(self) -> int:
self.count_rows(None)
@property @property
def schema(self) -> pa.Schema: def schema(self) -> pa.Schema:
"""The [Arrow Schema](https://arrow.apache.org/docs/python/api/datatypes.html#) """The [Arrow Schema](https://arrow.apache.org/docs/python/api/datatypes.html#)
@@ -97,7 +91,7 @@ class RemoteTable(Table):
def checkout_latest(self): def checkout_latest(self):
return LOOP.run(self._table.checkout_latest()) return LOOP.run(self._table.checkout_latest())
def restore(self, version: Optional[Union[int, str]] = None): def restore(self, version: Optional[int] = None):
return LOOP.run(self._table.restore(version)) return LOOP.run(self._table.restore(version))
def list_indices(self) -> Iterable[IndexConfig]: def list_indices(self) -> Iterable[IndexConfig]:
@@ -149,15 +143,15 @@ class RemoteTable(Table):
*, *,
replace: bool = False, replace: bool = False,
wait_timeout: timedelta = None, wait_timeout: timedelta = None,
with_position: bool = False, with_position: bool = True,
# tokenizer configs: # tokenizer configs:
base_tokenizer: str = "simple", base_tokenizer: str = "simple",
language: str = "English", language: str = "English",
max_token_length: Optional[int] = 40, max_token_length: Optional[int] = 40,
lower_case: bool = True, lower_case: bool = True,
stem: bool = True, stem: bool = False,
remove_stop_words: bool = True, remove_stop_words: bool = False,
ascii_folding: bool = True, ascii_folding: bool = False,
): ):
config = FTS( config = FTS(
with_position=with_position, with_position=with_position,
@@ -269,7 +263,7 @@ class RemoteTable(Table):
mode: str = "append", mode: str = "append",
on_bad_vectors: str = "error", on_bad_vectors: str = "error",
fill_value: float = 0.0, fill_value: float = 0.0,
) -> AddResult: ) -> int:
"""Add more data to the [Table](Table). It has the same API signature as """Add more data to the [Table](Table). It has the same API signature as
the OSS version. the OSS version.
@@ -292,12 +286,8 @@ class RemoteTable(Table):
fill_value: float, default 0. fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill". The value to use when filling vectors. Only used if on_bad_vectors="fill".
Returns
-------
AddResult
An object containing the new version number of the table after adding data.
""" """
return LOOP.run( LOOP.run(
self._table.add( self._table.add(
data, mode=mode, on_bad_vectors=on_bad_vectors, fill_value=fill_value data, mode=mode, on_bad_vectors=on_bad_vectors, fill_value=fill_value
) )
@@ -423,12 +413,10 @@ class RemoteTable(Table):
new_data: DATA, new_data: DATA,
on_bad_vectors: str, on_bad_vectors: str,
fill_value: float, fill_value: float,
) -> MergeResult: ):
return LOOP.run( LOOP.run(self._table._do_merge(merge, new_data, on_bad_vectors, fill_value))
self._table._do_merge(merge, new_data, on_bad_vectors, fill_value)
)
def delete(self, predicate: str) -> DeleteResult: def delete(self, predicate: str):
"""Delete rows from the table. """Delete rows from the table.
This can be used to delete a single row, many rows, all rows, or This can be used to delete a single row, many rows, all rows, or
@@ -443,11 +431,6 @@ class RemoteTable(Table):
The filter must not be empty, or it will error. The filter must not be empty, or it will error.
Returns
-------
DeleteResult
An object containing the new version number of the table after deletion.
Examples Examples
-------- --------
>>> import lancedb >>> import lancedb
@@ -480,7 +463,7 @@ class RemoteTable(Table):
x vector _distance # doctest: +SKIP x vector _distance # doctest: +SKIP
0 2 [3.0, 4.0] 85.0 # doctest: +SKIP 0 2 [3.0, 4.0] 85.0 # doctest: +SKIP
""" """
return LOOP.run(self._table.delete(predicate)) LOOP.run(self._table.delete(predicate))
def update( def update(
self, self,
@@ -488,7 +471,7 @@ class RemoteTable(Table):
values: Optional[dict] = None, values: Optional[dict] = None,
*, *,
values_sql: Optional[Dict[str, str]] = None, values_sql: Optional[Dict[str, str]] = None,
) -> UpdateResult: ):
""" """
This can be used to update zero to all rows depending on how many This can be used to update zero to all rows depending on how many
rows match the where clause. rows match the where clause.
@@ -506,12 +489,6 @@ class RemoteTable(Table):
reference existing columns. For example, {"x": "x + 1"} will increment reference existing columns. For example, {"x": "x + 1"} will increment
the x column by 1. the x column by 1.
Returns
-------
UpdateResult
- rows_updated: The number of rows that were updated
- version: The new version number of the table after the update
Examples Examples
-------- --------
>>> import lancedb >>> import lancedb
@@ -536,7 +513,7 @@ class RemoteTable(Table):
2 2 [10.0, 10.0] # doctest: +SKIP 2 2 [10.0, 10.0] # doctest: +SKIP
""" """
return LOOP.run( LOOP.run(
self._table.update(where=where, updates=values, updates_sql=values_sql) self._table.update(where=where, updates=values, updates_sql=values_sql)
) )
@@ -584,15 +561,13 @@ class RemoteTable(Table):
def count_rows(self, filter: Optional[str] = None) -> int: def count_rows(self, filter: Optional[str] = None) -> int:
return LOOP.run(self._table.count_rows(filter)) return LOOP.run(self._table.count_rows(filter))
def add_columns(self, transforms: Dict[str, str]) -> AddColumnsResult: def add_columns(self, transforms: Dict[str, str]):
return LOOP.run(self._table.add_columns(transforms)) return LOOP.run(self._table.add_columns(transforms))
def alter_columns( def alter_columns(self, *alterations: Iterable[Dict[str, str]]):
self, *alterations: Iterable[Dict[str, str]]
) -> AlterColumnsResult:
return LOOP.run(self._table.alter_columns(*alterations)) return LOOP.run(self._table.alter_columns(*alterations))
def drop_columns(self, columns: Iterable[str]) -> DropColumnsResult: def drop_columns(self, columns: Iterable[str]):
return LOOP.run(self._table.drop_columns(columns)) return LOOP.run(self._table.drop_columns(columns))
def drop_index(self, index_name: str): def drop_index(self, index_name: str):

View File

@@ -78,13 +78,6 @@ if TYPE_CHECKING:
CleanupStats, CleanupStats,
CompactionStats, CompactionStats,
Tag, Tag,
AddColumnsResult,
AddResult,
AlterColumnsResult,
DeleteResult,
DropColumnsResult,
MergeResult,
UpdateResult,
) )
from .db import LanceDBConnection from .db import LanceDBConnection
from .index import IndexConfig from .index import IndexConfig
@@ -557,7 +550,6 @@ class Table(ABC):
Can append new data with [Table.add()][lancedb.table.Table.add]. Can append new data with [Table.add()][lancedb.table.Table.add].
>>> table.add([{"vector": [0.5, 1.3], "b": 4}]) >>> table.add([{"vector": [0.5, 1.3], "b": 4}])
AddResult(version=2)
Can query the table with [Table.search][lancedb.table.Table.search]. Can query the table with [Table.search][lancedb.table.Table.search].
@@ -620,10 +612,6 @@ class Table(ABC):
""" """
raise NotImplementedError raise NotImplementedError
def __len__(self) -> int:
"""The number of rows in this Table"""
return self.count_rows(None)
@property @property
@abstractmethod @abstractmethod
def embedding_functions(self) -> Dict[str, EmbeddingFunctionConfig]: def embedding_functions(self) -> Dict[str, EmbeddingFunctionConfig]:
@@ -829,15 +817,15 @@ class Table(ABC):
writer_heap_size: Optional[int] = 1024 * 1024 * 1024, writer_heap_size: Optional[int] = 1024 * 1024 * 1024,
use_tantivy: bool = True, use_tantivy: bool = True,
tokenizer_name: Optional[str] = None, tokenizer_name: Optional[str] = None,
with_position: bool = False, with_position: bool = True,
# tokenizer configs: # tokenizer configs:
base_tokenizer: BaseTokenizerType = "simple", base_tokenizer: BaseTokenizerType = "simple",
language: str = "English", language: str = "English",
max_token_length: Optional[int] = 40, max_token_length: Optional[int] = 40,
lower_case: bool = True, lower_case: bool = True,
stem: bool = True, stem: bool = False,
remove_stop_words: bool = True, remove_stop_words: bool = False,
ascii_folding: bool = True, ascii_folding: bool = False,
wait_timeout: Optional[timedelta] = None, wait_timeout: Optional[timedelta] = None,
): ):
"""Create a full-text search index on the table. """Create a full-text search index on the table.
@@ -867,7 +855,7 @@ class Table(ABC):
use_tantivy: bool, default True use_tantivy: bool, default True
If True, use the legacy full-text search implementation based on tantivy. If True, use the legacy full-text search implementation based on tantivy.
If False, use the new full-text search implementation based on lance-index. If False, use the new full-text search implementation based on lance-index.
with_position: bool, default False with_position: bool, default True
Only available with use_tantivy=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.
@@ -885,13 +873,13 @@ class Table(ABC):
lower_case : bool, default True lower_case : bool, default True
Whether to convert the token to lower case. This makes queries Whether to convert the token to lower case. This makes queries
case-insensitive. case-insensitive.
stem : bool, default True stem : bool, default False
Whether to stem the token. Stemming reduces words to their root form. Whether to stem the token. Stemming reduces words to their root form.
For example, in English "running" and "runs" would both be reduced to "run". For example, in English "running" and "runs" would both be reduced to "run".
remove_stop_words : bool, default True remove_stop_words : bool, default False
Whether to remove stop words. Stop words are common words that are often Whether to remove stop words. Stop words are common words that are often
removed from text before indexing. For example, in English "the" and "and". removed from text before indexing. For example, in English "the" and "and".
ascii_folding : bool, default True ascii_folding : bool, default False
Whether to fold ASCII characters. This converts accented characters to Whether to fold ASCII characters. This converts accented characters to
their ASCII equivalent. For example, "café" would be converted to "cafe". their ASCII equivalent. For example, "café" would be converted to "cafe".
wait_timeout: timedelta, optional wait_timeout: timedelta, optional
@@ -906,7 +894,7 @@ class Table(ABC):
mode: AddMode = "append", mode: AddMode = "append",
on_bad_vectors: OnBadVectorsType = "error", on_bad_vectors: OnBadVectorsType = "error",
fill_value: float = 0.0, fill_value: float = 0.0,
) -> AddResult: ):
"""Add more data to the [Table](Table). """Add more data to the [Table](Table).
Parameters Parameters
@@ -928,10 +916,6 @@ class Table(ABC):
fill_value: float, default 0. fill_value: float, default 0.
The value to use when filling vectors. Only used if on_bad_vectors="fill". The value to use when filling vectors. Only used if on_bad_vectors="fill".
Returns
-------
AddResult
An object containing the new version number of the table after adding data.
""" """
raise NotImplementedError raise NotImplementedError
@@ -978,12 +962,12 @@ class Table(ABC):
>>> table = db.create_table("my_table", data) >>> table = db.create_table("my_table", data)
>>> new_data = pa.table({"a": [2, 3, 4], "b": ["x", "y", "z"]}) >>> new_data = pa.table({"a": [2, 3, 4], "b": ["x", "y", "z"]})
>>> # Perform a "upsert" operation >>> # Perform a "upsert" operation
>>> res = table.merge_insert("a") \\ >>> stats = table.merge_insert("a") \\
... .when_matched_update_all() \\ ... .when_matched_update_all() \\
... .when_not_matched_insert_all() \\ ... .when_not_matched_insert_all() \\
... .execute(new_data) ... .execute(new_data)
>>> res >>> stats
MergeResult(version=2, num_updated_rows=2, num_inserted_rows=1, num_deleted_rows=0) {'num_inserted_rows': 1, 'num_updated_rows': 2, 'num_deleted_rows': 0}
>>> # The order of new rows is non-deterministic since we use >>> # The order of new rows is non-deterministic since we use
>>> # a hash-join as part of this operation and so we sort here >>> # a hash-join as part of this operation and so we sort here
>>> table.to_arrow().sort_by("a").to_pandas() >>> table.to_arrow().sort_by("a").to_pandas()
@@ -992,7 +976,7 @@ class Table(ABC):
1 2 x 1 2 x
2 3 y 2 3 y
3 4 z 3 4 z
""" # noqa: E501 """
on = [on] if isinstance(on, str) else list(iter(on)) on = [on] if isinstance(on, str) else list(iter(on))
return LanceMergeInsertBuilder(self, on) return LanceMergeInsertBuilder(self, on)
@@ -1107,10 +1091,10 @@ class Table(ABC):
new_data: DATA, new_data: DATA,
on_bad_vectors: OnBadVectorsType, on_bad_vectors: OnBadVectorsType,
fill_value: float, fill_value: float,
) -> MergeResult: ... ): ...
@abstractmethod @abstractmethod
def delete(self, where: str) -> DeleteResult: def delete(self, where: str):
"""Delete rows from the table. """Delete rows from the table.
This can be used to delete a single row, many rows, all rows, or This can be used to delete a single row, many rows, all rows, or
@@ -1125,11 +1109,6 @@ class Table(ABC):
The filter must not be empty, or it will error. The filter must not be empty, or it will error.
Returns
-------
DeleteResult
An object containing the new version number of the table after deletion.
Examples Examples
-------- --------
>>> import lancedb >>> import lancedb
@@ -1146,7 +1125,6 @@ class Table(ABC):
1 2 [3.0, 4.0] 1 2 [3.0, 4.0]
2 3 [5.0, 6.0] 2 3 [5.0, 6.0]
>>> table.delete("x = 2") >>> table.delete("x = 2")
DeleteResult(version=2)
>>> table.to_pandas() >>> table.to_pandas()
x vector x vector
0 1 [1.0, 2.0] 0 1 [1.0, 2.0]
@@ -1160,7 +1138,6 @@ class Table(ABC):
>>> to_remove >>> to_remove
'1, 5' '1, 5'
>>> table.delete(f"x IN ({to_remove})") >>> table.delete(f"x IN ({to_remove})")
DeleteResult(version=3)
>>> table.to_pandas() >>> table.to_pandas()
x vector x vector
0 3 [5.0, 6.0] 0 3 [5.0, 6.0]
@@ -1174,7 +1151,7 @@ class Table(ABC):
values: Optional[dict] = None, values: Optional[dict] = None,
*, *,
values_sql: Optional[Dict[str, str]] = None, values_sql: Optional[Dict[str, str]] = None,
) -> UpdateResult: ):
""" """
This can be used to update zero to all rows depending on how many This can be used to update zero to all rows depending on how many
rows match the where clause. If no where clause is provided, then rows match the where clause. If no where clause is provided, then
@@ -1196,12 +1173,6 @@ class Table(ABC):
reference existing columns. For example, {"x": "x + 1"} will increment reference existing columns. For example, {"x": "x + 1"} will increment
the x column by 1. the x column by 1.
Returns
-------
UpdateResult
- rows_updated: The number of rows that were updated
- version: The new version number of the table after the update
Examples Examples
-------- --------
>>> import lancedb >>> import lancedb
@@ -1215,14 +1186,12 @@ class Table(ABC):
1 2 [3.0, 4.0] 1 2 [3.0, 4.0]
2 3 [5.0, 6.0] 2 3 [5.0, 6.0]
>>> table.update(where="x = 2", values={"vector": [10.0, 10]}) >>> table.update(where="x = 2", values={"vector": [10.0, 10]})
UpdateResult(rows_updated=1, version=2)
>>> table.to_pandas() >>> table.to_pandas()
x vector x vector
0 1 [1.0, 2.0] 0 1 [1.0, 2.0]
1 3 [5.0, 6.0] 1 3 [5.0, 6.0]
2 2 [10.0, 10.0] 2 2 [10.0, 10.0]
>>> table.update(values_sql={"x": "x + 1"}) >>> table.update(values_sql={"x": "x + 1"})
UpdateResult(rows_updated=3, version=3)
>>> table.to_pandas() >>> table.to_pandas()
x vector x vector
0 2 [1.0, 2.0] 0 2 [1.0, 2.0]
@@ -1385,11 +1354,6 @@ class Table(ABC):
Alternatively, a pyarrow Field or Schema can be provided to add Alternatively, a pyarrow Field or Schema can be provided to add
new columns with the specified data types. The new columns will new columns with the specified data types. The new columns will
be initialized with null values. be initialized with null values.
Returns
-------
AddColumnsResult
version: the new version number of the table after adding columns.
""" """
@abstractmethod @abstractmethod
@@ -1415,15 +1379,10 @@ class Table(ABC):
nullability is not changed. Only non-nullable columns can be changed nullability is not changed. Only non-nullable columns can be changed
to nullable. Currently, you cannot change a nullable column to to nullable. Currently, you cannot change a nullable column to
non-nullable. non-nullable.
Returns
-------
AlterColumnsResult
version: the new version number of the table after the alteration.
""" """
@abstractmethod @abstractmethod
def drop_columns(self, columns: Iterable[str]) -> DropColumnsResult: def drop_columns(self, columns: Iterable[str]):
""" """
Drop columns from the table. Drop columns from the table.
@@ -1431,11 +1390,6 @@ class Table(ABC):
---------- ----------
columns : Iterable[str] columns : Iterable[str]
The names of the columns to drop. The names of the columns to drop.
Returns
-------
DropColumnsResult
version: the new version number of the table dropping the columns.
""" """
@abstractmethod @abstractmethod
@@ -1474,7 +1428,7 @@ class Table(ABC):
""" """
@abstractmethod @abstractmethod
def restore(self, version: Optional[Union[int, str]] = None): def restore(self, version: Optional[int] = None):
"""Restore a version of the table. This is an in-place operation. """Restore a version of the table. This is an in-place operation.
This creates a new version where the data is equivalent to the This creates a new version where the data is equivalent to the
@@ -1482,10 +1436,9 @@ class Table(ABC):
Parameters Parameters
---------- ----------
version : int or str, default None version : int, default None
The version number or version tag to restore. The version to restore. If unspecified then restores the currently
If unspecified then restores the currently checked out version. checked out version. If the currently checked out version is the
If the currently checked out version is the
latest version then this is a no-op. latest version then this is a no-op.
""" """
@@ -1658,7 +1611,6 @@ class LanceTable(Table):
... [{"vector": [1.1, 0.9], "type": "vector"}]) ... [{"vector": [1.1, 0.9], "type": "vector"}])
>>> table.tags.create("v1", table.version) >>> table.tags.create("v1", table.version)
>>> table.add([{"vector": [0.5, 0.2], "type": "vector"}]) >>> table.add([{"vector": [0.5, 0.2], "type": "vector"}])
AddResult(version=2)
>>> tags = table.tags.list() >>> tags = table.tags.list()
>>> print(tags["v1"]["version"]) >>> print(tags["v1"]["version"])
1 1
@@ -1697,7 +1649,6 @@ class LanceTable(Table):
vector type vector type
0 [1.1, 0.9] vector 0 [1.1, 0.9] vector
>>> table.add([{"vector": [0.5, 0.2], "type": "vector"}]) >>> table.add([{"vector": [0.5, 0.2], "type": "vector"}])
AddResult(version=2)
>>> table.version >>> table.version
2 2
>>> table.checkout(1) >>> table.checkout(1)
@@ -1715,7 +1666,7 @@ class LanceTable(Table):
""" """
LOOP.run(self._table.checkout_latest()) LOOP.run(self._table.checkout_latest())
def restore(self, version: Optional[Union[int, str]] = None): def restore(self, version: Optional[int] = None):
"""Restore a version of the table. This is an in-place operation. """Restore a version of the table. This is an in-place operation.
This creates a new version where the data is equivalent to the This creates a new version where the data is equivalent to the
@@ -1723,10 +1674,9 @@ class LanceTable(Table):
Parameters Parameters
---------- ----------
version : int or str, default None version : int, default None
The version number or version tag to restore. The version to restore. If unspecified then restores the currently
If unspecified then restores the currently checked out version. checked out version. If the currently checked out version is the
If the currently checked out version is the
latest version then this is a no-op. latest version then this is a no-op.
Examples Examples
@@ -1741,23 +1691,14 @@ class LanceTable(Table):
vector type vector type
0 [1.1, 0.9] vector 0 [1.1, 0.9] vector
>>> table.add([{"vector": [0.5, 0.2], "type": "vector"}]) >>> table.add([{"vector": [0.5, 0.2], "type": "vector"}])
AddResult(version=2)
>>> table.version >>> table.version
2 2
>>> table.tags.create("v2", 2)
>>> table.restore(1) >>> table.restore(1)
>>> table.to_pandas() >>> table.to_pandas()
vector type vector type
0 [1.1, 0.9] vector 0 [1.1, 0.9] vector
>>> len(table.list_versions()) >>> len(table.list_versions())
3 3
>>> table.restore("v2")
>>> table.to_pandas()
vector type
0 [1.1, 0.9] vector
1 [0.5, 0.2] vector
>>> len(table.list_versions())
4
""" """
if version is not None: if version is not None:
LOOP.run(self._table.checkout(version)) LOOP.run(self._table.checkout(version))
@@ -1766,6 +1707,9 @@ class LanceTable(Table):
def count_rows(self, filter: Optional[str] = None) -> int: def count_rows(self, filter: Optional[str] = None) -> int:
return LOOP.run(self._table.count_rows(filter)) return LOOP.run(self._table.count_rows(filter))
def __len__(self) -> int:
return self.count_rows()
def __repr__(self) -> str: def __repr__(self) -> str:
val = f"{self.__class__.__name__}(name={self.name!r}, version={self.version}" val = f"{self.__class__.__name__}(name={self.name!r}, version={self.version}"
if self._conn.read_consistency_interval is not None: if self._conn.read_consistency_interval is not None:
@@ -1972,15 +1916,15 @@ class LanceTable(Table):
writer_heap_size: Optional[int] = 1024 * 1024 * 1024, writer_heap_size: Optional[int] = 1024 * 1024 * 1024,
use_tantivy: bool = True, use_tantivy: bool = True,
tokenizer_name: Optional[str] = None, tokenizer_name: Optional[str] = None,
with_position: bool = False, with_position: bool = True,
# tokenizer configs: # tokenizer configs:
base_tokenizer: BaseTokenizerType = "simple", base_tokenizer: BaseTokenizerType = "simple",
language: str = "English", language: str = "English",
max_token_length: Optional[int] = 40, max_token_length: Optional[int] = 40,
lower_case: bool = True, lower_case: bool = True,
stem: bool = True, stem: bool = False,
remove_stop_words: bool = True, remove_stop_words: bool = False,
ascii_folding: bool = True, ascii_folding: bool = False,
): ):
if not use_tantivy: if not use_tantivy:
if not isinstance(field_names, str): if not isinstance(field_names, str):
@@ -1990,7 +1934,6 @@ class LanceTable(Table):
tokenizer_configs = { tokenizer_configs = {
"base_tokenizer": base_tokenizer, "base_tokenizer": base_tokenizer,
"language": language, "language": language,
"with_position": with_position,
"max_token_length": max_token_length, "max_token_length": max_token_length,
"lower_case": lower_case, "lower_case": lower_case,
"stem": stem, "stem": stem,
@@ -2001,6 +1944,7 @@ class LanceTable(Table):
tokenizer_configs = self.infer_tokenizer_configs(tokenizer_name) tokenizer_configs = self.infer_tokenizer_configs(tokenizer_name)
config = FTS( config = FTS(
with_position=with_position,
**tokenizer_configs, **tokenizer_configs,
) )
@@ -2111,7 +2055,7 @@ class LanceTable(Table):
mode: AddMode = "append", mode: AddMode = "append",
on_bad_vectors: OnBadVectorsType = "error", on_bad_vectors: OnBadVectorsType = "error",
fill_value: float = 0.0, fill_value: float = 0.0,
) -> AddResult: ):
"""Add data to the table. """Add data to the table.
If vector columns are missing and the table If vector columns are missing and the table
has embedding functions, then the vector columns has embedding functions, then the vector columns
@@ -2135,7 +2079,7 @@ class LanceTable(Table):
int int
The number of vectors in the table. The number of vectors in the table.
""" """
return LOOP.run( LOOP.run(
self._table.add( self._table.add(
data, mode=mode, on_bad_vectors=on_bad_vectors, fill_value=fill_value data, mode=mode, on_bad_vectors=on_bad_vectors, fill_value=fill_value
) )
@@ -2465,8 +2409,8 @@ class LanceTable(Table):
) )
return self return self
def delete(self, where: str) -> DeleteResult: def delete(self, where: str):
return LOOP.run(self._table.delete(where)) LOOP.run(self._table.delete(where))
def update( def update(
self, self,
@@ -2474,7 +2418,7 @@ class LanceTable(Table):
values: Optional[dict] = None, values: Optional[dict] = None,
*, *,
values_sql: Optional[Dict[str, str]] = None, values_sql: Optional[Dict[str, str]] = None,
) -> UpdateResult: ):
""" """
This can be used to update zero to all rows depending on how many This can be used to update zero to all rows depending on how many
rows match the where clause. rows match the where clause.
@@ -2492,12 +2436,6 @@ class LanceTable(Table):
reference existing columns. For example, {"x": "x + 1"} will increment reference existing columns. For example, {"x": "x + 1"} will increment
the x column by 1. the x column by 1.
Returns
-------
UpdateResult
- rows_updated: The number of rows that were updated
- version: The new version number of the table after the update
Examples Examples
-------- --------
>>> import lancedb >>> import lancedb
@@ -2511,7 +2449,6 @@ class LanceTable(Table):
1 2 [3.0, 4.0] 1 2 [3.0, 4.0]
2 3 [5.0, 6.0] 2 3 [5.0, 6.0]
>>> table.update(where="x = 2", values={"vector": [10.0, 10]}) >>> table.update(where="x = 2", values={"vector": [10.0, 10]})
UpdateResult(rows_updated=1, version=2)
>>> table.to_pandas() >>> table.to_pandas()
x vector x vector
0 1 [1.0, 2.0] 0 1 [1.0, 2.0]
@@ -2519,7 +2456,7 @@ class LanceTable(Table):
2 2 [10.0, 10.0] 2 2 [10.0, 10.0]
""" """
return LOOP.run(self._table.update(values, where=where, updates_sql=values_sql)) LOOP.run(self._table.update(values, where=where, updates_sql=values_sql))
def _execute_query( def _execute_query(
self, self,
@@ -2553,7 +2490,7 @@ class LanceTable(Table):
new_data: DATA, new_data: DATA,
on_bad_vectors: OnBadVectorsType, on_bad_vectors: OnBadVectorsType,
fill_value: float, fill_value: float,
) -> MergeResult: ):
return LOOP.run( return LOOP.run(
self._table._do_merge(merge, new_data, on_bad_vectors, fill_value) self._table._do_merge(merge, new_data, on_bad_vectors, fill_value)
) )
@@ -2698,16 +2635,14 @@ class LanceTable(Table):
def add_columns( def add_columns(
self, transforms: Dict[str, str] | pa.field | List[pa.field] | pa.Schema self, transforms: Dict[str, str] | pa.field | List[pa.field] | pa.Schema
) -> AddColumnsResult: ):
return LOOP.run(self._table.add_columns(transforms)) LOOP.run(self._table.add_columns(transforms))
def alter_columns( def alter_columns(self, *alterations: Iterable[Dict[str, str]]):
self, *alterations: Iterable[Dict[str, str]] LOOP.run(self._table.alter_columns(*alterations))
) -> AlterColumnsResult:
return LOOP.run(self._table.alter_columns(*alterations))
def drop_columns(self, columns: Iterable[str]) -> DropColumnsResult: def drop_columns(self, columns: Iterable[str]):
return LOOP.run(self._table.drop_columns(columns)) LOOP.run(self._table.drop_columns(columns))
def uses_v2_manifest_paths(self) -> bool: def uses_v2_manifest_paths(self) -> bool:
""" """
@@ -3262,7 +3197,7 @@ class AsyncTable:
mode: Optional[Literal["append", "overwrite"]] = "append", mode: Optional[Literal["append", "overwrite"]] = "append",
on_bad_vectors: Optional[OnBadVectorsType] = None, on_bad_vectors: Optional[OnBadVectorsType] = None,
fill_value: Optional[float] = None, fill_value: Optional[float] = None,
) -> AddResult: ):
"""Add more data to the [Table](Table). """Add more data to the [Table](Table).
Parameters Parameters
@@ -3301,7 +3236,7 @@ class AsyncTable:
if isinstance(data, pa.Table): if isinstance(data, pa.Table):
data = data.to_reader() data = data.to_reader()
return await self._inner.add(data, mode or "append") await self._inner.add(data, mode or "append")
def merge_insert(self, on: Union[str, Iterable[str]]) -> LanceMergeInsertBuilder: def merge_insert(self, on: Union[str, Iterable[str]]) -> LanceMergeInsertBuilder:
""" """
@@ -3346,12 +3281,12 @@ class AsyncTable:
>>> table = db.create_table("my_table", data) >>> table = db.create_table("my_table", data)
>>> new_data = pa.table({"a": [2, 3, 4], "b": ["x", "y", "z"]}) >>> new_data = pa.table({"a": [2, 3, 4], "b": ["x", "y", "z"]})
>>> # Perform a "upsert" operation >>> # Perform a "upsert" operation
>>> res = table.merge_insert("a") \\ >>> stats = table.merge_insert("a") \\
... .when_matched_update_all() \\ ... .when_matched_update_all() \\
... .when_not_matched_insert_all() \\ ... .when_not_matched_insert_all() \\
... .execute(new_data) ... .execute(new_data)
>>> res >>> stats
MergeResult(version=2, num_updated_rows=2, num_inserted_rows=1, num_deleted_rows=0) {'num_inserted_rows': 1, 'num_updated_rows': 2, 'num_deleted_rows': 0}
>>> # The order of new rows is non-deterministic since we use >>> # The order of new rows is non-deterministic since we use
>>> # a hash-join as part of this operation and so we sort here >>> # a hash-join as part of this operation and so we sort here
>>> table.to_arrow().sort_by("a").to_pandas() >>> table.to_arrow().sort_by("a").to_pandas()
@@ -3360,7 +3295,7 @@ class AsyncTable:
1 2 x 1 2 x
2 3 y 2 3 y
3 4 z 3 4 z
""" # noqa: E501 """
on = [on] if isinstance(on, str) else list(iter(on)) on = [on] if isinstance(on, str) else list(iter(on))
return LanceMergeInsertBuilder(self, on) return LanceMergeInsertBuilder(self, on)
@@ -3691,7 +3626,7 @@ class AsyncTable:
new_data: DATA, new_data: DATA,
on_bad_vectors: OnBadVectorsType, on_bad_vectors: OnBadVectorsType,
fill_value: float, fill_value: float,
) -> MergeResult: ):
schema = await self.schema() schema = await self.schema()
if on_bad_vectors is None: if on_bad_vectors is None:
on_bad_vectors = "error" on_bad_vectors = "error"
@@ -3716,11 +3651,10 @@ class AsyncTable:
when_not_matched_insert_all=merge._when_not_matched_insert_all, when_not_matched_insert_all=merge._when_not_matched_insert_all,
when_not_matched_by_source_delete=merge._when_not_matched_by_source_delete, when_not_matched_by_source_delete=merge._when_not_matched_by_source_delete,
when_not_matched_by_source_condition=merge._when_not_matched_by_source_condition, when_not_matched_by_source_condition=merge._when_not_matched_by_source_condition,
timeout=merge._timeout,
), ),
) )
async def delete(self, where: str) -> DeleteResult: async def delete(self, where: str):
"""Delete rows from the table. """Delete rows from the table.
This can be used to delete a single row, many rows, all rows, or This can be used to delete a single row, many rows, all rows, or
@@ -3751,7 +3685,6 @@ class AsyncTable:
1 2 [3.0, 4.0] 1 2 [3.0, 4.0]
2 3 [5.0, 6.0] 2 3 [5.0, 6.0]
>>> table.delete("x = 2") >>> table.delete("x = 2")
DeleteResult(version=2)
>>> table.to_pandas() >>> table.to_pandas()
x vector x vector
0 1 [1.0, 2.0] 0 1 [1.0, 2.0]
@@ -3765,7 +3698,6 @@ class AsyncTable:
>>> to_remove >>> to_remove
'1, 5' '1, 5'
>>> table.delete(f"x IN ({to_remove})") >>> table.delete(f"x IN ({to_remove})")
DeleteResult(version=3)
>>> table.to_pandas() >>> table.to_pandas()
x vector x vector
0 3 [5.0, 6.0] 0 3 [5.0, 6.0]
@@ -3778,7 +3710,7 @@ class AsyncTable:
*, *,
where: Optional[str] = None, where: Optional[str] = None,
updates_sql: Optional[Dict[str, str]] = None, updates_sql: Optional[Dict[str, str]] = None,
) -> UpdateResult: ):
""" """
This can be used to update zero to all rows in the table. This can be used to update zero to all rows in the table.
@@ -3800,13 +3732,6 @@ class AsyncTable:
literals (e.g. "7" or "'foo'") or they can be expressions based on the literals (e.g. "7" or "'foo'") or they can be expressions based on the
previous value of the row (e.g. "x + 1" to increment the x column by 1) previous value of the row (e.g. "x + 1" to increment the x column by 1)
Returns
-------
UpdateResult
An object containing:
- rows_updated: The number of rows that were updated
- version: The new version number of the table after the update
Examples Examples
-------- --------
>>> import asyncio >>> import asyncio
@@ -3835,7 +3760,7 @@ class AsyncTable:
async def add_columns( async def add_columns(
self, transforms: dict[str, str] | pa.field | List[pa.field] | pa.Schema self, transforms: dict[str, str] | pa.field | List[pa.field] | pa.Schema
) -> AddColumnsResult: ):
""" """
Add new columns with defined values. Add new columns with defined values.
@@ -3847,12 +3772,6 @@ class AsyncTable:
each row in the table, and can reference existing columns. each row in the table, and can reference existing columns.
Alternatively, you can pass a pyarrow field or schema to add Alternatively, you can pass a pyarrow field or schema to add
new columns with NULLs. new columns with NULLs.
Returns
-------
AddColumnsResult
version: the new version number of the table after adding columns.
""" """
if isinstance(transforms, pa.Field): if isinstance(transforms, pa.Field):
transforms = [transforms] transforms = [transforms]
@@ -3861,13 +3780,11 @@ class AsyncTable:
): ):
transforms = pa.schema(transforms) transforms = pa.schema(transforms)
if isinstance(transforms, pa.Schema): if isinstance(transforms, pa.Schema):
return await self._inner.add_columns_with_schema(transforms) await self._inner.add_columns_with_schema(transforms)
else: else:
return await self._inner.add_columns(list(transforms.items())) await self._inner.add_columns(list(transforms.items()))
async def alter_columns( async def alter_columns(self, *alterations: Iterable[dict[str, Any]]):
self, *alterations: Iterable[dict[str, Any]]
) -> AlterColumnsResult:
""" """
Alter column names and nullability. Alter column names and nullability.
@@ -3887,13 +3804,8 @@ class AsyncTable:
nullability is not changed. Only non-nullable columns can be changed nullability is not changed. Only non-nullable columns can be changed
to nullable. Currently, you cannot change a nullable column to to nullable. Currently, you cannot change a nullable column to
non-nullable. non-nullable.
Returns
-------
AlterColumnsResult
version: the new version number of the table after the alteration.
""" """
return await self._inner.alter_columns(alterations) await self._inner.alter_columns(alterations)
async def drop_columns(self, columns: Iterable[str]): async def drop_columns(self, columns: Iterable[str]):
""" """
@@ -3904,7 +3816,7 @@ class AsyncTable:
columns : Iterable[str] columns : Iterable[str]
The names of the columns to drop. The names of the columns to drop.
""" """
return await self._inner.drop_columns(columns) await self._inner.drop_columns(columns)
async def version(self) -> int: async def version(self) -> int:
""" """
@@ -3974,7 +3886,7 @@ class AsyncTable:
""" """
await self._inner.checkout_latest() await self._inner.checkout_latest()
async def restore(self, version: Optional[int | str] = None): async def restore(self, version: Optional[int] = None):
""" """
Restore the table to the currently checked out version Restore the table to the currently checked out version

View File

@@ -25,10 +25,6 @@ import numpy as np
from lancedb.pydantic import Vector, LanceModel from lancedb.pydantic import Vector, LanceModel
# --8<-- [end:import-lancedb-pydantic] # --8<-- [end:import-lancedb-pydantic]
# --8<-- [start:import-session-context]
from datafusion import SessionContext
# --8<-- [end:import-session-context]
# --8<-- [start:import-datetime] # --8<-- [start:import-datetime]
from datetime import timedelta from datetime import timedelta
@@ -37,10 +33,6 @@ from datetime import timedelta
from lancedb.embeddings import get_registry from lancedb.embeddings import get_registry
# --8<-- [end:import-embeddings] # --8<-- [end:import-embeddings]
# --8<-- [start:import-ffi-dataset]
from lance import FFILanceTableProvider
# --8<-- [end:import-ffi-dataset]
# --8<-- [start:import-pydantic-basemodel] # --8<-- [start:import-pydantic-basemodel]
from pydantic import BaseModel from pydantic import BaseModel
@@ -349,27 +341,6 @@ def test_table_with_embedding():
# --8<-- [end:create_table_with_embedding] # --8<-- [end:create_table_with_embedding]
def test_sql_query():
db = lancedb.connect("data/sample-lancedb")
data = [
{"vector": [1.1, 1.2], "lat": 45.5, "long": -122.7},
{"vector": [0.2, 1.8], "lat": 40.1, "long": -74.1},
]
table = db.create_table("lance_table", data)
# --8<-- [start:lance_sql_basic]
ctx = SessionContext()
ffi_lance_table = FFILanceTableProvider(
table.to_lance(), with_row_id=False, with_row_addr=False
)
ctx.register_table_provider("ffi_lance_table", ffi_lance_table)
ctx.table("ffi_lance_table")
ctx.sql("SELECT vector FROM ffi_lance_table")
# --8<-- [end:lance_sql_basic]
@pytest.mark.skip @pytest.mark.skip
async def test_table_with_embedding_async(): async def test_table_with_embedding_async():
async_db = await lancedb.connect_async("data/sample-lancedb") async_db = await lancedb.connect_async("data/sample-lancedb")

View File

@@ -18,19 +18,19 @@ def test_upsert(mem_db):
{"id": 1, "name": "Bobby"}, {"id": 1, "name": "Bobby"},
{"id": 2, "name": "Charlie"}, {"id": 2, "name": "Charlie"},
] ]
res = ( stats = (
table.merge_insert("id") table.merge_insert("id")
.when_matched_update_all() .when_matched_update_all()
.when_not_matched_insert_all() .when_not_matched_insert_all()
.execute(new_users) .execute(new_users)
) )
table.count_rows() # 3 table.count_rows() # 3
res # {'num_inserted_rows': 1, 'num_updated_rows': 1, 'num_deleted_rows': 0} stats # {'num_inserted_rows': 1, 'num_updated_rows': 1, 'num_deleted_rows': 0}
# --8<-- [end:upsert_basic] # --8<-- [end:upsert_basic]
assert table.count_rows() == 3 assert table.count_rows() == 3
assert res.num_inserted_rows == 1 assert stats["num_inserted_rows"] == 1
assert res.num_deleted_rows == 0 assert stats["num_updated_rows"] == 1
assert res.num_updated_rows == 1 assert stats["num_deleted_rows"] == 0
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -48,22 +48,19 @@ async def test_upsert_async(mem_db_async):
{"id": 1, "name": "Bobby"}, {"id": 1, "name": "Bobby"},
{"id": 2, "name": "Charlie"}, {"id": 2, "name": "Charlie"},
] ]
res = await ( stats = await (
table.merge_insert("id") table.merge_insert("id")
.when_matched_update_all() .when_matched_update_all()
.when_not_matched_insert_all() .when_not_matched_insert_all()
.execute(new_users) .execute(new_users)
) )
await table.count_rows() # 3 await table.count_rows() # 3
res stats # {'num_inserted_rows': 1, 'num_updated_rows': 1, 'num_deleted_rows': 0}
# MergeResult(version=2, num_updated_rows=1,
# num_inserted_rows=1, num_deleted_rows=0)
# --8<-- [end:upsert_basic_async] # --8<-- [end:upsert_basic_async]
assert await table.count_rows() == 3 assert await table.count_rows() == 3
assert res.version == 2 assert stats["num_inserted_rows"] == 1
assert res.num_inserted_rows == 1 assert stats["num_updated_rows"] == 1
assert res.num_deleted_rows == 0 assert stats["num_deleted_rows"] == 0
assert res.num_updated_rows == 1
def test_insert_if_not_exists(mem_db): def test_insert_if_not_exists(mem_db):
@@ -80,19 +77,16 @@ def test_insert_if_not_exists(mem_db):
{"domain": "google.com", "name": "Google"}, {"domain": "google.com", "name": "Google"},
{"domain": "facebook.com", "name": "Facebook"}, {"domain": "facebook.com", "name": "Facebook"},
] ]
res = ( stats = (
table.merge_insert("domain").when_not_matched_insert_all().execute(new_domains) table.merge_insert("domain").when_not_matched_insert_all().execute(new_domains)
) )
table.count_rows() # 3 table.count_rows() # 3
res stats # {'num_inserted_rows': 1, 'num_updated_rows': 0, 'num_deleted_rows': 0}
# MergeResult(version=2, num_updated_rows=0,
# num_inserted_rows=1, num_deleted_rows=0)
# --8<-- [end:insert_if_not_exists] # --8<-- [end:insert_if_not_exists]
assert table.count_rows() == 3 assert table.count_rows() == 3
assert res.version == 2 assert stats["num_inserted_rows"] == 1
assert res.num_inserted_rows == 1 assert stats["num_updated_rows"] == 0
assert res.num_deleted_rows == 0 assert stats["num_deleted_rows"] == 0
assert res.num_updated_rows == 0
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -110,19 +104,16 @@ async def test_insert_if_not_exists_async(mem_db_async):
{"domain": "google.com", "name": "Google"}, {"domain": "google.com", "name": "Google"},
{"domain": "facebook.com", "name": "Facebook"}, {"domain": "facebook.com", "name": "Facebook"},
] ]
res = await ( stats = await (
table.merge_insert("domain").when_not_matched_insert_all().execute(new_domains) table.merge_insert("domain").when_not_matched_insert_all().execute(new_domains)
) )
await table.count_rows() # 3 await table.count_rows() # 3
res stats # {'num_inserted_rows': 1, 'num_updated_rows': 0, 'num_deleted_rows': 0}
# MergeResult(version=2, num_updated_rows=0, # --8<-- [end:insert_if_not_exists_async]
# num_inserted_rows=1, num_deleted_rows=0)
# --8<-- [end:insert_if_not_exists]
assert await table.count_rows() == 3 assert await table.count_rows() == 3
assert res.version == 2 assert stats["num_inserted_rows"] == 1
assert res.num_inserted_rows == 1 assert stats["num_updated_rows"] == 0
assert res.num_deleted_rows == 0 assert stats["num_deleted_rows"] == 0
assert res.num_updated_rows == 0
def test_replace_range(mem_db): def test_replace_range(mem_db):
@@ -140,7 +131,7 @@ def test_replace_range(mem_db):
new_chunks = [ new_chunks = [
{"doc_id": 1, "chunk_id": 0, "text": "Baz"}, {"doc_id": 1, "chunk_id": 0, "text": "Baz"},
] ]
res = ( stats = (
table.merge_insert(["doc_id", "chunk_id"]) table.merge_insert(["doc_id", "chunk_id"])
.when_matched_update_all() .when_matched_update_all()
.when_not_matched_insert_all() .when_not_matched_insert_all()
@@ -148,15 +139,12 @@ def test_replace_range(mem_db):
.execute(new_chunks) .execute(new_chunks)
) )
table.count_rows("doc_id = 1") # 1 table.count_rows("doc_id = 1") # 1
res stats # {'num_inserted_rows': 0, 'num_updated_rows': 1, 'num_deleted_rows': 1}
# MergeResult(version=2, num_updated_rows=1, # --8<-- [end:replace_range]
# num_inserted_rows=0, num_deleted_rows=1)
# --8<-- [end:insert_if_not_exists]
assert table.count_rows("doc_id = 1") == 1 assert table.count_rows("doc_id = 1") == 1
assert res.version == 2 assert stats["num_inserted_rows"] == 0
assert res.num_inserted_rows == 0 assert stats["num_updated_rows"] == 1
assert res.num_deleted_rows == 1 assert stats["num_deleted_rows"] == 1
assert res.num_updated_rows == 1
@pytest.mark.asyncio @pytest.mark.asyncio
@@ -175,7 +163,7 @@ async def test_replace_range_async(mem_db_async):
new_chunks = [ new_chunks = [
{"doc_id": 1, "chunk_id": 0, "text": "Baz"}, {"doc_id": 1, "chunk_id": 0, "text": "Baz"},
] ]
res = await ( stats = await (
table.merge_insert(["doc_id", "chunk_id"]) table.merge_insert(["doc_id", "chunk_id"])
.when_matched_update_all() .when_matched_update_all()
.when_not_matched_insert_all() .when_not_matched_insert_all()
@@ -183,12 +171,9 @@ async def test_replace_range_async(mem_db_async):
.execute(new_chunks) .execute(new_chunks)
) )
await table.count_rows("doc_id = 1") # 1 await table.count_rows("doc_id = 1") # 1
res stats # {'num_inserted_rows': 0, 'num_updated_rows': 1, 'num_deleted_rows': 1}
# MergeResult(version=2, num_updated_rows=1, # --8<-- [end:replace_range_async]
# num_inserted_rows=0, num_deleted_rows=1)
# --8<-- [end:insert_if_not_exists]
assert await table.count_rows("doc_id = 1") == 1 assert await table.count_rows("doc_id = 1") == 1
assert res.version == 2 assert stats["num_inserted_rows"] == 0
assert res.num_inserted_rows == 0 assert stats["num_updated_rows"] == 1
assert res.num_deleted_rows == 1 assert stats["num_deleted_rows"] == 1
assert res.num_updated_rows == 1

View File

@@ -156,9 +156,6 @@ async def test_vector_search_async():
# --8<-- [end:search_result_async_as_list] # --8<-- [end:search_result_async_as_list]
@pytest.mark.skipif(
os.name == "nt", reason="Need to fix https://github.com/lancedb/lance/issues/3905"
)
def test_fts_fuzzy_query(): def test_fts_fuzzy_query():
uri = "data/fuzzy-example" uri = "data/fuzzy-example"
db = lancedb.connect(uri) db = lancedb.connect(uri)
@@ -192,9 +189,6 @@ def test_fts_fuzzy_query():
} }
@pytest.mark.skipif(
os.name == "nt", reason="Need to fix https://github.com/lancedb/lance/issues/3905"
)
def test_fts_boost_query(): def test_fts_boost_query():
uri = "data/boost-example" uri = "data/boost-example"
db = lancedb.connect(uri) db = lancedb.connect(uri)
@@ -240,9 +234,6 @@ def test_fts_boost_query():
) )
@pytest.mark.skipif(
os.name == "nt", reason="Need to fix https://github.com/lancedb/lance/issues/3905"
)
def test_fts_native(): def test_fts_native():
# --8<-- [start:basic_fts] # --8<-- [start:basic_fts]
uri = "data/sample-lancedb" uri = "data/sample-lancedb"
@@ -291,9 +282,6 @@ def test_fts_native():
# --8<-- [end:fts_incremental_index] # --8<-- [end:fts_incremental_index]
@pytest.mark.skipif(
os.name == "nt", reason="Need to fix https://github.com/lancedb/lance/issues/3905"
)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_fts_native_async(): async def test_fts_native_async():
# --8<-- [start:basic_fts_async] # --8<-- [start:basic_fts_async]

View File

@@ -287,7 +287,7 @@ def test_search_fts_phrase_query(table):
assert False assert False
except Exception: except Exception:
pass pass
table.create_fts_index("text", use_tantivy=False, with_position=True, replace=True) table.create_fts_index("text", use_tantivy=False, replace=True)
results = table.search("puppy").limit(100).to_list() results = table.search("puppy").limit(100).to_list()
phrase_results = table.search('"puppy runs"').limit(100).to_list() phrase_results = table.search('"puppy runs"').limit(100).to_list()
assert len(results) > len(phrase_results) assert len(results) > len(phrase_results)
@@ -312,7 +312,7 @@ async def test_search_fts_phrase_query_async(async_table):
assert False assert False
except Exception: except Exception:
pass pass
await async_table.create_index("text", config=FTS(with_position=True)) await async_table.create_index("text", config=FTS())
results = await async_table.query().nearest_to_text("puppy").limit(100).to_list() results = await async_table.query().nearest_to_text("puppy").limit(100).to_list()
phrase_results = ( phrase_results = (
await async_table.query().nearest_to_text('"puppy runs"').limit(100).to_list() await async_table.query().nearest_to_text('"puppy runs"').limit(100).to_list()
@@ -649,7 +649,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", use_tantivy=False, with_position=True) table.create_fts_index("text", use_tantivy=False)
res = table.search("lance").limit(5).to_list() res = table.search("lance").limit(5).to_list()
assert len(res) == 3 assert len(res) == 3

View File

@@ -149,24 +149,6 @@ async def test_async_checkout():
assert await table.count_rows() == 300 assert await table.count_rows() == 300
def test_table_len_sync():
def handler(request):
if request.path == "/v1/table/test/create/?mode=create":
request.send_response(200)
request.send_header("Content-Type", "application/json")
request.end_headers()
request.wfile.write(b"{}")
request.send_response(200)
request.send_header("Content-Type", "application/json")
request.end_headers()
request.wfile.write(json.dumps(1).encode())
with mock_lancedb_connection(handler) as db:
table = db.create_table("test", [{"id": 1}])
assert len(table) == 1
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_http_error(): async def test_http_error():
request_id_holder = {"request_id": None} request_id_holder = {"request_id": None}

View File

@@ -106,22 +106,15 @@ async def test_update_async(mem_db_async: AsyncConnection):
table = await mem_db_async.create_table("some_table", data=[{"id": 0}]) table = await mem_db_async.create_table("some_table", data=[{"id": 0}])
assert await table.count_rows("id == 0") == 1 assert await table.count_rows("id == 0") == 1
assert await table.count_rows("id == 7") == 0 assert await table.count_rows("id == 7") == 0
update_res = await table.update({"id": 7}) await table.update({"id": 7})
assert update_res.rows_updated == 1
assert update_res.version == 2
assert await table.count_rows("id == 7") == 1 assert await table.count_rows("id == 7") == 1
assert await table.count_rows("id == 0") == 0 assert await table.count_rows("id == 0") == 0
add_res = await table.add([{"id": 2}]) await table.add([{"id": 2}])
assert add_res.version == 3 await table.update(where="id % 2 == 0", updates_sql={"id": "5"})
update_res = await table.update(where="id % 2 == 0", updates_sql={"id": "5"})
assert update_res.rows_updated == 1
assert update_res.version == 4
assert await table.count_rows("id == 7") == 1 assert await table.count_rows("id == 7") == 1
assert await table.count_rows("id == 2") == 0 assert await table.count_rows("id == 2") == 0
assert await table.count_rows("id == 5") == 1 assert await table.count_rows("id == 5") == 1
update_res = await table.update({"id": 10}, where="id == 5") await table.update({"id": 10}, where="id == 5")
assert update_res.rows_updated == 1
assert update_res.version == 5
assert await table.count_rows("id == 10") == 1 assert await table.count_rows("id == 10") == 1
@@ -444,8 +437,7 @@ def test_add_pydantic_model(mem_db: DBConnection):
content="foo", meta=Metadata(source="bar", timestamp=datetime.now()) content="foo", meta=Metadata(source="bar", timestamp=datetime.now())
), ),
) )
add_res = tbl.add([expected]) tbl.add([expected])
assert add_res.version == 2
result = tbl.search([0.0, 0.0]).limit(1).to_pydantic(LanceSchema)[0] result = tbl.search([0.0, 0.0]).limit(1).to_pydantic(LanceSchema)[0]
assert result == expected assert result == expected
@@ -467,12 +459,11 @@ async def test_add_async(mem_db_async: AsyncConnection):
], ],
) )
assert await table.count_rows() == 2 assert await table.count_rows() == 2
add_res = await table.add( await table.add(
data=[ data=[
{"vector": [10.0, 11.0], "item": "baz", "price": 30.0}, {"vector": [10.0, 11.0], "item": "baz", "price": 30.0},
], ],
) )
assert add_res.version == 2
assert await table.count_rows() == 3 assert await table.count_rows() == 3
@@ -769,29 +760,6 @@ def test_restore(mem_db: DBConnection):
table.restore(0) table.restore(0)
def test_restore_with_tags(mem_db: DBConnection):
table = mem_db.create_table(
"my_table",
data=[{"vector": [1.1, 0.9], "type": "vector"}],
)
tag = "tag1"
table.tags.create(tag, 1)
table.add([{"vector": [0.5, 0.2], "type": "vector"}])
table.restore(tag)
assert len(table.list_versions()) == 3
assert len(table) == 1
expected = table.to_arrow()
table.add([{"vector": [0.3, 0.3], "type": "vector"}])
table.checkout("tag1")
table.restore()
assert len(table.list_versions()) == 5
assert table.to_arrow() == expected
with pytest.raises(ValueError):
table.restore("tag_unknown")
def test_merge(tmp_db: DBConnection, tmp_path): def test_merge(tmp_db: DBConnection, tmp_path):
pytest.importorskip("lance") pytest.importorskip("lance")
import lance import lance
@@ -827,8 +795,7 @@ def test_delete(mem_db: DBConnection):
) )
assert len(table) == 2 assert len(table) == 2
assert len(table.list_versions()) == 1 assert len(table.list_versions()) == 1
delete_res = table.delete("id=0") table.delete("id=0")
assert delete_res.version == 2
assert len(table.list_versions()) == 2 assert len(table.list_versions()) == 2
assert table.version == 2 assert table.version == 2
assert len(table) == 1 assert len(table) == 1
@@ -842,9 +809,7 @@ def test_update(mem_db: DBConnection):
) )
assert len(table) == 2 assert len(table) == 2
assert len(table.list_versions()) == 1 assert len(table.list_versions()) == 1
update_res = table.update(where="id=0", values={"vector": [1.1, 1.1]}) table.update(where="id=0", values={"vector": [1.1, 1.1]})
assert update_res.version == 2
assert update_res.rows_updated == 1
assert len(table.list_versions()) == 2 assert len(table.list_versions()) == 2
assert table.version == 2 assert table.version == 2
assert len(table) == 2 assert len(table) == 2
@@ -933,16 +898,9 @@ def test_merge_insert(mem_db: DBConnection):
new_data = pa.table({"a": [2, 3, 4], "b": ["x", "y", "z"]}) new_data = pa.table({"a": [2, 3, 4], "b": ["x", "y", "z"]})
# upsert # upsert
merge_insert_res = ( table.merge_insert(
table.merge_insert("a") "a"
.when_matched_update_all() ).when_matched_update_all().when_not_matched_insert_all().execute(new_data)
.when_not_matched_insert_all()
.execute(new_data, timeout=timedelta(seconds=10))
)
assert merge_insert_res.version == 2
assert merge_insert_res.num_inserted_rows == 1
assert merge_insert_res.num_updated_rows == 2
assert merge_insert_res.num_deleted_rows == 0
expected = pa.table({"a": [1, 2, 3, 4], "b": ["a", "x", "y", "z"]}) expected = pa.table({"a": [1, 2, 3, 4], "b": ["a", "x", "y", "z"]})
assert table.to_arrow().sort_by("a") == expected assert table.to_arrow().sort_by("a") == expected
@@ -950,28 +908,17 @@ def test_merge_insert(mem_db: DBConnection):
table.restore(version) table.restore(version)
# conditional update # conditional update
merge_insert_res = ( table.merge_insert("a").when_matched_update_all(where="target.b = 'b'").execute(
table.merge_insert("a") new_data
.when_matched_update_all(where="target.b = 'b'")
.execute(new_data)
) )
assert merge_insert_res.version == 4
assert merge_insert_res.num_inserted_rows == 0
assert merge_insert_res.num_updated_rows == 1
assert merge_insert_res.num_deleted_rows == 0
expected = pa.table({"a": [1, 2, 3], "b": ["a", "x", "c"]}) expected = pa.table({"a": [1, 2, 3], "b": ["a", "x", "c"]})
assert table.to_arrow().sort_by("a") == expected assert table.to_arrow().sort_by("a") == expected
table.restore(version) table.restore(version)
# insert-if-not-exists # insert-if-not-exists
merge_insert_res = ( table.merge_insert("a").when_not_matched_insert_all().execute(new_data)
table.merge_insert("a").when_not_matched_insert_all().execute(new_data)
)
assert merge_insert_res.version == 6
assert merge_insert_res.num_inserted_rows == 1
assert merge_insert_res.num_updated_rows == 0
assert merge_insert_res.num_deleted_rows == 0
expected = pa.table({"a": [1, 2, 3, 4], "b": ["a", "b", "c", "z"]}) expected = pa.table({"a": [1, 2, 3, 4], "b": ["a", "b", "c", "z"]})
assert table.to_arrow().sort_by("a") == expected assert table.to_arrow().sort_by("a") == expected
@@ -980,17 +927,13 @@ def test_merge_insert(mem_db: DBConnection):
new_data = pa.table({"a": [2, 4], "b": ["x", "z"]}) new_data = pa.table({"a": [2, 4], "b": ["x", "z"]})
# replace-range # replace-range
merge_insert_res = ( (
table.merge_insert("a") table.merge_insert("a")
.when_matched_update_all() .when_matched_update_all()
.when_not_matched_insert_all() .when_not_matched_insert_all()
.when_not_matched_by_source_delete("a > 2") .when_not_matched_by_source_delete("a > 2")
.execute(new_data) .execute(new_data)
) )
assert merge_insert_res.version == 8
assert merge_insert_res.num_inserted_rows == 1
assert merge_insert_res.num_updated_rows == 1
assert merge_insert_res.num_deleted_rows == 1
expected = pa.table({"a": [1, 2, 4], "b": ["a", "x", "z"]}) expected = pa.table({"a": [1, 2, 4], "b": ["a", "x", "z"]})
assert table.to_arrow().sort_by("a") == expected assert table.to_arrow().sort_by("a") == expected
@@ -998,27 +941,15 @@ def test_merge_insert(mem_db: DBConnection):
table.restore(version) table.restore(version)
# replace-range no condition # replace-range no condition
merge_insert_res = ( table.merge_insert(
table.merge_insert("a") "a"
.when_matched_update_all() ).when_matched_update_all().when_not_matched_insert_all().when_not_matched_by_source_delete().execute(
.when_not_matched_insert_all() new_data
.when_not_matched_by_source_delete()
.execute(new_data)
) )
assert merge_insert_res.version == 10
assert merge_insert_res.num_inserted_rows == 1
assert merge_insert_res.num_updated_rows == 1
assert merge_insert_res.num_deleted_rows == 2
expected = pa.table({"a": [2, 4], "b": ["x", "z"]}) expected = pa.table({"a": [2, 4], "b": ["x", "z"]})
assert table.to_arrow().sort_by("a") == expected assert table.to_arrow().sort_by("a") == expected
# timeout
with pytest.raises(Exception, match="merge insert timed out"):
table.merge_insert("a").when_matched_update_all().execute(
new_data, timeout=timedelta(0)
)
# We vary the data format because there are slight differences in how # We vary the data format because there are slight differences in how
# subschemas are handled in different formats # subschemas are handled in different formats
@@ -1547,13 +1478,11 @@ def test_restore_consistency(tmp_path):
def test_add_columns(mem_db: DBConnection): def test_add_columns(mem_db: DBConnection):
data = pa.table({"id": [0, 1]}) data = pa.table({"id": [0, 1]})
table = LanceTable.create(mem_db, "my_table", data=data) table = LanceTable.create(mem_db, "my_table", data=data)
add_columns_res = table.add_columns({"new_col": "id + 2"}) table.add_columns({"new_col": "id + 2"})
assert add_columns_res.version == 2
assert table.to_arrow().column_names == ["id", "new_col"] assert table.to_arrow().column_names == ["id", "new_col"]
assert table.to_arrow()["new_col"].to_pylist() == [2, 3] assert table.to_arrow()["new_col"].to_pylist() == [2, 3]
add_columns_res = table.add_columns({"null_int": "cast(null as bigint)"}) table.add_columns({"null_int": "cast(null as bigint)"})
assert add_columns_res.version == 3
assert table.schema.field("null_int").type == pa.int64() assert table.schema.field("null_int").type == pa.int64()
@@ -1561,8 +1490,7 @@ def test_add_columns(mem_db: DBConnection):
async def test_add_columns_async(mem_db_async: AsyncConnection): async def test_add_columns_async(mem_db_async: AsyncConnection):
data = pa.table({"id": [0, 1]}) data = pa.table({"id": [0, 1]})
table = await mem_db_async.create_table("my_table", data=data) table = await mem_db_async.create_table("my_table", data=data)
add_columns_res = await table.add_columns({"new_col": "id + 2"}) await table.add_columns({"new_col": "id + 2"})
assert add_columns_res.version == 2
data = await table.to_arrow() data = await table.to_arrow()
assert data.column_names == ["id", "new_col"] assert data.column_names == ["id", "new_col"]
assert data["new_col"].to_pylist() == [2, 3] assert data["new_col"].to_pylist() == [2, 3]
@@ -1572,10 +1500,9 @@ async def test_add_columns_async(mem_db_async: AsyncConnection):
async def test_add_columns_with_schema(mem_db_async: AsyncConnection): async def test_add_columns_with_schema(mem_db_async: AsyncConnection):
data = pa.table({"id": [0, 1]}) data = pa.table({"id": [0, 1]})
table = await mem_db_async.create_table("my_table", data=data) table = await mem_db_async.create_table("my_table", data=data)
add_columns_res = await table.add_columns( await table.add_columns(
[pa.field("x", pa.int64()), pa.field("vector", pa.list_(pa.float32(), 8))] [pa.field("x", pa.int64()), pa.field("vector", pa.list_(pa.float32(), 8))]
) )
assert add_columns_res.version == 2
assert await table.schema() == pa.schema( assert await table.schema() == pa.schema(
[ [
@@ -1586,12 +1513,11 @@ async def test_add_columns_with_schema(mem_db_async: AsyncConnection):
) )
table = await mem_db_async.create_table("table2", data=data) table = await mem_db_async.create_table("table2", data=data)
add_columns_res = await table.add_columns( await table.add_columns(
pa.schema( pa.schema(
[pa.field("y", pa.int64()), pa.field("emb", pa.list_(pa.float32(), 8))] [pa.field("y", pa.int64()), pa.field("emb", pa.list_(pa.float32(), 8))]
) )
) )
assert add_columns_res.version == 2
assert await table.schema() == pa.schema( assert await table.schema() == pa.schema(
[ [
pa.field("id", pa.int64()), pa.field("id", pa.int64()),
@@ -1604,8 +1530,7 @@ async def test_add_columns_with_schema(mem_db_async: AsyncConnection):
def test_alter_columns(mem_db: DBConnection): def test_alter_columns(mem_db: DBConnection):
data = pa.table({"id": [0, 1]}) data = pa.table({"id": [0, 1]})
table = mem_db.create_table("my_table", data=data) table = mem_db.create_table("my_table", data=data)
alter_columns_res = table.alter_columns({"path": "id", "rename": "new_id"}) table.alter_columns({"path": "id", "rename": "new_id"})
assert alter_columns_res.version == 2
assert table.to_arrow().column_names == ["new_id"] assert table.to_arrow().column_names == ["new_id"]
@@ -1613,13 +1538,9 @@ def test_alter_columns(mem_db: DBConnection):
async def test_alter_columns_async(mem_db_async: AsyncConnection): async def test_alter_columns_async(mem_db_async: AsyncConnection):
data = pa.table({"id": [0, 1]}) data = pa.table({"id": [0, 1]})
table = await mem_db_async.create_table("my_table", data=data) table = await mem_db_async.create_table("my_table", data=data)
alter_columns_res = await table.alter_columns({"path": "id", "rename": "new_id"}) await table.alter_columns({"path": "id", "rename": "new_id"})
assert alter_columns_res.version == 2
assert (await table.to_arrow()).column_names == ["new_id"] assert (await table.to_arrow()).column_names == ["new_id"]
alter_columns_res = await table.alter_columns( await table.alter_columns(dict(path="new_id", data_type=pa.int16(), nullable=True))
dict(path="new_id", data_type=pa.int16(), nullable=True)
)
assert alter_columns_res.version == 3
data = await table.to_arrow() data = await table.to_arrow()
assert data.column(0).type == pa.int16() assert data.column(0).type == pa.int16()
assert data.schema.field(0).nullable assert data.schema.field(0).nullable
@@ -1628,8 +1549,7 @@ async def test_alter_columns_async(mem_db_async: AsyncConnection):
def test_drop_columns(mem_db: DBConnection): def test_drop_columns(mem_db: DBConnection):
data = pa.table({"id": [0, 1], "category": ["a", "b"]}) data = pa.table({"id": [0, 1], "category": ["a", "b"]})
table = mem_db.create_table("my_table", data=data) table = mem_db.create_table("my_table", data=data)
drop_columns_res = table.drop_columns(["category"]) table.drop_columns(["category"])
assert drop_columns_res.version == 2
assert table.to_arrow().column_names == ["id"] assert table.to_arrow().column_names == ["id"]
@@ -1637,8 +1557,7 @@ def test_drop_columns(mem_db: DBConnection):
async def test_drop_columns_async(mem_db_async: AsyncConnection): async def test_drop_columns_async(mem_db_async: AsyncConnection):
data = pa.table({"id": [0, 1], "category": ["a", "b"]}) data = pa.table({"id": [0, 1], "category": ["a", "b"]})
table = await mem_db_async.create_table("my_table", data=data) table = await mem_db_async.create_table("my_table", data=data)
drop_columns_res = await table.drop_columns(["category"]) await table.drop_columns(["category"])
assert drop_columns_res.version == 2
assert (await table.to_arrow()).column_names == ["id"] assert (await table.to_arrow()).column_names == ["id"]

View File

@@ -3,7 +3,7 @@
use lancedb::index::vector::IvfFlatIndexBuilder; use lancedb::index::vector::IvfFlatIndexBuilder;
use lancedb::index::{ use lancedb::index::{
scalar::{BTreeIndexBuilder, FtsIndexBuilder}, scalar::{BTreeIndexBuilder, FtsIndexBuilder, TokenizerConfig},
vector::{IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder}, vector::{IvfHnswPqIndexBuilder, IvfHnswSqIndexBuilder, IvfPqIndexBuilder},
Index as LanceDbIndex, Index as LanceDbIndex,
}; };
@@ -38,17 +38,19 @@ pub fn extract_index_params(source: &Option<Bound<'_, PyAny>>) -> PyResult<Lance
"LabelList" => Ok(LanceDbIndex::LabelList(Default::default())), "LabelList" => Ok(LanceDbIndex::LabelList(Default::default())),
"FTS" => { "FTS" => {
let params = source.extract::<FtsParams>()?; let params = source.extract::<FtsParams>()?;
let inner_opts = FtsIndexBuilder::default() let inner_opts = TokenizerConfig::default()
.base_tokenizer(params.base_tokenizer) .base_tokenizer(params.base_tokenizer)
.language(&params.language) .language(&params.language)
.map_err(|_| PyValueError::new_err(format!("LanceDB does not support the requested language: '{}'", params.language)))? .map_err(|_| PyValueError::new_err(format!("LanceDB does not support the requested language: '{}'", params.language)))?
.with_position(params.with_position)
.lower_case(params.lower_case) .lower_case(params.lower_case)
.max_token_length(params.max_token_length) .max_token_length(params.max_token_length)
.remove_stop_words(params.remove_stop_words) .remove_stop_words(params.remove_stop_words)
.stem(params.stem) .stem(params.stem)
.ascii_folding(params.ascii_folding); .ascii_folding(params.ascii_folding);
Ok(LanceDbIndex::FTS(inner_opts)) let mut opts = FtsIndexBuilder::default()
.with_position(params.with_position);
opts.tokenizer_configs = inner_opts;
Ok(LanceDbIndex::FTS(opts))
}, },
"IvfFlat" => { "IvfFlat" => {
let params = source.extract::<IvfFlatParams>()?; let params = source.extract::<IvfFlatParams>()?;

View File

@@ -11,10 +11,7 @@ use pyo3::{
wrap_pyfunction, Bound, PyResult, Python, wrap_pyfunction, Bound, PyResult, Python,
}; };
use query::{FTSQuery, HybridQuery, Query, VectorQuery}; use query::{FTSQuery, HybridQuery, Query, VectorQuery};
use table::{ use table::Table;
AddColumnsResult, AddResult, AlterColumnsResult, DeleteResult, DropColumnsResult, MergeResult,
Table, UpdateResult,
};
pub mod arrow; pub mod arrow;
pub mod connection; pub mod connection;
@@ -38,13 +35,6 @@ pub fn _lancedb(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<HybridQuery>()?; m.add_class::<HybridQuery>()?;
m.add_class::<VectorQuery>()?; m.add_class::<VectorQuery>()?;
m.add_class::<RecordBatchStream>()?; m.add_class::<RecordBatchStream>()?;
m.add_class::<AddColumnsResult>()?;
m.add_class::<AlterColumnsResult>()?;
m.add_class::<AddResult>()?;
m.add_class::<MergeResult>()?;
m.add_class::<DeleteResult>()?;
m.add_class::<DropColumnsResult>()?;
m.add_class::<UpdateResult>()?;
m.add_function(wrap_pyfunction!(connect, m)?)?; m.add_function(wrap_pyfunction!(connect, m)?)?;
m.add_function(wrap_pyfunction!(util::validate_table_name, m)?)?; m.add_function(wrap_pyfunction!(util::validate_table_name, m)?)?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?; m.add("__version__", env!("CARGO_PKG_VERSION"))?;

View File

@@ -17,10 +17,10 @@ use lancedb::table::{
Table as LanceDbTable, Table as LanceDbTable,
}; };
use pyo3::{ use pyo3::{
exceptions::{PyKeyError, PyRuntimeError, PyValueError}, exceptions::{PyIOError, PyKeyError, PyRuntimeError, PyValueError},
pyclass, pymethods, pyclass, pymethods,
types::{IntoPyDict, PyAnyMethods, PyDict, PyDictMethods}, types::{IntoPyDict, PyAnyMethods, PyDict, PyDictMethods, PyInt, PyString},
Bound, FromPyObject, PyAny, PyRef, PyResult, Python, Bound, FromPyObject, PyAny, PyObject, PyRef, PyResult, Python,
}; };
use pyo3_async_runtimes::tokio::future_into_py; use pyo3_async_runtimes::tokio::future_into_py;
@@ -58,170 +58,6 @@ pub struct OptimizeStats {
pub prune: RemovalStats, pub prune: RemovalStats,
} }
#[pyclass(get_all)]
#[derive(Clone, Debug)]
pub struct UpdateResult {
pub rows_updated: u64,
pub version: u64,
}
#[pymethods]
impl UpdateResult {
pub fn __repr__(&self) -> String {
format!(
"UpdateResult(rows_updated={}, version={})",
self.rows_updated, self.version
)
}
}
impl From<lancedb::table::UpdateResult> for UpdateResult {
fn from(result: lancedb::table::UpdateResult) -> Self {
Self {
rows_updated: result.rows_updated,
version: result.version,
}
}
}
#[pyclass(get_all)]
#[derive(Clone, Debug)]
pub struct AddResult {
pub version: u64,
}
#[pymethods]
impl AddResult {
pub fn __repr__(&self) -> String {
format!("AddResult(version={})", self.version)
}
}
impl From<lancedb::table::AddResult> for AddResult {
fn from(result: lancedb::table::AddResult) -> Self {
Self {
version: result.version,
}
}
}
#[pyclass(get_all)]
#[derive(Clone, Debug)]
pub struct DeleteResult {
pub version: u64,
}
#[pymethods]
impl DeleteResult {
pub fn __repr__(&self) -> String {
format!("DeleteResult(version={})", self.version)
}
}
impl From<lancedb::table::DeleteResult> for DeleteResult {
fn from(result: lancedb::table::DeleteResult) -> Self {
Self {
version: result.version,
}
}
}
#[pyclass(get_all)]
#[derive(Clone, Debug)]
pub struct MergeResult {
pub version: u64,
pub num_updated_rows: u64,
pub num_inserted_rows: u64,
pub num_deleted_rows: u64,
}
#[pymethods]
impl MergeResult {
pub fn __repr__(&self) -> String {
format!(
"MergeResult(version={}, num_updated_rows={}, num_inserted_rows={}, num_deleted_rows={})",
self.version,
self.num_updated_rows,
self.num_inserted_rows,
self.num_deleted_rows
)
}
}
impl From<lancedb::table::MergeResult> for MergeResult {
fn from(result: lancedb::table::MergeResult) -> Self {
Self {
version: result.version,
num_updated_rows: result.num_updated_rows,
num_inserted_rows: result.num_inserted_rows,
num_deleted_rows: result.num_deleted_rows,
}
}
}
#[pyclass(get_all)]
#[derive(Clone, Debug)]
pub struct AddColumnsResult {
pub version: u64,
}
#[pymethods]
impl AddColumnsResult {
pub fn __repr__(&self) -> String {
format!("AddColumnsResult(version={})", self.version)
}
}
impl From<lancedb::table::AddColumnsResult> for AddColumnsResult {
fn from(result: lancedb::table::AddColumnsResult) -> Self {
Self {
version: result.version,
}
}
}
#[pyclass(get_all)]
#[derive(Clone, Debug)]
pub struct AlterColumnsResult {
pub version: u64,
}
#[pymethods]
impl AlterColumnsResult {
pub fn __repr__(&self) -> String {
format!("AlterColumnsResult(version={})", self.version)
}
}
impl From<lancedb::table::AlterColumnsResult> for AlterColumnsResult {
fn from(result: lancedb::table::AlterColumnsResult) -> Self {
Self {
version: result.version,
}
}
}
#[pyclass(get_all)]
#[derive(Clone, Debug)]
pub struct DropColumnsResult {
pub version: u64,
}
#[pymethods]
impl DropColumnsResult {
pub fn __repr__(&self) -> String {
format!("DropColumnsResult(version={})", self.version)
}
}
impl From<lancedb::table::DropColumnsResult> for DropColumnsResult {
fn from(result: lancedb::table::DropColumnsResult) -> Self {
Self {
version: result.version,
}
}
}
#[pyclass] #[pyclass]
pub struct Table { pub struct Table {
// We keep a copy of the name to use if the inner table is dropped // We keep a copy of the name to use if the inner table is dropped
@@ -296,16 +132,15 @@ impl Table {
} }
future_into_py(self_.py(), async move { future_into_py(self_.py(), async move {
let result = op.execute().await.infer_error()?; op.execute().await.infer_error()?;
Ok(AddResult::from(result)) Ok(())
}) })
} }
pub fn delete(self_: PyRef<'_, Self>, condition: String) -> PyResult<Bound<'_, PyAny>> { pub fn delete(self_: PyRef<'_, Self>, condition: String) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.inner_ref()?.clone(); let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move { future_into_py(self_.py(), async move {
let result = inner.delete(&condition).await.infer_error()?; inner.delete(&condition).await.infer_error()
Ok(DeleteResult::from(result))
}) })
} }
@@ -325,8 +160,8 @@ impl Table {
op = op.column(column_name, value); op = op.column(column_name, value);
} }
future_into_py(self_.py(), async move { future_into_py(self_.py(), async move {
let result = op.execute().await.infer_error()?; op.execute().await.infer_error()?;
Ok(UpdateResult::from(result)) Ok(())
}) })
} }
@@ -520,15 +355,25 @@ impl Table {
}) })
} }
pub fn checkout(self_: PyRef<'_, Self>, version: LanceVersion) -> PyResult<Bound<'_, PyAny>> { pub fn checkout(self_: PyRef<'_, Self>, version: PyObject) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.inner_ref()?.clone(); let inner = self_.inner_ref()?.clone();
let py = self_.py(); let py = self_.py();
let (is_int, int_value, string_value) = if let Ok(i) = version.downcast_bound::<PyInt>(py) {
let num: u64 = i.extract()?;
(true, num, String::new())
} else if let Ok(s) = version.downcast_bound::<PyString>(py) {
let str_value = s.to_string();
(false, 0, str_value)
} else {
return Err(PyIOError::new_err(
"version must be an integer or a string.",
));
};
future_into_py(py, async move { future_into_py(py, async move {
match version { if is_int {
LanceVersion::Version(version_num) => { inner.checkout(int_value).await.infer_error()
inner.checkout(version_num).await.infer_error() } else {
} inner.checkout_tag(&string_value).await.infer_error()
LanceVersion::Tag(tag) => inner.checkout_tag(&tag).await.infer_error(),
} }
}) })
} }
@@ -541,19 +386,12 @@ impl Table {
} }
#[pyo3(signature = (version=None))] #[pyo3(signature = (version=None))]
pub fn restore( pub fn restore(self_: PyRef<'_, Self>, version: Option<u64>) -> PyResult<Bound<'_, PyAny>> {
self_: PyRef<'_, Self>,
version: Option<LanceVersion>,
) -> PyResult<Bound<'_, PyAny>> {
let inner = self_.inner_ref()?.clone(); let inner = self_.inner_ref()?.clone();
let py = self_.py();
future_into_py(py, async move { future_into_py(self_.py(), async move {
if let Some(version) = version { if let Some(version) = version {
match version { inner.checkout(version).await.infer_error()?;
LanceVersion::Version(num) => inner.checkout(num).await.infer_error()?,
LanceVersion::Tag(tag) => inner.checkout_tag(&tag).await.infer_error()?,
}
} }
inner.restore().await.infer_error() inner.restore().await.infer_error()
}) })
@@ -649,13 +487,16 @@ impl Table {
builder builder
.when_not_matched_by_source_delete(parameters.when_not_matched_by_source_condition); .when_not_matched_by_source_delete(parameters.when_not_matched_by_source_condition);
} }
if let Some(timeout) = parameters.timeout {
builder.timeout(timeout);
}
future_into_py(self_.py(), async move { future_into_py(self_.py(), async move {
let res = builder.execute(Box::new(batches)).await.infer_error()?; let stats = builder.execute(Box::new(batches)).await.infer_error()?;
Ok(MergeResult::from(res)) Python::with_gil(|py| {
let dict = PyDict::new(py);
dict.set_item("num_inserted_rows", stats.num_inserted_rows)?;
dict.set_item("num_updated_rows", stats.num_updated_rows)?;
dict.set_item("num_deleted_rows", stats.num_deleted_rows)?;
Ok(dict.unbind())
})
}) })
} }
@@ -691,8 +532,8 @@ impl Table {
let inner = self_.inner_ref()?.clone(); let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move { future_into_py(self_.py(), async move {
let result = inner.add_columns(definitions, None).await.infer_error()?; inner.add_columns(definitions, None).await.infer_error()?;
Ok(AddColumnsResult::from(result)) Ok(())
}) })
} }
@@ -705,8 +546,8 @@ impl Table {
let inner = self_.inner_ref()?.clone(); let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move { future_into_py(self_.py(), async move {
let result = inner.add_columns(transform, None).await.infer_error()?; inner.add_columns(transform, None).await.infer_error()?;
Ok(AddColumnsResult::from(result)) Ok(())
}) })
} }
@@ -749,8 +590,8 @@ impl Table {
let inner = self_.inner_ref()?.clone(); let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move { future_into_py(self_.py(), async move {
let result = inner.alter_columns(&alterations).await.infer_error()?; inner.alter_columns(&alterations).await.infer_error()?;
Ok(AlterColumnsResult::from(result)) Ok(())
}) })
} }
@@ -758,8 +599,8 @@ impl Table {
let inner = self_.inner_ref()?.clone(); let inner = self_.inner_ref()?.clone();
future_into_py(self_.py(), async move { future_into_py(self_.py(), async move {
let column_refs = columns.iter().map(String::as_str).collect::<Vec<&str>>(); let column_refs = columns.iter().map(String::as_str).collect::<Vec<&str>>();
let result = inner.drop_columns(&column_refs).await.infer_error()?; inner.drop_columns(&column_refs).await.infer_error()?;
Ok(DropColumnsResult::from(result)) Ok(())
}) })
} }
@@ -795,12 +636,6 @@ impl Table {
} }
} }
#[derive(FromPyObject)]
pub enum LanceVersion {
Version(u64),
Tag(String),
}
#[derive(FromPyObject)] #[derive(FromPyObject)]
#[pyo3(from_item_all)] #[pyo3(from_item_all)]
pub struct MergeInsertParams { pub struct MergeInsertParams {
@@ -810,7 +645,6 @@ pub struct MergeInsertParams {
when_not_matched_insert_all: bool, when_not_matched_insert_all: bool,
when_not_matched_by_source_delete: bool, when_not_matched_by_source_delete: bool,
when_not_matched_by_source_condition: Option<String>, when_not_matched_by_source_condition: Option<String>,
timeout: Option<std::time::Duration>,
} }
#[pyclass] #[pyclass]

View File

@@ -1,2 +1,2 @@
[toolchain] [toolchain]
channel = "1.86.0" channel = "1.83.0"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lancedb-node" name = "lancedb-node"
version = "0.20.0-beta.2" version = "0.19.1-beta.1"
description = "Serverless, low-latency vector database for AI applications" description = "Serverless, low-latency vector database for AI applications"
license.workspace = true license.workspace = true
edition.workspace = true edition.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "lancedb" name = "lancedb"
version = "0.20.0-beta.2" version = "0.19.1-beta.1"
edition.workspace = true edition.workspace = true
description = "LanceDB: A serverless, low-latency vector database for AI applications" description = "LanceDB: A serverless, low-latency vector database for AI applications"
license.workspace = true license.workspace = true
@@ -60,15 +60,15 @@ reqwest = { version = "0.12.0", default-features = false, features = [
"macos-system-configuration", "macos-system-configuration",
"stream", "stream",
], optional = true } ], optional = true }
rand = { version = "0.9", features = ["small_rng"], optional = true } rand = { version = "0.8.3", features = ["small_rng"], optional = true }
http = { version = "1", optional = true } # Matching what is in reqwest http = { version = "1", optional = true } # Matching what is in reqwest
uuid = { version = "1.7.0", features = ["v4"], optional = true } uuid = { version = "1.7.0", features = ["v4"], optional = true }
polars-arrow = { version = ">=0.37,<0.40.0", optional = true } polars-arrow = { version = ">=0.37,<0.40.0", optional = true }
polars = { version = ">=0.37,<0.40.0", optional = true } polars = { version = ">=0.37,<0.40.0", optional = true }
hf-hub = { version = "0.4.1", optional = true, default-features = false, features = ["rustls-tls", "tokio", "ureq"]} hf-hub = { version = "0.4.1", optional = true, default-features = false, features = ["rustls-tls", "tokio", "ureq"]}
candle-core = { version = "0.9.1", optional = true } candle-core = { version = "0.6.0", optional = true }
candle-transformers = { version = "0.9.1", optional = true } candle-transformers = { version = "0.6.0", optional = true }
candle-nn = { version = "0.9.1", optional = true } candle-nn = { version = "0.6.0", optional = true }
tokenizers = { version = "0.19.1", optional = true } tokenizers = { version = "0.19.1", optional = true }
semver = { workspace = true } semver = { workspace = true }
@@ -78,7 +78,7 @@ bytemuck_derive.workspace = true
[dev-dependencies] [dev-dependencies]
tempfile = "3.5.0" tempfile = "3.5.0"
rand = { version = "0.9", features = ["small_rng"] } rand = { version = "0.8.3", features = ["small_rng"] }
random_word = { version = "0.4.3", features = ["en"] } random_word = { version = "0.4.3", features = ["en"] }
uuid = { version = "1.7.0", features = ["v4"] } uuid = { version = "1.7.0", features = ["v4"] }
walkdir = "2" walkdir = "2"

View File

@@ -51,7 +51,7 @@ fn create_some_records() -> Result<Box<dyn RecordBatchReader + Send>> {
Arc::new(Int32Array::from_iter_values(0..TOTAL as i32)), Arc::new(Int32Array::from_iter_values(0..TOTAL as i32)),
Arc::new(StringArray::from_iter_values((0..TOTAL).map(|_| { Arc::new(StringArray::from_iter_values((0..TOTAL).map(|_| {
(0..n_terms) (0..n_terms)
.map(|_| words[random::<u32>() as usize % words.len()]) .map(|_| words[random::<usize>() % words.len()])
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" ") .join(" ")
}))), }))),

View File

@@ -214,7 +214,7 @@ impl SentenceTransformersEmbeddings {
let embeddings = self let embeddings = self
.model .model
.forward(&input_ids, &token_type_ids, None) .forward(&input_ids, &token_type_ids)
// TODO: it'd be nice to support other devices // TODO: it'd be nice to support other devices
.and_then(|output| output.to_device(&Device::Cpu))?; .and_then(|output| output.to_device(&Device::Cpu))?;
@@ -310,7 +310,7 @@ impl SentenceTransformersEmbeddings {
let embeddings = Tensor::stack(&tokens, 0) let embeddings = Tensor::stack(&tokens, 0)
.and_then(|tokens| { .and_then(|tokens| {
let token_type_ids = tokens.zeros_like()?; let token_type_ids = tokens.zeros_like()?;
self.model.forward(&tokens, &token_type_ids, None) self.model.forward(&tokens, &token_type_ids)
}) })
// TODO: it'd be nice to support other devices // TODO: it'd be nice to support other devices
.and_then(|tokens| tokens.to_device(&Device::Cpu)) .and_then(|tokens| tokens.to_device(&Device::Cpu))

View File

@@ -51,7 +51,35 @@ pub struct BitmapIndexBuilder {}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct LabelListIndexBuilder {} pub struct LabelListIndexBuilder {}
/// Builder for a full text search index
///
/// A full text search index is an index on a string column that allows for full text search
#[derive(Debug, Clone)]
pub struct FtsIndexBuilder {
/// Whether to store the position of the tokens
/// This is used for phrase queries
pub with_position: bool,
pub tokenizer_configs: TokenizerConfig,
}
impl Default for FtsIndexBuilder {
fn default() -> Self {
Self {
with_position: true,
tokenizer_configs: TokenizerConfig::default(),
}
}
}
impl FtsIndexBuilder {
/// Set the with_position flag
pub fn with_position(mut self, with_position: bool) -> Self {
self.with_position = with_position;
self
}
}
pub use lance_index::scalar::inverted::query::*; pub use lance_index::scalar::inverted::query::*;
pub use lance_index::scalar::inverted::TokenizerConfig;
pub use lance_index::scalar::FullTextSearchQuery; pub use lance_index::scalar::FullTextSearchQuery;
pub use lance_index::scalar::InvertedIndexParams as FtsIndexBuilder;
pub use lance_index::scalar::InvertedIndexParams;

View File

@@ -197,8 +197,16 @@ mod test {
#[tokio::test] #[tokio::test]
async fn test_e2e() { async fn test_e2e() {
let dir1 = tempfile::tempdir().unwrap().keep().canonicalize().unwrap(); let dir1 = tempfile::tempdir()
let dir2 = tempfile::tempdir().unwrap().keep().canonicalize().unwrap(); .unwrap()
.into_path()
.canonicalize()
.unwrap();
let dir2 = tempfile::tempdir()
.unwrap()
.into_path()
.canonicalize()
.unwrap();
let secondary_store = LocalFileSystem::new_with_prefix(dir2.to_str().unwrap()).unwrap(); let secondary_store = LocalFileSystem::new_with_prefix(dir2.to_str().unwrap()).unwrap();
let object_store_wrapper = Arc::new(MirroringObjectStoreWrapper { let object_store_wrapper = Arc::new(MirroringObjectStoreWrapper {

View File

@@ -4,14 +4,7 @@
use crate::index::Index; use crate::index::Index;
use crate::index::IndexStatistics; use crate::index::IndexStatistics;
use crate::query::{QueryFilter, QueryRequest, Select, VectorQueryRequest}; use crate::query::{QueryFilter, QueryRequest, Select, VectorQueryRequest};
use crate::table::AddColumnsResult;
use crate::table::AddResult;
use crate::table::AlterColumnsResult;
use crate::table::DeleteResult;
use crate::table::DropColumnsResult;
use crate::table::MergeResult;
use crate::table::Tags; use crate::table::Tags;
use crate::table::UpdateResult;
use crate::table::{AddDataMode, AnyQuery, Filter, TableStatistics}; use crate::table::{AddDataMode, AnyQuery, Filter, TableStatistics};
use crate::utils::{supported_btree_data_type, supported_vector_data_type}; use crate::utils::{supported_btree_data_type, supported_vector_data_type};
use crate::{DistanceType, Error, Table}; use crate::{DistanceType, Error, Table};
@@ -54,6 +47,7 @@ use crate::{
TableDefinition, UpdateBuilder, TableDefinition, UpdateBuilder,
}, },
}; };
use lance::dataset::MergeStats;
const REQUEST_TIMEOUT_HEADER: HeaderName = HeaderName::from_static("x-request-timeout-ms"); const REQUEST_TIMEOUT_HEADER: HeaderName = HeaderName::from_static("x-request-timeout-ms");
@@ -741,7 +735,7 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
&self, &self,
add: AddDataBuilder<NoData>, add: AddDataBuilder<NoData>,
data: Box<dyn RecordBatchReader + Send>, data: Box<dyn RecordBatchReader + Send>,
) -> Result<AddResult> { ) -> Result<()> {
self.check_mutable().await?; self.check_mutable().await?;
let mut request = self let mut request = self
.client .client
@@ -756,19 +750,9 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
} }
let (request_id, response) = self.send_streaming(request, data, true).await?; let (request_id, response) = self.send_streaming(request, data, true).await?;
let response = self.check_table_response(&request_id, response).await?; self.check_table_response(&request_id, response).await?;
let body = response.text().await.err_to_http(request_id.clone())?;
if body.trim().is_empty() {
// Backward compatible with old servers
return Ok(AddResult { version: 0 });
}
let add_response: AddResult = serde_json::from_str(&body).map_err(|e| Error::Http { Ok(())
source: format!("Failed to parse add response: {}", e).into(),
request_id,
status_code: None,
})?;
Ok(add_response)
} }
async fn create_plan( async fn create_plan(
@@ -901,7 +885,7 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
Ok(final_analyze) Ok(final_analyze)
} }
async fn update(&self, update: UpdateBuilder) -> Result<UpdateResult> { async fn update(&self, update: UpdateBuilder) -> Result<u64> {
self.check_mutable().await?; self.check_mutable().await?;
let request = self let request = self
.client .client
@@ -918,28 +902,13 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
})); }));
let (request_id, response) = self.send(request, true).await?; let (request_id, response) = self.send(request, true).await?;
let response = self.check_table_response(&request_id, response).await?;
let body = response.text().await.err_to_http(request_id.clone())?;
if body.trim().is_empty() { self.check_table_response(&request_id, response).await?;
// Backward compatible with old servers
return Ok(UpdateResult {
rows_updated: 0,
version: 0,
});
}
let update_response: UpdateResult = Ok(0) // TODO: support returning number of modified rows once supported in SaaS.
serde_json::from_str(&body).map_err(|e| Error::Http {
source: format!("Failed to parse update response: {}", e).into(),
request_id,
status_code: None,
})?;
Ok(update_response)
} }
async fn delete(&self, predicate: &str) -> Result<DeleteResult> { async fn delete(&self, predicate: &str) -> Result<()> {
self.check_mutable().await?; self.check_mutable().await?;
let body = serde_json::json!({ "predicate": predicate }); let body = serde_json::json!({ "predicate": predicate });
let request = self let request = self
@@ -947,19 +916,8 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
.post(&format!("/v1/table/{}/delete/", self.name)) .post(&format!("/v1/table/{}/delete/", self.name))
.json(&body); .json(&body);
let (request_id, response) = self.send(request, true).await?; let (request_id, response) = self.send(request, true).await?;
let response = self.check_table_response(&request_id, response).await?; self.check_table_response(&request_id, response).await?;
let body = response.text().await.err_to_http(request_id.clone())?; Ok(())
if body.trim().is_empty() {
// Backward compatible with old servers
return Ok(DeleteResult { version: 0 });
}
let delete_response: DeleteResult =
serde_json::from_str(&body).map_err(|e| Error::Http {
source: format!("Failed to parse delete response: {}", e).into(),
request_id,
status_code: None,
})?;
Ok(delete_response)
} }
async fn create_index(&self, mut index: IndexBuilder) -> Result<()> { async fn create_index(&self, mut index: IndexBuilder) -> Result<()> {
@@ -995,12 +953,16 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
Index::Bitmap(_) => ("BITMAP", None), Index::Bitmap(_) => ("BITMAP", None),
Index::LabelList(_) => ("LABEL_LIST", None), Index::LabelList(_) => ("LABEL_LIST", None),
Index::FTS(fts) => { Index::FTS(fts) => {
let params = serde_json::to_value(&fts).map_err(|e| Error::InvalidInput { let with_position = fts.with_position;
message: format!("failed to serialize FTS index params {:?}", e), let configs = serde_json::to_value(fts.tokenizer_configs).map_err(|e| {
Error::InvalidInput {
message: format!("failed to serialize FTS index params {:?}", e),
}
})?; })?;
for (key, value) in params.as_object().unwrap() { for (key, value) in configs.as_object().unwrap() {
body[key] = value.clone(); body[key] = value.clone();
} }
body["with_position"] = serde_json::Value::Bool(with_position);
("FTS", None) ("FTS", None)
} }
Index::Auto => { Index::Auto => {
@@ -1061,48 +1023,23 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
&self, &self,
params: MergeInsertBuilder, params: MergeInsertBuilder,
new_data: Box<dyn RecordBatchReader + Send>, new_data: Box<dyn RecordBatchReader + Send>,
) -> Result<MergeResult> { ) -> Result<MergeStats> {
self.check_mutable().await?; self.check_mutable().await?;
let timeout = params.timeout;
let query = MergeInsertRequest::try_from(params)?; let query = MergeInsertRequest::try_from(params)?;
let mut request = self let request = self
.client .client
.post(&format!("/v1/table/{}/merge_insert/", self.name)) .post(&format!("/v1/table/{}/merge_insert/", self.name))
.query(&query) .query(&query)
.header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE); .header(CONTENT_TYPE, ARROW_STREAM_CONTENT_TYPE);
if let Some(timeout) = timeout {
// (If it doesn't fit into u64, it's not worth sending anyways.)
if let Ok(timeout_ms) = u64::try_from(timeout.as_millis()) {
request = request.header(REQUEST_TIMEOUT_HEADER, timeout_ms);
}
}
let (request_id, response) = self.send_streaming(request, new_data, true).await?; let (request_id, response) = self.send_streaming(request, new_data, true).await?;
let response = self.check_table_response(&request_id, response).await?; // TODO: server can response with these stats in response body.
let body = response.text().await.err_to_http(request_id.clone())?; // We should test that we can handle both empty response from old server
// and response with stats from new server.
if body.trim().is_empty() { self.check_table_response(&request_id, response).await?;
// Backward compatible with old servers Ok(MergeStats::default())
return Ok(MergeResult {
version: 0,
num_deleted_rows: 0,
num_inserted_rows: 0,
num_updated_rows: 0,
});
}
let merge_insert_response: MergeResult =
serde_json::from_str(&body).map_err(|e| Error::Http {
source: format!("Failed to parse merge_insert response: {}", e).into(),
request_id,
status_code: None,
})?;
Ok(merge_insert_response)
} }
async fn tags(&self) -> Result<Box<dyn Tags + '_>> { async fn tags(&self) -> Result<Box<dyn Tags + '_>> {
@@ -1125,7 +1062,7 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
&self, &self,
transforms: NewColumnTransform, transforms: NewColumnTransform,
_read_columns: Option<Vec<String>>, _read_columns: Option<Vec<String>>,
) -> Result<AddColumnsResult> { ) -> Result<()> {
self.check_mutable().await?; self.check_mutable().await?;
match transforms { match transforms {
NewColumnTransform::SqlExpressions(expressions) => { NewColumnTransform::SqlExpressions(expressions) => {
@@ -1143,23 +1080,9 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
.client .client
.post(&format!("/v1/table/{}/add_columns/", self.name)) .post(&format!("/v1/table/{}/add_columns/", self.name))
.json(&body); .json(&body);
let (request_id, response) = self.send(request, true).await?; let (request_id, response) = self.send(request, true).await?; // todo:
let response = self.check_table_response(&request_id, response).await?; self.check_table_response(&request_id, response).await?;
let body = response.text().await.err_to_http(request_id.clone())?; Ok(())
if body.trim().is_empty() {
// Backward compatible with old servers
return Ok(AddColumnsResult { version: 0 });
}
let result: AddColumnsResult =
serde_json::from_str(&body).map_err(|e| Error::Http {
source: format!("Failed to parse add_columns response: {}", e).into(),
request_id,
status_code: None,
})?;
Ok(result)
} }
_ => { _ => {
return Err(Error::NotSupported { return Err(Error::NotSupported {
@@ -1169,7 +1092,7 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
} }
} }
async fn alter_columns(&self, alterations: &[ColumnAlteration]) -> Result<AlterColumnsResult> { async fn alter_columns(&self, alterations: &[ColumnAlteration]) -> Result<()> {
self.check_mutable().await?; self.check_mutable().await?;
let body = alterations let body = alterations
.iter() .iter()
@@ -1197,24 +1120,11 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
.post(&format!("/v1/table/{}/alter_columns/", self.name)) .post(&format!("/v1/table/{}/alter_columns/", self.name))
.json(&body); .json(&body);
let (request_id, response) = self.send(request, true).await?; let (request_id, response) = self.send(request, true).await?;
let response = self.check_table_response(&request_id, response).await?; self.check_table_response(&request_id, response).await?;
let body = response.text().await.err_to_http(request_id.clone())?; Ok(())
if body.trim().is_empty() {
// Backward compatible with old servers
return Ok(AlterColumnsResult { version: 0 });
}
let result: AlterColumnsResult = serde_json::from_str(&body).map_err(|e| Error::Http {
source: format!("Failed to parse alter_columns response: {}", e).into(),
request_id,
status_code: None,
})?;
Ok(result)
} }
async fn drop_columns(&self, columns: &[&str]) -> Result<DropColumnsResult> { async fn drop_columns(&self, columns: &[&str]) -> Result<()> {
self.check_mutable().await?; self.check_mutable().await?;
let body = serde_json::json!({ "columns": columns }); let body = serde_json::json!({ "columns": columns });
let request = self let request = self
@@ -1222,21 +1132,8 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
.post(&format!("/v1/table/{}/drop_columns/", self.name)) .post(&format!("/v1/table/{}/drop_columns/", self.name))
.json(&body); .json(&body);
let (request_id, response) = self.send(request, true).await?; let (request_id, response) = self.send(request, true).await?;
let response = self.check_table_response(&request_id, response).await?; self.check_table_response(&request_id, response).await?;
let body = response.text().await.err_to_http(request_id.clone())?; Ok(())
if body.trim().is_empty() {
// Backward compatible with old servers
return Ok(DropColumnsResult { version: 0 });
}
let result: DropColumnsResult = serde_json::from_str(&body).map_err(|e| Error::Http {
source: format!("Failed to parse drop_columns response: {}", e).into(),
request_id,
status_code: None,
})?;
Ok(result)
} }
async fn list_indices(&self) -> Result<Vec<IndexConfig>> { async fn list_indices(&self) -> Result<Vec<IndexConfig>> {
@@ -1330,12 +1227,7 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
self.name, index_name self.name, index_name
)); ));
let (request_id, response) = self.send(request, true).await?; let (request_id, response) = self.send(request, true).await?;
if response.status() == StatusCode::NOT_FOUND { self.check_table_response(&request_id, response).await?;
return Err(Error::IndexNotFound {
name: index_name.to_string(),
});
};
self.client.check_response(&request_id, response).await?;
Ok(()) Ok(())
} }
@@ -1465,20 +1357,16 @@ mod tests {
.execute(example_data()) .execute(example_data())
.map_ok(|_| ()), .map_ok(|_| ()),
), ),
Box::pin(table.delete("false").map_ok(|_| ())), Box::pin(table.delete("false")),
Box::pin( Box::pin(table.add_columns(
table NewColumnTransform::SqlExpressions(vec![("x".into(), "y".into())]),
.add_columns( None,
NewColumnTransform::SqlExpressions(vec![("x".into(), "y".into())]), )),
None,
)
.map_ok(|_| ()),
),
Box::pin(async { Box::pin(async {
let alterations = vec![ColumnAlteration::new("x".into()).rename("y".into())]; let alterations = vec![ColumnAlteration::new("x".into()).rename("y".into())];
table.alter_columns(&alterations).await.map(|_| ()) table.alter_columns(&alterations).await
}), }),
Box::pin(table.drop_columns(&["a"]).map_ok(|_| ())), Box::pin(table.drop_columns(&["a"])),
// TODO: other endpoints. // TODO: other endpoints.
]; ];
@@ -1609,66 +1497,52 @@ mod tests {
body body
} }
#[rstest]
#[case("", 0)]
#[case("{}", 0)]
#[case(r#"{"request_id": "test-request-id"}"#, 0)]
#[case(r#"{"version": 43}"#, 43)]
#[tokio::test] #[tokio::test]
async fn test_add_append(#[case] response_body: &str, #[case] expected_version: u64) { async fn test_add_append() {
let data = RecordBatch::try_new( let data = RecordBatch::try_new(
Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])), Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])),
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))], vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
) )
.unwrap(); .unwrap();
// Clone response_body to give it 'static lifetime for the closure
let response_body = response_body.to_string();
let (sender, receiver) = std::sync::mpsc::channel(); let (sender, receiver) = std::sync::mpsc::channel();
let table = Table::new_with_handler("my_table", move |mut request| { let table = Table::new_with_handler("my_table", move |mut request| {
if request.url().path() == "/v1/table/my_table/insert/" { assert_eq!(request.method(), "POST");
assert_eq!(request.method(), "POST"); assert_eq!(request.url().path(), "/v1/table/my_table/insert/");
assert!(request // If mode is specified, it should be "append". Append is default
.url() // so it's not required.
.query_pairs() assert!(request
.filter(|(k, _)| k == "mode") .url()
.all(|(_, v)| v == "append")); .query_pairs()
assert_eq!( .filter(|(k, _)| k == "mode")
request.headers().get("Content-Type").unwrap(), .all(|(_, v)| v == "append"));
ARROW_STREAM_CONTENT_TYPE
); assert_eq!(
let mut body_out = reqwest::Body::from(Vec::new()); request.headers().get("Content-Type").unwrap(),
std::mem::swap(request.body_mut().as_mut().unwrap(), &mut body_out); ARROW_STREAM_CONTENT_TYPE
sender.send(body_out).unwrap(); );
http::Response::builder()
.status(200) let mut body_out = reqwest::Body::from(Vec::new());
.body(response_body.clone()) std::mem::swap(request.body_mut().as_mut().unwrap(), &mut body_out);
.unwrap() sender.send(body_out).unwrap();
} else {
panic!("Unexpected request path: {}", request.url().path()); http::Response::builder().status(200).body("").unwrap()
}
}); });
let result = table
table
.add(RecordBatchIterator::new([Ok(data.clone())], data.schema())) .add(RecordBatchIterator::new([Ok(data.clone())], data.schema()))
.execute() .execute()
.await .await
.unwrap(); .unwrap();
// Check version matches expected value
assert_eq!(result.version, expected_version);
let body = receiver.recv().unwrap(); let body = receiver.recv().unwrap();
let body = collect_body(body).await; let body = collect_body(body).await;
let expected_body = write_ipc_stream(&data); let expected_body = write_ipc_stream(&data);
assert_eq!(&body, &expected_body); assert_eq!(&body, &expected_body);
} }
#[rstest]
#[case(true)]
#[case(false)]
#[tokio::test] #[tokio::test]
async fn test_add_overwrite(#[case] old_server: bool) { async fn test_add_overwrite() {
let data = RecordBatch::try_new( let data = RecordBatch::try_new(
Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])), Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])),
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))], vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
@@ -1699,78 +1573,56 @@ mod tests {
std::mem::swap(request.body_mut().as_mut().unwrap(), &mut body_out); std::mem::swap(request.body_mut().as_mut().unwrap(), &mut body_out);
sender.send(body_out).unwrap(); sender.send(body_out).unwrap();
if old_server { http::Response::builder().status(200).body("").unwrap()
http::Response::builder().status(200).body("").unwrap()
} else {
http::Response::builder()
.status(200)
.body(r#"{"version": 43}"#)
.unwrap()
}
}); });
let result = table table
.add(RecordBatchIterator::new([Ok(data.clone())], data.schema())) .add(RecordBatchIterator::new([Ok(data.clone())], data.schema()))
.mode(AddDataMode::Overwrite) .mode(AddDataMode::Overwrite)
.execute() .execute()
.await .await
.unwrap(); .unwrap();
assert_eq!(result.version, if old_server { 0 } else { 43 });
let body = receiver.recv().unwrap(); let body = receiver.recv().unwrap();
let body = collect_body(body).await; let body = collect_body(body).await;
let expected_body = write_ipc_stream(&data); let expected_body = write_ipc_stream(&data);
assert_eq!(&body, &expected_body); assert_eq!(&body, &expected_body);
} }
#[rstest]
#[case(true)]
#[case(false)]
#[tokio::test] #[tokio::test]
async fn test_update(#[case] old_server: bool) { async fn test_update() {
let table = Table::new_with_handler("my_table", move |request| { let table = Table::new_with_handler("my_table", |request| {
if request.url().path() == "/v1/table/my_table/update/" { assert_eq!(request.method(), "POST");
assert_eq!(request.method(), "POST"); assert_eq!(request.url().path(), "/v1/table/my_table/update/");
assert_eq!( assert_eq!(
request.headers().get("Content-Type").unwrap(), request.headers().get("Content-Type").unwrap(),
JSON_CONTENT_TYPE JSON_CONTENT_TYPE
); );
if let Some(body) = request.body().unwrap().as_bytes() { if let Some(body) = request.body().unwrap().as_bytes() {
let body = std::str::from_utf8(body).unwrap(); let body = std::str::from_utf8(body).unwrap();
let value: serde_json::Value = serde_json::from_str(body).unwrap(); let value: serde_json::Value = serde_json::from_str(body).unwrap();
let updates = value.get("updates").unwrap().as_array().unwrap(); let updates = value.get("updates").unwrap().as_array().unwrap();
assert!(updates.len() == 2); assert!(updates.len() == 2);
let col_name = updates[0][0].as_str().unwrap(); let col_name = updates[0][0].as_str().unwrap();
let expression = updates[0][1].as_str().unwrap(); let expression = updates[0][1].as_str().unwrap();
assert_eq!(col_name, "a"); assert_eq!(col_name, "a");
assert_eq!(expression, "a + 1"); assert_eq!(expression, "a + 1");
let col_name = updates[1][0].as_str().unwrap(); let col_name = updates[1][0].as_str().unwrap();
let expression = updates[1][1].as_str().unwrap(); let expression = updates[1][1].as_str().unwrap();
assert_eq!(col_name, "b"); assert_eq!(col_name, "b");
assert_eq!(expression, "b - 1"); assert_eq!(expression, "b - 1");
let only_if = value.get("predicate").unwrap().as_str().unwrap(); let only_if = value.get("predicate").unwrap().as_str().unwrap();
assert_eq!(only_if, "b > 10"); assert_eq!(only_if, "b > 10");
}
if old_server {
http::Response::builder().status(200).body("").unwrap()
} else {
http::Response::builder()
.status(200)
.body(r#"{"rows_updated": 5, "version": 43}"#)
.unwrap()
}
} else {
panic!("Unexpected request path: {}", request.url().path());
} }
http::Response::builder().status(200).body("{}").unwrap()
}); });
let result = table table
.update() .update()
.column("a", "a + 1") .column("a", "a + 1")
.column("b", "b - 1") .column("b", "b - 1")
@@ -1778,73 +1630,10 @@ mod tests {
.execute() .execute()
.await .await
.unwrap(); .unwrap();
assert_eq!(result.version, if old_server { 0 } else { 43 });
assert_eq!(result.rows_updated, if old_server { 0 } else { 5 });
} }
#[rstest]
#[case(true)]
#[case(false)]
#[tokio::test] #[tokio::test]
async fn test_alter_columns(#[case] old_server: bool) { async fn test_merge_insert() {
let table = Table::new_with_handler("my_table", move |request| {
if request.url().path() == "/v1/table/my_table/alter_columns/" {
assert_eq!(request.method(), "POST");
assert_eq!(
request.headers().get("Content-Type").unwrap(),
JSON_CONTENT_TYPE
);
let body = request.body().unwrap().as_bytes().unwrap();
let body = std::str::from_utf8(body).unwrap();
let value: serde_json::Value = serde_json::from_str(body).unwrap();
let alterations = value.get("alterations").unwrap().as_array().unwrap();
assert!(alterations.len() == 2);
let path = alterations[0]["path"].as_str().unwrap();
let data_type = alterations[0]["data_type"]["type"].as_str().unwrap();
assert_eq!(path, "b.c");
assert_eq!(data_type, "int32");
let path = alterations[1]["path"].as_str().unwrap();
let nullable = alterations[1]["nullable"].as_bool().unwrap();
let rename = alterations[1]["rename"].as_str().unwrap();
assert_eq!(path, "x");
assert!(nullable);
assert_eq!(rename, "y");
if old_server {
http::Response::builder().status(200).body("{}").unwrap()
} else {
http::Response::builder()
.status(200)
.body(r#"{"version": 43}"#)
.unwrap()
}
} else {
panic!("Unexpected request path: {}", request.url().path());
}
});
let result = table
.alter_columns(&[
ColumnAlteration::new("b.c".into()).cast_to(DataType::Int32),
ColumnAlteration::new("x".into())
.rename("y".into())
.set_nullable(true),
])
.await
.unwrap();
assert_eq!(result.version, if old_server { 0 } else { 43 });
}
#[rstest]
#[case(true)]
#[case(false)]
#[tokio::test]
async fn test_merge_insert(#[case] old_server: bool) {
let batch = RecordBatch::try_new( let batch = RecordBatch::try_new(
Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])), Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)])),
vec![Arc::new(Int32Array::from(vec![1, 2, 3]))], vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
@@ -1855,43 +1644,66 @@ mod tests {
batch.schema(), batch.schema(),
)); ));
let table = Table::new_with_handler("my_table", move |request| { // Default parameters
if request.url().path() == "/v1/table/my_table/merge_insert/" { let table = Table::new_with_handler("my_table", |request| {
assert_eq!(request.method(), "POST"); assert_eq!(request.method(), "POST");
assert_eq!(request.url().path(), "/v1/table/my_table/merge_insert/");
let params = request.url().query_pairs().collect::<HashMap<_, _>>(); let params = request.url().query_pairs().collect::<HashMap<_, _>>();
assert_eq!(params["on"], "some_col"); assert_eq!(params["on"], "some_col");
assert_eq!(params["when_matched_update_all"], "false"); assert_eq!(params["when_matched_update_all"], "false");
assert_eq!(params["when_not_matched_insert_all"], "false"); assert_eq!(params["when_not_matched_insert_all"], "false");
assert_eq!(params["when_not_matched_by_source_delete"], "false"); assert_eq!(params["when_not_matched_by_source_delete"], "false");
assert!(!params.contains_key("when_matched_update_all_filt")); assert!(!params.contains_key("when_matched_update_all_filt"));
assert!(!params.contains_key("when_not_matched_by_source_delete_filt")); assert!(!params.contains_key("when_not_matched_by_source_delete_filt"));
if old_server { http::Response::builder().status(200).body("").unwrap()
http::Response::builder().status(200).body("{}").unwrap()
} else {
http::Response::builder()
.status(200)
.body(r#"{"version": 43, "num_deleted_rows": 0, "num_inserted_rows": 3, "num_updated_rows": 0}"#)
.unwrap()
}
} else {
panic!("Unexpected request path: {}", request.url().path());
}
}); });
let result = table table
.merge_insert(&["some_col"]) .merge_insert(&["some_col"])
.execute(data) .execute(data)
.await .await
.unwrap(); .unwrap();
assert_eq!(result.version, if old_server { 0 } else { 43 }); // All parameters specified
if !old_server { let (sender, receiver) = std::sync::mpsc::channel();
assert_eq!(result.num_deleted_rows, 0); let table = Table::new_with_handler("my_table", move |mut request| {
assert_eq!(result.num_inserted_rows, 3); assert_eq!(request.method(), "POST");
assert_eq!(result.num_updated_rows, 0); assert_eq!(request.url().path(), "/v1/table/my_table/merge_insert/");
} assert_eq!(
request.headers().get("Content-Type").unwrap(),
ARROW_STREAM_CONTENT_TYPE
);
let params = request.url().query_pairs().collect::<HashMap<_, _>>();
assert_eq!(params["on"], "some_col");
assert_eq!(params["when_matched_update_all"], "true");
assert_eq!(params["when_not_matched_insert_all"], "false");
assert_eq!(params["when_not_matched_by_source_delete"], "true");
assert_eq!(params["when_matched_update_all_filt"], "a = 1");
assert_eq!(params["when_not_matched_by_source_delete_filt"], "b = 2");
let mut body_out = reqwest::Body::from(Vec::new());
std::mem::swap(request.body_mut().as_mut().unwrap(), &mut body_out);
sender.send(body_out).unwrap();
http::Response::builder().status(200).body("").unwrap()
});
let mut builder = table.merge_insert(&["some_col"]);
builder
.when_matched_update_all(Some("a = 1".into()))
.when_not_matched_by_source_delete(Some("b = 2".into()));
let data = Box::new(RecordBatchIterator::new(
[Ok(batch.clone())],
batch.schema(),
));
builder.execute(data).await.unwrap();
let body = receiver.recv().unwrap();
let body = collect_body(body).await;
let expected_body = write_ipc_stream(&batch);
assert_eq!(&body, &expected_body);
} }
#[tokio::test] #[tokio::test]
@@ -1930,80 +1742,25 @@ mod tests {
assert!(e.to_string().contains("Hit retry limit")); assert!(e.to_string().contains("Hit retry limit"));
} }
#[rstest]
#[case(true)]
#[case(false)]
#[tokio::test] #[tokio::test]
async fn test_delete(#[case] old_server: bool) { async fn test_delete() {
let table = Table::new_with_handler("my_table", move |request| { let table = Table::new_with_handler("my_table", |request| {
if request.url().path() == "/v1/table/my_table/delete/" { assert_eq!(request.method(), "POST");
assert_eq!(request.method(), "POST"); assert_eq!(request.url().path(), "/v1/table/my_table/delete/");
assert_eq!( assert_eq!(
request.headers().get("Content-Type").unwrap(), request.headers().get("Content-Type").unwrap(),
JSON_CONTENT_TYPE JSON_CONTENT_TYPE
); );
let body = request.body().unwrap().as_bytes().unwrap(); let body = request.body().unwrap().as_bytes().unwrap();
let body: serde_json::Value = serde_json::from_slice(body).unwrap(); let body: serde_json::Value = serde_json::from_slice(body).unwrap();
let predicate = body.get("predicate").unwrap().as_str().unwrap(); let predicate = body.get("predicate").unwrap().as_str().unwrap();
assert_eq!(predicate, "id in (1, 2, 3)"); assert_eq!(predicate, "id in (1, 2, 3)");
if old_server { http::Response::builder().status(200).body("").unwrap()
http::Response::builder().status(200).body("{}").unwrap()
} else {
http::Response::builder()
.status(200)
.body(r#"{"version": 43}"#)
.unwrap()
}
} else {
panic!("Unexpected request path: {}", request.url().path());
}
}); });
let result = table.delete("id in (1, 2, 3)").await.unwrap(); table.delete("id in (1, 2, 3)").await.unwrap();
assert_eq!(result.version, if old_server { 0 } else { 43 });
}
#[rstest]
#[case(true)]
#[case(false)]
#[tokio::test]
async fn test_drop_columns(#[case] old_server: bool) {
let table = Table::new_with_handler("my_table", move |request| {
if request.url().path() == "/v1/table/my_table/drop_columns/" {
assert_eq!(request.method(), "POST");
assert_eq!(
request.headers().get("Content-Type").unwrap(),
JSON_CONTENT_TYPE
);
let body = request.body().unwrap().as_bytes().unwrap();
let body = std::str::from_utf8(body).unwrap();
let value: serde_json::Value = serde_json::from_str(body).unwrap();
let columns = value.get("columns").unwrap().as_array().unwrap();
assert!(columns.len() == 2);
let col1 = columns[0].as_str().unwrap();
let col2 = columns[1].as_str().unwrap();
assert_eq!(col1, "a");
assert_eq!(col2, "b");
if old_server {
http::Response::builder().status(200).body("{}").unwrap()
} else {
http::Response::builder()
.status(200)
.body(r#"{"version": 43}"#)
.unwrap()
}
} else {
panic!("Unexpected request path: {}", request.url().path());
}
});
let result = table.drop_columns(&["a", "b"]).await.unwrap();
assert_eq!(result.version, if old_server { 0 } else { 43 });
} }
#[tokio::test] #[tokio::test]
@@ -2456,10 +2213,14 @@ mod tests {
expected_body["metric_type"] = distance_type.to_lowercase().into(); expected_body["metric_type"] = distance_type.to_lowercase().into();
} }
if let Index::FTS(fts) = &params { if let Index::FTS(fts) = &params {
let params = serde_json::to_value(fts).unwrap(); expected_body["with_position"] = fts.with_position.into();
for (key, value) in params.as_object().unwrap() { expected_body["base_tokenizer"] = "simple".into();
expected_body[key] = value.clone(); expected_body["language"] = "English".into();
} expected_body["max_token_length"] = 40.into();
expected_body["lower_case"] = true.into();
expected_body["stem"] = false.into();
expected_body["remove_stop_words"] = false.into();
expected_body["ascii_folding"] = false.into();
} }
assert_eq!(body, expected_body); assert_eq!(body, expected_body);
@@ -2816,49 +2577,36 @@ mod tests {
assert!(matches!(res, Err(Error::NotSupported { .. }))); assert!(matches!(res, Err(Error::NotSupported { .. })));
} }
#[rstest]
#[case(true)]
#[case(false)]
#[tokio::test] #[tokio::test]
async fn test_add_columns(#[case] old_server: bool) { async fn test_add_columns() {
let table = Table::new_with_handler("my_table", move |request| { let table = Table::new_with_handler("my_table", |request| {
if request.url().path() == "/v1/table/my_table/add_columns/" { assert_eq!(request.method(), "POST");
assert_eq!(request.method(), "POST"); assert_eq!(request.url().path(), "/v1/table/my_table/add_columns/");
assert_eq!( assert_eq!(
request.headers().get("Content-Type").unwrap(), request.headers().get("Content-Type").unwrap(),
JSON_CONTENT_TYPE JSON_CONTENT_TYPE
); );
let body = request.body().unwrap().as_bytes().unwrap(); let body = request.body().unwrap().as_bytes().unwrap();
let body = std::str::from_utf8(body).unwrap(); let body = std::str::from_utf8(body).unwrap();
let value: serde_json::Value = serde_json::from_str(body).unwrap(); let value: serde_json::Value = serde_json::from_str(body).unwrap();
let new_columns = value.get("new_columns").unwrap().as_array().unwrap(); let new_columns = value.get("new_columns").unwrap().as_array().unwrap();
assert!(new_columns.len() == 2); assert!(new_columns.len() == 2);
let col_name = new_columns[0]["name"].as_str().unwrap(); let col_name = new_columns[0]["name"].as_str().unwrap();
let expression = new_columns[0]["expression"].as_str().unwrap(); let expression = new_columns[0]["expression"].as_str().unwrap();
assert_eq!(col_name, "b"); assert_eq!(col_name, "b");
assert_eq!(expression, "a + 1"); assert_eq!(expression, "a + 1");
let col_name = new_columns[1]["name"].as_str().unwrap(); let col_name = new_columns[1]["name"].as_str().unwrap();
let expression = new_columns[1]["expression"].as_str().unwrap(); let expression = new_columns[1]["expression"].as_str().unwrap();
assert_eq!(col_name, "x"); assert_eq!(col_name, "x");
assert_eq!(expression, "cast(NULL as int32)"); assert_eq!(expression, "cast(NULL as int32)");
if old_server { http::Response::builder().status(200).body("{}").unwrap()
http::Response::builder().status(200).body("{}").unwrap()
} else {
http::Response::builder()
.status(200)
.body(r#"{"version": 43}"#)
.unwrap()
}
} else {
panic!("Unexpected request path: {}", request.url().path());
}
}); });
let result = table table
.add_columns( .add_columns(
NewColumnTransform::SqlExpressions(vec![ NewColumnTransform::SqlExpressions(vec![
("b".into(), "a + 1".into()), ("b".into(), "a + 1".into()),
@@ -2868,8 +2616,75 @@ mod tests {
) )
.await .await
.unwrap(); .unwrap();
}
assert_eq!(result.version, if old_server { 0 } else { 43 }); #[tokio::test]
async fn test_alter_columns() {
let table = Table::new_with_handler("my_table", |request| {
assert_eq!(request.method(), "POST");
assert_eq!(request.url().path(), "/v1/table/my_table/alter_columns/");
assert_eq!(
request.headers().get("Content-Type").unwrap(),
JSON_CONTENT_TYPE
);
let body = request.body().unwrap().as_bytes().unwrap();
let body = std::str::from_utf8(body).unwrap();
let value: serde_json::Value = serde_json::from_str(body).unwrap();
let alterations = value.get("alterations").unwrap().as_array().unwrap();
assert!(alterations.len() == 2);
let path = alterations[0]["path"].as_str().unwrap();
let data_type = alterations[0]["data_type"]["type"].as_str().unwrap();
assert_eq!(path, "b.c");
assert_eq!(data_type, "int32");
let path = alterations[1]["path"].as_str().unwrap();
let nullable = alterations[1]["nullable"].as_bool().unwrap();
let rename = alterations[1]["rename"].as_str().unwrap();
assert_eq!(path, "x");
assert!(nullable);
assert_eq!(rename, "y");
http::Response::builder().status(200).body("{}").unwrap()
});
table
.alter_columns(&[
ColumnAlteration::new("b.c".into()).cast_to(DataType::Int32),
ColumnAlteration::new("x".into())
.rename("y".into())
.set_nullable(true),
])
.await
.unwrap();
}
#[tokio::test]
async fn test_drop_columns() {
let table = Table::new_with_handler("my_table", |request| {
assert_eq!(request.method(), "POST");
assert_eq!(request.url().path(), "/v1/table/my_table/drop_columns/");
assert_eq!(
request.headers().get("Content-Type").unwrap(),
JSON_CONTENT_TYPE
);
let body = request.body().unwrap().as_bytes().unwrap();
let body = std::str::from_utf8(body).unwrap();
let value: serde_json::Value = serde_json::from_str(body).unwrap();
let columns = value.get("columns").unwrap().as_array().unwrap();
assert!(columns.len() == 2);
let col1 = columns[0].as_str().unwrap();
let col2 = columns[1].as_str().unwrap();
assert_eq!(col1, "a");
assert_eq!(col2, "b");
http::Response::builder().status(200).body("{}").unwrap()
});
table.drop_columns(&["a", "b"]).await.unwrap();
} }
#[tokio::test] #[tokio::test]
@@ -2885,22 +2700,6 @@ mod tests {
table.drop_index("my_index").await.unwrap(); table.drop_index("my_index").await.unwrap();
} }
#[tokio::test]
async fn test_drop_index_not_exists() {
let table = Table::new_with_handler("my_table", |request| {
assert_eq!(request.method(), "POST");
assert_eq!(
request.url().path(),
"/v1/table/my_table/index/my_index/drop/"
);
http::Response::builder().status(404).body("{}").unwrap()
});
// Assert that the error is IndexNotFound
let e = table.drop_index("my_index").await.unwrap_err();
assert!(matches!(e, Error::IndexNotFound { .. }));
}
#[tokio::test] #[tokio::test]
async fn test_wait_for_index() { async fn test_wait_for_index() {
let table = _make_table_with_indices(0); let table = _make_table_with_indices(0);

View File

@@ -14,12 +14,13 @@ use datafusion_physical_plan::projection::ProjectionExec;
use datafusion_physical_plan::repartition::RepartitionExec; use datafusion_physical_plan::repartition::RepartitionExec;
use datafusion_physical_plan::union::UnionExec; use datafusion_physical_plan::union::UnionExec;
use datafusion_physical_plan::ExecutionPlan; use datafusion_physical_plan::ExecutionPlan;
use futures::{FutureExt, StreamExt, TryFutureExt}; use futures::{StreamExt, TryStreamExt};
use lance::dataset::builder::DatasetBuilder; use lance::dataset::builder::DatasetBuilder;
use lance::dataset::cleanup::RemovalStats; use lance::dataset::cleanup::RemovalStats;
use lance::dataset::optimize::{compact_files, CompactionMetrics, IndexRemapperOptions}; use lance::dataset::optimize::{compact_files, CompactionMetrics, IndexRemapperOptions};
use lance::dataset::scanner::Scanner; use lance::dataset::scanner::Scanner;
pub use lance::dataset::ColumnAlteration; pub use lance::dataset::ColumnAlteration;
pub use lance::dataset::MergeStats;
pub use lance::dataset::NewColumnTransform; pub use lance::dataset::NewColumnTransform;
pub use lance::dataset::ReadParams; pub use lance::dataset::ReadParams;
pub use lance::dataset::Version; pub use lance::dataset::Version;
@@ -80,12 +81,11 @@ pub mod merge;
use crate::index::waiter::wait_for_index; use crate::index::waiter::wait_for_index;
pub use chrono::Duration; pub use chrono::Duration;
use futures::future::{join_all, Either}; use futures::future::join_all;
pub use lance::dataset::optimize::CompactionOptions; pub use lance::dataset::optimize::CompactionOptions;
pub use lance::dataset::refs::{TagContents, Tags as LanceTags}; pub use lance::dataset::refs::{TagContents, Tags as LanceTags};
pub use lance::dataset::scanner::DatasetRecordBatchStream; pub use lance::dataset::scanner::DatasetRecordBatchStream;
use lance::dataset::statistics::DatasetStatisticsExt; use lance::dataset::statistics::DatasetStatisticsExt;
use lance_index::frag_reuse::FRAG_REUSE_INDEX_NAME;
pub use lance_index::optimize::OptimizeOptions; pub use lance_index::optimize::OptimizeOptions;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
@@ -312,7 +312,7 @@ impl<T: IntoArrow> AddDataBuilder<T> {
self self
} }
pub async fn execute(self) -> Result<AddResult> { pub async fn execute(self) -> Result<()> {
let parent = self.parent.clone(); let parent = self.parent.clone();
let data = self.data.into_arrow()?; let data = self.data.into_arrow()?;
let without_data = AddDataBuilder::<NoData> { let without_data = AddDataBuilder::<NoData> {
@@ -380,8 +380,8 @@ impl UpdateBuilder {
} }
/// Executes the update operation. /// Executes the update operation.
/// Returns the update result /// Returns the number of rows that were updated.
pub async fn execute(self) -> Result<UpdateResult> { pub async fn execute(self) -> Result<u64> {
if self.columns.is_empty() { if self.columns.is_empty() {
Err(Error::InvalidInput { Err(Error::InvalidInput {
message: "at least one column must be specified in an update operation".to_string(), message: "at least one column must be specified in an update operation".to_string(),
@@ -424,82 +424,6 @@ pub trait Tags: Send + Sync {
async fn update(&mut self, tag: &str, version: u64) -> Result<()>; async fn update(&mut self, tag: &str, version: u64) -> Result<()>;
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct UpdateResult {
#[serde(default)]
pub rows_updated: u64,
// The commit version associated with the operation.
// A version of `0` indicates compatibility with legacy servers that do not return
/// a commit version.
#[serde(default)]
pub version: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AddResult {
// The commit version associated with the operation.
// A version of `0` indicates compatibility with legacy servers that do not return
/// a commit version.
#[serde(default)]
pub version: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct DeleteResult {
// The commit version associated with the operation.
// A version of `0` indicates compatibility with legacy servers that do not return
/// a commit version.
#[serde(default)]
pub version: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct MergeResult {
// The commit version associated with the operation.
// A version of `0` indicates compatibility with legacy servers that do not return
/// a commit version.
#[serde(default)]
pub version: u64,
/// Number of inserted rows (for user statistics)
#[serde(default)]
pub num_inserted_rows: u64,
/// Number of updated rows (for user statistics)
#[serde(default)]
pub num_updated_rows: u64,
/// Number of deleted rows (for user statistics)
/// Note: This is different from internal references to 'deleted_rows', since we technically "delete" updated rows during processing.
/// However those rows are not shared with the user.
#[serde(default)]
pub num_deleted_rows: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AddColumnsResult {
// The commit version associated with the operation.
// A version of `0` indicates compatibility with legacy servers that do not return
/// a commit version.
#[serde(default)]
pub version: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct AlterColumnsResult {
// The commit version associated with the operation.
// A version of `0` indicates compatibility with legacy servers that do not return
/// a commit version.
#[serde(default)]
pub version: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct DropColumnsResult {
// The commit version associated with the operation.
// A version of `0` indicates compatibility with legacy servers that do not return
/// a commit version.
#[serde(default)]
pub version: u64,
}
/// A trait for anything "table-like". This is used for both native tables (which target /// A trait for anything "table-like". This is used for both native tables (which target
/// Lance datasets) and remote tables (which target LanceDB cloud) /// Lance datasets) and remote tables (which target LanceDB cloud)
/// ///
@@ -544,11 +468,11 @@ pub trait BaseTable: std::fmt::Display + std::fmt::Debug + Send + Sync {
&self, &self,
add: AddDataBuilder<NoData>, add: AddDataBuilder<NoData>,
data: Box<dyn arrow_array::RecordBatchReader + Send>, data: Box<dyn arrow_array::RecordBatchReader + Send>,
) -> Result<AddResult>; ) -> Result<()>;
/// Delete rows from the table. /// Delete rows from the table.
async fn delete(&self, predicate: &str) -> Result<DeleteResult>; async fn delete(&self, predicate: &str) -> Result<()>;
/// Update rows in the table. /// Update rows in the table.
async fn update(&self, update: UpdateBuilder) -> Result<UpdateResult>; async fn update(&self, update: UpdateBuilder) -> Result<u64>;
/// Create an index on the provided column(s). /// Create an index on the provided column(s).
async fn create_index(&self, index: IndexBuilder) -> Result<()>; async fn create_index(&self, index: IndexBuilder) -> Result<()>;
/// List the indices on the table. /// List the indices on the table.
@@ -564,7 +488,7 @@ pub trait BaseTable: std::fmt::Display + std::fmt::Debug + Send + Sync {
&self, &self,
params: MergeInsertBuilder, params: MergeInsertBuilder,
new_data: Box<dyn RecordBatchReader + Send>, new_data: Box<dyn RecordBatchReader + Send>,
) -> Result<MergeResult>; ) -> Result<MergeStats>;
/// Gets the table tag manager. /// Gets the table tag manager.
async fn tags(&self) -> Result<Box<dyn Tags + '_>>; async fn tags(&self) -> Result<Box<dyn Tags + '_>>;
/// Optimize the dataset. /// Optimize the dataset.
@@ -574,11 +498,11 @@ pub trait BaseTable: std::fmt::Display + std::fmt::Debug + Send + Sync {
&self, &self,
transforms: NewColumnTransform, transforms: NewColumnTransform,
read_columns: Option<Vec<String>>, read_columns: Option<Vec<String>>,
) -> Result<AddColumnsResult>; ) -> Result<()>;
/// Alter columns in the table. /// Alter columns in the table.
async fn alter_columns(&self, alterations: &[ColumnAlteration]) -> Result<AlterColumnsResult>; async fn alter_columns(&self, alterations: &[ColumnAlteration]) -> Result<()>;
/// Drop columns from the table. /// Drop columns from the table.
async fn drop_columns(&self, columns: &[&str]) -> Result<DropColumnsResult>; async fn drop_columns(&self, columns: &[&str]) -> Result<()>;
/// Get the version of the table. /// Get the version of the table.
async fn version(&self) -> Result<u64>; async fn version(&self) -> Result<u64>;
/// Checkout a specific version of the table. /// Checkout a specific version of the table.
@@ -807,7 +731,7 @@ impl Table {
/// tbl.delete("id > 5").await.unwrap(); /// tbl.delete("id > 5").await.unwrap();
/// # }); /// # });
/// ``` /// ```
pub async fn delete(&self, predicate: &str) -> Result<DeleteResult> { pub async fn delete(&self, predicate: &str) -> Result<()> {
self.inner.delete(predicate).await self.inner.delete(predicate).await
} }
@@ -1122,20 +1046,17 @@ impl Table {
&self, &self,
transforms: NewColumnTransform, transforms: NewColumnTransform,
read_columns: Option<Vec<String>>, read_columns: Option<Vec<String>>,
) -> Result<AddColumnsResult> { ) -> Result<()> {
self.inner.add_columns(transforms, read_columns).await self.inner.add_columns(transforms, read_columns).await
} }
/// Change a column's name or nullability. /// Change a column's name or nullability.
pub async fn alter_columns( pub async fn alter_columns(&self, alterations: &[ColumnAlteration]) -> Result<()> {
&self,
alterations: &[ColumnAlteration],
) -> Result<AlterColumnsResult> {
self.inner.alter_columns(alterations).await self.inner.alter_columns(alterations).await
} }
/// Remove columns from the table. /// Remove columns from the table.
pub async fn drop_columns(&self, columns: &[&str]) -> Result<DropColumnsResult> { pub async fn drop_columns(&self, columns: &[&str]) -> Result<()> {
self.inner.drop_columns(columns).await self.inner.drop_columns(columns).await
} }
@@ -1978,12 +1899,16 @@ impl NativeTable {
} }
let mut dataset = self.dataset.get_mut().await?; let mut dataset = self.dataset.get_mut().await?;
let fts_params = lance_index::scalar::InvertedIndexParams {
with_position: fts_opts.with_position,
tokenizer_config: fts_opts.tokenizer_configs,
};
dataset dataset
.create_index( .create_index(
&[field.name()], &[field.name()],
IndexType::Inverted, IndexType::Inverted,
None, None,
&fts_opts, &fts_params,
replace, replace,
) )
.await?; .await?;
@@ -2011,7 +1936,7 @@ impl NativeTable {
/// more information. /// more information.
pub async fn uses_v2_manifest_paths(&self) -> Result<bool> { pub async fn uses_v2_manifest_paths(&self) -> Result<bool> {
let dataset = self.dataset.get().await?; let dataset = self.dataset.get().await?;
Ok(dataset.manifest_location().naming_scheme == ManifestNamingScheme::V2) Ok(dataset.manifest_naming_scheme == ManifestNamingScheme::V2)
} }
/// Migrate the table to use the new manifest path scheme. /// Migrate the table to use the new manifest path scheme.
@@ -2164,7 +2089,7 @@ impl BaseTable for NativeTable {
&self, &self,
add: AddDataBuilder<NoData>, add: AddDataBuilder<NoData>,
data: Box<dyn RecordBatchReader + Send>, data: Box<dyn RecordBatchReader + Send>,
) -> Result<AddResult> { ) -> Result<()> {
let data = Box::new(MaybeEmbedded::try_new( let data = Box::new(MaybeEmbedded::try_new(
data, data,
self.table_definition().await?, self.table_definition().await?,
@@ -2187,9 +2112,9 @@ impl BaseTable for NativeTable {
.execute_stream(data) .execute_stream(data)
.await? .await?
}; };
let version = dataset.manifest().version;
self.dataset.set_latest(dataset).await; self.dataset.set_latest(dataset).await;
Ok(AddResult { version }) Ok(())
} }
async fn create_index(&self, opts: IndexBuilder) -> Result<()> { async fn create_index(&self, opts: IndexBuilder) -> Result<()> {
@@ -2235,7 +2160,7 @@ impl BaseTable for NativeTable {
Ok(dataset.prewarm_index(index_name).await?) Ok(dataset.prewarm_index(index_name).await?)
} }
async fn update(&self, update: UpdateBuilder) -> Result<UpdateResult> { async fn update(&self, update: UpdateBuilder) -> Result<u64> {
let dataset = self.dataset.get().await?.clone(); let dataset = self.dataset.get().await?.clone();
let mut builder = LanceUpdateBuilder::new(Arc::new(dataset)); let mut builder = LanceUpdateBuilder::new(Arc::new(dataset));
if let Some(predicate) = update.filter { if let Some(predicate) = update.filter {
@@ -2251,10 +2176,7 @@ impl BaseTable for NativeTable {
self.dataset self.dataset
.set_latest(res.new_dataset.as_ref().clone()) .set_latest(res.new_dataset.as_ref().clone())
.await; .await;
Ok(UpdateResult { Ok(res.rows_updated)
rows_updated: res.rows_updated,
version: res.new_dataset.version().version,
})
} }
async fn create_plan( async fn create_plan(
@@ -2446,7 +2368,7 @@ impl BaseTable for NativeTable {
&self, &self,
params: MergeInsertBuilder, params: MergeInsertBuilder,
new_data: Box<dyn RecordBatchReader + Send>, new_data: Box<dyn RecordBatchReader + Send>,
) -> Result<MergeResult> { ) -> Result<MergeStats> {
let dataset = Arc::new(self.dataset.get().await?.clone()); let dataset = Arc::new(self.dataset.get().await?.clone());
let mut builder = LanceMergeInsertBuilder::try_new(dataset.clone(), params.on)?; let mut builder = LanceMergeInsertBuilder::try_new(dataset.clone(), params.on)?;
match ( match (
@@ -2472,43 +2394,16 @@ impl BaseTable for NativeTable {
} else { } else {
builder.when_not_matched_by_source(WhenNotMatchedBySource::Keep); builder.when_not_matched_by_source(WhenNotMatchedBySource::Keep);
} }
let job = builder.try_build()?;
let future = if let Some(timeout) = params.timeout { let (new_dataset, stats) = job.execute_reader(new_data).await?;
// The default retry timeout is 30s, so we pass the full timeout down
// as well in case it is longer than that.
let future = builder
.retry_timeout(timeout)
.try_build()?
.execute_reader(new_data);
Either::Left(tokio::time::timeout(timeout, future).map(|res| match res {
Ok(Ok((new_dataset, stats))) => Ok((new_dataset, stats)),
Ok(Err(e)) => Err(e.into()),
Err(_) => Err(Error::Runtime {
message: "merge insert timed out".to_string(),
}),
}))
} else {
let job = builder.try_build()?;
Either::Right(job.execute_reader(new_data).map_err(|e| e.into()))
};
let (new_dataset, stats) = future.await?;
let version = new_dataset.manifest().version;
self.dataset.set_latest(new_dataset.as_ref().clone()).await; self.dataset.set_latest(new_dataset.as_ref().clone()).await;
Ok(MergeResult { Ok(stats)
version,
num_updated_rows: stats.num_updated_rows,
num_inserted_rows: stats.num_inserted_rows,
num_deleted_rows: stats.num_deleted_rows,
})
} }
/// Delete rows from the table /// Delete rows from the table
async fn delete(&self, predicate: &str) -> Result<DeleteResult> { async fn delete(&self, predicate: &str) -> Result<()> {
let mut dataset = self.dataset.get_mut().await?; self.dataset.get_mut().await?.delete(predicate).await?;
dataset.delete(predicate).await?; Ok(())
Ok(DeleteResult {
version: dataset.version().version,
})
} }
async fn tags(&self) -> Result<Box<dyn Tags + '_>> { async fn tags(&self) -> Result<Box<dyn Tags + '_>> {
@@ -2575,83 +2470,54 @@ impl BaseTable for NativeTable {
&self, &self,
transforms: NewColumnTransform, transforms: NewColumnTransform,
read_columns: Option<Vec<String>>, read_columns: Option<Vec<String>>,
) -> Result<AddColumnsResult> { ) -> Result<()> {
let mut dataset = self.dataset.get_mut().await?; self.dataset
dataset.add_columns(transforms, read_columns, None).await?; .get_mut()
Ok(AddColumnsResult { .await?
version: dataset.version().version, .add_columns(transforms, read_columns, None)
}) .await?;
Ok(())
} }
async fn alter_columns(&self, alterations: &[ColumnAlteration]) -> Result<AlterColumnsResult> { async fn alter_columns(&self, alterations: &[ColumnAlteration]) -> Result<()> {
let mut dataset = self.dataset.get_mut().await?; self.dataset
dataset.alter_columns(alterations).await?; .get_mut()
Ok(AlterColumnsResult { .await?
version: dataset.version().version, .alter_columns(alterations)
}) .await?;
Ok(())
} }
async fn drop_columns(&self, columns: &[&str]) -> Result<DropColumnsResult> { async fn drop_columns(&self, columns: &[&str]) -> Result<()> {
let mut dataset = self.dataset.get_mut().await?; self.dataset.get_mut().await?.drop_columns(columns).await?;
dataset.drop_columns(columns).await?; Ok(())
Ok(DropColumnsResult {
version: dataset.version().version,
})
} }
async fn list_indices(&self) -> Result<Vec<IndexConfig>> { async fn list_indices(&self) -> Result<Vec<IndexConfig>> {
let dataset = self.dataset.get().await?; let dataset = self.dataset.get().await?;
let indices = dataset.load_indices().await?; let indices = dataset.load_indices().await?;
let results = futures::stream::iter(indices.as_slice()).then(|idx| async { futures::stream::iter(indices.as_slice()).then(|idx| async {
let stats = dataset.index_statistics(idx.name.as_str()).await?;
// skip Lance internal indexes let stats: serde_json::Value = serde_json::from_str(&stats).map_err(|e| Error::Runtime {
if idx.name == FRAG_REUSE_INDEX_NAME { message: format!("error deserializing index statistics: {}", e),
return None; })?;
} let index_type = stats.get("index_type").and_then(|v| v.as_str())
.ok_or_else(|| Error::Runtime {
let stats = match dataset.index_statistics(idx.name.as_str()).await { message: "index statistics was missing index type".to_string(),
Ok(stats) => stats, })?;
Err(e) => { let index_type: crate::index::IndexType = index_type.parse().map_err(|e| Error::Runtime {
log::warn!("Failed to get statistics for index {} ({}): {}", idx.name, idx.uuid, e); message: format!("error parsing index type: {}", e),
return None; })?;
}
};
let stats: serde_json::Value = match serde_json::from_str(&stats) {
Ok(stats) => stats,
Err(e) => {
log::warn!("Failed to deserialize index statistics for index {} ({}): {}", idx.name, idx.uuid, e);
return None;
}
};
let Some(index_type) = stats.get("index_type").and_then(|v| v.as_str()) else {
log::warn!("Index statistics was missing 'index_type' field for index {} ({})", idx.name, idx.uuid);
return None;
};
let index_type: crate::index::IndexType = match index_type.parse() {
Ok(index_type) => index_type,
Err(e) => {
log::warn!("Failed to parse index type for index {} ({}): {}", idx.name, idx.uuid, e);
return None;
}
};
let mut columns = Vec::with_capacity(idx.fields.len()); let mut columns = Vec::with_capacity(idx.fields.len());
for field_id in &idx.fields { for field_id in &idx.fields {
let Some(field) = dataset.schema().field_by_id(*field_id) else { let field = dataset.schema().field_by_id(*field_id).ok_or_else(|| Error::Runtime { message: format!("The index with name {} and uuid {} referenced a field with id {} which does not exist in the schema", idx.name, idx.uuid, field_id) })?;
log::warn!("The index {} ({}) referenced a field with id {} which does not exist in the schema", idx.name, idx.uuid, field_id);
return None;
};
columns.push(field.name.clone()); columns.push(field.name.clone());
} }
let name = idx.name.clone(); let name = idx.name.clone();
Some(IndexConfig { index_type, columns, name }) Ok(IndexConfig { index_type, columns, name })
}).collect::<Vec<_>>().await; }).try_collect::<Vec<_>>().await
Ok(results.into_iter().flatten().collect())
} }
fn dataset_uri(&self) -> &str { fn dataset_uri(&self) -> &str {
@@ -2844,7 +2710,7 @@ mod tests {
use super::*; use super::*;
use crate::connect; use crate::connect;
use crate::connection::ConnectBuilder; use crate::connection::ConnectBuilder;
use crate::index::scalar::{BTreeIndexBuilder, BitmapIndexBuilder}; use crate::index::scalar::BTreeIndexBuilder;
use crate::query::{ExecutableQuery, QueryBase}; use crate::query::{ExecutableQuery, QueryBase};
#[tokio::test] #[tokio::test]
@@ -4296,65 +4162,4 @@ mod tests {
} }
) )
} }
#[tokio::test]
pub async fn test_list_indices_skip_frag_reuse() {
let tmp_dir = tempdir().unwrap();
let uri = tmp_dir.path().to_str().unwrap();
let conn = ConnectBuilder::new(uri).execute().await.unwrap();
let schema = Arc::new(Schema::new(vec![
Field::new("id", DataType::Int32, false),
Field::new("foo", DataType::Int32, true),
]));
let batch = RecordBatch::try_new(
schema.clone(),
vec![
Arc::new(Int32Array::from_iter_values(0..100)),
Arc::new(Int32Array::from_iter_values(0..100)),
],
)
.unwrap();
let table = conn
.create_table(
"test_list_indices_skip_frag_reuse",
RecordBatchIterator::new(vec![Ok(batch.clone())], batch.schema()),
)
.execute()
.await
.unwrap();
table
.add(RecordBatchIterator::new(
vec![Ok(batch.clone())],
batch.schema(),
))
.execute()
.await
.unwrap();
table
.create_index(&["id"], Index::Bitmap(BitmapIndexBuilder {}))
.execute()
.await
.unwrap();
table
.optimize(OptimizeAction::Compact {
options: CompactionOptions {
target_rows_per_fragment: 2_000,
defer_index_remap: true,
..Default::default()
},
remap_options: None,
})
.await
.unwrap();
let result = table.list_indices().await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].index_type, crate::index::IndexType::Bitmap);
}
} }

View File

@@ -1,13 +1,14 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: Copyright The LanceDB Authors // SPDX-FileCopyrightText: Copyright The LanceDB Authors
use std::{sync::Arc, time::Duration}; use std::sync::Arc;
use arrow_array::RecordBatchReader; use arrow_array::RecordBatchReader;
use lance::dataset::MergeStats;
use crate::Result; use crate::Result;
use super::{BaseTable, MergeResult}; use super::BaseTable;
/// A builder used to create and run a merge insert operation /// A builder used to create and run a merge insert operation
/// ///
@@ -21,7 +22,6 @@ pub struct MergeInsertBuilder {
pub(crate) when_not_matched_insert_all: bool, pub(crate) when_not_matched_insert_all: bool,
pub(crate) when_not_matched_by_source_delete: bool, pub(crate) when_not_matched_by_source_delete: bool,
pub(crate) when_not_matched_by_source_delete_filt: Option<String>, pub(crate) when_not_matched_by_source_delete_filt: Option<String>,
pub(crate) timeout: Option<Duration>,
} }
impl MergeInsertBuilder { impl MergeInsertBuilder {
@@ -34,7 +34,6 @@ impl MergeInsertBuilder {
when_not_matched_insert_all: false, when_not_matched_insert_all: false,
when_not_matched_by_source_delete: false, when_not_matched_by_source_delete: false,
when_not_matched_by_source_delete_filt: None, when_not_matched_by_source_delete_filt: None,
timeout: None,
} }
} }
@@ -86,26 +85,11 @@ impl MergeInsertBuilder {
self self
} }
/// Maximum time to run the operation before cancelling it.
///
/// By default, there is a 30-second timeout that is only enforced after the
/// first attempt. This is to prevent spending too long retrying to resolve
/// conflicts. For example, if a write attempt takes 20 seconds and fails,
/// the second attempt will be cancelled after 10 seconds, hitting the
/// 30-second timeout. However, a write that takes one hour and succeeds on the
/// first attempt will not be cancelled.
///
/// When this is set, the timeout is enforced on all attempts, including the first.
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
self.timeout = Some(timeout);
self
}
/// Executes the merge insert operation /// Executes the merge insert operation
/// ///
/// Returns version and statistics about the merge operation including the number of rows /// Returns statistics about the merge operation including the number of rows
/// inserted, updated, and deleted. /// inserted, updated, and deleted.
pub async fn execute(self, new_data: Box<dyn RecordBatchReader + Send>) -> Result<MergeResult> { pub async fn execute(self, new_data: Box<dyn RecordBatchReader + Send>) -> Result<MergeStats> {
self.table.clone().merge_insert(self, new_data).await self.table.clone().merge_insert(self, new_data).await
} }
} }