mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-27 16:38:31 +00:00
Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d3962686e | |||
| 25645d82d4 | |||
| 0dd9dfdfc7 | |||
| d24b2dcacc | |||
| 2deccf21cf | |||
| ead4d27bfc | |||
| 5153e5a023 | |||
| 79f626b09e | |||
| ae81d73563 | |||
| 8b7e13b0c6 | |||
| b78f2a5044 | |||
| 06872463cf | |||
| 2fbf6d6211 | |||
| 391cac9034 | |||
| 21530432a0 | |||
| 9b825c5f29 | |||
| 8083232dd5 | |||
| 302b21aa94 | |||
| 35b5d015ac | |||
| a57fb68891 | |||
| a614400755 | |||
| 1d880f11ff | |||
| ec4ad54ba2 | |||
| d0bcc6c6fe | |||
| 81c3f108ce | |||
| c988e4848d | |||
| 2fea7cd48d | |||
| 0e65123bd8 | |||
| 6ed3074d4c | |||
| c1a8c3f089 | |||
| fce45ba9fc | |||
| 5013c176dd | |||
| 71f85a8d9f | |||
| c72f5b2960 | |||
| 93f47b8aab | |||
| 105fd73bc6 | |||
| 94d484f539 | |||
| b0dae5eb0b | |||
| 242ade8017 | |||
| 40d4d012e7 | |||
| 000e3b506b | |||
| 1b950188c3 | |||
| 6cc77b573c | |||
| 1f1d03f306 | |||
| 45cd053478 | |||
| 68749ecfa3 | |||
| e98d8ac685 | |||
| 851fa16b47 | |||
| d04ac7ed20 | |||
| a578e9ff7f | |||
| 7801e2746a | |||
| 5468f3d490 | |||
| c0df2c63b6 | |||
| 9e8f1c1a6d | |||
| 01679e37fd | |||
| c7cb0b9afa | |||
| a35f7044ee | |||
| 29822306d2 | |||
| f39a7a4dd9 | |||
| 1baada89ef | |||
| ecf4555cfd | |||
| fa3d9b2ce2 | |||
| 217ea1a799 | |||
| bacd0e4c3c | |||
| 7fd881bbe3 | |||
| 5c3bc7f643 | |||
| fe992bf4ee | |||
| 944398d807 | |||
| fd2a202a46 | |||
| 6a0df4de47 | |||
| fbfb53e30f | |||
| 593ef1c471 | |||
| cf27f6902e | |||
| f76ee304b8 | |||
| a588208de6 | |||
| 685cb01d6d | |||
| 4ba2421254 | |||
| 09843410ec | |||
| 7adcffc2b4 | |||
| c1331e5083 | |||
| 426684cf1b | |||
| e517ba5205 | |||
| 5c1b44020a | |||
| 061a3da8b9 | |||
| 4e042af12f | |||
| 27cea03b7d | |||
| f1c4967eeb | |||
| 11c1d81638 | |||
| f6efdc9e9f | |||
| cdebea118d | |||
| 76942306b7 | |||
| d742b174c4 | |||
| a075aa62f8 | |||
| 040a4120c8 | |||
| 928c3dde2d | |||
| 980818df26 | |||
| c429863122 | |||
| fc0d917d32 | |||
| def869bb78 | |||
| 9e4d8bd1c7 | |||
| 4148dfef72 | |||
| 0ac70a8b9f | |||
| 91c5f344d2 | |||
| ffd35c1a8f | |||
| 790d0c684c | |||
| 251f194696 | |||
| 4b7325bd74 | |||
| 1d75638dea | |||
| 031c3585a8 | |||
| 6fb976cf89 | |||
| a615306f39 | |||
| 920fc0e455 | |||
| 5acce6782e | |||
| 12405a4077 | |||
| 36054be576 | |||
| 77a93fee76 | |||
| 7bb501839a | |||
| 5b347afd99 | |||
| 706a9c327f |
@@ -5,7 +5,3 @@ This directory contains repo-scoped code agent skills for the LanceDB project.
|
||||
Each skill is a folder that contains a required `SKILL.md` and optional bundled resources.
|
||||
|
||||
Codex discovers skills from `.agents/skills` in the current working directory and parent directories.
|
||||
|
||||
The `lancedb` skill lives in the `plugins/lancedb` plugin (see `plugins/lancedb/skills/lancedb`)
|
||||
so it can be installed via the plugin marketplaces (`.claude-plugin/marketplace.json` and
|
||||
`.agents/plugins/marketplace.json`); the `lancedb` entry here is a symlink into that plugin.
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../plugins/lancedb/skills/lancedb
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.bumpversion]
|
||||
current_version = "0.37.1-beta.0"
|
||||
current_version = "0.38.0-beta.11"
|
||||
parse = """(?x)
|
||||
(?P<major>0|[1-9]\\d*)\\.
|
||||
(?P<minor>0|[1-9]\\d*)\\.
|
||||
|
||||
@@ -9,6 +9,18 @@ debug = true
|
||||
codegen-units = 16
|
||||
lto = "thin"
|
||||
|
||||
[profile.release-no-lto]
|
||||
inherits = "release"
|
||||
debug = true
|
||||
lto = false
|
||||
# Prioritize compile time when LTO is not relevant to the measurement.
|
||||
codegen-units = 16
|
||||
|
||||
[profile.bench]
|
||||
inherits = "release"
|
||||
lto = "thin"
|
||||
codegen-units = 16
|
||||
|
||||
[target.'cfg(all())']
|
||||
rustflags = [
|
||||
"-Wclippy::all",
|
||||
|
||||
@@ -17,6 +17,18 @@ updates:
|
||||
# newer minimum versions.
|
||||
versioning-strategy: lockfile-only
|
||||
groups:
|
||||
# The arrow-rs and datafusion crates are released in lockstep and have to
|
||||
# move together, so keep them in one PR instead of one per sub-crate.
|
||||
# Listed first: a dependency joins the first group it matches.
|
||||
arrow-datafusion:
|
||||
patterns:
|
||||
- arrow
|
||||
- arrow-*
|
||||
- parquet
|
||||
- parquet-*
|
||||
- datafusion
|
||||
- datafusion-*
|
||||
- object_store
|
||||
rust-minor-patch:
|
||||
update-types:
|
||||
- minor
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
name: CI scripts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- ci/set_lance_version.py
|
||||
- ci/tests/**
|
||||
- .github/workflows/ci-scripts.yml
|
||||
pull_request:
|
||||
paths:
|
||||
- ci/set_lance_version.py
|
||||
- ci/tests/**
|
||||
- .github/workflows/ci-scripts.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test CI scripts
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Run tests
|
||||
run: python -m unittest discover -s ci/tests -v
|
||||
@@ -4,14 +4,14 @@ on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag name from Lance. If omitted, the skill will use the latest Lance release that needs an update."
|
||||
description: "Tag name from Lance (e.g. `v7.2.0-beta.1`). If omitted, the newest release is resolved automatically — stable releases are preferred over pre-releases — and the run is skipped if it is not newer than the version currently pinned in Cargo.toml."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: "Tag name from Lance. Leave empty to use the latest Lance release that needs an update."
|
||||
description: "Tag name from Lance (e.g. `v7.2.0-beta.1`). Leave empty to resolve the newest release automatically — stable releases are preferred over pre-releases — and skip the run if it is not newer than the version currently pinned in Cargo.toml."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
|
||||
@@ -36,7 +36,9 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
checker_outcome: ${{ steps.lychee.outcome }}
|
||||
exit_code: ${{ steps.lychee.outputs.exit_code }}
|
||||
status: ${{ steps.validate.outputs.status }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
@@ -50,6 +52,7 @@ jobs:
|
||||
|
||||
- name: Check links
|
||||
id: lychee
|
||||
continue-on-error: true
|
||||
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
|
||||
with:
|
||||
# Restricted to http(s) on purpose. Much of docs/src is generated
|
||||
@@ -68,38 +71,50 @@ jobs:
|
||||
format: json
|
||||
output: ./lychee/out.json
|
||||
jobSummary: false
|
||||
# The report, not a red build, is the signal for broken links. The
|
||||
# validation step below still fails the run if the check itself
|
||||
# breaks.
|
||||
# The report issue, not a red workflow run, is the signal for link
|
||||
# findings and checker failures alike.
|
||||
fail: false
|
||||
|
||||
- name: Validate report
|
||||
id: validate
|
||||
# lychee does not reserve exit code 2 for broken links: its CLI
|
||||
# parser also exits 2 on an invalid option, before any link was
|
||||
# checked or any report written. Only a parseable report whose
|
||||
# counts agree with the exit code counts as a link verdict; anything
|
||||
# else fails here, and the report job below is skipped entirely, so
|
||||
# the tracking issue is never touched. Exit 2 covers timeouts as
|
||||
# well as errors, and a timed-out host is exactly the transient
|
||||
# unavailability this report exists to surface, so both count as
|
||||
# findings. Requiring total > 0 also catches a glob that silently
|
||||
# stopped matching any file.
|
||||
if: steps.lychee.outputs.exit_code == 0 || steps.lychee.outputs.exit_code == 2
|
||||
# counts agree with a completed exit code (0 or 2) counts as a link
|
||||
# verdict. Everything else becomes a checker-error report instead of
|
||||
# failing the workflow. Exit 2 covers timeouts as well as errors, and a
|
||||
# timed-out host is exactly the transient unavailability this report
|
||||
# exists to surface, so both count as findings. Requiring total > 0
|
||||
# also catches a glob that silently stopped matching any file.
|
||||
if: always()
|
||||
env:
|
||||
CHECKER_OUTCOME: ${{ steps.lychee.outcome }}
|
||||
EXIT_CODE: ${{ steps.lychee.outputs.exit_code }}
|
||||
run: |
|
||||
jq -e --argjson code "$EXIT_CODE" '
|
||||
(.total > 0) and
|
||||
(if $code == 0
|
||||
then .errors == 0 and .timeouts == 0
|
||||
and (.error_map | length == 0) and (.timeout_map | length == 0)
|
||||
else (.errors + .timeouts) > 0
|
||||
and ((.error_map | length) + (.timeout_map | length)) > 0
|
||||
end)
|
||||
' ./lychee/out.json
|
||||
status=checker-error
|
||||
if [[ "$CHECKER_OUTCOME" == success ]] &&
|
||||
[[ "$EXIT_CODE" == 0 || "$EXIT_CODE" == 2 ]] &&
|
||||
jq -e --argjson code "$EXIT_CODE" '
|
||||
(.total > 0) and
|
||||
(if $code == 0
|
||||
then .errors == 0 and .timeouts == 0
|
||||
and (.error_map | length == 0) and (.timeout_map | length == 0)
|
||||
else (.errors + .timeouts) > 0
|
||||
and ((.error_map | length) + (.timeout_map | length)) > 0
|
||||
end)
|
||||
' ./lychee/out.json
|
||||
then
|
||||
if [[ "$EXIT_CODE" == 0 ]]; then
|
||||
status=healthy
|
||||
else
|
||||
status=findings
|
||||
fi
|
||||
fi
|
||||
echo "status=$status" >> "$GITHUB_OUTPUT"
|
||||
echo "Validated link check as $status"
|
||||
|
||||
- name: Upload report
|
||||
if: steps.lychee.outputs.exit_code == 2
|
||||
if: steps.validate.outputs.status == 'findings'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: link-report
|
||||
@@ -115,26 +130,11 @@ jobs:
|
||||
permissions:
|
||||
issues: write
|
||||
env:
|
||||
CHECKER_OUTCOME: ${{ needs.scan.outputs.checker_outcome }}
|
||||
EXIT_CODE: ${{ needs.scan.outputs.exit_code }}
|
||||
STATUS: ${{ needs.scan.outputs.status }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Classify checker result
|
||||
# lychee exits 0 when every link resolves and 2 when links fail,
|
||||
# both already cross-checked against the report by the scan job's
|
||||
# validation step. Anything else (1 runtime, 3 bad config) means the
|
||||
# check never produced a link verdict, which must surface as a failed
|
||||
# run rather than be published as "broken documentation links".
|
||||
run: |
|
||||
case "$EXIT_CODE" in
|
||||
0|2)
|
||||
echo "lychee exit code $EXIT_CODE"
|
||||
;;
|
||||
*)
|
||||
echo "::error::lychee exited with '$EXIT_CODE': the link check did not complete. Leaving the report issue untouched."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Find existing report issue
|
||||
id: report
|
||||
# Matched on title alone, and through search rather than a listing:
|
||||
@@ -144,7 +144,7 @@ jobs:
|
||||
# Closed issues are included because a healthy run closes the report:
|
||||
# an open-only lookup would forget that identity and the next failing
|
||||
# run would open a duplicate. The oldest match stays the canonical
|
||||
# report and is reopened below when links break again.
|
||||
# report and is reopened below when a problem recurs.
|
||||
run: |
|
||||
match=$(gh issue list --repo "$GITHUB_REPOSITORY" --state all \
|
||||
--search "in:title \"$REPORT_TITLE\" author:app/github-actions" \
|
||||
@@ -154,14 +154,14 @@ jobs:
|
||||
echo "state=$(jq -r '.state // empty' <<<"$match")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download report
|
||||
if: env.EXIT_CODE == 2
|
||||
if: env.STATUS == 'findings'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: link-report
|
||||
path: ./lychee
|
||||
|
||||
- name: Compose report
|
||||
if: env.EXIT_CODE == 2
|
||||
if: env.STATUS == 'findings'
|
||||
run: |
|
||||
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
||||
{
|
||||
@@ -185,22 +185,41 @@ jobs:
|
||||
' ./lychee/out.json
|
||||
} > ./lychee/issue.md
|
||||
|
||||
- name: Compose checker error report
|
||||
if: env.STATUS == 'checker-error'
|
||||
run: |
|
||||
mkdir -p ./lychee
|
||||
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
||||
{
|
||||
echo "The documentation link check did not complete in [the latest run]($run_url)."
|
||||
echo
|
||||
echo "This issue is rewritten by every scheduled run and closed automatically once a trustworthy run finds that all links resolve."
|
||||
echo
|
||||
echo "The checker did not produce a trustworthy link verdict. Treat the previous result, if any, as stale until a later run completes."
|
||||
echo
|
||||
echo "* Action outcome: \`$CHECKER_OUTCOME\`"
|
||||
echo "* Exit code: \`${EXIT_CODE:-not reported}\`"
|
||||
echo "* Verdict validation: \`failed\`"
|
||||
} > ./lychee/issue.md
|
||||
|
||||
- name: Reopen report issue
|
||||
# A healthy run closes the report, and the issue action below only
|
||||
# rewrites the body of whatever number it is given. Without an
|
||||
# explicit reopen, the 2 -> 0 -> 2 sequence would keep rewriting a
|
||||
# closed issue while links are broken. A CLOSED state implies the
|
||||
# lookup found a canonical issue, so no separate emptiness check.
|
||||
if: env.EXIT_CODE == 2 && steps.report.outputs.state == 'CLOSED'
|
||||
# explicit reopen, a later finding or checker error would rewrite a
|
||||
# closed issue. A CLOSED state implies the lookup found a canonical
|
||||
# issue, so no separate emptiness check.
|
||||
if: >-
|
||||
env.STATUS != 'healthy' &&
|
||||
steps.report.outputs.state == 'CLOSED'
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ steps.report.outputs.number }}
|
||||
run: |
|
||||
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
||||
gh issue reopen "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" \
|
||||
--comment "Broken documentation links found again in [the latest run]($run_url)."
|
||||
--comment "The documentation link checker reported a problem again in [the latest run]($run_url)."
|
||||
|
||||
- name: Report broken links
|
||||
if: env.EXIT_CODE == 2
|
||||
- name: Report link-check problem
|
||||
if: env.STATUS != 'healthy'
|
||||
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
|
||||
with:
|
||||
# Empty on the first failing run, which creates the issue; afterwards
|
||||
@@ -213,7 +232,9 @@ jobs:
|
||||
- name: Close report issue once links are healthy
|
||||
# An OPEN state implies the lookup found a canonical issue; a report
|
||||
# that is already closed needs nothing.
|
||||
if: env.EXIT_CODE == 0 && steps.report.outputs.state == 'OPEN'
|
||||
if: >-
|
||||
env.STATUS == 'healthy' &&
|
||||
steps.report.outputs.state == 'OPEN'
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ steps.report.outputs.number }}
|
||||
run: |
|
||||
|
||||
@@ -69,6 +69,16 @@ jobs:
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Add swap for Arm fat LTO
|
||||
if: matrix.config.platform == 'aarch64'
|
||||
shell: bash
|
||||
run: |
|
||||
swap_file="$RUNNER_TEMP/lancedb-swap"
|
||||
sudo fallocate --length 16G "$swap_file"
|
||||
sudo chmod 600 "$swap_file"
|
||||
sudo mkswap "$swap_file"
|
||||
sudo swapon "$swap_file"
|
||||
free -h
|
||||
- uses: ./.github/workflows/build_linux_wheel
|
||||
with:
|
||||
python-minor-version: 10
|
||||
@@ -119,6 +129,12 @@ jobs:
|
||||
# link.exe is single-threaded and the long pole on Windows builds. Use
|
||||
# rustc's bundled lld-link instead.
|
||||
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER: rust-lld
|
||||
# Fat LTO of the cdylib is single-threaded and the peak-memory step of the
|
||||
# build. ThinLTO parallelizes it across the runner's cores, at some cost
|
||||
# to runtime performance on our least performance-sensitive platform.
|
||||
# Matches what the nodejs Windows builds already do in npm-publish.yml.
|
||||
CARGO_PROFILE_RELEASE_LTO: thin
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 16
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
|
||||
@@ -229,7 +229,8 @@ jobs:
|
||||
# Make sure wheels are not included in the Rust cache
|
||||
- name: Delete wheels
|
||||
run: rm -rf target/wheels
|
||||
pydantic1x:
|
||||
min-deps:
|
||||
name: "Minimum dependencies"
|
||||
timeout-minutes: 60
|
||||
runs-on: "ubuntu-24.04"
|
||||
defaults:
|
||||
@@ -259,8 +260,7 @@ jobs:
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install lancedb
|
||||
run: |
|
||||
pip install "pydantic<2"
|
||||
pip install pyarrow==16
|
||||
pip install "pydantic==2.7.4" "pyarrow==16"
|
||||
pip install --extra-index-url https://pypi.fury.io/lance-format/ --extra-index-url https://pypi.fury.io/lancedb/ -e .[tests]
|
||||
- name: Run tests
|
||||
run: pytest -m "not slow and not s3_test" -x -v --durations=30 python/tests
|
||||
|
||||
@@ -121,7 +121,6 @@ jobs:
|
||||
# Need up-to-date compilers for kernels
|
||||
CC: clang-18
|
||||
CXX: clang++-18
|
||||
GH_TOKEN: ${{ secrets.SOPHON_READ_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
@@ -165,11 +164,40 @@ jobs:
|
||||
- name: Run feature tests
|
||||
run: CARGO_ARGS="--profile ci" make -C ./lancedb feature-tests
|
||||
- name: Run examples
|
||||
run: cargo run --profile ci --example simple --locked
|
||||
run: cargo run --profile ci --all-features --example simple --locked
|
||||
|
||||
remote:
|
||||
timeout-minutes: 30
|
||||
# Running this requires access to secrets, so skip if this is a PR from a
|
||||
# fork. Keep it separate from the all-features build so Cargo does not
|
||||
# retain both dependency graphs in one target directory.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
|
||||
runs-on: ubuntu-2404-4x-x64
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: rust
|
||||
env:
|
||||
CC: clang-18
|
||||
CXX: clang++-18
|
||||
GH_TOKEN: ${{ secrets.SOPHON_READ_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
lfs: true
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# Remote tests use a different feature graph from the main Linux
|
||||
# job. Cache downloads, but build into a fresh target directory.
|
||||
cache-targets: false
|
||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y protobuf-compiler libssl-dev
|
||||
- uses: rui314/setup-mold@v1
|
||||
- name: Run remote tests
|
||||
# Running this requires access to secrets, so skip if this is
|
||||
# a PR from a fork.
|
||||
if: github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork
|
||||
run: CARGO_ARGS="--profile ci" make -C ./lancedb remote-tests
|
||||
|
||||
macos:
|
||||
|
||||
@@ -18,6 +18,9 @@ Common commands:
|
||||
* Run specific test: `cargo test --quiet --features remote -p <package_name> --test <test_name>`
|
||||
* Lint: `cargo clippy --quiet --features remote --tests --examples`
|
||||
* Format Rust: `cargo fmt --all`
|
||||
* Use repository-defined Cargo profiles instead of ad hoc LTO overrides.
|
||||
* Use `release-with-debug` for benchmarks and profiling so optimized builds keep debug symbols without a rebuild.
|
||||
* Use `release-no-lto` only for local debugging, IO-bound benchmarks, or compile-time-sensitive performance investigation where LTO would not affect the measured bottleneck.
|
||||
* Format Python: `ruff format .`
|
||||
* Lint Python: `ruff check .`
|
||||
* Bootstrap Python dev env: `cd python && uv run --extra tests --extra dev maturin develop --extras tests,dev`
|
||||
|
||||
Generated
+77
-120
@@ -959,7 +959,7 @@ dependencies = [
|
||||
"aws-smithy-runtime-api",
|
||||
"aws-smithy-types",
|
||||
"h2 0.3.27",
|
||||
"h2 0.4.14",
|
||||
"h2 0.4.16",
|
||||
"http 0.2.12",
|
||||
"http 1.5.0",
|
||||
"http-body 0.4.6",
|
||||
@@ -1740,9 +1740,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cmov"
|
||||
version = "0.5.3"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746"
|
||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
@@ -1756,7 +1756,7 @@ version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3034,7 +3034,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3257,7 +3257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3455,8 +3455,8 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "fsst"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"rand 0.9.5",
|
||||
@@ -3877,9 +3877,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.14"
|
||||
version = "0.4.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
|
||||
checksum = "a9f37a958b41b3b19ee2707c06439c0e9e547e847223eb791ecb0cb821c65e27"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@@ -4188,7 +4188,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"h2 0.4.14",
|
||||
"h2 0.4.16",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"httparse",
|
||||
@@ -4561,7 +4561,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4815,8 +4815,8 @@ checksum = "e037a2e1d8d5fdbd49b16a4ea09d5d6401c1f29eca5ff29d03d3824dba16256a"
|
||||
|
||||
[[package]]
|
||||
name = "lance"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"arrow",
|
||||
@@ -4832,7 +4832,6 @@ dependencies = [
|
||||
"async-recursion",
|
||||
"async-trait",
|
||||
"async_cell",
|
||||
"aws-credential-types",
|
||||
"aws-sdk-dynamodb",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
@@ -4848,7 +4847,6 @@ dependencies = [
|
||||
"either",
|
||||
"fst",
|
||||
"futures",
|
||||
"half",
|
||||
"humantime",
|
||||
"itertools 0.14.0",
|
||||
"lance-arrow",
|
||||
@@ -4890,8 +4888,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-arrow"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -4913,7 +4911,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lance-arrow-scalar"
|
||||
version = "58.0.0"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -4927,7 +4925,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "lance-arrow-stats"
|
||||
version = "58.0.0"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-schema",
|
||||
@@ -4936,8 +4934,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-bitpacking"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"crunchy",
|
||||
@@ -4947,8 +4945,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-core"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -4956,12 +4954,10 @@ dependencies = [
|
||||
"arrow-schema",
|
||||
"async-trait",
|
||||
"blake3",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"datafusion-common",
|
||||
"datafusion-sql",
|
||||
"futures",
|
||||
"itertools 0.14.0",
|
||||
"lance-arrow",
|
||||
"lance-derive",
|
||||
"libc",
|
||||
@@ -4979,7 +4975,6 @@ dependencies = [
|
||||
"snafu 0.9.0",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"twox-hash",
|
||||
@@ -4988,8 +4983,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-datafusion"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-array",
|
||||
@@ -5008,7 +5003,6 @@ dependencies = [
|
||||
"jsonb",
|
||||
"lance-arrow",
|
||||
"lance-core",
|
||||
"lance-datagen",
|
||||
"log",
|
||||
"pin-project",
|
||||
"prost",
|
||||
@@ -5019,8 +5013,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-datagen"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-array",
|
||||
@@ -5037,8 +5031,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-derive"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5047,8 +5041,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-encoding"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-arith",
|
||||
"arrow-array",
|
||||
@@ -5073,7 +5067,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"rand 0.9.5",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"xxhash-rust",
|
||||
@@ -5082,8 +5075,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-file"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-arith",
|
||||
"arrow-array",
|
||||
@@ -5114,8 +5107,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-index"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"arrow",
|
||||
@@ -5130,7 +5123,6 @@ dependencies = [
|
||||
"async-trait",
|
||||
"bitvec",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crossbeam-queue",
|
||||
"datafusion",
|
||||
"datafusion-common",
|
||||
@@ -5148,7 +5140,6 @@ dependencies = [
|
||||
"lance-bitpacking",
|
||||
"lance-core",
|
||||
"lance-datafusion",
|
||||
"lance-datagen",
|
||||
"lance-encoding",
|
||||
"lance-file",
|
||||
"lance-index-core",
|
||||
@@ -5177,13 +5168,12 @@ dependencies = [
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lance-index-core"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-schema",
|
||||
@@ -5205,8 +5195,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-io"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-array",
|
||||
@@ -5220,7 +5210,6 @@ dependencies = [
|
||||
"futures",
|
||||
"http 1.5.0",
|
||||
"io-uring",
|
||||
"lance-arrow",
|
||||
"lance-core",
|
||||
"lance-namespace",
|
||||
"log",
|
||||
@@ -5228,42 +5217,42 @@ dependencies = [
|
||||
"moka",
|
||||
"object_store",
|
||||
"object_store_opendal",
|
||||
"opendal 0.58.1 (git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a)",
|
||||
"opendal",
|
||||
"path_abs",
|
||||
"pin-project",
|
||||
"prost",
|
||||
"rand 0.9.5",
|
||||
"reqsign-aws-v4",
|
||||
"reqsign-core",
|
||||
"reqsign-file-read-tokio",
|
||||
"reqsign-google",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lance-linalg"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
"arrow-schema",
|
||||
"cc",
|
||||
"half",
|
||||
"lance-arrow",
|
||||
"lance-core",
|
||||
"num-traits",
|
||||
"rand 0.9.5",
|
||||
"rayon",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lance-namespace"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"async-trait",
|
||||
@@ -5275,8 +5264,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-namespace-impls"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-ipc",
|
||||
@@ -5315,9 +5304,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-namespace-reqwest-client"
|
||||
version = "0.8.6"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba3f0a235e3ed5f8805205649ccc7d7d0f3df23ce1294242c9265ad488d7f19d"
|
||||
checksum = "0a030196da1c994b63a96a4f0bf5b0cfa459fe6dadc9e962320246ca328da22a"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
@@ -5329,14 +5318,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-select"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
"arrow-schema",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"itertools 0.14.0",
|
||||
"lance-core",
|
||||
"roaring",
|
||||
@@ -5345,8 +5333,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-table"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"arrow-array",
|
||||
@@ -5386,8 +5374,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-testing"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-schema",
|
||||
@@ -5400,8 +5388,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lance-tokenizer"
|
||||
version = "11.0.0-beta.3"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
version = "12.0.0-beta.2"
|
||||
source = "git+https://github.com/lance-format/lance.git?tag=v12.0.0-beta.2#dafa4642658d996b3e31dde91e02f72db7860d7e"
|
||||
dependencies = [
|
||||
"frostem",
|
||||
"icu_segmenter",
|
||||
@@ -5414,7 +5402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb"
|
||||
version = "0.37.1-beta.0"
|
||||
version = "0.38.0-beta.11"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
@@ -5450,7 +5438,6 @@ dependencies = [
|
||||
"datafusion-physical-plan",
|
||||
"datafusion-sql",
|
||||
"futures",
|
||||
"goosefs-sdk",
|
||||
"half",
|
||||
"hf-hub",
|
||||
"http 1.5.0",
|
||||
@@ -5475,8 +5462,6 @@ dependencies = [
|
||||
"moka",
|
||||
"num-traits",
|
||||
"object_store",
|
||||
"object_store_opendal",
|
||||
"opendal 0.58.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pin-project",
|
||||
"polars",
|
||||
"polars-arrow",
|
||||
@@ -5485,6 +5470,7 @@ dependencies = [
|
||||
"random_word",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"roaring",
|
||||
"rstest",
|
||||
"semver",
|
||||
"serde",
|
||||
@@ -5504,7 +5490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb-nodejs"
|
||||
version = "0.37.1-beta.0"
|
||||
version = "0.38.0-beta.11"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -5529,7 +5515,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb-python"
|
||||
version = "0.37.1-beta.0"
|
||||
version = "0.38.0-beta.11"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"async-trait",
|
||||
@@ -6254,7 +6240,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6444,7 +6430,7 @@ dependencies = [
|
||||
"futures",
|
||||
"mea",
|
||||
"object_store",
|
||||
"opendal 0.58.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"opendal",
|
||||
"pin-project",
|
||||
"tokio",
|
||||
]
|
||||
@@ -6500,15 +6486,6 @@ name = "opendal"
|
||||
version = "0.58.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f20562cc7447fcc915fc5c23df305a412ea80a733c9f2fd9e2d267e2815be6d"
|
||||
dependencies = [
|
||||
"opendal-core",
|
||||
"opendal-service-s3 0.58.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opendal"
|
||||
version = "0.58.1"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
dependencies = [
|
||||
"ctor 1.0.12",
|
||||
"opendal-core",
|
||||
@@ -6524,7 +6501,7 @@ dependencies = [
|
||||
"opendal-service-goosefs",
|
||||
"opendal-service-hf",
|
||||
"opendal-service-oss",
|
||||
"opendal-service-s3 0.58.1 (git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a)",
|
||||
"opendal-service-s3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6768,26 +6745,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opendal-service-s3"
|
||||
version = "0.58.1"
|
||||
source = "git+https://github.com/lancedb/lancedb.git?rev=31b68dd21b60fcbee4354a175c9d65ff6ae02b6a#31b68dd21b60fcbee4354a175c9d65ff6ae02b6a"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"bytes",
|
||||
"crc-fast",
|
||||
"http 1.5.0",
|
||||
"log",
|
||||
"md-5 0.11.0",
|
||||
"opendal-core",
|
||||
"quick-xml 0.41.0",
|
||||
"reqsign-aws-v4",
|
||||
"reqsign-core",
|
||||
"reqsign-file-read-tokio",
|
||||
"serde",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.2.1"
|
||||
@@ -7675,8 +7632,8 @@ version = "0.14.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"itertools 0.11.0",
|
||||
"heck 0.5.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"multimap",
|
||||
"petgraph",
|
||||
@@ -7695,7 +7652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.11.0",
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -7966,7 +7923,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2 0.6.3",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8473,7 +8430,7 @@ dependencies = [
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2 0.4.14",
|
||||
"h2 0.4.16",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
@@ -8744,7 +8701,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8815,7 +8772,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9371,7 +9328,7 @@ version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -9383,7 +9340,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54254b8531cafa275c5e096f62d48c81435d1015405a91198ddb11e967301d40"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
@@ -9805,7 +9762,7 @@ dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10129,7 +10086,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"h2 0.4.14",
|
||||
"h2 0.4.16",
|
||||
"http 1.5.0",
|
||||
"http-body 1.1.0",
|
||||
"http-body-util",
|
||||
@@ -10782,7 +10739,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+22
-16
@@ -1,6 +1,5 @@
|
||||
[workspace]
|
||||
members = ["rust/lancedb", "nodejs", "python"]
|
||||
exclude = ["lance-artifact"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
@@ -14,20 +13,21 @@ categories = ["database-implementations"]
|
||||
rust-version = "1.91.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
lance = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a", default-features = false }
|
||||
lance-core = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-datagen = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-file = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-io = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a", default-features = false }
|
||||
lance-index = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-linalg = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-namespace = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-namespace-impls = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a", default-features = false }
|
||||
lance-table = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-testing = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-datafusion = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-encoding = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance-arrow = { version = "=11.0.0-beta.3", git = "https://github.com/lancedb/lancedb.git", rev = "31b68dd21b60fcbee4354a175c9d65ff6ae02b6a" }
|
||||
lance = { "version" = "=12.0.0-beta.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-core = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datagen = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-file = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-io = { "version" = "=12.0.0-beta.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-index = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-linalg = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace-impls = { "version" = "=12.0.0-beta.2", default-features = false, "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-table = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-testing = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datafusion = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-encoding = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-arrow = { "version" = "=12.0.0-beta.2", "tag" = "v12.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lancedb = { path = "rust/lancedb", default-features = false }
|
||||
ahash = "0.8"
|
||||
# Note that this one does not include pyarrow
|
||||
arrow = { version = "58.0.0", optional = false }
|
||||
@@ -40,6 +40,7 @@ arrow-schema = "58.0.0"
|
||||
arrow-select = "58.0.0"
|
||||
arrow-cast = "58.0.0"
|
||||
async-trait = "0"
|
||||
bytes = "1"
|
||||
datafusion = { version = "54.0.0", default-features = false }
|
||||
datafusion-catalog = "54.0.0"
|
||||
datafusion-common = { version = "54.0.0", default-features = false }
|
||||
@@ -66,7 +67,12 @@ url = "2"
|
||||
num-traits = "0.2"
|
||||
regex = "1.10"
|
||||
semver = "1.0.25"
|
||||
chrono = "0.4"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
tempfile = "3.5.0"
|
||||
tokio = { version = "1.23", features = ["rt-multi-thread", "sync"] }
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
|
||||
[profile.ci]
|
||||
debug = "line-tables-only"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Check whether there are any breaking changes in the PRs between the base and head commits.
|
||||
If there are, assert that we have incremented the minor version.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from packaging.version import parse
|
||||
@@ -27,7 +28,7 @@ if __name__ == "__main__":
|
||||
else:
|
||||
print("No breaking changes found.")
|
||||
exit(0)
|
||||
|
||||
|
||||
last_stable_version = parse(args.last_stable_version)
|
||||
current_version = parse(args.current_version)
|
||||
if current_version.minor <= last_stable_version.minor:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Determine whether a newer Lance tag exists and expose results for CI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
@@ -36,8 +37,16 @@ class SemVer:
|
||||
prerelease: Tuple[Union[int, str], ...]
|
||||
|
||||
def __lt__(self, other: "SemVer") -> bool: # pragma: no cover - simple comparison
|
||||
if (self.major, self.minor, self.patch) != (other.major, other.minor, other.patch):
|
||||
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
|
||||
if (self.major, self.minor, self.patch) != (
|
||||
other.major,
|
||||
other.minor,
|
||||
other.patch,
|
||||
):
|
||||
return (self.major, self.minor, self.patch) < (
|
||||
other.major,
|
||||
other.minor,
|
||||
other.patch,
|
||||
)
|
||||
if self.prerelease == other.prerelease:
|
||||
return False
|
||||
if not self.prerelease:
|
||||
@@ -142,7 +151,9 @@ def read_current_version(repo_root: Path) -> str:
|
||||
deps = data["workspace"]["dependencies"]
|
||||
entry = deps["lance"]
|
||||
except KeyError as exc: # pragma: no cover - configuration guard
|
||||
raise RuntimeError("Failed to locate workspace.dependencies.lance in Cargo.toml") from exc
|
||||
raise RuntimeError(
|
||||
"Failed to locate workspace.dependencies.lance in Cargo.toml"
|
||||
) from exc
|
||||
|
||||
if isinstance(entry, str):
|
||||
raw_version = entry
|
||||
|
||||
+9
-6
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
"""A zero-dependency mock OpenAI embeddings API endpoint for testing purposes."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import http.server
|
||||
@@ -22,11 +23,13 @@ class MockOpenAIRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
data = []
|
||||
for i in range(num_inputs):
|
||||
data.append({
|
||||
"object": "embedding",
|
||||
"embedding": [0.1] * 1536,
|
||||
"index": i,
|
||||
})
|
||||
data.append(
|
||||
{
|
||||
"object": "embedding",
|
||||
"embedding": [0.1] * 1536,
|
||||
"index": i,
|
||||
}
|
||||
)
|
||||
|
||||
response = {
|
||||
"object": "list",
|
||||
@@ -35,7 +38,7 @@ class MockOpenAIRequestHandler(http.server.BaseHTTPRequestHandler):
|
||||
"usage": {
|
||||
"prompt_tokens": 0,
|
||||
"total_tokens": 0,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.send_response(200)
|
||||
|
||||
@@ -7,6 +7,7 @@ from packaging.version import parse, InvalidVersion
|
||||
|
||||
if __name__ == "__main__":
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("prefix", default="v")
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -22,7 +22,7 @@ def run_command(command: str) -> str:
|
||||
def get_latest_stable_version() -> str:
|
||||
version_line = run_command("cargo info lance | grep '^version:'")
|
||||
# Example output: "version: 0.35.0 (latest 0.37.0)"
|
||||
match = re.search(r'\(latest ([0-9.]+)\)', version_line)
|
||||
match = re.search(r"\(latest ([0-9.]+)\)", version_line)
|
||||
if match:
|
||||
return match.group(1)
|
||||
# Fallback: use the first version after 'version:'
|
||||
@@ -69,7 +69,7 @@ def extract_default_features(line: str) -> bool:
|
||||
"""
|
||||
import re
|
||||
|
||||
match = re.search(r'default-features\s*=\s*false', line)
|
||||
match = re.search(r"default-features\s*=\s*false", line)
|
||||
return match is not None
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ def dict_to_toml_line(package_name: str, config: dict) -> str:
|
||||
# This shouldn't happen with our current usage
|
||||
parts.append(f'"{key}" = {json.dumps(value)}')
|
||||
|
||||
return f'{package_name} = {{ {", ".join(parts)} }}\n'
|
||||
return f"{package_name} = {{ {', '.join(parts)} }}\n"
|
||||
|
||||
|
||||
def update_cargo_toml(line_updater):
|
||||
@@ -119,7 +119,7 @@ def update_cargo_toml(line_updater):
|
||||
lance_line = ""
|
||||
is_parsing_lance_line = False
|
||||
for line in lines:
|
||||
if line.startswith("lance"):
|
||||
if re.match(r"^lance(?:\s|[-_])", line):
|
||||
# Check if this is a single-line or multi-line entry
|
||||
# Single-line entries either:
|
||||
# 1. End with } (complete inline table)
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT = REPO_ROOT / "ci" / "set_lance_version.py"
|
||||
LANCE_GIT_URL = "https://github.com/lance-format/lance.git"
|
||||
|
||||
CARGO_TOML = """\
|
||||
[workspace.dependencies]
|
||||
lance = { "version" = "=1.0.0", default-features = false, "features" = ["dynamodb"] }
|
||||
lance-core = "1.0.0"
|
||||
lance_datafusion = {
|
||||
"version" = "=1.0.0",
|
||||
"features" = ["substrait"]
|
||||
}
|
||||
lancedb = { path = "rust/lancedb", default-features = false }
|
||||
lancedb-common = { path = "rust/lancedb-common" }
|
||||
lancewood = "1.0.0"
|
||||
my-lance = "1.0.0"
|
||||
"""
|
||||
|
||||
UNTOUCHED_DEPENDENCIES = """\
|
||||
lancedb = { path = "rust/lancedb", default-features = false }
|
||||
lancedb-common = { path = "rust/lancedb-common" }
|
||||
lancewood = "1.0.0"
|
||||
my-lance = "1.0.0"
|
||||
"""
|
||||
|
||||
|
||||
class SetLanceVersionTest(unittest.TestCase):
|
||||
def test_supported_update_modes_only_rewrite_lance_dependencies(self):
|
||||
cases = {
|
||||
"stable": (
|
||||
"""\
|
||||
lance = { "version" = "=9.9.9", default-features = false, "features" = ["dynamodb"] }
|
||||
lance-core = "=9.9.9"
|
||||
lance_datafusion = { "version" = "=9.9.9", "features" = ["substrait"] }
|
||||
""",
|
||||
["cargo info lance", "cargo metadata"],
|
||||
),
|
||||
"preview": (
|
||||
f"""\
|
||||
lance = {{ "version" = "=10.0.0-beta.3", default-features = false, "features" = ["dynamodb"], "tag" = "v10.0.0-beta.3", "git" = "{LANCE_GIT_URL}" }}
|
||||
lance-core = {{ "version" = "=10.0.0-beta.3", "tag" = "v10.0.0-beta.3", "git" = "{LANCE_GIT_URL}" }}
|
||||
lance_datafusion = {{ "version" = "=10.0.0-beta.3", "features" = ["substrait"], "tag" = "v10.0.0-beta.3", "git" = "{LANCE_GIT_URL}" }}
|
||||
""",
|
||||
["git ls-remote --tags", "cargo metadata"],
|
||||
),
|
||||
"local": (
|
||||
"""\
|
||||
lance = { "path" = "../lance/rust/lance", default-features = false, "features" = ["dynamodb"] }
|
||||
lance-core = { "path" = "../lance/rust/lance-core" }
|
||||
lance_datafusion = { "path" = "../lance/rust/lance_datafusion", "features" = ["substrait"] }
|
||||
""",
|
||||
["cargo metadata"],
|
||||
),
|
||||
"v8.1.2": (
|
||||
"""\
|
||||
lance = { "version" = "=8.1.2", default-features = false, "features" = ["dynamodb"] }
|
||||
lance-core = "=8.1.2"
|
||||
lance_datafusion = { "version" = "=8.1.2", "features" = ["substrait"] }
|
||||
""",
|
||||
["cargo metadata"],
|
||||
),
|
||||
"v8.2.0-beta.4": (
|
||||
f"""\
|
||||
lance = {{ "version" = "=8.2.0-beta.4", default-features = false, "features" = ["dynamodb"], "tag" = "v8.2.0-beta.4", "git" = "{LANCE_GIT_URL}" }}
|
||||
lance-core = {{ "version" = "=8.2.0-beta.4", "tag" = "v8.2.0-beta.4", "git" = "{LANCE_GIT_URL}" }}
|
||||
lance_datafusion = {{ "version" = "=8.2.0-beta.4", "features" = ["substrait"], "tag" = "v8.2.0-beta.4", "git" = "{LANCE_GIT_URL}" }}
|
||||
""",
|
||||
["cargo metadata"],
|
||||
),
|
||||
}
|
||||
|
||||
for version, (updated_dependencies, expected_commands) in cases.items():
|
||||
with self.subTest(version=version), tempfile.TemporaryDirectory() as tmp:
|
||||
workdir = Path(tmp)
|
||||
(workdir / "Cargo.toml").write_text(CARGO_TOML)
|
||||
command_log = workdir / "commands.log"
|
||||
fake_bin = workdir / "bin"
|
||||
fake_bin.mkdir()
|
||||
self._write_fake_executables(fake_bin)
|
||||
self._write_fake_python_dependencies(workdir)
|
||||
|
||||
env = os.environ.copy()
|
||||
env["PATH"] = os.pathsep.join([str(fake_bin), env["PATH"]])
|
||||
env["FAKE_COMMAND_LOG"] = str(command_log)
|
||||
env["PYTHONPATH"] = os.pathsep.join(
|
||||
filter(None, [str(workdir), env.get("PYTHONPATH")])
|
||||
)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT), version],
|
||||
cwd=workdir,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(
|
||||
(workdir / "Cargo.toml").read_text(),
|
||||
"[workspace.dependencies]\n"
|
||||
+ updated_dependencies
|
||||
+ UNTOUCHED_DEPENDENCIES,
|
||||
)
|
||||
commands = command_log.read_text().splitlines()
|
||||
for command in expected_commands:
|
||||
self.assertTrue(
|
||||
any(line.startswith(command) for line in commands),
|
||||
f"{command!r} not found in {commands!r}",
|
||||
)
|
||||
|
||||
def _write_fake_executables(self, fake_bin: Path) -> None:
|
||||
cargo = fake_bin / "cargo"
|
||||
cargo.write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
#!/bin/sh
|
||||
printf 'cargo %s\\n' "$*" >> "$FAKE_COMMAND_LOG"
|
||||
case "$1" in
|
||||
info)
|
||||
printf '%s\\n' 'version: 8.8.8 (latest 9.9.9)'
|
||||
;;
|
||||
metadata)
|
||||
;;
|
||||
*)
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
"""
|
||||
)
|
||||
)
|
||||
cargo.chmod(cargo.stat().st_mode | stat.S_IXUSR)
|
||||
|
||||
git = fake_bin / "git"
|
||||
git.write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
#!/bin/sh
|
||||
printf 'git %s\\n' "$*" >> "$FAKE_COMMAND_LOG"
|
||||
if [ "$1" != "ls-remote" ]; then
|
||||
exit 2
|
||||
fi
|
||||
printf '%s\\n' \\
|
||||
'111111 refs/tags/v9.9.9' \\
|
||||
'222222 refs/tags/v10.0.0-beta.1' \\
|
||||
'333333 refs/tags/v10.0.0-beta.3'
|
||||
"""
|
||||
)
|
||||
)
|
||||
git.chmod(git.stat().st_mode | stat.S_IXUSR)
|
||||
|
||||
def _write_fake_python_dependencies(self, workdir: Path) -> None:
|
||||
packaging = workdir / "packaging"
|
||||
packaging.mkdir()
|
||||
(packaging / "__init__.py").write_text("")
|
||||
(packaging / "version.py").write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
class Version:
|
||||
def __init__(self, value):
|
||||
release, _, prerelease = value.partition("-beta.")
|
||||
self._key = (
|
||||
tuple(int(part) for part in release.split(".")),
|
||||
not prerelease,
|
||||
int(prerelease or 0),
|
||||
)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self._key < other._key
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,7 +12,7 @@ with open("Cargo.toml", "rb") as f:
|
||||
elif isinstance(dep, dict):
|
||||
# Version doesn't have the beta tag in it, so we instead look
|
||||
# at the git tag.
|
||||
version = dep.get('tag', dep.get('version'))
|
||||
version = dep.get("tag", dep.get("version"))
|
||||
else:
|
||||
raise ValueError("Unexpected type for dependency: " + str(dep))
|
||||
|
||||
|
||||
@@ -101,6 +101,19 @@ ignore = [
|
||||
# https://rustsec.org/advisories/RUSTSEC-2026-0195
|
||||
{ id = "RUSTSEC-2026-0194", reason = "transitive via inferno/lance/opendal; XML from trusted cloud endpoints, not attacker-controlled" },
|
||||
{ id = "RUSTSEC-2026-0195", reason = "transitive via inferno/lance/opendal; XML from trusted cloud endpoints, not attacker-controlled" },
|
||||
# smartstring: unmaintained — the repository was archived by its author on
|
||||
# 2026-05-03. Not a vulnerability. Reached only transitively through polars
|
||||
# (polars-core/-io/-ops/-time/-utils); nothing in LanceDB depends on it directly.
|
||||
# The advisory states no safe upgrade is available: upstream recommends
|
||||
# compact_str/smol_str, so clearing this requires polars to migrate.
|
||||
# https://rustsec.org/advisories/RUSTSEC-2026-0249
|
||||
{ id = "RUSTSEC-2026-0249", reason = "smartstring unmaintained via polars; no fixed upstream release" },
|
||||
|
||||
# h2 0.3: empty DATA frames can be queued without limit. The patched
|
||||
# h2 0.4 line is locked to 0.4.16, but no patched 0.3 release exists.
|
||||
# The old copy is pulled in by aws-smithy's legacy hyper 0.14 client.
|
||||
# https://rustsec.org/advisories/RUSTSEC-2026-0258
|
||||
{ id = "RUSTSEC-2026-0258", reason = "h2 0.3 via legacy aws-smithy/hyper 0.14; no patched 0.3 release" },
|
||||
]
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -164,6 +177,11 @@ multiple-versions = "warn"
|
||||
# Wildcard version requirements (`foo = "*"`) are a footgun — they let any
|
||||
# future release in without review. Ban them outright.
|
||||
wildcards = "deny"
|
||||
# Lint every dependency declared by a workspace member against the shared
|
||||
# `[workspace.dependencies]` table: any crate used by more than one member must
|
||||
# go through `workspace = true`, and entries nothing uses are an error. This
|
||||
# keeps versions from drifting between the core crate and the bindings.
|
||||
workspace-dependencies = { duplicates = "deny", unused = "deny" }
|
||||
# Internal workspace crates reference each other via `path = "..."`, which
|
||||
# cargo-deny sees as a wildcard version. That's fine for private workspace
|
||||
# members (not published to crates.io), so allow it specifically for paths.
|
||||
|
||||
@@ -5,5 +5,5 @@ mkdocs-autorefs>=0.5,<=1.0
|
||||
mkdocstrings[python]>=0.24,<1.0
|
||||
griffe>=0.40,<1.0
|
||||
mkdocs-render-swagger-plugin>=0.1.0
|
||||
pydantic>=2.0,<3.0
|
||||
mkdocs-redirects>=1.2.0
|
||||
pydantic>=2.7.4,<3
|
||||
mkdocs-redirects>=1.2.0
|
||||
|
||||
+33
-1
@@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`:
|
||||
<dependency>
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-core</artifactId>
|
||||
<version>0.37.1-beta.0</version>
|
||||
<version>0.38.0-beta.11</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
@@ -55,6 +55,38 @@ LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
|
||||
| `region(String)` | AWS region (default: "us-east-1") | No |
|
||||
| `config(String, String)` | Additional configuration parameters | No |
|
||||
|
||||
### Opening a Table with Vended Credentials
|
||||
|
||||
When the catalog vends temporary object store credentials, open the table through the
|
||||
namespace client. The Lance dataset builder fetches the table location and storage options
|
||||
from the catalog and refreshes the credentials when they expire.
|
||||
|
||||
```java
|
||||
import com.lancedb.LanceDbNamespaceClientBuilder;
|
||||
import org.lance.Dataset;
|
||||
import org.lance.namespace.LanceNamespace;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
|
||||
.apiKey(System.getenv("LANCEDB_API_KEY"))
|
||||
.database(System.getenv("LANCEDB_DATABASE"))
|
||||
// Set the endpoint for a LanceDB Enterprise deployment.
|
||||
// .endpoint("https://your-enterprise-endpoint")
|
||||
.build();
|
||||
|
||||
try (Dataset dataset = Dataset.open()
|
||||
.namespaceClient(namespaceClient)
|
||||
.tableId(Arrays.asList("my_namespace", "my_table"))
|
||||
.build()) {
|
||||
System.out.println("Rows: " + dataset.countRows());
|
||||
}
|
||||
```
|
||||
|
||||
Do not call `describeTable()` and then open the returned location with `Dataset.open(uri)`.
|
||||
Opening through `namespaceClient()` is what applies the vended storage options and enables
|
||||
automatic credential refresh. No object store credentials need to be passed by the application.
|
||||
|
||||
## Metadata Operations
|
||||
|
||||
### Creating a Namespace Path
|
||||
|
||||
@@ -0,0 +1,518 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / AutoQuery
|
||||
|
||||
# Class: AutoQuery
|
||||
|
||||
A builder for automatic string searches.
|
||||
|
||||
Automatic search determines whether to use full-text or vector search from
|
||||
the table revision selected for each execution. This builder exposes the
|
||||
common operations supported by both query families.
|
||||
|
||||
## Extends
|
||||
|
||||
- `StandardQueryBase`<`NativeQuery` \| `NativeVectorQuery`>
|
||||
|
||||
## Properties
|
||||
|
||||
### inner
|
||||
|
||||
```ts
|
||||
protected inner: Query | VectorQuery | Promise<Query | VectorQuery>;
|
||||
```
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.inner`
|
||||
|
||||
## Methods
|
||||
|
||||
### analyzePlan()
|
||||
|
||||
```ts
|
||||
analyzePlan(distributedMetrics?): Promise<string>
|
||||
```
|
||||
|
||||
Executes the query and returns the physical query plan annotated with runtime metrics.
|
||||
|
||||
This is useful for debugging and performance analysis, as it shows how the query was executed
|
||||
and includes metrics such as elapsed time, rows processed, and I/O statistics.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **distributedMetrics?**: [`AnalyzePlanDistributedMetrics`](../type-aliases/AnalyzePlanDistributedMetrics.md)
|
||||
How distributed worker metrics are displayed for remote query plans.
|
||||
Defaults to `"aggregate"`.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`string`>
|
||||
|
||||
A query execution plan with runtime metrics for each step.
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
import * as lancedb from "@lancedb/lancedb"
|
||||
|
||||
const db = await lancedb.connect("./.lancedb");
|
||||
const table = await db.createTable("my_table", [
|
||||
{ vector: [1.1, 0.9], id: "1" },
|
||||
]);
|
||||
|
||||
const plan = await table.query().nearestTo([0.5, 0.2]).analyzePlan();
|
||||
|
||||
Example output (with runtime metrics inlined):
|
||||
AnalyzeExec verbose=true, metrics=[]
|
||||
ProjectionExec: expr=[id@3 as id, vector@0 as vector, _distance@2 as _distance], metrics=[output_rows=1, elapsed_compute=3.292µs]
|
||||
Take: columns="vector, _rowid, _distance, (id)", metrics=[output_rows=1, elapsed_compute=66.001µs, batches_processed=1, bytes_read=8, iops=1, requests=1]
|
||||
CoalesceBatchesExec: target_batch_size=1024, metrics=[output_rows=1, elapsed_compute=3.333µs]
|
||||
GlobalLimitExec: skip=0, fetch=10, metrics=[output_rows=1, elapsed_compute=167ns]
|
||||
FilterExec: _distance@2 IS NOT NULL, metrics=[output_rows=1, elapsed_compute=8.542µs]
|
||||
SortExec: TopK(fetch=10), expr=[_distance@2 ASC NULLS LAST], metrics=[output_rows=1, elapsed_compute=63.25µs, row_replacements=1]
|
||||
KNNVectorDistance: metric=l2, metrics=[output_rows=1, elapsed_compute=114.333µs, output_batches=1]
|
||||
LanceScan: uri=/path/to/data, projection=[vector], row_id=true, row_addr=false, ordered=false, metrics=[output_rows=1, elapsed_compute=103.626µs, bytes_read=549, iops=2, requests=2]
|
||||
```
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.analyzePlan`
|
||||
|
||||
***
|
||||
|
||||
### execute()
|
||||
|
||||
```ts
|
||||
protected execute(options?): AsyncGenerator<RecordBatch<any>, void, unknown>
|
||||
```
|
||||
|
||||
Execute the query and return the results as an
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **options?**: `Partial`<[`QueryExecutionOptions`](../interfaces/QueryExecutionOptions.md)>
|
||||
|
||||
#### Returns
|
||||
|
||||
`AsyncGenerator`<`RecordBatch`<`any`>, `void`, `unknown`>
|
||||
|
||||
#### See
|
||||
|
||||
- AsyncIterator
|
||||
of
|
||||
- RecordBatch.
|
||||
|
||||
By default, LanceDb will use many threads to calculate results and, when
|
||||
the result set is large, multiple batches will be processed at one time.
|
||||
This readahead is limited however and backpressure will be applied if this
|
||||
stream is consumed slowly (this constrains the maximum memory used by a
|
||||
single query)
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.execute`
|
||||
|
||||
***
|
||||
|
||||
### explainPlan()
|
||||
|
||||
```ts
|
||||
explainPlan(verbose): Promise<string>
|
||||
```
|
||||
|
||||
Generates an explanation of the query execution plan.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **verbose**: `boolean` = `false`
|
||||
If true, provides a more detailed explanation. Defaults to false.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`string`>
|
||||
|
||||
A Promise that resolves to a string containing the query execution plan explanation.
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
import * as lancedb from "@lancedb/lancedb"
|
||||
const db = await lancedb.connect("./.lancedb");
|
||||
const table = await db.createTable("my_table", [
|
||||
{ vector: [1.1, 0.9], id: "1" },
|
||||
]);
|
||||
const plan = await table.query().nearestTo([0.5, 0.2]).explainPlan();
|
||||
```
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.explainPlan`
|
||||
|
||||
***
|
||||
|
||||
### fastSearch()
|
||||
|
||||
```ts
|
||||
fastSearch(): this
|
||||
```
|
||||
|
||||
Skip searching un-indexed data. This can make search faster, but will miss
|
||||
any data that is not yet indexed.
|
||||
|
||||
Use [Table#optimize](Table.md#optimize) to index all un-indexed data.
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.fastSearch`
|
||||
|
||||
***
|
||||
|
||||
### ~~filter()~~
|
||||
|
||||
```ts
|
||||
filter(predicate): this
|
||||
```
|
||||
|
||||
A filter statement to be applied to this query.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **predicate**: `string`
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### See
|
||||
|
||||
where
|
||||
|
||||
#### Deprecated
|
||||
|
||||
Use `where` instead
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.filter`
|
||||
|
||||
***
|
||||
|
||||
### fullTextSearch()
|
||||
|
||||
```ts
|
||||
fullTextSearch(query, options?): this
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **query**: `string` \| [`FullTextQuery`](../interfaces/FullTextQuery.md)
|
||||
|
||||
* **options?**: `Partial`<[`FullTextSearchOptions`](../interfaces/FullTextSearchOptions.md)>
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.fullTextSearch`
|
||||
|
||||
***
|
||||
|
||||
### limit()
|
||||
|
||||
```ts
|
||||
limit(limit): this
|
||||
```
|
||||
|
||||
Set the maximum number of results to return.
|
||||
|
||||
By default, a plain search has no limit. If this method is not
|
||||
called then every valid row from the table will be returned.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **limit**: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.limit`
|
||||
|
||||
***
|
||||
|
||||
### offset()
|
||||
|
||||
```ts
|
||||
offset(offset): this
|
||||
```
|
||||
|
||||
Set the number of rows to skip before returning results.
|
||||
|
||||
This is useful for pagination.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **offset**: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.offset`
|
||||
|
||||
***
|
||||
|
||||
### orderBy()
|
||||
|
||||
```ts
|
||||
orderBy(ordering): this
|
||||
```
|
||||
|
||||
Sort the results by the specified column(s).
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **ordering**: [`ColumnOrdering`](../interfaces/ColumnOrdering.md) \| [`ColumnOrdering`](../interfaces/ColumnOrdering.md)[]
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
This query builder.
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.orderBy`
|
||||
|
||||
***
|
||||
|
||||
### outputSchema()
|
||||
|
||||
```ts
|
||||
outputSchema(): Promise<Schema<any>>
|
||||
```
|
||||
|
||||
Returns the schema of the output that will be returned by this query.
|
||||
|
||||
This can be used to inspect the types and names of the columns that will be
|
||||
returned by the query before executing it.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`Schema`<`any`>>
|
||||
|
||||
An Arrow Schema describing the output columns.
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.outputSchema`
|
||||
|
||||
***
|
||||
|
||||
### select()
|
||||
|
||||
```ts
|
||||
select(columns): this
|
||||
```
|
||||
|
||||
Return only the specified columns.
|
||||
|
||||
By default a query will return all columns from the table. However, this can have
|
||||
a very significant impact on latency. LanceDb stores data in a columnar fashion. This
|
||||
means we can finely tune our I/O to select exactly the columns we need.
|
||||
|
||||
As a best practice you should always limit queries to the columns that you need. If you
|
||||
pass in an array of column names then only those columns will be returned.
|
||||
|
||||
You can also use this method to create new "dynamic" columns based on your existing columns.
|
||||
For example, you may not care about "a" or "b" but instead simply want "a + b". This is often
|
||||
seen in the SELECT clause of an SQL query (e.g. `SELECT a+b FROM my_table`).
|
||||
|
||||
To create dynamic columns you can pass in a Map<string, string>. A column will be returned
|
||||
for each entry in the map. The key provides the name of the column. The value is
|
||||
an SQL string used to specify how the column is calculated.
|
||||
|
||||
For example, an SQL query might state `SELECT a + b AS combined, c`. The equivalent
|
||||
input to this method would be:
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **columns**: `string` \| `string`[] \| `Record`<`string`, `string`> \| `Map`<`string`, `string`>
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
new Map([["combined", "a + b"], ["c", "c"]])
|
||||
|
||||
Columns will always be returned in the order given, even if that order is different than
|
||||
the order used when adding the data.
|
||||
|
||||
Note that you can pass in a `Record<string, string>` (e.g. an object literal). This method
|
||||
uses `Object.entries` which should preserve the insertion order of the object. However,
|
||||
object insertion order is easy to get wrong and `Map` is more foolproof.
|
||||
```
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.select`
|
||||
|
||||
***
|
||||
|
||||
### toArray()
|
||||
|
||||
```ts
|
||||
toArray(options?): Promise<any[]>
|
||||
```
|
||||
|
||||
Collect the results as an array of objects.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **options?**: `Partial`<[`QueryExecutionOptions`](../interfaces/QueryExecutionOptions.md)>
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`any`[]>
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.toArray`
|
||||
|
||||
***
|
||||
|
||||
### toArrow()
|
||||
|
||||
```ts
|
||||
toArrow(options?): Promise<Table<any>>
|
||||
```
|
||||
|
||||
Collect the results as an Arrow
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **options?**: `Partial`<[`QueryExecutionOptions`](../interfaces/QueryExecutionOptions.md)>
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`Table`<`any`>>
|
||||
|
||||
#### See
|
||||
|
||||
ArrowTable.
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.toArrow`
|
||||
|
||||
***
|
||||
|
||||
### useLsm()
|
||||
|
||||
```ts
|
||||
useLsm(enable): this
|
||||
```
|
||||
|
||||
Control MemWAL read routing for this query.
|
||||
|
||||
By default (unset), when the table carries a MemWAL write spec (see
|
||||
[Table#setLsmWriteSpec](Table.md#setlsmwritespec)), reads are routed through the LSM scanner so
|
||||
they also return data written via the `mergeInsert` LSM path that has not yet
|
||||
been compacted into the base table (the active/frozen in-memory memtables and
|
||||
the flushed generations), deduplicated by primary key; a table without a spec
|
||||
reads the base table.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **enable**: `boolean`
|
||||
`true` forces the LSM scanner and errors if the table has no
|
||||
MemWAL write spec. `false` bypasses the MemWAL and reads the base table only,
|
||||
even when a spec is present.
|
||||
Note: the LSM scanner does not support every query shape (e.g. reranking,
|
||||
hybrid search, `orderBy`). On a MemWAL table those shapes error unless
|
||||
`useLsm(false)` is set, because a base-only read would silently exclude
|
||||
un-compacted MemWAL data.
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.useLsm`
|
||||
|
||||
***
|
||||
|
||||
### where()
|
||||
|
||||
```ts
|
||||
where(predicate): this
|
||||
```
|
||||
|
||||
A filter statement to be applied to this query.
|
||||
|
||||
The filter should be supplied as an SQL query string. For example:
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **predicate**: `string`
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
x > 10
|
||||
y > 0 AND y < 100
|
||||
x > 5 OR y = 'test'
|
||||
|
||||
Filtering performance can often be improved by creating a scalar index
|
||||
on the filter column(s).
|
||||
|
||||
Calling this multiple times combines the filters with a logical AND rather
|
||||
than replacing the previous filter.
|
||||
```
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.where`
|
||||
|
||||
***
|
||||
|
||||
### withRowId()
|
||||
|
||||
```ts
|
||||
withRowId(): this
|
||||
```
|
||||
|
||||
Whether to return the row id in the results.
|
||||
|
||||
This column can be used to match results between different queries. For
|
||||
example, to match results from a full text search and a vector search in
|
||||
order to perform hybrid search.
|
||||
|
||||
#### Returns
|
||||
|
||||
`this`
|
||||
|
||||
#### Inherited from
|
||||
|
||||
`StandardQueryBase.withRowId`
|
||||
@@ -37,6 +37,31 @@ latest and stays writable.
|
||||
|
||||
***
|
||||
|
||||
### cherryPick()
|
||||
|
||||
```ts
|
||||
cherryPick(fromBranch, dryRun): Promise<CherryPickResult>
|
||||
```
|
||||
|
||||
Cherry-pick a branch onto main.
|
||||
|
||||
Set `dryRun` to `true` to preview. A failed cherry-pick resolves
|
||||
with `status: "failed"` instead of throwing.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **fromBranch**: `string`
|
||||
Branch to cherry-pick from.
|
||||
|
||||
* **dryRun**: `boolean` = `false`
|
||||
When true, only preview. Defaults to false.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`CherryPickResult`](../interfaces/CherryPickResult.md)>
|
||||
|
||||
***
|
||||
|
||||
### create()
|
||||
|
||||
```ts
|
||||
@@ -112,28 +137,3 @@ List all branches, mapping name to branch metadata.
|
||||
#### Returns
|
||||
|
||||
`Promise`<`Record`<`string`, [`BranchContents`](BranchContents.md)>>
|
||||
|
||||
***
|
||||
|
||||
### merge()
|
||||
|
||||
```ts
|
||||
merge(fromBranch, dryRun): Promise<MergeBranchResult>
|
||||
```
|
||||
|
||||
Merge a branch into main.
|
||||
|
||||
Set `dryRun` to `true` to preview the merge. A rejected merge resolves
|
||||
with `status: "rejected"` instead of throwing.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **fromBranch**: `string`
|
||||
Branch to merge from.
|
||||
|
||||
* **dryRun**: `boolean` = `false`
|
||||
When true, only preview the merge. Defaults to false.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`MergeBranchResult`](../interfaces/MergeBranchResult.md)>
|
||||
|
||||
@@ -169,6 +169,45 @@ Creates a new empty Table
|
||||
|
||||
***
|
||||
|
||||
### createMaterializedView()
|
||||
|
||||
```ts
|
||||
abstract createMaterializedView(
|
||||
name,
|
||||
source,
|
||||
options?): Promise<MaterializedView>
|
||||
```
|
||||
|
||||
Define a materialized view named `name` over the table `source`.
|
||||
|
||||
The view is created empty, with the query recorded in its schema
|
||||
metadata; `view.refresh()` computes the rows. The view is a normal
|
||||
table: it can be queried, indexed and searched, and it appears in
|
||||
`tableNames`. The source table must have stable row ids (create it with
|
||||
the `newTableEnableStableRowIds` storage option); they keep the view's
|
||||
provenance valid across source compactions and cannot be enabled after
|
||||
a table exists. Local databases only.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **name**: `string`
|
||||
|
||||
* **source**: `string`
|
||||
|
||||
* **options?**
|
||||
|
||||
* **options.limit?**: `number`
|
||||
|
||||
* **options.select?**: [`MaterializedViewSelect`](../type-aliases/MaterializedViewSelect.md)
|
||||
|
||||
* **options.where?**: `string`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`MaterializedView`](MaterializedView.md)>
|
||||
|
||||
***
|
||||
|
||||
### createNamespace()
|
||||
|
||||
```ts
|
||||
@@ -386,6 +425,29 @@ Drop an existing table.
|
||||
|
||||
***
|
||||
|
||||
### dropTableAsync()
|
||||
|
||||
```ts
|
||||
abstract dropTableAsync(name, namespacePath?): Promise<Job>
|
||||
```
|
||||
|
||||
Start dropping a table and return its cleanup job.
|
||||
|
||||
The table may become unavailable before its data files are removed. Wait
|
||||
on the returned job to know when cleanup has finished.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **name**: `string`
|
||||
|
||||
* **namespacePath?**: `string`[]
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`Job`](Job.md)>
|
||||
|
||||
***
|
||||
|
||||
### getJob()
|
||||
|
||||
```ts
|
||||
@@ -476,6 +538,22 @@ List server-side jobs across the database's tables.
|
||||
|
||||
***
|
||||
|
||||
### listMaterializedViews()
|
||||
|
||||
```ts
|
||||
abstract listMaterializedViews(): Promise<string[]>
|
||||
```
|
||||
|
||||
The names of the materialized views in this database.
|
||||
|
||||
Found by reading every table's schema, so this costs an open per table.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`string`[]>
|
||||
|
||||
***
|
||||
|
||||
### listNamespaces()
|
||||
|
||||
```ts
|
||||
@@ -506,6 +584,90 @@ Child namespace names and
|
||||
|
||||
***
|
||||
|
||||
### listTables()
|
||||
|
||||
#### listTables(options)
|
||||
|
||||
```ts
|
||||
abstract listTables(options?): Promise<ListTablesResponse>
|
||||
```
|
||||
|
||||
List a page of the tables in this database.
|
||||
|
||||
To retrieve the tables after the page, pass the `pageToken` the response
|
||||
carries back in. A page can be shorter than `limit` without being the last
|
||||
one, so walk until a response carries no page token:
|
||||
|
||||
```ts
|
||||
const names = [];
|
||||
let pageToken = undefined;
|
||||
do {
|
||||
const page = await conn.listTables({ pageToken, limit: 100 });
|
||||
names.push(...page.tables);
|
||||
pageToken = page.pageToken;
|
||||
} while (pageToken);
|
||||
```
|
||||
|
||||
##### Parameters
|
||||
|
||||
* **options?**: `Partial`<[`ListTablesOptions`](../interfaces/ListTablesOptions.md)>
|
||||
Pagination options
|
||||
(`pageToken`, `limit`).
|
||||
|
||||
##### Returns
|
||||
|
||||
`Promise`<[`ListTablesResponse`](../interfaces/ListTablesResponse.md)>
|
||||
|
||||
A page of table names and an
|
||||
optional token for the tables after it.
|
||||
|
||||
#### listTables(namespacePath, options)
|
||||
|
||||
```ts
|
||||
abstract listTables(namespacePath?, options?): Promise<ListTablesResponse>
|
||||
```
|
||||
|
||||
List a page of the tables in this database.
|
||||
|
||||
##### Parameters
|
||||
|
||||
* **namespacePath?**: `string`[]
|
||||
The namespace path to list tables from
|
||||
(defaults to root namespace)
|
||||
|
||||
* **options?**: `Partial`<[`ListTablesOptions`](../interfaces/ListTablesOptions.md)>
|
||||
Pagination options
|
||||
(`pageToken`, `limit`).
|
||||
|
||||
##### Returns
|
||||
|
||||
`Promise`<[`ListTablesResponse`](../interfaces/ListTablesResponse.md)>
|
||||
|
||||
A page of table names and an
|
||||
optional token for the tables after it.
|
||||
|
||||
***
|
||||
|
||||
### openMaterializedView()
|
||||
|
||||
```ts
|
||||
abstract openMaterializedView(name): Promise<MaterializedView>
|
||||
```
|
||||
|
||||
Open the materialized view named `name`.
|
||||
|
||||
Rejects a table that exists but is not a materialized view.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **name**: `string`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`MaterializedView`](MaterializedView.md)>
|
||||
|
||||
***
|
||||
|
||||
### openTable()
|
||||
|
||||
```ts
|
||||
@@ -515,18 +677,13 @@ abstract openTable(
|
||||
options?): Promise<Table>
|
||||
```
|
||||
|
||||
Open a table in the database.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **name**: `string`
|
||||
The name of the table
|
||||
|
||||
* **namespacePath?**: `string`[]
|
||||
The namespace path of the table (defaults to root namespace)
|
||||
|
||||
* **options?**: `Partial`<[`OpenTableOptions`](../interfaces/OpenTableOptions.md)>
|
||||
Additional options
|
||||
|
||||
#### Returns
|
||||
|
||||
@@ -567,7 +724,7 @@ a "not supported" error.
|
||||
|
||||
***
|
||||
|
||||
### tableNames()
|
||||
### ~~tableNames()~~
|
||||
|
||||
#### tableNames(options)
|
||||
|
||||
@@ -589,6 +746,10 @@ Tables will be returned in lexicographical order.
|
||||
|
||||
`Promise`<`string`[]>
|
||||
|
||||
##### Deprecated
|
||||
|
||||
Use [Connection.listTables](Connection.md#listtables) instead.
|
||||
|
||||
#### tableNames(namespacePath, options)
|
||||
|
||||
```ts
|
||||
@@ -611,3 +772,7 @@ Tables will be returned in lexicographical order.
|
||||
##### Returns
|
||||
|
||||
`Promise`<`string`[]>
|
||||
|
||||
##### Deprecated
|
||||
|
||||
Use [Connection.listTables](Connection.md#listtables) instead.
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / MaterializedView
|
||||
|
||||
# Class: MaterializedView
|
||||
|
||||
A handle on a materialized view: its table plus its definition.
|
||||
|
||||
Obtained from [Connection#createMaterializedView](Connection.md#creatematerializedview) or
|
||||
[Connection#openMaterializedView](Connection.md#openmaterializedview). The view is a normal table --
|
||||
queries, indexes and search all apply through [MaterializedView#table](MaterializedView.md#table)
|
||||
-- whose contents are maintained by [MaterializedView#refresh](MaterializedView.md#refresh).
|
||||
|
||||
## Constructors
|
||||
|
||||
### new MaterializedView()
|
||||
|
||||
```ts
|
||||
new MaterializedView(table): MaterializedView
|
||||
```
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **table**: [`Table`](Table.md)
|
||||
|
||||
#### Returns
|
||||
|
||||
[`MaterializedView`](MaterializedView.md)
|
||||
|
||||
## Accessors
|
||||
|
||||
### name
|
||||
|
||||
```ts
|
||||
get name(): string
|
||||
```
|
||||
|
||||
#### Returns
|
||||
|
||||
`string`
|
||||
|
||||
## Methods
|
||||
|
||||
### definition()
|
||||
|
||||
```ts
|
||||
definition(): Promise<MaterializedViewDefinition>
|
||||
```
|
||||
|
||||
The query that defines the view, read from its stored schema.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`MaterializedViewDefinition`](../interfaces/MaterializedViewDefinition.md)>
|
||||
|
||||
***
|
||||
|
||||
### refresh()
|
||||
|
||||
```ts
|
||||
refresh(options?): Promise<RefreshMaterializedViewResult>
|
||||
```
|
||||
|
||||
Recompute the view from its source.
|
||||
|
||||
The refresh is incremental when the source's changes can be reconciled
|
||||
into the view -- rows added, changed or removed since the last one --
|
||||
and otherwise rebuilds. `full` forces a rebuild; `sourceVersion`
|
||||
refreshes to that source version instead of the latest.
|
||||
|
||||
Concurrent refreshes of one view do not duplicate its rows. Two that
|
||||
plan the same source rows conflict on commit, and the loser throws
|
||||
rather than writing them a second time.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **options?**
|
||||
|
||||
* **options.full?**: `boolean`
|
||||
|
||||
* **options.sourceVersion?**: `number`
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`RefreshMaterializedViewResult`](../interfaces/RefreshMaterializedViewResult.md)>
|
||||
|
||||
***
|
||||
|
||||
### table()
|
||||
|
||||
```ts
|
||||
table(): Table
|
||||
```
|
||||
|
||||
The view, as the table it is.
|
||||
|
||||
#### Returns
|
||||
|
||||
[`Table`](Table.md)
|
||||
@@ -69,14 +69,34 @@ abstract addColumns(newColumnTransforms): Promise<AddColumnsResult>
|
||||
|
||||
Add new columns with defined values.
|
||||
|
||||
The `{ computed }` form stores the expression rather than evaluating it
|
||||
now: the column is committed with no values, and rows get them from
|
||||
[Table#refreshColumn](Table.md#refreshcolumn). Declaring one therefore costs the same on a
|
||||
large table as on an empty one.
|
||||
|
||||
A refresh does not revisit rows it has already filled, so mutating an
|
||||
input leaves the value computed at fill time; recomputing means dropping
|
||||
the column and declaring it again. While a declaration reads a column,
|
||||
that column cannot be renamed, retyped or dropped.
|
||||
|
||||
On LanceDB Cloud and Enterprise the expression is planned by the
|
||||
server, and the refresh runs as a server job -- see
|
||||
[Table#refreshColumnAsync](Table.md#refreshcolumnasync).
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **newColumnTransforms**: `Field`<`any`> \| `Field`<`any`>[] \| `Schema`<`any`> \| [`AddColumnsSql`](../interfaces/AddColumnsSql.md)[]
|
||||
* **newColumnTransforms**:
|
||||
\| `Field`<`any`>
|
||||
\| `Field`<`any`>[]
|
||||
\| `Schema`<`any`>
|
||||
\| [`AddColumnsSql`](../interfaces/AddColumnsSql.md)[]
|
||||
\| `object`
|
||||
Either:
|
||||
- An array of objects with column names and SQL expressions to calculate values
|
||||
- A single Arrow Field defining one column with its data type (column will be initialized with null values)
|
||||
- An array of Arrow Fields defining columns with their data types (columns will be initialized with null values)
|
||||
- An Arrow Schema defining columns with their data types (columns will be initialized with null values)
|
||||
- `{ computed }`, declaring columns defined by a SQL expression whose type and inputs are derived from it
|
||||
|
||||
#### Returns
|
||||
|
||||
@@ -85,6 +105,13 @@ Add new columns with defined values.
|
||||
A promise that resolves to an object
|
||||
containing the new version number of the table after adding the columns.
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
await table.addColumns({ computed: [{ name: "doubled", valueSql: "x * 2" }] });
|
||||
const { rowsFilled } = await table.refreshColumn("doubled");
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### alterColumns()
|
||||
@@ -186,6 +213,39 @@ version of the table.
|
||||
|
||||
***
|
||||
|
||||
### checkpointLsm()
|
||||
|
||||
```ts
|
||||
abstract checkpointLsm(): Promise<void>
|
||||
```
|
||||
|
||||
Converge this table's LSM write path into its base table.
|
||||
|
||||
Seals once, then triggers compaction and polls until the L0 that existed
|
||||
at the start is gone. The target set is fixed at the start, so
|
||||
generations created *during* the checkpoint are ignored — that is what
|
||||
lets it terminate under write load, and what makes it best-effort: it
|
||||
converges the fresh tier as of some instant. Idempotent, abandonable at
|
||||
any point, and safe to run on a cadence.
|
||||
|
||||
There is no liveness bound — the compactor pool is shared across tables,
|
||||
so a checkpoint queued behind unrelated work looks exactly like one that
|
||||
is merging. The caller owns the deadline.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`void`>
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
const before = await table.getLsmStats();
|
||||
await table.checkpointLsm();
|
||||
const after = await table.getLsmStats();
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### close()
|
||||
|
||||
```ts
|
||||
@@ -223,6 +283,24 @@ It is a no-op when no writers are cached.
|
||||
|
||||
***
|
||||
|
||||
### compactLsm()
|
||||
|
||||
```ts
|
||||
abstract compactLsm(): Promise<void>
|
||||
```
|
||||
|
||||
Trigger a background L0 → base compaction pass per bucket.
|
||||
|
||||
Returns once the passes are *dispatched*, not once they finish — watch
|
||||
[Table#getLsmStats](Table.md#getlsmstats) for progress, or use
|
||||
[Table#checkpointLsm](Table.md#checkpointlsm) to wait for convergence.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`void`>
|
||||
|
||||
***
|
||||
|
||||
### countRows()
|
||||
|
||||
```ts
|
||||
@@ -421,6 +499,48 @@ Drop an index from the table.
|
||||
|
||||
***
|
||||
|
||||
### flushLsm()
|
||||
|
||||
```ts
|
||||
abstract flushLsm(): Promise<void>
|
||||
```
|
||||
|
||||
Seal every bucket's active memtable into a new L0 generation.
|
||||
|
||||
Returns once the seal is committed. Sealing an empty memtable is a no-op,
|
||||
so this is safe to call repeatedly.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`void`>
|
||||
|
||||
***
|
||||
|
||||
### getLsmStats()
|
||||
|
||||
```ts
|
||||
abstract getLsmStats(includeGenerationRows?): Promise<undefined | LsmStats>
|
||||
```
|
||||
|
||||
Read live per-bucket LSM state.
|
||||
|
||||
Answers "how far behind is my fresh tier", "which bucket is hot", and
|
||||
"why is my fresh-tier vector search brute-force". Mutates no table state.
|
||||
|
||||
Resolves to `undefined` only when the LSM write path is not enabled.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **includeGenerationRows?**: `boolean`
|
||||
Also count rows per L0 generation.
|
||||
Off by default because each count opens an uncached Lance dataset.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<`undefined` \| [`LsmStats`](../interfaces/LsmStats.md)>
|
||||
|
||||
***
|
||||
|
||||
### getLsmWriteSpec()
|
||||
|
||||
```ts
|
||||
@@ -431,9 +551,10 @@ Read the [LsmWriteSpec](../interfaces/LsmWriteSpec.md) currently installed on th
|
||||
|
||||
Resolves to `undefined` when the MemWAL LSM write path is not enabled (no
|
||||
spec has been set, or it was removed with [Table#unsetLsmWriteSpec](Table.md#unsetlsmwritespec)).
|
||||
The returned spec — including its `maintainedIndexes` and
|
||||
`writerConfigDefaults` — mirrors what was passed to
|
||||
[Table#setLsmWriteSpec](Table.md#setlsmwritespec).
|
||||
The returned spec mirrors what was passed to
|
||||
[Table#setLsmWriteSpec](Table.md#setlsmwritespec), except that `maintainedIndexes` always
|
||||
reports the concrete list resolved when the spec was set — `undefined`
|
||||
never round-trips.
|
||||
|
||||
#### Returns
|
||||
|
||||
@@ -717,6 +838,67 @@ for await (const batch of table.query()) {
|
||||
|
||||
***
|
||||
|
||||
### refreshColumn()
|
||||
|
||||
```ts
|
||||
abstract refreshColumn(column): Promise<RefreshColumnResult>
|
||||
```
|
||||
|
||||
Fill the rows of a computed column that hold no value yet.
|
||||
|
||||
Rows appended since the last refresh are filled by the next one; rows
|
||||
already filled are left as they are, so the call is idempotent and does
|
||||
not observe a mutated input. Local tables only: a remote refresh runs
|
||||
as a server job, through [Table#refreshColumnAsync](Table.md#refreshcolumnasync).
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **column**: `string`
|
||||
The name of the computed column to fill.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`RefreshColumnResult`](../interfaces/RefreshColumnResult.md)>
|
||||
|
||||
A promise that resolves to the
|
||||
number of rows filled and the new version number of the table.
|
||||
|
||||
***
|
||||
|
||||
### refreshColumnAsync()
|
||||
|
||||
```ts
|
||||
abstract refreshColumnAsync(column): Promise<Job>
|
||||
```
|
||||
|
||||
Like [Table#refreshColumn](Table.md#refreshcolumn), but returns a handle to the refresh
|
||||
job instead of blocking until it completes.
|
||||
|
||||
The job may already be complete when returned; callers must not assume
|
||||
the column is filled until [Job.wait](Job.md#wait) resolves. Invalid input --
|
||||
an unknown column, or one that is not computed -- rejects here rather
|
||||
than failing the job. On local tables the job runs in-process; on
|
||||
LanceDB Cloud and Enterprise it is the server's backfill job.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **column**: `string`
|
||||
The name of the computed column to fill.
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<[`Job`](Job.md)>
|
||||
|
||||
#### Example
|
||||
|
||||
```ts
|
||||
const job = await table.refreshColumnAsync("doubled");
|
||||
await job.wait();
|
||||
console.log(await job.status()); // "finished"
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### restore()
|
||||
|
||||
```ts
|
||||
@@ -760,7 +942,7 @@ Get the schema of the table.
|
||||
abstract search(
|
||||
query,
|
||||
queryType?,
|
||||
ftsColumns?): Query | VectorQuery
|
||||
ftsColumns?): Query | VectorQuery | AutoQuery
|
||||
```
|
||||
|
||||
Create a search query to find the nearest neighbors
|
||||
@@ -782,7 +964,7 @@ of the given query
|
||||
|
||||
#### Returns
|
||||
|
||||
[`Query`](Query.md) \| [`VectorQuery`](VectorQuery.md)
|
||||
[`Query`](Query.md) \| [`VectorQuery`](VectorQuery.md) \| [`AutoQuery`](AutoQuery.md)
|
||||
|
||||
***
|
||||
|
||||
@@ -806,6 +988,11 @@ All variants require the table to have an unenforced primary key
|
||||
([Table#setUnenforcedPrimaryKey](Table.md#setunenforcedprimarykey)); bucket sharding additionally
|
||||
requires it to be the single column being bucketed.
|
||||
|
||||
Omitting `maintainedIndexes` maintains every index on the table, resolved
|
||||
here, failing if one cannot be maintained — name them to install anyway.
|
||||
Naming them pins an exact set, and a still-building index is rejected
|
||||
rather than quietly omitted.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **spec**: [`LsmWriteSpec`](../interfaces/LsmWriteSpec.md)
|
||||
@@ -1105,6 +1292,18 @@ abstract updateFieldMetadata(updates): Promise<UpdateFieldMetadataResult>
|
||||
|
||||
Update per-field (column) metadata.
|
||||
|
||||
The following keys are treated specially, by convention, and should be
|
||||
used when appropriate:
|
||||
|
||||
- `lancedb:description`: for a human-readable description of a field.
|
||||
- `lancedb:tag:<name>`: for a user-defined key-value tag, where the suffix
|
||||
names the tag category; e.g. `lancedb:tag:model: "clip"`.
|
||||
- `lancedb:logical-column`: for a column grouping; e.g. `feature_v1` and
|
||||
`feature_v2` might be in the same logical column.
|
||||
- `lancedb:status`: for status options (`production`, `candidate`,
|
||||
`deprecated`, `archived`) to designate the current life cycle state of
|
||||
this column.
|
||||
|
||||
#### Parameters
|
||||
|
||||
* **updates**: [`FieldMetadataUpdate`](../interfaces/FieldMetadataUpdate.md)[]
|
||||
|
||||
+15
-3
@@ -18,6 +18,7 @@
|
||||
|
||||
## Classes
|
||||
|
||||
- [AutoQuery](classes/AutoQuery.md)
|
||||
- [BooleanQuery](classes/BooleanQuery.md)
|
||||
- [BoostQuery](classes/BoostQuery.md)
|
||||
- [BranchContents](classes/BranchContents.md)
|
||||
@@ -28,6 +29,7 @@
|
||||
- [Job](classes/Job.md)
|
||||
- [MakeArrowTableOptions](classes/MakeArrowTableOptions.md)
|
||||
- [MatchQuery](classes/MatchQuery.md)
|
||||
- [MaterializedView](classes/MaterializedView.md)
|
||||
- [MergeInsertBuilder](classes/MergeInsertBuilder.md)
|
||||
- [MultiMatchQuery](classes/MultiMatchQuery.md)
|
||||
- [NativeJsHeaderProvider](classes/NativeJsHeaderProvider.md)
|
||||
@@ -58,6 +60,10 @@
|
||||
- [BranchDiff](interfaces/BranchDiff.md)
|
||||
- [BranchIndexSummary](interfaces/BranchIndexSummary.md)
|
||||
- [BranchRowCountSummary](interfaces/BranchRowCountSummary.md)
|
||||
- [BucketStats](interfaces/BucketStats.md)
|
||||
- [CherryPickError](interfaces/CherryPickError.md)
|
||||
- [CherryPickPreview](interfaces/CherryPickPreview.md)
|
||||
- [CherryPickResult](interfaces/CherryPickResult.md)
|
||||
- [ClientConfig](interfaces/ClientConfig.md)
|
||||
- [ColumnAlteration](interfaces/ColumnAlteration.md)
|
||||
- [ColumnOrdering](interfaces/ColumnOrdering.md)
|
||||
@@ -81,6 +87,7 @@
|
||||
- [FtsToken](interfaces/FtsToken.md)
|
||||
- [FullTextQuery](interfaces/FullTextQuery.md)
|
||||
- [FullTextSearchOptions](interfaces/FullTextSearchOptions.md)
|
||||
- [GenerationStats](interfaces/GenerationStats.md)
|
||||
- [HnswPqOptions](interfaces/HnswPqOptions.md)
|
||||
- [HnswSqOptions](interfaces/HnswSqOptions.md)
|
||||
- [IndexConfig](interfaces/IndexConfig.md)
|
||||
@@ -94,10 +101,12 @@
|
||||
- [JobInfo](interfaces/JobInfo.md)
|
||||
- [ListNamespacesOptions](interfaces/ListNamespacesOptions.md)
|
||||
- [ListNamespacesResponse](interfaces/ListNamespacesResponse.md)
|
||||
- [ListTablesOptions](interfaces/ListTablesOptions.md)
|
||||
- [ListTablesResponse](interfaces/ListTablesResponse.md)
|
||||
- [LsmStats](interfaces/LsmStats.md)
|
||||
- [LsmWriteSpec](interfaces/LsmWriteSpec.md)
|
||||
- [MergeBlocker](interfaces/MergeBlocker.md)
|
||||
- [MergeBranchResult](interfaces/MergeBranchResult.md)
|
||||
- [MergePreview](interfaces/MergePreview.md)
|
||||
- [MaterializedViewDefinition](interfaces/MaterializedViewDefinition.md)
|
||||
- [MemtableStats](interfaces/MemtableStats.md)
|
||||
- [MergeResult](interfaces/MergeResult.md)
|
||||
- [NativeOAuthConfig](interfaces/NativeOAuthConfig.md)
|
||||
- [OAuthConfig](interfaces/OAuthConfig.md)
|
||||
@@ -105,6 +114,8 @@
|
||||
- [OptimizeOptions](interfaces/OptimizeOptions.md)
|
||||
- [OptimizeStats](interfaces/OptimizeStats.md)
|
||||
- [QueryExecutionOptions](interfaces/QueryExecutionOptions.md)
|
||||
- [RefreshColumnResult](interfaces/RefreshColumnResult.md)
|
||||
- [RefreshMaterializedViewResult](interfaces/RefreshMaterializedViewResult.md)
|
||||
- [RemovalStats](interfaces/RemovalStats.md)
|
||||
- [RenameTableOptions](interfaces/RenameTableOptions.md)
|
||||
- [RestNamespaceConfig](interfaces/RestNamespaceConfig.md)
|
||||
@@ -137,6 +148,7 @@
|
||||
- [FieldLike](type-aliases/FieldLike.md)
|
||||
- [IntoSql](type-aliases/IntoSql.md)
|
||||
- [IntoVector](type-aliases/IntoVector.md)
|
||||
- [MaterializedViewSelect](type-aliases/MaterializedViewSelect.md)
|
||||
- [MultiVector](type-aliases/MultiVector.md)
|
||||
- [RecordBatchLike](type-aliases/RecordBatchLike.md)
|
||||
- [SchemaLike](type-aliases/SchemaLike.md)
|
||||
|
||||
@@ -50,6 +50,14 @@ changedColumns: BranchColumnChange[];
|
||||
|
||||
***
|
||||
|
||||
### errors
|
||||
|
||||
```ts
|
||||
errors: CherryPickError[];
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### fromBranch
|
||||
|
||||
```ts
|
||||
@@ -66,22 +74,6 @@ mainVersion: number;
|
||||
|
||||
***
|
||||
|
||||
### mergeBlockers
|
||||
|
||||
```ts
|
||||
mergeBlockers: MergeBlocker[];
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### mergeable
|
||||
|
||||
```ts
|
||||
mergeable: boolean;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### parentVersion
|
||||
|
||||
```ts
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / BucketStats
|
||||
|
||||
# Interface: BucketStats
|
||||
|
||||
Live state of one bucket. A table is N buckets on one node; flattening to a
|
||||
single number hides the one hot bucket that is usually why someone opened
|
||||
this endpoint.
|
||||
|
||||
## Properties
|
||||
|
||||
### compacting
|
||||
|
||||
```ts
|
||||
compacting: boolean;
|
||||
```
|
||||
|
||||
Whether a pass owns this bucket's compaction latch right now. Says *a*
|
||||
driver is running, not *whose*, and the latch is held from dispatch —
|
||||
including while the pass queues for a pod-wide compactor permit. Read it
|
||||
as "do not pile on", never as "mine is progressing".
|
||||
|
||||
***
|
||||
|
||||
### currentGeneration
|
||||
|
||||
```ts
|
||||
currentGeneration: number;
|
||||
```
|
||||
|
||||
The generation the active memtable will become.
|
||||
|
||||
***
|
||||
|
||||
### generations
|
||||
|
||||
```ts
|
||||
generations: GenerationStats[];
|
||||
```
|
||||
|
||||
Flushed L0 generations not yet merged into the base table.
|
||||
|
||||
***
|
||||
|
||||
### manifestVersion
|
||||
|
||||
```ts
|
||||
manifestVersion: number;
|
||||
```
|
||||
|
||||
Version of the shard manifest these numbers were read from.
|
||||
|
||||
***
|
||||
|
||||
### memtables?
|
||||
|
||||
```ts
|
||||
optional memtables: MemtableStats[];
|
||||
```
|
||||
|
||||
Oldest first, active last. Absent for a `"Sealed"` bucket, whose
|
||||
in-memory state is torn down.
|
||||
|
||||
***
|
||||
|
||||
### replayAfterWalEntryPosition
|
||||
|
||||
```ts
|
||||
replayAfterWalEntryPosition: number;
|
||||
```
|
||||
|
||||
WAL position replay resumes from.
|
||||
|
||||
***
|
||||
|
||||
### shardId
|
||||
|
||||
```ts
|
||||
shardId: string;
|
||||
```
|
||||
|
||||
The shard this bucket writes.
|
||||
|
||||
***
|
||||
|
||||
### status
|
||||
|
||||
```ts
|
||||
status: string;
|
||||
```
|
||||
|
||||
`"Active"` or `"Sealed"` (drop-table 2PC in flight).
|
||||
|
||||
***
|
||||
|
||||
### walEntryPositionLastSeen
|
||||
|
||||
```ts
|
||||
walEntryPositionLastSeen: number;
|
||||
```
|
||||
|
||||
Highest WAL position the writer has seen. The difference against
|
||||
`replayAfterWalEntryPosition` is the WAL lag.
|
||||
|
||||
***
|
||||
|
||||
### writerEpoch
|
||||
|
||||
```ts
|
||||
writerEpoch: number;
|
||||
```
|
||||
|
||||
Epoch of the writer that currently owns the shard.
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / MergeBlocker
|
||||
[@lancedb/lancedb](../globals.md) / CherryPickError
|
||||
|
||||
# Interface: MergeBlocker
|
||||
# Interface: CherryPickError
|
||||
|
||||
A reason why a branch cannot currently be merged.
|
||||
A reason why a cherry-pick cannot currently land.
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / CherryPickPreview
|
||||
|
||||
# Interface: CherryPickPreview
|
||||
|
||||
Changes that would be, or were, promoted by a cherry-pick.
|
||||
|
||||
## Properties
|
||||
|
||||
### promotedColumns
|
||||
|
||||
```ts
|
||||
promotedColumns: string[];
|
||||
```
|
||||
+6
-6
@@ -2,11 +2,11 @@
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / MergeBranchResult
|
||||
[@lancedb/lancedb](../globals.md) / CherryPickResult
|
||||
|
||||
# Interface: MergeBranchResult
|
||||
# Interface: CherryPickResult
|
||||
|
||||
Result of previewing or attempting a branch merge.
|
||||
Result of previewing or attempting a cherry-pick.
|
||||
|
||||
## Properties
|
||||
|
||||
@@ -29,7 +29,7 @@ optional mainVersionAfter: number;
|
||||
### preview
|
||||
|
||||
```ts
|
||||
preview: MergePreview;
|
||||
preview: CherryPickPreview;
|
||||
```
|
||||
|
||||
***
|
||||
@@ -38,9 +38,9 @@ preview: MergePreview;
|
||||
|
||||
```ts
|
||||
status:
|
||||
| "failed"
|
||||
| "unknown"
|
||||
| "rejected"
|
||||
| "ready"
|
||||
| "notImplemented"
|
||||
| "merged";
|
||||
| "cherryPicked";
|
||||
```
|
||||
@@ -17,7 +17,8 @@ metadata: Record<string, null | string>;
|
||||
```
|
||||
|
||||
Metadata key/value pairs. Merged into the field's existing metadata by
|
||||
default; a value of `null` deletes that key.
|
||||
default; a value of `null` deletes that key. See
|
||||
[Table.updateFieldMetadata](../classes/Table.md#updatefieldmetadata) for the conventional `lancedb:*` keys.
|
||||
|
||||
***
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / GenerationStats
|
||||
|
||||
# Interface: GenerationStats
|
||||
|
||||
One flushed L0 generation.
|
||||
|
||||
## Properties
|
||||
|
||||
### bytes
|
||||
|
||||
```ts
|
||||
bytes: number;
|
||||
```
|
||||
|
||||
On-disk size of the generation.
|
||||
|
||||
***
|
||||
|
||||
### generation
|
||||
|
||||
```ts
|
||||
generation: number;
|
||||
```
|
||||
|
||||
The generation number. Increases as memtables are sealed into L0.
|
||||
|
||||
***
|
||||
|
||||
### rows?
|
||||
|
||||
```ts
|
||||
optional rows: number;
|
||||
```
|
||||
|
||||
Present only when `includeGenerationRows` was requested. Off by default
|
||||
because each count opens an uncached Lance dataset.
|
||||
@@ -0,0 +1,34 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / ListTablesOptions
|
||||
|
||||
# Interface: ListTablesOptions
|
||||
|
||||
## Properties
|
||||
|
||||
### limit?
|
||||
|
||||
```ts
|
||||
optional limit: number;
|
||||
```
|
||||
|
||||
An upper bound on how many tables to return.
|
||||
|
||||
A page may hold fewer than this and still not be the last one, so keep
|
||||
going while the response carries a page token rather than while pages are
|
||||
full.
|
||||
|
||||
***
|
||||
|
||||
### pageToken?
|
||||
|
||||
```ts
|
||||
optional pageToken: string;
|
||||
```
|
||||
|
||||
Token from a previous response, to resume listing where it left off.
|
||||
|
||||
The token is opaque: it carries whatever the database needs to resume, and
|
||||
callers should not construct or interpret one.
|
||||
@@ -0,0 +1,23 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / ListTablesResponse
|
||||
|
||||
# Interface: ListTablesResponse
|
||||
|
||||
## Properties
|
||||
|
||||
### pageToken?
|
||||
|
||||
```ts
|
||||
optional pageToken: string;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### tables
|
||||
|
||||
```ts
|
||||
tables: string[];
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / LsmStats
|
||||
|
||||
# Interface: LsmStats
|
||||
|
||||
Live per-bucket LSM state, as returned by `Table#getLsmStats`.
|
||||
|
||||
Nothing here is derived: sums and differences (total L0 bytes, WAL lag) are
|
||||
the caller's to compute.
|
||||
|
||||
## Properties
|
||||
|
||||
### buckets
|
||||
|
||||
```ts
|
||||
buckets: BucketStats[];
|
||||
```
|
||||
|
||||
One entry per bucket backing this table.
|
||||
@@ -34,7 +34,9 @@ Bucket and identity variants: the sharding column.
|
||||
optional maintainedIndexes: string[];
|
||||
```
|
||||
|
||||
Names of indexes the MemWAL should keep up to date during writes.
|
||||
Indexes the MemWAL keeps up to date. Omit to maintain every supported
|
||||
index, resolved on install — a snapshot, so indexes created later are not
|
||||
maintained. Pass `[]` for none.
|
||||
|
||||
***
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / MaterializedViewDefinition
|
||||
|
||||
# Interface: MaterializedViewDefinition
|
||||
|
||||
The query that defines a materialized view.
|
||||
|
||||
## Properties
|
||||
|
||||
### filter?
|
||||
|
||||
```ts
|
||||
optional filter: string;
|
||||
```
|
||||
|
||||
SQL predicate selecting the source rows the view holds.
|
||||
|
||||
***
|
||||
|
||||
### inputs
|
||||
|
||||
```ts
|
||||
inputs: string[];
|
||||
```
|
||||
|
||||
Source columns the projections and filter read.
|
||||
|
||||
***
|
||||
|
||||
### limit?
|
||||
|
||||
```ts
|
||||
optional limit: number;
|
||||
```
|
||||
|
||||
Cap on the number of rows the view holds.
|
||||
|
||||
***
|
||||
|
||||
### projections
|
||||
|
||||
```ts
|
||||
projections: [string, string][];
|
||||
```
|
||||
|
||||
`[output column, SQL expression]` pairs, in view schema order.
|
||||
|
||||
***
|
||||
|
||||
### sourceTable
|
||||
|
||||
```ts
|
||||
sourceTable: string;
|
||||
```
|
||||
|
||||
Name of the source table, in the same database as the view.
|
||||
@@ -0,0 +1,60 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / MemtableStats
|
||||
|
||||
# Interface: MemtableStats
|
||||
|
||||
One in-memory memtable.
|
||||
|
||||
## Properties
|
||||
|
||||
### batches
|
||||
|
||||
```ts
|
||||
batches: number;
|
||||
```
|
||||
|
||||
Record batches currently buffered.
|
||||
|
||||
***
|
||||
|
||||
### bytes
|
||||
|
||||
```ts
|
||||
bytes: number;
|
||||
```
|
||||
|
||||
Estimated in-memory size.
|
||||
|
||||
***
|
||||
|
||||
### generation
|
||||
|
||||
```ts
|
||||
generation: number;
|
||||
```
|
||||
|
||||
The generation this memtable will become once sealed.
|
||||
|
||||
***
|
||||
|
||||
### indexes
|
||||
|
||||
```ts
|
||||
indexes: string[];
|
||||
```
|
||||
|
||||
Names of the indexes this memtable carries. An absent name is the whole
|
||||
answer to "why is my fresh-tier search on that column brute-force".
|
||||
|
||||
***
|
||||
|
||||
### rows
|
||||
|
||||
```ts
|
||||
rows: number;
|
||||
```
|
||||
|
||||
Rows currently buffered.
|
||||
@@ -1,17 +0,0 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / MergePreview
|
||||
|
||||
# Interface: MergePreview
|
||||
|
||||
Changes that would be, or were, promoted by a branch merge.
|
||||
|
||||
## Properties
|
||||
|
||||
### promotedColumns
|
||||
|
||||
```ts
|
||||
promotedColumns: string[];
|
||||
```
|
||||
@@ -0,0 +1,23 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / RefreshColumnResult
|
||||
|
||||
# Interface: RefreshColumnResult
|
||||
|
||||
## Properties
|
||||
|
||||
### rowsFilled
|
||||
|
||||
```ts
|
||||
rowsFilled: number;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### version
|
||||
|
||||
```ts
|
||||
version: number;
|
||||
```
|
||||
@@ -0,0 +1,41 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / RefreshMaterializedViewResult
|
||||
|
||||
# Interface: RefreshMaterializedViewResult
|
||||
|
||||
## Properties
|
||||
|
||||
### mode
|
||||
|
||||
```ts
|
||||
mode: string;
|
||||
```
|
||||
|
||||
How the view was brought up to date: "rebuild", "incremental" or "no_op".
|
||||
|
||||
***
|
||||
|
||||
### rowsWritten
|
||||
|
||||
```ts
|
||||
rowsWritten: number;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### sourceVersion
|
||||
|
||||
```ts
|
||||
sourceVersion: number;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
### version
|
||||
|
||||
```ts
|
||||
version: number;
|
||||
```
|
||||
@@ -4,11 +4,16 @@
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / TableNamesOptions
|
||||
|
||||
# Interface: TableNamesOptions
|
||||
# Interface: ~~TableNamesOptions~~
|
||||
|
||||
## Deprecated
|
||||
|
||||
Use [ListTablesOptions](ListTablesOptions.md) with [Connection.listTables](../classes/Connection.md#listtables)
|
||||
instead.
|
||||
|
||||
## Properties
|
||||
|
||||
### limit?
|
||||
### ~~limit?~~
|
||||
|
||||
```ts
|
||||
optional limit: number;
|
||||
@@ -18,7 +23,7 @@ An optional limit to the number of results to return.
|
||||
|
||||
***
|
||||
|
||||
### startAfter?
|
||||
### ~~startAfter?~~
|
||||
|
||||
```ts
|
||||
optional startAfter: string;
|
||||
|
||||
@@ -44,4 +44,7 @@ The number of rows in the table
|
||||
totalBytes: number;
|
||||
```
|
||||
|
||||
The total number of bytes in the table
|
||||
The total size, in bytes, of the table's data files, index files, and
|
||||
overlay files
|
||||
|
||||
Read from the manifest, so this excludes deletion files and manifests.
|
||||
|
||||
@@ -25,9 +25,12 @@
|
||||
### Type Aliases
|
||||
|
||||
- [CreateReturnType](type-aliases/CreateReturnType.md)
|
||||
- [EmbeddingMetadataEntry](type-aliases/EmbeddingMetadataEntry.md)
|
||||
- [ResolvedEmbeddingFunctionConfig](type-aliases/ResolvedEmbeddingFunctionConfig.md)
|
||||
|
||||
### Functions
|
||||
|
||||
- [LanceSchema](functions/LanceSchema.md)
|
||||
- [getRegistry](functions/getRegistry.md)
|
||||
- [parseEmbeddingMetadata](functions/parseEmbeddingMetadata.md)
|
||||
- [register](functions/register.md)
|
||||
|
||||
@@ -10,16 +10,12 @@
|
||||
function getRegistry(): EmbeddingFunctionRegistry
|
||||
```
|
||||
|
||||
Utility function to get the global instance of the registry
|
||||
Get the global embedding function registry.
|
||||
|
||||
LanceDB built-in providers are initialized when this public API is first
|
||||
used, so importing the root package does not change automatic search
|
||||
selection for tables without embedding metadata.
|
||||
|
||||
## Returns
|
||||
|
||||
[`EmbeddingFunctionRegistry`](../classes/EmbeddingFunctionRegistry.md)
|
||||
|
||||
`EmbeddingFunctionRegistry` The global instance of the registry
|
||||
|
||||
## Example
|
||||
|
||||
```ts
|
||||
const registry = getRegistry();
|
||||
const openai = registry.get("openai").create();
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
[**@lancedb/lancedb**](../../../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../../../globals.md) / [embedding](../README.md) / parseEmbeddingMetadata
|
||||
|
||||
# Function: parseEmbeddingMetadata()
|
||||
|
||||
```ts
|
||||
function parseEmbeddingMetadata(json): EmbeddingMetadataEntry[]
|
||||
```
|
||||
|
||||
The single parser for `embedding_functions` schema metadata: every reader
|
||||
goes through here, so the wire contract cannot fork between them.
|
||||
|
||||
## Parameters
|
||||
|
||||
* **json**: `string`
|
||||
|
||||
## Returns
|
||||
|
||||
[`EmbeddingMetadataEntry`](../type-aliases/EmbeddingMetadataEntry.md)[]
|
||||
@@ -0,0 +1,40 @@
|
||||
[**@lancedb/lancedb**](../../../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../../../globals.md) / [embedding](../README.md) / EmbeddingMetadataEntry
|
||||
|
||||
# Type Alias: EmbeddingMetadataEntry
|
||||
|
||||
```ts
|
||||
type EmbeddingMetadataEntry: object;
|
||||
```
|
||||
|
||||
One entry of the `embedding_functions` schema metadata, with the column
|
||||
keys normalized across the bindings' spellings.
|
||||
|
||||
## Type declaration
|
||||
|
||||
### model
|
||||
|
||||
```ts
|
||||
model: EmbeddingFunction["TOptions"];
|
||||
```
|
||||
|
||||
### name
|
||||
|
||||
```ts
|
||||
name: string;
|
||||
```
|
||||
|
||||
### sourceColumn
|
||||
|
||||
```ts
|
||||
sourceColumn: string;
|
||||
```
|
||||
|
||||
### vectorColumn
|
||||
|
||||
```ts
|
||||
vectorColumn: string;
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
[**@lancedb/lancedb**](../../../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../../../globals.md) / [embedding](../README.md) / ResolvedEmbeddingFunctionConfig
|
||||
|
||||
# Type Alias: ResolvedEmbeddingFunctionConfig
|
||||
|
||||
```ts
|
||||
type ResolvedEmbeddingFunctionConfig: EmbeddingFunctionConfig & object;
|
||||
```
|
||||
|
||||
An [EmbeddingFunctionConfig] read back from table metadata, where the
|
||||
vector column is always recorded.
|
||||
|
||||
## Type declaration
|
||||
|
||||
### vectorColumn
|
||||
|
||||
```ts
|
||||
vectorColumn: string;
|
||||
```
|
||||
@@ -0,0 +1,14 @@
|
||||
[**@lancedb/lancedb**](../README.md) • **Docs**
|
||||
|
||||
***
|
||||
|
||||
[@lancedb/lancedb](../globals.md) / MaterializedViewSelect
|
||||
|
||||
# Type Alias: MaterializedViewSelect
|
||||
|
||||
```ts
|
||||
type MaterializedViewSelect: (string | [string, string])[] | Record<string, string>;
|
||||
```
|
||||
|
||||
The view's columns: column names, `[alias, SQL expression]` pairs, or a
|
||||
record of the same. A bare name projects itself.
|
||||
@@ -52,6 +52,62 @@ listing a storage directory.
|
||||
|
||||
::: lancedb.table.Branches
|
||||
|
||||
::: lancedb.LsmWriteSpec
|
||||
|
||||
## Functions and Jobs
|
||||
|
||||
::: lancedb.functions.FunctionArtifact
|
||||
|
||||
::: lancedb.functions.FunctionParameter
|
||||
|
||||
::: lancedb.functions.FunctionResultField
|
||||
|
||||
::: lancedb.functions.FunctionOutput
|
||||
|
||||
::: lancedb.functions.FunctionSignature
|
||||
|
||||
::: lancedb.functions.PythonEnvironmentSpec
|
||||
|
||||
::: lancedb.functions.udf
|
||||
|
||||
::: lancedb.functions.UdfDefinition
|
||||
|
||||
::: lancedb.functions.FunctionRegistrationRequest
|
||||
|
||||
::: lancedb.functions.FunctionArtifactRequest
|
||||
|
||||
::: lancedb.functions.FunctionArtifactContent
|
||||
|
||||
::: lancedb.functions.PythonAdapterSpec
|
||||
|
||||
::: lancedb.functions.FunctionVersion
|
||||
|
||||
::: lancedb.functions.PythonRuntimeSpec
|
||||
|
||||
::: lancedb.functions.FunctionVersionRef
|
||||
|
||||
::: lancedb.functions.ApplicationInput
|
||||
|
||||
::: lancedb.functions.FunctionApplication
|
||||
|
||||
::: lancedb.functions.InputBinding
|
||||
|
||||
::: lancedb.functions.OutputMapping
|
||||
|
||||
::: lancedb.functions.FunctionBinding
|
||||
|
||||
::: lancedb.functions.RefreshColumnResult
|
||||
|
||||
::: lancedb.job.Job
|
||||
|
||||
::: lancedb.job.AsyncJob
|
||||
|
||||
## Materialized Views (Synchronous)
|
||||
|
||||
::: lancedb.materialized_view.MaterializedView
|
||||
|
||||
::: lancedb.materialized_view.MaterializedViewDefinition
|
||||
|
||||
## Expressions
|
||||
|
||||
Type-safe expression builder for filters and projections. Use these instead
|
||||
@@ -103,6 +159,8 @@ and combined with [BooleanQuery][lancedb.query.BooleanQuery].
|
||||
|
||||
::: lancedb.query.FullTextOperator
|
||||
|
||||
::: lancedb.query.DocumentGranularity
|
||||
|
||||
::: lancedb.query.Occur
|
||||
|
||||
## Embeddings
|
||||
@@ -151,8 +209,9 @@ The same option is available on `lancedb.tokenize(...)` and the deprecated
|
||||
```python
|
||||
import lancedb
|
||||
|
||||
tokens = list(lancedb.tokenize("acme makes searchable data",
|
||||
custom_stop_words=["acme"]))
|
||||
tokens = list(
|
||||
lancedb.tokenize("acme makes searchable data", custom_stop_words=["acme"])
|
||||
)
|
||||
```
|
||||
|
||||
::: lancedb.tokenize
|
||||
@@ -204,6 +263,8 @@ instead of being materialized with the rest of the row.
|
||||
|
||||
::: lancedb.streaming.StreamingDataset
|
||||
|
||||
::: lancedb.streaming.StreamingDataLoader
|
||||
|
||||
::: lancedb.permutation.permutation_builder
|
||||
|
||||
::: lancedb.permutation.PermutationBuilder
|
||||
@@ -244,6 +305,10 @@ Table hold your actual data as a collection of records / rows.
|
||||
|
||||
::: lancedb.table.AsyncBranches
|
||||
|
||||
## Materialized Views (Asynchronous)
|
||||
|
||||
::: lancedb.materialized_view.AsyncMaterializedView
|
||||
|
||||
## Indices (Asynchronous)
|
||||
|
||||
Indices can be created on a table to speed up queries. This section
|
||||
|
||||
@@ -29,6 +29,48 @@ LanceNamespace namespaceClient = LanceDbNamespaceClientBuilder.newBuilder()
|
||||
.build();
|
||||
```
|
||||
|
||||
## MemWAL LSM write path
|
||||
|
||||
Most table operations reach LanceDB through the `LanceNamespace` above, which is
|
||||
generated from the Lance Namespace specification. The MemWAL LSM routes are not part
|
||||
of that specification, so they are issued through a separate client:
|
||||
|
||||
```java
|
||||
import com.lancedb.LanceDbRestClient;
|
||||
import com.lancedb.LanceDbTableLsm;
|
||||
import com.lancedb.LsmWriteSpec;
|
||||
|
||||
LanceDbRestClient client = LanceDbNamespaceClientBuilder.newBuilder()
|
||||
.apiKey("your_lancedb_cloud_api_key")
|
||||
.database("your_database_name")
|
||||
.buildRestClient();
|
||||
|
||||
LanceDbTableLsm lsm = new LanceDbTableLsm(client, "my_table");
|
||||
|
||||
// Route future merge_insert upserts through the MemWAL, hash-bucketed by `id`.
|
||||
lsm.setLsmWriteSpec(LsmWriteSpec.bucket("id", 16));
|
||||
|
||||
// ... merge_insert traffic ...
|
||||
|
||||
// Converge the fresh tier into the base table.
|
||||
lsm.checkpointLsm();
|
||||
|
||||
// Inspect live per-bucket state.
|
||||
lsm.getLsmStats().ifPresent(stats -> stats.buckets().forEach(bucket ->
|
||||
System.out.println(bucket.shardId() + ": " + bucket.generations().size() + " L0 generations")));
|
||||
|
||||
client.close();
|
||||
```
|
||||
|
||||
`maintainedIndexes` is tri-state, and the null default is the opposite of what a Java
|
||||
reader usually expects:
|
||||
|
||||
| Value | Meaning |
|
||||
| --- | --- |
|
||||
| unset (null) | Maintain **every** index the MemWAL can, resolved on install |
|
||||
| `Collections.emptyList()` | Maintain **none** |
|
||||
| `Arrays.asList("id_idx")` | Maintain exactly those |
|
||||
|
||||
## Development
|
||||
|
||||
Build:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-parent</artifactId>
|
||||
<version>0.37.1-beta.0</version>
|
||||
<version>0.38.0-beta.11</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
@@ -33,6 +33,20 @@
|
||||
<artifactId>arrow-memory-netty</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Transport for the LanceDB routes outside the Lance Namespace spec.
|
||||
Versions match what lance-namespace-apache-client resolves to. -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
||||
<artifactId>httpclient5</artifactId>
|
||||
<version>5.2.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.17.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
/**
|
||||
* Live state of one bucket. A table is N buckets on one node; flattening to a single number hides
|
||||
* the one hot bucket that is usually why someone opened this endpoint.
|
||||
*/
|
||||
public class BucketStats {
|
||||
private static final String CONTEXT = "bucket stats";
|
||||
|
||||
private final String shardId;
|
||||
private final String status;
|
||||
private final long writerEpoch;
|
||||
private final long manifestVersion;
|
||||
private final long currentGeneration;
|
||||
private final long replayAfterWalEntryPosition;
|
||||
private final long walEntryPositionLastSeen;
|
||||
private final List<GenerationStats> generations;
|
||||
private final boolean compacting;
|
||||
private final List<MemtableStats> memtables;
|
||||
|
||||
BucketStats(
|
||||
String shardId,
|
||||
String status,
|
||||
long writerEpoch,
|
||||
long manifestVersion,
|
||||
long currentGeneration,
|
||||
long replayAfterWalEntryPosition,
|
||||
long walEntryPositionLastSeen,
|
||||
List<GenerationStats> generations,
|
||||
boolean compacting,
|
||||
List<MemtableStats> memtables) {
|
||||
this.shardId = shardId;
|
||||
this.status = status;
|
||||
this.writerEpoch = writerEpoch;
|
||||
this.manifestVersion = manifestVersion;
|
||||
this.currentGeneration = currentGeneration;
|
||||
this.replayAfterWalEntryPosition = replayAfterWalEntryPosition;
|
||||
this.walEntryPositionLastSeen = walEntryPositionLastSeen;
|
||||
this.generations = Collections.unmodifiableList(generations);
|
||||
this.compacting = compacting;
|
||||
this.memtables = memtables == null ? null : Collections.unmodifiableList(memtables);
|
||||
}
|
||||
|
||||
/** The shard this bucket writes. */
|
||||
public String shardId() {
|
||||
return shardId;
|
||||
}
|
||||
|
||||
/** {@code "Active"} or {@code "Sealed"} (drop-table 2PC in flight). */
|
||||
public String status() {
|
||||
return status;
|
||||
}
|
||||
|
||||
/** Epoch of the writer that currently owns the shard. */
|
||||
public long writerEpoch() {
|
||||
return writerEpoch;
|
||||
}
|
||||
|
||||
/** Version of the shard manifest these numbers were read from. */
|
||||
public long manifestVersion() {
|
||||
return manifestVersion;
|
||||
}
|
||||
|
||||
/** The generation the active memtable will become. */
|
||||
public long currentGeneration() {
|
||||
return currentGeneration;
|
||||
}
|
||||
|
||||
/** WAL position replay resumes from. */
|
||||
public long replayAfterWalEntryPosition() {
|
||||
return replayAfterWalEntryPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highest WAL position the writer has seen. The difference against {@link
|
||||
* #replayAfterWalEntryPosition()} is the WAL lag.
|
||||
*/
|
||||
public long walEntryPositionLastSeen() {
|
||||
return walEntryPositionLastSeen;
|
||||
}
|
||||
|
||||
/** Flushed L0 generations not yet merged into the base table. */
|
||||
public List<GenerationStats> generations() {
|
||||
return generations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a pass owns this bucket's compaction latch right now. Says <em>a</em> driver is
|
||||
* running, not <em>whose</em>, and the latch is held from dispatch — including while the pass
|
||||
* queues for a pod-wide compactor permit. Read it as "do not pile on", never as "mine is
|
||||
* progressing".
|
||||
*/
|
||||
public boolean compacting() {
|
||||
return compacting;
|
||||
}
|
||||
|
||||
/** Oldest first, active last. Empty for a {@code "Sealed"} bucket, whose state is torn down. */
|
||||
public Optional<List<MemtableStats>> memtables() {
|
||||
return Optional.ofNullable(memtables);
|
||||
}
|
||||
|
||||
/** The newest flushed generation, or empty when L0 is empty. */
|
||||
OptionalLong newestGeneration() {
|
||||
OptionalLong newest = OptionalLong.empty();
|
||||
for (GenerationStats generation : generations) {
|
||||
if (!newest.isPresent() || generation.generation() > newest.getAsLong()) {
|
||||
newest = OptionalLong.of(generation.generation());
|
||||
}
|
||||
}
|
||||
return newest;
|
||||
}
|
||||
|
||||
/**
|
||||
* How many generations at or below {@code target} are still in L0.
|
||||
*
|
||||
* <p>A count, not a boolean: one pass drains a bounded prefix rather than the whole target set,
|
||||
* so a boolean would read as "no progress" for every pass but the last. Compaction drains
|
||||
* oldest-first, so this decreases monotonically.
|
||||
*/
|
||||
long outstandingGenerations(long target) {
|
||||
long count = 0;
|
||||
for (GenerationStats generation : generations) {
|
||||
if (generation.generation() <= target) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static BucketStats fromJson(JsonNode node) {
|
||||
JsonFields.requiredObject(node, CONTEXT);
|
||||
List<GenerationStats> generations = new ArrayList<GenerationStats>();
|
||||
for (JsonNode generation : JsonFields.requiredArray(node, "generations", CONTEXT)) {
|
||||
generations.add(GenerationStats.fromJson(generation));
|
||||
}
|
||||
|
||||
JsonNode memtablesNode = JsonFields.optionalArray(node, "memtables", CONTEXT);
|
||||
List<MemtableStats> memtables = null;
|
||||
if (memtablesNode != null) {
|
||||
memtables = new ArrayList<MemtableStats>();
|
||||
for (JsonNode memtable : memtablesNode) {
|
||||
memtables.add(MemtableStats.fromJson(memtable));
|
||||
}
|
||||
}
|
||||
|
||||
return new BucketStats(
|
||||
JsonFields.requiredText(node, "shard_id", CONTEXT),
|
||||
JsonFields.requiredText(node, "status", CONTEXT),
|
||||
JsonFields.requiredLong(node, "writer_epoch", CONTEXT),
|
||||
JsonFields.requiredLong(node, "manifest_version", CONTEXT),
|
||||
JsonFields.requiredLong(node, "current_generation", CONTEXT),
|
||||
JsonFields.requiredLong(node, "replay_after_wal_entry_position", CONTEXT),
|
||||
JsonFields.requiredLong(node, "wal_entry_position_last_seen", CONTEXT),
|
||||
generations,
|
||||
JsonFields.requiredBoolean(node, "compacting", CONTEXT),
|
||||
memtables);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BucketStats{shardId="
|
||||
+ shardId
|
||||
+ ", status="
|
||||
+ status
|
||||
+ ", currentGeneration="
|
||||
+ currentGeneration
|
||||
+ ", generations="
|
||||
+ generations
|
||||
+ ", compacting="
|
||||
+ compacting
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.OptionalLong;
|
||||
|
||||
/** One flushed L0 generation. */
|
||||
public class GenerationStats {
|
||||
private static final String CONTEXT = "generation stats";
|
||||
|
||||
private final long generation;
|
||||
private final long bytes;
|
||||
private final Long rows;
|
||||
|
||||
GenerationStats(long generation, long bytes, Long rows) {
|
||||
this.generation = generation;
|
||||
this.bytes = bytes;
|
||||
this.rows = rows;
|
||||
}
|
||||
|
||||
/** The generation number. Increases as memtables are sealed into L0. */
|
||||
public long generation() {
|
||||
return generation;
|
||||
}
|
||||
|
||||
/** On-disk size of the generation. */
|
||||
public long bytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rows in this generation, present only when {@code includeGenerationRows} was requested. Off by
|
||||
* default because each count opens an uncached Lance dataset.
|
||||
*/
|
||||
public OptionalLong rows() {
|
||||
return rows == null ? OptionalLong.empty() : OptionalLong.of(rows);
|
||||
}
|
||||
|
||||
static GenerationStats fromJson(JsonNode node) {
|
||||
JsonFields.requiredObject(node, CONTEXT);
|
||||
return new GenerationStats(
|
||||
JsonFields.requiredLong(node, "generation", CONTEXT),
|
||||
JsonFields.requiredLong(node, "bytes", CONTEXT),
|
||||
JsonFields.optionalLong(node, "rows", CONTEXT));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GenerationStats{generation=" + generation + ", bytes=" + bytes + ", rows=" + rows + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
/**
|
||||
* Strict readers for decoding LanceDB JSON responses.
|
||||
*
|
||||
* <p>Every reader fails closed: a missing, null, or wrong-typed field throws rather than
|
||||
* defaulting. That mirrors the serde decoding the Rust client applies to the same payloads in
|
||||
* {@code rust/lancedb/src/table/lsm_stats.rs}, where a required field has no default and a
|
||||
* malformed response is an error rather than a zero.
|
||||
*
|
||||
* <p>The alternative — Jackson's {@code path()}, which yields a missing node that reads as an empty
|
||||
* array or a zero — is unsafe here because {@link LanceDbTableLsm#checkpointLsm()} decides
|
||||
* convergence from these numbers. A defaulted {@code generations} array is indistinguishable from a
|
||||
* drained one, so a malformed response would report a checkpoint that never happened.
|
||||
*/
|
||||
final class JsonFields {
|
||||
private JsonFields() {}
|
||||
|
||||
/** The node itself, once confirmed to be a JSON object. */
|
||||
static JsonNode requiredObject(JsonNode node, String context) {
|
||||
if (node == null || !node.isObject()) {
|
||||
throw new IllegalStateException(context + " is not a JSON object: " + node);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static String requiredText(JsonNode owner, String field, String context) {
|
||||
JsonNode value = required(owner, field, context);
|
||||
if (!value.isTextual()) {
|
||||
throw new IllegalStateException(fieldIs(context, field, "a string", value));
|
||||
}
|
||||
return value.asText();
|
||||
}
|
||||
|
||||
static long requiredLong(JsonNode owner, String field, String context) {
|
||||
JsonNode value = required(owner, field, context);
|
||||
if (!value.isIntegralNumber()) {
|
||||
throw new IllegalStateException(fieldIs(context, field, "an integer", value));
|
||||
}
|
||||
return value.asLong();
|
||||
}
|
||||
|
||||
static boolean requiredBoolean(JsonNode owner, String field, String context) {
|
||||
JsonNode value = required(owner, field, context);
|
||||
if (!value.isBoolean()) {
|
||||
throw new IllegalStateException(fieldIs(context, field, "a boolean", value));
|
||||
}
|
||||
return value.asBoolean();
|
||||
}
|
||||
|
||||
static JsonNode requiredArray(JsonNode owner, String field, String context) {
|
||||
JsonNode value = required(owner, field, context);
|
||||
if (!value.isArray()) {
|
||||
throw new IllegalStateException(fieldIs(context, field, "an array", value));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Null when the field is absent or JSON null, mirroring a serde {@code Option}. */
|
||||
static Long optionalLong(JsonNode owner, String field, String context) {
|
||||
JsonNode value = owner.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (!value.isIntegralNumber()) {
|
||||
throw new IllegalStateException(fieldIs(context, field, "an integer", value));
|
||||
}
|
||||
return value.asLong();
|
||||
}
|
||||
|
||||
/** Null when the field is absent or JSON null, mirroring a serde {@code Option}. */
|
||||
static JsonNode optionalArray(JsonNode owner, String field, String context) {
|
||||
JsonNode value = owner.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
if (!value.isArray()) {
|
||||
throw new IllegalStateException(fieldIs(context, field, "an array", value));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static JsonNode required(JsonNode owner, String field, String context) {
|
||||
JsonNode value = owner.get(field);
|
||||
if (value == null || value.isNull()) {
|
||||
throw new IllegalStateException(context + " is missing required field '" + field + "'");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String fieldIs(String context, String field, String expected, JsonNode value) {
|
||||
return context + " field '" + field + "' is not " + expected + ": " + value;
|
||||
}
|
||||
}
|
||||
@@ -136,29 +136,48 @@ public class LanceDbNamespaceClientBuilder {
|
||||
* @throws IllegalStateException if required parameters are missing
|
||||
*/
|
||||
public LanceNamespace build() {
|
||||
// Validate required fields
|
||||
validate();
|
||||
|
||||
// Build configuration map
|
||||
Map<String, String> config = new HashMap<>(additionalConfig);
|
||||
config.put("header.x-lancedb-database", database);
|
||||
config.put("header.x-api-key", apiKey);
|
||||
config.put("uri", resolveUri());
|
||||
|
||||
return LanceNamespace.connect("rest", config, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a {@link LanceDbRestClient} for the same endpoint.
|
||||
*
|
||||
* <p>Needed only for LanceDB routes that the Lance Namespace specification does not cover — the
|
||||
* MemWAL LSM write path, reached through {@link LanceDbTableLsm}. Every other table operation
|
||||
* belongs on the {@link LanceNamespace} from {@link #build()}.
|
||||
*
|
||||
* <p>The returned client owns an HTTP connection pool; close it when you are done with it.
|
||||
*
|
||||
* @return A configured LanceDbRestClient
|
||||
* @throws IllegalStateException if required parameters are missing
|
||||
*/
|
||||
public LanceDbRestClient buildRestClient() {
|
||||
validate();
|
||||
return new LanceDbRestClient(resolveUri(), apiKey, database);
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
if (apiKey == null) {
|
||||
throw new IllegalStateException("API key is required");
|
||||
}
|
||||
if (database == null) {
|
||||
throw new IllegalStateException("Database is required");
|
||||
}
|
||||
}
|
||||
|
||||
// Build configuration map
|
||||
Map<String, String> config = new HashMap<>(additionalConfig);
|
||||
config.put("header.x-lancedb-database", database);
|
||||
config.put("header.x-api-key", apiKey);
|
||||
|
||||
// Determine base URL
|
||||
String uri;
|
||||
/** The custom endpoint when set, else the LanceDB Cloud URL for this database and region. */
|
||||
private String resolveUri() {
|
||||
if (endpoint.isPresent()) {
|
||||
uri = endpoint.get();
|
||||
} else {
|
||||
String effectiveRegion = region.orElse(DEFAULT_REGION);
|
||||
uri = String.format(CLOUD_URL_PATTERN, database, effectiveRegion);
|
||||
return endpoint.get();
|
||||
}
|
||||
config.put("uri", uri);
|
||||
|
||||
return LanceNamespace.connect("rest", config, null);
|
||||
return String.format(CLOUD_URL_PATTERN, database, region.orElse(DEFAULT_REGION));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.hc.client5.http.classic.methods.HttpPost;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||
import org.apache.hc.core5.http.ContentType;
|
||||
import org.apache.hc.core5.http.io.entity.EntityUtils;
|
||||
import org.apache.hc.core5.http.io.entity.StringEntity;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
|
||||
/**
|
||||
* Minimal HTTP client for LanceDB Cloud and Enterprise routes that the Lance Namespace
|
||||
* specification does not cover.
|
||||
*
|
||||
* <p>Most table operations reach LanceDB through {@link org.lance.namespace.LanceNamespace}, which
|
||||
* is generated from the namespace spec. A handful of routes — the MemWAL LSM write path in
|
||||
* particular — are served by the same endpoint but are not part of that spec, so they are issued
|
||||
* directly here. See {@link LanceDbTableLsm}.
|
||||
*
|
||||
* <p>Obtain one from {@link LanceDbNamespaceClientBuilder#buildRestClient()}.
|
||||
*/
|
||||
public class LanceDbRestClient implements Closeable {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private final String baseUri;
|
||||
private final String apiKey;
|
||||
private final String database;
|
||||
private final CloseableHttpClient http;
|
||||
|
||||
LanceDbRestClient(String baseUri, String apiKey, String database) {
|
||||
this.baseUri = baseUri.endsWith("/") ? baseUri.substring(0, baseUri.length() - 1) : baseUri;
|
||||
this.apiKey = apiKey;
|
||||
this.database = database;
|
||||
// Automatic retries off, deliberately. The default strategy retries 429 and 503 —
|
||||
// exactly the two statuses LanceDbTableLsm.checkpointLsm() acts on — which would
|
||||
// silently double its explicit retry budget and would also retry compact_lsm in
|
||||
// place, where the loop is designed to fall through to a fresh stats poll instead.
|
||||
// The checkpoint loop owns the 421/429/503 transitions; the transport must not.
|
||||
this.http = HttpClients.custom().disableAutomaticRetries().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST {@code path}, sending {@code body} as JSON when it is non-null.
|
||||
*
|
||||
* @param path Absolute request path, beginning with {@code /}.
|
||||
* @param body Object to serialize as the request body, or null to send no body.
|
||||
* @return The parsed response body, or null when the response carried no content.
|
||||
* @throws HttpException if the server returned a non-2xx status.
|
||||
*/
|
||||
public JsonNode post(String path, Object body) {
|
||||
HttpPost request = new HttpPost(baseUri + path);
|
||||
request.setHeader("x-api-key", apiKey);
|
||||
request.setHeader("x-lancedb-database", database);
|
||||
try {
|
||||
if (body != null) {
|
||||
request.setEntity(
|
||||
new StringEntity(MAPPER.writeValueAsString(body), ContentType.APPLICATION_JSON));
|
||||
}
|
||||
return http.execute(
|
||||
request,
|
||||
response -> {
|
||||
String text =
|
||||
response.getEntity() == null ? "" : EntityUtils.toString(response.getEntity());
|
||||
int status = response.getCode();
|
||||
if (status < 200 || status >= 300) {
|
||||
throw new HttpException(status, "LanceDB request to " + path + " failed: " + text);
|
||||
}
|
||||
return text.isEmpty() ? null : MAPPER.readTree(text);
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("LanceDB request to " + path + " failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
http.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* A non-2xx response.
|
||||
*
|
||||
* <p>The status is exposed because callers act on it: {@link LanceDbTableLsm#checkpointLsm()}
|
||||
* treats 429 and 503 as retryable and 421 as a lost node claim.
|
||||
*/
|
||||
public static class HttpException extends RuntimeException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final int statusCode;
|
||||
|
||||
public HttpException(int statusCode, String message) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
/** The HTTP status the failed response carried. */
|
||||
public int statusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
/**
|
||||
* The MemWAL LSM write path for one LanceDB Cloud or Enterprise table.
|
||||
*
|
||||
* <p>Installing an {@link LsmWriteSpec} routes {@code mergeInsert} upserts through Lance's MemWAL —
|
||||
* an LSM-style append — instead of the standard merge path. Rows land in an in-memory memtable,
|
||||
* seal into L0 generations, and are merged into the base table by compaction.
|
||||
*
|
||||
* <p>These routes are not part of the Lance Namespace specification, so they are issued directly
|
||||
* rather than through {@link org.lance.namespace.LanceNamespace}.
|
||||
*
|
||||
* <pre>{@code
|
||||
* LanceDbRestClient client = LanceDbNamespaceClientBuilder.newBuilder()
|
||||
* .apiKey("your_lancedb_cloud_api_key")
|
||||
* .database("your_database_name")
|
||||
* .buildRestClient();
|
||||
*
|
||||
* LanceDbTableLsm lsm = new LanceDbTableLsm(client, "my_table");
|
||||
* lsm.setLsmWriteSpec(LsmWriteSpec.bucket("id", 16));
|
||||
* // ... merge_insert traffic ...
|
||||
* lsm.checkpointLsm();
|
||||
* }</pre>
|
||||
*/
|
||||
public class LanceDbTableLsm {
|
||||
|
||||
/**
|
||||
* Interval between {@code get_lsm_stats} polls during a checkpoint. One interval is roughly one
|
||||
* compaction pass, the granularity at which the answer can change.
|
||||
*/
|
||||
private static final long POLL_INTERVAL_MS = 5_000L;
|
||||
|
||||
/**
|
||||
* Cap on re-issues from {@code flushLsm} after a 421, so a crash-looping node cannot turn flush →
|
||||
* compact → 421 → flush into a spin.
|
||||
*
|
||||
* <p>Deliberately not shared with {@link #MAX_RETRIES}: a claim that keeps evaporating is a
|
||||
* broken node, while contention is routine and wants a real budget.
|
||||
*/
|
||||
private static final int MAX_REISSUES = 3;
|
||||
|
||||
/**
|
||||
* Retryable faults tolerated on a <em>single</em> request, reset on every success — scattered
|
||||
* contention across a long checkpoint must not accumulate toward a cap.
|
||||
*/
|
||||
private static final int MAX_RETRIES = 8;
|
||||
|
||||
private static final long RETRY_BACKOFF_BASE_MS = 100L;
|
||||
private static final long RETRY_BACKOFF_MAX_MS = 5_000L;
|
||||
|
||||
private final LanceDbRestClient client;
|
||||
private final String tableIdentifier;
|
||||
|
||||
/**
|
||||
* Bind the LSM routes for one table.
|
||||
*
|
||||
* @param client Transport for the LanceDB endpoint.
|
||||
* @param tableIdentifier The table's full identifier, {@code $}-delimited when it sits inside a
|
||||
* namespace, such as {@code analytics$events}.
|
||||
*/
|
||||
public LanceDbTableLsm(LanceDbRestClient client, String tableIdentifier) {
|
||||
if (client == null) {
|
||||
throw new IllegalArgumentException("Client cannot be null");
|
||||
}
|
||||
if (tableIdentifier == null || tableIdentifier.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Table identifier cannot be null or empty");
|
||||
}
|
||||
this.client = client;
|
||||
this.tableIdentifier = tableIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Install an {@link LsmWriteSpec} on this table, selecting the MemWAL LSM write path for future
|
||||
* {@code mergeInsert} calls.
|
||||
*
|
||||
* <p>All variants require the table to have an unenforced primary key; bucket sharding
|
||||
* additionally requires it to be the single column being bucketed.
|
||||
*/
|
||||
public void setLsmWriteSpec(LsmWriteSpec spec) {
|
||||
if (spec == null) {
|
||||
throw new IllegalArgumentException("Spec cannot be null");
|
||||
}
|
||||
client.post(route("set_lsm_write_spec"), spec.toRequestBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the {@link LsmWriteSpec} from this table, reverting to the standard {@code mergeInsert}
|
||||
* write path.
|
||||
*
|
||||
* <p>Errors if no spec is currently set.
|
||||
*/
|
||||
public void unsetLsmWriteSpec() {
|
||||
client.post(route("unset_lsm_write_spec"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the {@link LsmWriteSpec} currently installed on this table.
|
||||
*
|
||||
* <p>Empty when the LSM write path is not enabled. The returned spec mirrors what was installed,
|
||||
* except that {@link LsmWriteSpec#maintainedIndexes()} always reports the concrete list resolved
|
||||
* when the spec was set — a null selection never round-trips.
|
||||
*/
|
||||
public Optional<LsmWriteSpec> getLsmWriteSpec() {
|
||||
JsonNode response = client.post(route("get_lsm_write_spec"), null);
|
||||
if (response == null || !response.hasNonNull("lsm_write_spec")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(LsmWriteSpec.fromJson(response.get("lsm_write_spec")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Seal every bucket's active memtable into a new L0 generation.
|
||||
*
|
||||
* <p>Returns once the seal is committed. Sealing an empty memtable is a no-op, so this is safe to
|
||||
* call repeatedly.
|
||||
*/
|
||||
public void flushLsm() {
|
||||
client.post(route("flush_lsm"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a background L0 → base compaction pass per bucket.
|
||||
*
|
||||
* <p>Returns once the passes are <em>dispatched</em>, not once they finish — watch {@link
|
||||
* #getLsmStats}, or use {@link #checkpointLsm} to wait for convergence.
|
||||
*/
|
||||
public void compactLsm() {
|
||||
client.post(route("compact_lsm"), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read live per-bucket LSM state.
|
||||
*
|
||||
* <p>Answers "how far behind is my fresh tier", "which bucket is hot", and "why is my fresh-tier
|
||||
* vector search brute-force". Mutates no table state.
|
||||
*
|
||||
* <p>Empty only when the LSM write path is not enabled — that is, when the server sends an absent
|
||||
* or null {@code lsm_stats}. A stats object that is present is decoded strictly, and a malformed
|
||||
* one throws rather than decoding to something empty, because {@link #checkpointLsm} reads
|
||||
* convergence out of these numbers and cannot tell a defaulted array from a drained one.
|
||||
*
|
||||
* @param includeGenerationRows Also count rows per L0 generation. Off by default because each
|
||||
* count opens an uncached Lance dataset.
|
||||
* @throws IllegalStateException if the response is absent or does not decode.
|
||||
*/
|
||||
public Optional<LsmStats> getLsmStats(boolean includeGenerationRows) {
|
||||
Map<String, Object> body = new LinkedHashMap<String, Object>();
|
||||
body.put("include_generation_rows", includeGenerationRows);
|
||||
JsonNode response = client.post(route("get_lsm_stats"), body);
|
||||
if (response == null) {
|
||||
throw new IllegalStateException("get_lsm_stats returned an empty response body");
|
||||
}
|
||||
JsonNode stats = response.get("lsm_stats");
|
||||
if (stats == null || stats.isNull()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.of(LsmStats.fromJson(stats));
|
||||
}
|
||||
|
||||
/** Equivalent to {@code getLsmStats(false)}. */
|
||||
public Optional<LsmStats> getLsmStats() {
|
||||
return getLsmStats(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converge this table's LSM write path into its base table.
|
||||
*
|
||||
* <p>Seals once, fixes a target watermark from the resulting L0, then triggers compaction and
|
||||
* polls until that L0 is gone. The target set is fixed at the start, so generations created
|
||||
* <em>during</em> the checkpoint are ignored — that is what lets it terminate under write load,
|
||||
* and what makes it best-effort: it converges the fresh tier as of some instant. Idempotent,
|
||||
* abandonable at any point, safe on a cadence.
|
||||
*
|
||||
* <p>The loop runs here, not on the server: {@link #compactLsm} dispatches a pass and returns, so
|
||||
* nothing holds a socket and a client can vanish mid-operation with nothing to reconcile.
|
||||
* Completion is read from generation numbers in the shard manifest — durable state, unlike a
|
||||
* count in a compact response, which a concurrent write invalidates.
|
||||
*
|
||||
* <p>No liveness bound — the caller owns the deadline. The compactor pool is shared across
|
||||
* tables, so a checkpoint queued behind unrelated work looks exactly like one that is merging.
|
||||
*/
|
||||
public void checkpointLsm() {
|
||||
for (int reissue = 0; reissue <= MAX_REISSUES; reissue++) {
|
||||
// The seal turns everything written before this call into a generation, so the
|
||||
// watermark has to be read after it. Idempotent: sealing an empty memtable is a
|
||||
// no-op, so a re-issue does not churn empty generations.
|
||||
if (issueVoid(this::flushLsm)) {
|
||||
backoff(reissue);
|
||||
continue;
|
||||
}
|
||||
|
||||
Attempt<Optional<LsmStats>> stats = issue(() -> getLsmStats(false));
|
||||
if (stats.lostClaim) {
|
||||
backoff(reissue);
|
||||
continue;
|
||||
}
|
||||
if (!stats.value.isPresent()) {
|
||||
// Not WAL-backed; flushLsm would have errored first but for a race.
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Long> targets = newestGenerations(stats.value.get());
|
||||
if (targets.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (drainToTargets(targets)) {
|
||||
return;
|
||||
}
|
||||
backoff(reissue);
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"checkpointLsm: the owning node kept losing its claim; re-issued from flush the maximum "
|
||||
+ "number of times");
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger and poll until no bucket holds a generation at or below its target.
|
||||
*
|
||||
* @return true when the drain finished, false when the table needs re-claiming from flush.
|
||||
*/
|
||||
private boolean drainToTargets(Map<String, Long> targets) {
|
||||
while (true) {
|
||||
Attempt<Optional<LsmStats>> stats = issue(() -> getLsmStats(false));
|
||||
if (stats.lostClaim) {
|
||||
return false;
|
||||
}
|
||||
if (!stats.value.isPresent()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// `compacting` is the bucket's compaction latch, held from dispatch until the pass
|
||||
// ends — including while it waits on a pod-wide permit. So it answers one question
|
||||
// only: do not pile on. Buckets with nothing outstanding are skipped, not counted
|
||||
// as idle.
|
||||
long outstanding = 0;
|
||||
boolean allCompacting = true;
|
||||
for (BucketStats bucket : stats.value.get().buckets()) {
|
||||
Long target = targets.get(bucket.shardId());
|
||||
if (target == null) {
|
||||
continue;
|
||||
}
|
||||
long remaining = bucket.outstandingGenerations(target);
|
||||
if (remaining > 0) {
|
||||
outstanding += remaining;
|
||||
allCompacting &= bucket.compacting();
|
||||
}
|
||||
}
|
||||
if (outstanding == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!allCompacting) {
|
||||
try {
|
||||
compactLsm();
|
||||
} catch (LanceDbRestClient.HttpException e) {
|
||||
if (isLostClaim(e)) {
|
||||
return false;
|
||||
}
|
||||
if (!isRetryable(e)) {
|
||||
throw e;
|
||||
}
|
||||
// A 429 here means the server could latch no bucket at all, which the poll
|
||||
// above already handles. Not retried in place: the latch it would contend for
|
||||
// is the one doing the work, so fall through and re-read — POLL_INTERVAL_MS is
|
||||
// the backoff.
|
||||
}
|
||||
}
|
||||
sleep(POLL_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/** The newest generation held by each bucket, skipping buckets holding none. */
|
||||
private static Map<String, Long> newestGenerations(LsmStats stats) {
|
||||
Map<String, Long> targets = new HashMap<String, Long>();
|
||||
for (BucketStats bucket : stats.buckets()) {
|
||||
OptionalLong newest = bucket.newestGeneration();
|
||||
if (newest.isPresent()) {
|
||||
targets.put(bucket.shardId(), newest.getAsLong());
|
||||
}
|
||||
}
|
||||
return targets;
|
||||
}
|
||||
|
||||
/**
|
||||
* 429 (latch held, pool saturated, or the pod replaying its WAL) and 503 (a draining node, or a
|
||||
* proxy between here and it).
|
||||
*/
|
||||
private static boolean isRetryable(LanceDbRestClient.HttpException e) {
|
||||
return e.statusCode() == 429 || e.statusCode() == 503;
|
||||
}
|
||||
|
||||
/**
|
||||
* 421: the owning node holds no claim. Only {@code flush} re-claims and replays, so this cannot
|
||||
* be retried in place — the caller has to start over.
|
||||
*/
|
||||
private static boolean isLostClaim(LanceDbRestClient.HttpException e) {
|
||||
return e.statusCode() == 421;
|
||||
}
|
||||
|
||||
/**
|
||||
* Issue one LSM request, retrying in place while the fault is retryable.
|
||||
*
|
||||
* <p>The two recoverable faults have separate budgets: contention clears on its own and retries
|
||||
* here against {@link #MAX_RETRIES}, while a 421 needs {@code flush} to re-claim, which only the
|
||||
* caller can drive.
|
||||
*
|
||||
* <p>An exhausted budget propagates the last error as itself rather than a synthesized one — "429
|
||||
* after nine tries" beats "checkpoint failed".
|
||||
*/
|
||||
private static <T> Attempt<T> issue(Call<T> call) {
|
||||
int retries = 0;
|
||||
while (true) {
|
||||
try {
|
||||
return new Attempt<T>(call.run(), false);
|
||||
} catch (LanceDbRestClient.HttpException e) {
|
||||
if (isLostClaim(e)) {
|
||||
return new Attempt<T>(null, true);
|
||||
}
|
||||
if (!isRetryable(e) || retries >= MAX_RETRIES) {
|
||||
throw e;
|
||||
}
|
||||
backoff(retries);
|
||||
retries++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** {@link #issue} for a call with no return value. Returns true when the claim was lost. */
|
||||
private static boolean issueVoid(Runnable call) {
|
||||
return issue(
|
||||
() -> {
|
||||
call.run();
|
||||
return Boolean.TRUE;
|
||||
})
|
||||
.lostClaim;
|
||||
}
|
||||
|
||||
/** Sleep before re-issuing a retryable request. Doubles up to {@link #RETRY_BACKOFF_MAX_MS}. */
|
||||
private static void backoff(int attempt) {
|
||||
long delay = RETRY_BACKOFF_BASE_MS << Math.min(attempt, 8);
|
||||
sleep(Math.min(delay, RETRY_BACKOFF_MAX_MS));
|
||||
}
|
||||
|
||||
private static void sleep(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException("Interrupted while waiting on the LSM checkpoint", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String route(String operation) {
|
||||
return "/v1/table/" + tableIdentifier + "/" + operation + "/";
|
||||
}
|
||||
|
||||
/** What one LSM request produced: its value, or word that the owning node holds no claim. */
|
||||
private static final class Attempt<T> {
|
||||
private final T value;
|
||||
private final boolean lostClaim;
|
||||
|
||||
private Attempt(T value, boolean lostClaim) {
|
||||
this.value = value;
|
||||
this.lostClaim = lostClaim;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface Call<T> {
|
||||
T run();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Live per-bucket LSM state, as returned by {@link LanceDbTableLsm#getLsmStats()}.
|
||||
*
|
||||
* <p>Nothing here is derived: sums and differences (total L0 bytes, WAL lag) are the caller's to
|
||||
* compute. There is no "LSM is off" shape — that case is an empty {@link java.util.Optional},
|
||||
* because a stats object of zeros would read as measurements.
|
||||
*/
|
||||
public class LsmStats {
|
||||
private static final String CONTEXT = "lsm stats";
|
||||
|
||||
private final List<BucketStats> buckets;
|
||||
|
||||
LsmStats(List<BucketStats> buckets) {
|
||||
this.buckets = Collections.unmodifiableList(buckets);
|
||||
}
|
||||
|
||||
/** One entry per bucket. */
|
||||
public List<BucketStats> buckets() {
|
||||
return buckets;
|
||||
}
|
||||
|
||||
static LsmStats fromJson(JsonNode node) {
|
||||
JsonFields.requiredObject(node, CONTEXT);
|
||||
List<BucketStats> buckets = new ArrayList<BucketStats>();
|
||||
for (JsonNode bucket : JsonFields.requiredArray(node, "buckets", CONTEXT)) {
|
||||
buckets.add(BucketStats.fromJson(bucket));
|
||||
}
|
||||
return new LsmStats(buckets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LsmStats{buckets=" + buckets + "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Specification selecting Lance's MemWAL LSM-style write path for {@code mergeInsert}.
|
||||
*
|
||||
* <p>Construct via {@link #bucket}, {@link #identity}, or {@link #unsharded}, then optionally chain
|
||||
* {@link #withMaintainedIndexes} and {@link #withWriterConfigDefaults}. Install it with {@link
|
||||
* LanceDbTableLsm#setLsmWriteSpec} and remove it with {@link LanceDbTableLsm#unsetLsmWriteSpec}.
|
||||
*
|
||||
* <p>This is deliberately not {@code org.lance.memwal.InitializeMemWalParams}. That type is Lance's
|
||||
* own, and its maintained-index default is the opposite of this one: it defaults to maintaining
|
||||
* <em>nothing</em>, while a fresh spec here maintains <em>every</em> index. It also cannot express
|
||||
* the null that asks the server to resolve the set.
|
||||
*/
|
||||
public class LsmWriteSpec {
|
||||
|
||||
/** How writes are routed to MemWAL shards. */
|
||||
public enum Sharding {
|
||||
/** Hash-bucket writes by a scalar column. */
|
||||
BUCKET("bucket"),
|
||||
/** Shard by the raw value of a scalar column. */
|
||||
IDENTITY("identity"),
|
||||
/** Route every write to a single shard. */
|
||||
UNSHARDED("unsharded");
|
||||
|
||||
private final String wireName;
|
||||
|
||||
Sharding(String wireName) {
|
||||
this.wireName = wireName;
|
||||
}
|
||||
|
||||
String wireName() {
|
||||
return wireName;
|
||||
}
|
||||
|
||||
static Sharding fromWireName(String name) {
|
||||
for (Sharding s : values()) {
|
||||
if (s.wireName.equals(name)) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown sharding mode: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
private final Sharding sharding;
|
||||
private final String column;
|
||||
private final Integer numBuckets;
|
||||
private final List<String> maintainedIndexes;
|
||||
private final Map<String, String> writerConfigDefaults;
|
||||
|
||||
private LsmWriteSpec(
|
||||
Sharding sharding,
|
||||
String column,
|
||||
Integer numBuckets,
|
||||
List<String> maintainedIndexes,
|
||||
Map<String, String> writerConfigDefaults) {
|
||||
this.sharding = sharding;
|
||||
this.column = column;
|
||||
this.numBuckets = numBuckets;
|
||||
this.maintainedIndexes = maintainedIndexes;
|
||||
this.writerConfigDefaults = writerConfigDefaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash-bucket sharding by a scalar column, maintaining every index on the table.
|
||||
*
|
||||
* <p>Iceberg-compatible Murmur3-x86-32 (seed 0) is used, so each row's {@code bucket(column,
|
||||
* numBuckets)} value is stable across processes.
|
||||
*
|
||||
* @param column A non-nested column with a supported scalar type.
|
||||
* @param numBuckets The number of buckets, in {@code [1, 1024]}.
|
||||
*/
|
||||
public static LsmWriteSpec bucket(String column, int numBuckets) {
|
||||
if (column == null || column.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Column cannot be null or empty");
|
||||
}
|
||||
return new LsmWriteSpec(
|
||||
Sharding.BUCKET, column, numBuckets, null, new HashMap<String, String>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity sharding — shard by the raw value of {@code column} — maintaining every index on the
|
||||
* table.
|
||||
*
|
||||
* <p>{@code column} must be a deterministic function of the unenforced primary key: every row
|
||||
* with a given primary key must always produce the same {@code column} value, or upserts of that
|
||||
* key can land in different shards and a stale version can win.
|
||||
*/
|
||||
public static LsmWriteSpec identity(String column) {
|
||||
if (column == null || column.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("Column cannot be null or empty");
|
||||
}
|
||||
return new LsmWriteSpec(Sharding.IDENTITY, column, null, null, new HashMap<String, String>());
|
||||
}
|
||||
|
||||
/** No sharding — every write goes to a single MemWAL shard — maintaining every index. */
|
||||
public static LsmWriteSpec unsharded() {
|
||||
return new LsmWriteSpec(Sharding.UNSHARDED, null, null, null, new HashMap<String, String>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the indexes the MemWAL keeps up to date as rows are appended.
|
||||
*
|
||||
* <p>Pass {@code null} — the default for a fresh spec — to maintain every index the MemWAL can,
|
||||
* resolved when the spec is installed. That is a snapshot: indexes created later are not
|
||||
* maintained until the spec is unset and set again. Pass an empty list to maintain none.
|
||||
*
|
||||
* <p>Note that {@code null} and the empty list mean opposite things here.
|
||||
*/
|
||||
public LsmWriteSpec withMaintainedIndexes(List<String> maintainedIndexes) {
|
||||
return new LsmWriteSpec(
|
||||
sharding,
|
||||
column,
|
||||
numBuckets,
|
||||
maintainedIndexes == null ? null : new ArrayList<String>(maintainedIndexes),
|
||||
writerConfigDefaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default {@code ShardWriter} configuration recorded in the MemWAL index.
|
||||
*
|
||||
* <p>A sparse override map — only the keys you set are recorded. Recognized keys include {@code
|
||||
* durable_write}, {@code max_wal_buffer_size}, {@code max_memtable_size}, {@code
|
||||
* max_memtable_rows}, {@code max_memtable_batches}, {@code manifest_scan_batch_size}, {@code
|
||||
* max_unflushed_memtable_bytes}, and {@code enable_memtable}. Duration knobs carry an {@code _ms}
|
||||
* suffix, such as {@code max_wal_flush_interval_ms}.
|
||||
*/
|
||||
public LsmWriteSpec withWriterConfigDefaults(Map<String, String> writerConfigDefaults) {
|
||||
if (writerConfigDefaults == null) {
|
||||
throw new IllegalArgumentException("writerConfigDefaults cannot be null");
|
||||
}
|
||||
return new LsmWriteSpec(
|
||||
sharding,
|
||||
column,
|
||||
numBuckets,
|
||||
maintainedIndexes,
|
||||
new HashMap<String, String>(writerConfigDefaults));
|
||||
}
|
||||
|
||||
/** How writes are routed to shards. */
|
||||
public Sharding sharding() {
|
||||
return sharding;
|
||||
}
|
||||
|
||||
/** The sharding column for {@link Sharding#BUCKET} and {@link Sharding#IDENTITY}, else null. */
|
||||
public String column() {
|
||||
return column;
|
||||
}
|
||||
|
||||
/** The bucket count for {@link Sharding#BUCKET}, else null. */
|
||||
public Integer numBuckets() {
|
||||
return numBuckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* The indexes the MemWAL maintains, or null to have the server resolve every maintainable index
|
||||
* on install. An empty list means none.
|
||||
*/
|
||||
public List<String> maintainedIndexes() {
|
||||
return maintainedIndexes == null ? null : Collections.unmodifiableList(maintainedIndexes);
|
||||
}
|
||||
|
||||
/** Default {@code ShardWriter} configuration recorded in the MemWAL index. */
|
||||
public Map<String, String> writerConfigDefaults() {
|
||||
return Collections.unmodifiableMap(writerConfigDefaults);
|
||||
}
|
||||
|
||||
/** Render this spec as the {@code set_lsm_write_spec} request body. */
|
||||
Map<String, Object> toRequestBody() {
|
||||
Map<String, Object> shardingBody = new LinkedHashMap<String, Object>();
|
||||
shardingBody.put("mode", sharding.wireName());
|
||||
if (column != null) {
|
||||
shardingBody.put("column", column);
|
||||
}
|
||||
if (numBuckets != null) {
|
||||
shardingBody.put("num_buckets", numBuckets);
|
||||
}
|
||||
|
||||
Map<String, Object> body = new LinkedHashMap<String, Object>();
|
||||
body.put("sharding", shardingBody);
|
||||
// Null is meaningful: it asks the server to resolve every maintainable index.
|
||||
body.put("maintained_indexes", maintainedIndexes);
|
||||
body.put("writer_config_defaults", writerConfigDefaults);
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild a spec from a {@code get_lsm_write_spec} response body.
|
||||
*
|
||||
* <p>The server always reports a concrete maintained-index list, so a null selection never
|
||||
* round-trips.
|
||||
*/
|
||||
static LsmWriteSpec fromJson(JsonNode node) {
|
||||
JsonNode shardingNode = node.get("sharding");
|
||||
if (shardingNode == null || shardingNode.get("mode") == null) {
|
||||
throw new IllegalStateException("get_lsm_write_spec response has no sharding mode");
|
||||
}
|
||||
Sharding sharding = Sharding.fromWireName(shardingNode.get("mode").asText());
|
||||
|
||||
String column = shardingNode.hasNonNull("column") ? shardingNode.get("column").asText() : null;
|
||||
Integer numBuckets =
|
||||
shardingNode.hasNonNull("num_buckets") ? shardingNode.get("num_buckets").asInt() : null;
|
||||
|
||||
List<String> maintainedIndexes = new ArrayList<String>();
|
||||
JsonNode indexesNode = node.get("maintained_indexes");
|
||||
if (indexesNode != null && indexesNode.isArray()) {
|
||||
for (JsonNode index : indexesNode) {
|
||||
maintainedIndexes.add(index.asText());
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, String> defaults = new HashMap<String, String>();
|
||||
JsonNode defaultsNode = node.get("writer_config_defaults");
|
||||
if (defaultsNode != null && defaultsNode.isObject()) {
|
||||
defaultsNode
|
||||
.fieldNames()
|
||||
.forEachRemaining(name -> defaults.put(name, defaultsNode.get(name).asText()));
|
||||
}
|
||||
|
||||
return new LsmWriteSpec(sharding, column, numBuckets, maintainedIndexes, defaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LsmWriteSpec{sharding="
|
||||
+ sharding
|
||||
+ ", column="
|
||||
+ column
|
||||
+ ", numBuckets="
|
||||
+ numBuckets
|
||||
+ ", maintainedIndexes="
|
||||
+ maintainedIndexes
|
||||
+ ", writerConfigDefaults="
|
||||
+ writerConfigDefaults
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** One in-memory memtable. */
|
||||
public class MemtableStats {
|
||||
private static final String CONTEXT = "memtable stats";
|
||||
|
||||
private final long generation;
|
||||
private final long rows;
|
||||
private final long bytes;
|
||||
private final long batches;
|
||||
private final List<String> indexes;
|
||||
|
||||
MemtableStats(long generation, long rows, long bytes, long batches, List<String> indexes) {
|
||||
this.generation = generation;
|
||||
this.rows = rows;
|
||||
this.bytes = bytes;
|
||||
this.batches = batches;
|
||||
this.indexes = Collections.unmodifiableList(indexes);
|
||||
}
|
||||
|
||||
/** The generation this memtable will become once sealed. */
|
||||
public long generation() {
|
||||
return generation;
|
||||
}
|
||||
|
||||
/** Rows currently buffered. */
|
||||
public long rows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Estimated in-memory size. */
|
||||
public long bytes() {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/** Record batches currently buffered. */
|
||||
public long batches() {
|
||||
return batches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Names of the indexes this memtable carries. An absent name is the whole answer to "why is my
|
||||
* fresh-tier search on that column brute-force".
|
||||
*/
|
||||
public List<String> indexes() {
|
||||
return indexes;
|
||||
}
|
||||
|
||||
static MemtableStats fromJson(JsonNode node) {
|
||||
JsonFields.requiredObject(node, CONTEXT);
|
||||
List<String> indexes = new ArrayList<String>();
|
||||
for (JsonNode index : JsonFields.requiredArray(node, "indexes", CONTEXT)) {
|
||||
if (!index.isTextual()) {
|
||||
throw new IllegalStateException(CONTEXT + " has a non-string index name: " + index);
|
||||
}
|
||||
indexes.add(index.asText());
|
||||
}
|
||||
return new MemtableStats(
|
||||
JsonFields.requiredLong(node, "generation", CONTEXT),
|
||||
JsonFields.requiredLong(node, "rows", CONTEXT),
|
||||
JsonFields.requiredLong(node, "bytes", CONTEXT),
|
||||
JsonFields.requiredLong(node, "batches", CONTEXT),
|
||||
indexes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MemtableStats{generation="
|
||||
+ generation
|
||||
+ ", rows="
|
||||
+ rows
|
||||
+ ", bytes="
|
||||
+ bytes
|
||||
+ ", batches="
|
||||
+ batches
|
||||
+ ", indexes="
|
||||
+ indexes
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
/*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.lancedb;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Unit tests for the MemWAL LSM routes, run against a scripted local HTTP server.
|
||||
*
|
||||
* <p>The wire assertions mirror the Rust mocked-endpoint tests in {@code
|
||||
* rust/lancedb/src/remote/table.rs}, which are the contract these routes have to match.
|
||||
*/
|
||||
public class LanceDbTableLsmTest {
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
private HttpServer server;
|
||||
private LanceDbRestClient client;
|
||||
private LanceDbTableLsm lsm;
|
||||
|
||||
private final List<String> requestPaths = Collections.synchronizedList(new ArrayList<String>());
|
||||
private final List<String> requestBodies = Collections.synchronizedList(new ArrayList<String>());
|
||||
private final Map<String, Deque<Reply>> replies = new ConcurrentHashMap<String, Deque<Reply>>();
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() throws IOException {
|
||||
start();
|
||||
}
|
||||
|
||||
/** Tear down and restart the scripted server, for a test that scripts several exchanges. */
|
||||
private void setUpFresh() {
|
||||
try {
|
||||
client.close();
|
||||
server.stop(0);
|
||||
requestPaths.clear();
|
||||
requestBodies.clear();
|
||||
replies.clear();
|
||||
start();
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void start() throws IOException {
|
||||
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
|
||||
server.createContext(
|
||||
"/",
|
||||
exchange -> {
|
||||
String path = exchange.getRequestURI().getPath();
|
||||
requestPaths.add(path);
|
||||
requestBodies.add(readAll(exchange.getRequestBody()));
|
||||
|
||||
Reply reply = nextReply(path);
|
||||
byte[] out = reply.body.getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(reply.status, out.length == 0 ? -1 : out.length);
|
||||
if (out.length > 0) {
|
||||
exchange.getResponseBody().write(out);
|
||||
}
|
||||
exchange.close();
|
||||
});
|
||||
server.start();
|
||||
|
||||
client =
|
||||
LanceDbNamespaceClientBuilder.newBuilder()
|
||||
.apiKey("test-key")
|
||||
.database("test-db")
|
||||
.endpoint("http://127.0.0.1:" + server.getAddress().getPort())
|
||||
.buildRestClient();
|
||||
lsm = new LanceDbTableLsm(client, "my_table");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() throws IOException {
|
||||
client.close();
|
||||
server.stop(0);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// set / unset / get spec
|
||||
// ===========================================================================
|
||||
|
||||
@Test
|
||||
public void testSetLsmWriteSpecUnsharded() throws Exception {
|
||||
enqueue("set_lsm_write_spec", 200, "");
|
||||
|
||||
lsm.setLsmWriteSpec(LsmWriteSpec.unsharded());
|
||||
|
||||
assertEquals("/v1/table/my_table/set_lsm_write_spec/", requestPaths.get(0));
|
||||
JsonNode body = MAPPER.readTree(requestBodies.get(0));
|
||||
assertEquals("unsharded", body.get("sharding").get("mode").asText());
|
||||
assertFalse(body.get("sharding").has("column"));
|
||||
assertFalse(body.get("sharding").has("num_buckets"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetLsmWriteSpecBucket() throws Exception {
|
||||
enqueue("set_lsm_write_spec", 200, "");
|
||||
|
||||
lsm.setLsmWriteSpec(
|
||||
LsmWriteSpec.bucket("id", 16).withMaintainedIndexes(Arrays.asList("id_idx")));
|
||||
|
||||
JsonNode body = MAPPER.readTree(requestBodies.get(0));
|
||||
assertEquals("bucket", body.get("sharding").get("mode").asText());
|
||||
assertEquals("id", body.get("sharding").get("column").asText());
|
||||
assertEquals(16, body.get("sharding").get("num_buckets").asInt());
|
||||
assertEquals(1, body.get("maintained_indexes").size());
|
||||
assertEquals("id_idx", body.get("maintained_indexes").get(0).asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetLsmWriteSpecIdentity() throws Exception {
|
||||
enqueue("set_lsm_write_spec", 200, "");
|
||||
|
||||
lsm.setLsmWriteSpec(LsmWriteSpec.identity("tenant"));
|
||||
|
||||
JsonNode body = MAPPER.readTree(requestBodies.get(0));
|
||||
assertEquals("identity", body.get("sharding").get("mode").asText());
|
||||
assertEquals("tenant", body.get("sharding").get("column").asText());
|
||||
assertFalse(body.get("sharding").has("num_buckets"));
|
||||
}
|
||||
|
||||
/**
|
||||
* The tri-state that motivated a LanceDB-owned spec type: a null selection asks the server to
|
||||
* resolve every maintainable index, while an empty list asks for none. They must not collapse.
|
||||
*/
|
||||
@Test
|
||||
public void testMaintainedIndexesNullAndEmptyAreDistinctOnTheWire() throws Exception {
|
||||
enqueue("set_lsm_write_spec", 200, "");
|
||||
|
||||
lsm.setLsmWriteSpec(LsmWriteSpec.unsharded());
|
||||
JsonNode fresh = MAPPER.readTree(requestBodies.get(0));
|
||||
assertTrue(fresh.has("maintained_indexes"), "the key must be present");
|
||||
assertTrue(fresh.get("maintained_indexes").isNull(), "a fresh spec sends null, not []");
|
||||
|
||||
lsm.setLsmWriteSpec(
|
||||
LsmWriteSpec.unsharded().withMaintainedIndexes(Collections.<String>emptyList()));
|
||||
JsonNode none = MAPPER.readTree(requestBodies.get(1));
|
||||
assertTrue(none.get("maintained_indexes").isArray());
|
||||
assertEquals(0, none.get("maintained_indexes").size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetLsmWriteSpecWriterConfigDefaults() throws Exception {
|
||||
enqueue("set_lsm_write_spec", 200, "");
|
||||
|
||||
Map<String, String> defaults = new HashMap<String, String>();
|
||||
defaults.put("max_memtable_rows", "50000");
|
||||
lsm.setLsmWriteSpec(LsmWriteSpec.unsharded().withWriterConfigDefaults(defaults));
|
||||
|
||||
JsonNode body = MAPPER.readTree(requestBodies.get(0));
|
||||
assertEquals("50000", body.get("writer_config_defaults").get("max_memtable_rows").asText());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnsetLsmWriteSpec() {
|
||||
enqueue("unset_lsm_write_spec", 200, "");
|
||||
|
||||
lsm.unsetLsmWriteSpec();
|
||||
|
||||
assertEquals("/v1/table/my_table/unset_lsm_write_spec/", requestPaths.get(0));
|
||||
assertEquals("", requestBodies.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLsmWriteSpec() {
|
||||
enqueue(
|
||||
"get_lsm_write_spec",
|
||||
200,
|
||||
"{\"lsm_write_spec\":{\"sharding\":{\"mode\":\"bucket\",\"column\":\"id\","
|
||||
+ "\"num_buckets\":16},\"maintained_indexes\":[\"id_idx\"],"
|
||||
+ "\"writer_config_defaults\":{\"durable_write\":\"true\"}}}");
|
||||
|
||||
Optional<LsmWriteSpec> spec = lsm.getLsmWriteSpec();
|
||||
|
||||
assertTrue(spec.isPresent());
|
||||
assertEquals(LsmWriteSpec.Sharding.BUCKET, spec.get().sharding());
|
||||
assertEquals("id", spec.get().column());
|
||||
assertEquals(Integer.valueOf(16), spec.get().numBuckets());
|
||||
assertEquals(Arrays.asList("id_idx"), spec.get().maintainedIndexes());
|
||||
assertEquals("true", spec.get().writerConfigDefaults().get("durable_write"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLsmWriteSpecAbsent() {
|
||||
enqueue("get_lsm_write_spec", 200, "{\"lsm_write_spec\":null}");
|
||||
|
||||
assertFalse(lsm.getLsmWriteSpec().isPresent());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// stats
|
||||
// ===========================================================================
|
||||
|
||||
@Test
|
||||
public void testGetLsmStats() throws Exception {
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 7L, 8L)));
|
||||
|
||||
Optional<LsmStats> got = lsm.getLsmStats(true);
|
||||
|
||||
assertEquals("/v1/table/my_table/get_lsm_stats/", requestPaths.get(0));
|
||||
assertTrue(MAPPER.readTree(requestBodies.get(0)).get("include_generation_rows").asBoolean());
|
||||
assertTrue(got.isPresent());
|
||||
BucketStats decoded = got.get().buckets().get(0);
|
||||
assertEquals("shard-0", decoded.shardId());
|
||||
assertEquals("Active", decoded.status());
|
||||
assertEquals(1, decoded.writerEpoch());
|
||||
assertEquals(2, decoded.manifestVersion());
|
||||
assertEquals(9, decoded.currentGeneration());
|
||||
assertFalse(decoded.compacting());
|
||||
assertEquals(Arrays.asList(7L, 8L), generationNumbers(decoded));
|
||||
assertEquals(1024, decoded.generations().get(0).bytes());
|
||||
assertFalse(decoded.generations().get(0).rows().isPresent(), "rows absent unless requested");
|
||||
assertFalse(decoded.memtables().isPresent(), "absent memtables stay absent");
|
||||
}
|
||||
|
||||
/** The optional fields decode when the server does send them. */
|
||||
@Test
|
||||
public void testGetLsmStatsDecodesOptionalFields() {
|
||||
enqueue(
|
||||
"get_lsm_stats",
|
||||
200,
|
||||
"{\"lsm_stats\":{\"buckets\":[{\"shard_id\":\"shard-0\",\"status\":\"Active\","
|
||||
+ "\"writer_epoch\":1,\"manifest_version\":2,\"current_generation\":9,"
|
||||
+ "\"replay_after_wal_entry_position\":3,\"wal_entry_position_last_seen\":11,"
|
||||
+ "\"generations\":[{\"generation\":7,\"bytes\":1024,\"rows\":42}],"
|
||||
+ "\"compacting\":true,\"memtables\":[{\"generation\":8,\"rows\":5,"
|
||||
+ "\"bytes\":64,\"batches\":2,\"indexes\":[\"id_idx\"]}]}]}}");
|
||||
|
||||
BucketStats decoded = lsm.getLsmStats(true).get().buckets().get(0);
|
||||
|
||||
assertEquals(3, decoded.replayAfterWalEntryPosition());
|
||||
assertEquals(11, decoded.walEntryPositionLastSeen());
|
||||
assertTrue(decoded.compacting());
|
||||
assertEquals(42, decoded.generations().get(0).rows().getAsLong());
|
||||
assertTrue(decoded.memtables().isPresent());
|
||||
MemtableStats memtable = decoded.memtables().get().get(0);
|
||||
assertEquals(8, memtable.generation());
|
||||
assertEquals(5, memtable.rows());
|
||||
assertEquals(64, memtable.bytes());
|
||||
assertEquals(2, memtable.batches());
|
||||
assertEquals(Arrays.asList("id_idx"), memtable.indexes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLsmStatsAbsentWhenLsmDisabled() {
|
||||
enqueue("get_lsm_stats", 200, "{\"lsm_stats\":null}");
|
||||
|
||||
assertFalse(lsm.getLsmStats().isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetLsmStatsDefaultsToExcludingGenerationRows() throws Exception {
|
||||
enqueue("get_lsm_stats", 200, stats());
|
||||
|
||||
lsm.getLsmStats();
|
||||
|
||||
assertFalse(MAPPER.readTree(requestBodies.get(0)).get("include_generation_rows").asBoolean());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// flush / compact
|
||||
// ===========================================================================
|
||||
|
||||
@Test
|
||||
public void testFlushAndCompactRoutes() {
|
||||
enqueue("flush_lsm", 200, "");
|
||||
enqueue("compact_lsm", 200, "");
|
||||
|
||||
lsm.flushLsm();
|
||||
lsm.compactLsm();
|
||||
|
||||
assertEquals("/v1/table/my_table/flush_lsm/", requestPaths.get(0));
|
||||
assertEquals("/v1/table/my_table/compact_lsm/", requestPaths.get(1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHttpErrorCarriesStatus() {
|
||||
enqueue("flush_lsm", 404, "no such table");
|
||||
|
||||
LanceDbRestClient.HttpException e =
|
||||
assertThrows(LanceDbRestClient.HttpException.class, () -> lsm.flushLsm());
|
||||
assertEquals(404, e.statusCode());
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// checkpoint
|
||||
// ===========================================================================
|
||||
|
||||
@Test
|
||||
public void testCheckpointReturnsWhenLsmDisabled() {
|
||||
enqueue("flush_lsm", 200, "");
|
||||
enqueue("get_lsm_stats", 200, "{\"lsm_stats\":null}");
|
||||
|
||||
lsm.checkpointLsm();
|
||||
|
||||
assertEquals(0, countCalls("compact_lsm"), "nothing to compact when the LSM path is off");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointReturnsWhenNoGenerationsOutstanding() {
|
||||
enqueue("flush_lsm", 200, "");
|
||||
// A bucket with no L0 generations yields no target, so the drain never starts.
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false)));
|
||||
|
||||
lsm.checkpointLsm();
|
||||
|
||||
assertEquals(0, countCalls("compact_lsm"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointConvergesOnceTargetGenerationsAreGone() {
|
||||
enqueue("flush_lsm", 200, "");
|
||||
// Watermark read: shard-0 holds generations 7 and 8, so target = 8.
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 7L, 8L)));
|
||||
// First drain poll: both still outstanding, nothing compacting -> dispatch a pass.
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 7L, 8L)));
|
||||
// Second drain poll: drained past the target -> done.
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 9L)));
|
||||
enqueue("compact_lsm", 200, "");
|
||||
|
||||
lsm.checkpointLsm();
|
||||
|
||||
assertEquals(1, countCalls("compact_lsm"), "one pass dispatched");
|
||||
assertEquals(3, countCalls("get_lsm_stats"), "watermark read plus two drain polls");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointDoesNotPileOnWhileEveryTargetBucketIsCompacting() {
|
||||
enqueue("flush_lsm", 200, "");
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", true, 4L)));
|
||||
// Still compacting on the first poll, so no pass is dispatched; then it drains.
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", true, 4L)));
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false, 5L)));
|
||||
|
||||
lsm.checkpointLsm();
|
||||
|
||||
assertEquals(0, countCalls("compact_lsm"), "a latched bucket is left alone");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointRetriesFromFlushAfterLostClaim() {
|
||||
// 421 on the watermark read: the node lost its claim, so the whole thing restarts
|
||||
// from flush rather than retrying the read in place.
|
||||
enqueue("flush_lsm", 200, "");
|
||||
enqueue("get_lsm_stats", 421, "no claim");
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false)));
|
||||
|
||||
lsm.checkpointLsm();
|
||||
|
||||
assertEquals(2, countCalls("flush_lsm"), "re-issued from flush");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointRetriesRetryableStatusInPlace() {
|
||||
enqueue("flush_lsm", 429, "latch held");
|
||||
enqueue("flush_lsm", 200, "");
|
||||
enqueue("get_lsm_stats", 200, stats(bucket("shard-0", false)));
|
||||
|
||||
lsm.checkpointLsm();
|
||||
|
||||
assertEquals(2, countCalls("flush_lsm"), "429 retried in place, not re-issued");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointPropagatesTerminalStatus() {
|
||||
enqueue("flush_lsm", 400, "bad request");
|
||||
|
||||
LanceDbRestClient.HttpException e =
|
||||
assertThrows(LanceDbRestClient.HttpException.class, () -> lsm.checkpointLsm());
|
||||
assertEquals(400, e.statusCode());
|
||||
assertEquals(1, countCalls("flush_lsm"), "a terminal status is not retried");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckpointGivesUpAfterRepeatedLostClaims() {
|
||||
enqueue("flush_lsm", 421, "no claim");
|
||||
|
||||
IllegalStateException e = assertThrows(IllegalStateException.class, () -> lsm.checkpointLsm());
|
||||
assertTrue(e.getMessage().contains("kept losing its claim"), e.getMessage());
|
||||
assertEquals(4, countCalls("flush_lsm"), "the initial attempt plus MAX_REISSUES");
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// strict decoding
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* A stats payload that does not decode must fail closed. Every one of these bodies used to be
|
||||
* read as "no buckets", which is indistinguishable from a drained table, so {@code checkpointLsm}
|
||||
* reported convergence for a checkpoint that never ran.
|
||||
*/
|
||||
@Test
|
||||
public void testCheckpointRejectsMalformedStats() {
|
||||
Map<String, String> malformed = new LinkedHashMap<String, String>();
|
||||
malformed.put("no response body at all", "");
|
||||
malformed.put("stats object with no buckets", "{\"lsm_stats\":{}}");
|
||||
malformed.put("bucket missing its required fields", "{\"lsm_stats\":{\"buckets\":[{}]}}");
|
||||
malformed.put(
|
||||
"bucket missing generations",
|
||||
"{\"lsm_stats\":{\"buckets\":[{\"shard_id\":\"shard-0\",\"status\":\"Active\","
|
||||
+ "\"writer_epoch\":1,\"manifest_version\":2,\"current_generation\":9,"
|
||||
+ "\"replay_after_wal_entry_position\":0,\"wal_entry_position_last_seen\":0,"
|
||||
+ "\"compacting\":false}]}}");
|
||||
malformed.put(
|
||||
"generation with a non-numeric generation number",
|
||||
"{\"lsm_stats\":{\"buckets\":[{\"shard_id\":\"shard-0\",\"status\":\"Active\","
|
||||
+ "\"writer_epoch\":1,\"manifest_version\":2,\"current_generation\":9,"
|
||||
+ "\"replay_after_wal_entry_position\":0,\"wal_entry_position_last_seen\":0,"
|
||||
+ "\"generations\":[{\"generation\":\"7\",\"bytes\":1024}],"
|
||||
+ "\"compacting\":false}]}}");
|
||||
|
||||
for (Map.Entry<String, String> each : malformed.entrySet()) {
|
||||
setUpFresh();
|
||||
enqueue("flush_lsm", 200, "");
|
||||
enqueue("get_lsm_stats", 200, each.getValue());
|
||||
|
||||
assertThrows(
|
||||
IllegalStateException.class,
|
||||
() -> lsm.checkpointLsm(),
|
||||
each.getKey() + " must not report convergence");
|
||||
}
|
||||
}
|
||||
|
||||
/** The one shape that legitimately means "this table has no LSM write path". */
|
||||
@Test
|
||||
public void testCheckpointTreatsNullStatsAsNotWalBacked() {
|
||||
enqueue("flush_lsm", 200, "");
|
||||
enqueue("get_lsm_stats", 200, "{\"lsm_stats\":null}");
|
||||
|
||||
lsm.checkpointLsm();
|
||||
|
||||
assertEquals(1, countCalls("get_lsm_stats"));
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// retry budget
|
||||
// ===========================================================================
|
||||
|
||||
/**
|
||||
* The transport must not retry on the checkpoint loop's behalf. Apache HttpClient's default
|
||||
* strategy retries exactly 429 and 503 — the two statuses {@code isRetryable} owns — which
|
||||
* doubled every budget here and also retried {@code compact_lsm} in place, where the loop is
|
||||
* built to fall through to a fresh stats poll instead.
|
||||
*/
|
||||
@Test
|
||||
public void testCheckpointRetryBudgetIsNotDoubledByTheTransport() {
|
||||
enqueue("flush_lsm", 429, "latch held");
|
||||
|
||||
LanceDbRestClient.HttpException e =
|
||||
assertThrows(LanceDbRestClient.HttpException.class, () -> lsm.checkpointLsm());
|
||||
|
||||
assertEquals(429, e.statusCode(), "the exhausted budget propagates the last error as itself");
|
||||
assertEquals(9, countCalls("flush_lsm"), "the initial request plus MAX_RETRIES, and no more");
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// harness
|
||||
// ===========================================================================
|
||||
|
||||
private static List<Long> generationNumbers(BucketStats bucket) {
|
||||
List<Long> numbers = new ArrayList<Long>();
|
||||
for (GenerationStats generation : bucket.generations()) {
|
||||
numbers.add(generation.generation());
|
||||
}
|
||||
return numbers;
|
||||
}
|
||||
|
||||
/** Build an {@code lsm_stats} response body from bucket fragments. */
|
||||
private static String stats(String... buckets) {
|
||||
return "{\"lsm_stats\":{\"buckets\":[" + String.join(",", buckets) + "]}}";
|
||||
}
|
||||
|
||||
private static String bucket(String shardId, boolean compacting, Long... generations) {
|
||||
StringBuilder gens = new StringBuilder();
|
||||
for (Long generation : generations) {
|
||||
if (gens.length() > 0) {
|
||||
gens.append(",");
|
||||
}
|
||||
gens.append("{\"generation\":").append(generation).append(",\"bytes\":1024}");
|
||||
}
|
||||
return "{\"shard_id\":\""
|
||||
+ shardId
|
||||
+ "\",\"status\":\"Active\",\"writer_epoch\":1,\"manifest_version\":2,"
|
||||
+ "\"current_generation\":9,\"replay_after_wal_entry_position\":0,"
|
||||
+ "\"wal_entry_position_last_seen\":0,\"generations\":["
|
||||
+ gens
|
||||
+ "],\"compacting\":"
|
||||
+ compacting
|
||||
+ "}";
|
||||
}
|
||||
|
||||
/** Queue a reply for an operation. The last queued reply repeats once the queue drains. */
|
||||
private void enqueue(String operation, int status, String body) {
|
||||
replies.computeIfAbsent(operation, key -> new ArrayDeque<Reply>()).add(new Reply(status, body));
|
||||
}
|
||||
|
||||
private Reply nextReply(String path) {
|
||||
String operation = operationOf(path);
|
||||
Deque<Reply> queued = replies.get(operation);
|
||||
if (queued == null || queued.isEmpty()) {
|
||||
return new Reply(200, "");
|
||||
}
|
||||
return queued.size() > 1 ? queued.poll() : queued.peek();
|
||||
}
|
||||
|
||||
private long countCalls(String operation) {
|
||||
return requestPaths.stream().filter(path -> operationOf(path).equals(operation)).count();
|
||||
}
|
||||
|
||||
/** {@code /v1/table/my_table/flush_lsm/} -> {@code flush_lsm}. */
|
||||
private static String operationOf(String path) {
|
||||
String[] segments = path.split("/");
|
||||
return segments.length == 0 ? "" : segments[segments.length - 1];
|
||||
}
|
||||
|
||||
private static String readAll(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static final class Reply {
|
||||
private final int status;
|
||||
private final String body;
|
||||
|
||||
private Reply(int status, String body) {
|
||||
this.status = status;
|
||||
this.body = body;
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-parent</artifactId>
|
||||
<version>0.37.1-beta.0</version>
|
||||
<version>0.38.0-beta.11</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>LanceDB Java SDK Parent POM</description>
|
||||
@@ -28,7 +28,7 @@
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<arrow.version>15.0.0</arrow.version>
|
||||
<lance-core.version>11.0.0-beta.3</lance-core.version>
|
||||
<lance-core.version>12.0.0-beta.2</lance-core.version>
|
||||
<spotless.skip>false</spotless.skip>
|
||||
<spotless.version>2.30.0</spotless.version>
|
||||
<spotless.java.googlejavaformat.version>1.7</spotless.java.googlejavaformat.version>
|
||||
|
||||
@@ -1,271 +0,0 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"third-party/opendal",
|
||||
"third-party/opendal-service-s3",
|
||||
"rust/examples",
|
||||
"rust/lance",
|
||||
"rust/lance-arrow",
|
||||
"rust/lance-core",
|
||||
"rust/lance-datagen",
|
||||
"rust/lance-encoding",
|
||||
"rust/lance-file",
|
||||
"rust/lance-geo",
|
||||
"rust/lance-index",
|
||||
"rust/lance-index-core",
|
||||
"rust/lance-io",
|
||||
"rust/lance-linalg",
|
||||
"rust/lance-namespace",
|
||||
"rust/lance-namespace-impls",
|
||||
"rust/lance-namespace-datafusion",
|
||||
"rust/lance-select",
|
||||
"rust/lance-tokenizer",
|
||||
"rust/lance-table",
|
||||
"rust/lance-derive",
|
||||
"rust/lance-test-macros",
|
||||
"rust/lance-testing",
|
||||
"rust/lance-tools",
|
||||
"rust/compression/fsst",
|
||||
"rust/compression/bitpacking",
|
||||
"rust/arrow-scalar",
|
||||
"rust/arrow-stats",
|
||||
]
|
||||
exclude = ["python", "java/lance-jni"]
|
||||
# Python package needs to be built by maturin.
|
||||
resolver = "3"
|
||||
|
||||
|
||||
[workspace.package]
|
||||
version = "11.0.0-beta.3"
|
||||
edition = "2024"
|
||||
authors = ["Lance Devs <dev@lance.org>"]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/lance-format/lance"
|
||||
readme = "README.md"
|
||||
description = "A columnar data format that is 100x faster than Parquet for random access."
|
||||
keywords = [
|
||||
"data-format",
|
||||
"data-science",
|
||||
"machine-learning",
|
||||
"apache-arrow",
|
||||
"data-analytics",
|
||||
]
|
||||
categories = [
|
||||
"database-implementations",
|
||||
"data-structures",
|
||||
"development-tools",
|
||||
"science",
|
||||
]
|
||||
rust-version = "1.91.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
arc-swap = "1.7"
|
||||
libc = "0.2.176"
|
||||
lance = { version = "=11.0.0-beta.3", path = "./rust/lance", default-features = false }
|
||||
lance-arrow = { version = "=11.0.0-beta.3", path = "./rust/lance-arrow" }
|
||||
lance-core = { version = "=11.0.0-beta.3", path = "./rust/lance-core" }
|
||||
lance-datafusion = { version = "=11.0.0-beta.3", path = "./rust/lance-datafusion" }
|
||||
lance-datagen = { version = "=11.0.0-beta.3", path = "./rust/lance-datagen" }
|
||||
lance-derive = { version = "=11.0.0-beta.3", path = "./rust/lance-derive" }
|
||||
lance-encoding = { version = "=11.0.0-beta.3", path = "./rust/lance-encoding" }
|
||||
lance-file = { version = "=11.0.0-beta.3", path = "./rust/lance-file" }
|
||||
lance-geo = { version = "=11.0.0-beta.3", path = "./rust/lance-geo" }
|
||||
lance-index = { version = "=11.0.0-beta.3", path = "./rust/lance-index" }
|
||||
lance-index-core = { version = "=11.0.0-beta.3", path = "./rust/lance-index-core" }
|
||||
lance-io = { version = "=11.0.0-beta.3", path = "./rust/lance-io", default-features = false }
|
||||
lance-linalg = { version = "=11.0.0-beta.3", path = "./rust/lance-linalg" }
|
||||
lance-namespace = { version = "=11.0.0-beta.3", path = "./rust/lance-namespace" }
|
||||
lance-namespace-impls = { version = "=11.0.0-beta.3", path = "./rust/lance-namespace-impls" }
|
||||
lance-namespace-datafusion = { version = "=7.0.0-beta.9", path = "./rust/lance-namespace-datafusion" }
|
||||
lance-namespace-reqwest-client = "0.8.6"
|
||||
lance-select = { version = "=11.0.0-beta.3", path = "./rust/lance-select" }
|
||||
lance-tokenizer = { version = "=11.0.0-beta.3", path = "./rust/lance-tokenizer" }
|
||||
lance-table = { version = "=11.0.0-beta.3", path = "./rust/lance-table" }
|
||||
lance-test-macros = { version = "=11.0.0-beta.3", path = "./rust/lance-test-macros" }
|
||||
lance-testing = { version = "=11.0.0-beta.3", path = "./rust/lance-testing" }
|
||||
approx = "0.5.1"
|
||||
# Note that this one does not include pyarrow
|
||||
arrow = { version = "58.0.0", optional = false, features = ["prettyprint"] }
|
||||
lance-arrow-scalar = { version = "=58.0.0", path = "./rust/arrow-scalar" }
|
||||
lance-arrow-stats = { version = "=58.0.0", path = "./rust/arrow-stats" }
|
||||
arrow-arith = "58.0.0"
|
||||
arrow-array = "58.0.0"
|
||||
arrow-buffer = "58.0.0"
|
||||
arrow-cast = "58.0.0"
|
||||
arrow-data = "58.0.0"
|
||||
arrow-ipc = { version = "58.0.0", features = ["zstd"] }
|
||||
arrow-ord = "58.0.0"
|
||||
arrow-row = "58.0.0"
|
||||
arrow-schema = "58.0.0"
|
||||
arrow-select = "58.0.0"
|
||||
async-recursion = "1.0"
|
||||
async-trait = "0.1"
|
||||
axum = "0.7"
|
||||
aws-config = "1.2.0"
|
||||
aws-credential-types = "1.2.0"
|
||||
aws-sdk-dynamodb = { version = "1.38.0", default-features = false }
|
||||
aws-sdk-s3 = { version = "1.38.0", default-features = false }
|
||||
half = { "version" = "2.1", default-features = false, features = [
|
||||
"num-traits",
|
||||
"std",
|
||||
"bytemuck",
|
||||
] }
|
||||
lance-bitpacking = { version = "=11.0.0-beta.3", path = "./rust/compression/bitpacking" }
|
||||
bitpacking = "0.9"
|
||||
bitvec = "1"
|
||||
blake3 = "1.8.5"
|
||||
bytemuck = { version = "1", default-features = false, features = [
|
||||
"extern_crate_alloc",
|
||||
] }
|
||||
bytes = "1.11.1"
|
||||
byteorder = "1.5"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
chrono = { version = "0.4.41", default-features = false, features = [
|
||||
"std",
|
||||
"now",
|
||||
"serde",
|
||||
] }
|
||||
criterion = { version = "0.8.2", features = [
|
||||
"async",
|
||||
"async_tokio",
|
||||
"html_reports",
|
||||
] }
|
||||
crossbeam-queue = "0.3"
|
||||
crossbeam-skiplist = "0.1"
|
||||
datafusion = { version = "54.0.0", default-features = false, features = [
|
||||
"crypto_expressions",
|
||||
"datetime_expressions",
|
||||
"encoding_expressions",
|
||||
"nested_expressions",
|
||||
"regex_expressions",
|
||||
"sql",
|
||||
"string_expressions",
|
||||
"unicode_expressions",
|
||||
] }
|
||||
datafusion-common = "54.0.0"
|
||||
datafusion-functions = { version = "54.0.0", default-features = false, features = ["regex_expressions"] }
|
||||
datafusion-sql = "54.0.0"
|
||||
datafusion-expr = "54.0.0"
|
||||
datafusion-ffi = "54.0.0"
|
||||
datafusion-physical-expr = "54.0.0"
|
||||
datafusion-physical-plan = "54.0.0"
|
||||
datafusion-substrait = { version = "54.0.0", default-features = false }
|
||||
dirs = "6.0.0"
|
||||
either = "1.0"
|
||||
fst = { version = "0.4.7", features = ["levenshtein"] }
|
||||
fsst = { version = "=11.0.0-beta.3", path = "./rust/compression/fsst" }
|
||||
futures = "0.3"
|
||||
geoarrow-array = "0.8"
|
||||
geoarrow-schema = "0.8"
|
||||
geodatafusion = "0.5.0"
|
||||
geo-traits = "0.3.0"
|
||||
geo-types = "0.7.16"
|
||||
http = "1.1.0"
|
||||
humantime = "2.2.0"
|
||||
hyperloglogplus = { version = "0.4.1", features = ["const-loop"] }
|
||||
icu_segmenter = { version = "2.2", default-features = false, features = ["compiled_data"] }
|
||||
io-uring = "0.7"
|
||||
itertools = "0.14"
|
||||
jieba-rs = { version = "0.10.0", default-features = false }
|
||||
jsonb = { version = "0.5.3", default-features = false, features = ["databend"] }
|
||||
libm = "0.2.15"
|
||||
log = "0.4"
|
||||
metrics = { version = "0.24" }
|
||||
metrics-util = { version = "0.19" }
|
||||
mockall = { version = "0.14.0" }
|
||||
mock_instant = { version = "0.6.0" }
|
||||
moka = { version = "0.12", features = ["future", "sync"] }
|
||||
ndarray = { version = "0.16.1", features = ["matrixmultiply-threading"] }
|
||||
num-traits = "0.2"
|
||||
object_store = { version = "0.13.2" }
|
||||
opendal = { version = "0.58.1", path = "./third-party/opendal" }
|
||||
object_store_opendal = { version = "0.58" }
|
||||
reqsign-aws-v4 = { version = "3.1.0" }
|
||||
reqsign-core = { version = "3.2.1" }
|
||||
reqsign-file-read-tokio = { version = "3.0.4" }
|
||||
pin-project = "1.0"
|
||||
path_abs = "0.5"
|
||||
pprof = { version = "0.15.0", features = ["flamegraph"] }
|
||||
proptest = "1.3.1"
|
||||
prost = "0.14.1"
|
||||
prost-build = "0.14.1"
|
||||
prost-types = "0.14.1"
|
||||
rand = { version = "0.9.1", features = ["small_rng"] }
|
||||
rand_distr = { version = "0.5.1" }
|
||||
rand_xoshiro = "0.7.0"
|
||||
rangemap = { version = "1.0" }
|
||||
rayon = "1.10"
|
||||
regex-syntax = "0.8.10"
|
||||
roaring = "0.11.4"
|
||||
rstest = "0.26.1"
|
||||
serde = { version = "^1" }
|
||||
serde_json = { version = "1" }
|
||||
semver = "1.0"
|
||||
serial_test = "3"
|
||||
snafu = "0.9"
|
||||
lindera = { version = "3.0.7" }
|
||||
tempfile = "3"
|
||||
test-log = { version = "0.2.15" }
|
||||
tokio = { version = "1.23", features = [
|
||||
"rt-multi-thread",
|
||||
"macros",
|
||||
"fs",
|
||||
"sync",
|
||||
] }
|
||||
tokio-stream = "0.1.14"
|
||||
tokio-util = { version = "0.7.16" }
|
||||
tower = "0.5"
|
||||
tower-http = "0.5"
|
||||
tracing = "0.1"
|
||||
tracing-mock = { version = "=0.1.0-beta.3" }
|
||||
twox-hash = "2.0"
|
||||
url = "2.5.7"
|
||||
uuid = { version = "1.2", features = ["v4", "serde"] }
|
||||
wiremock = "0.6"
|
||||
pretty_assertions = "1.4.0"
|
||||
|
||||
[profile.bench]
|
||||
opt-level = 3
|
||||
debug = true
|
||||
strip = false
|
||||
|
||||
[profile.ci]
|
||||
debug = "line-tables-only"
|
||||
inherits = "dev"
|
||||
incremental = false
|
||||
|
||||
# This rule applies to every package except workspace members (dependencies
|
||||
# such as `arrow` and `tokio`). It disables debug info and related features on
|
||||
# dependencies so their binaries stay smaller, improving cache reuse.
|
||||
[profile.ci.package."*"]
|
||||
debug = false
|
||||
debug-assertions = false
|
||||
strip = "debuginfo"
|
||||
incremental = false
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(coverage,coverage_nightly)'] }
|
||||
unsafe_op_in_unsafe_fn = "allow"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
all = { level = "deny", priority = -1 }
|
||||
style = { level = "deny", priority = -1 }
|
||||
cargo = { level = "deny", priority = -1 }
|
||||
fallible_impl_from = "deny"
|
||||
manual_let_else = "deny"
|
||||
redundant_pub_crate = "deny"
|
||||
string_add_assign = "deny"
|
||||
string_add = "deny"
|
||||
string_lit_as_bytes = "deny"
|
||||
use_self = "deny"
|
||||
dbg_macro = "deny"
|
||||
trait_duplication_in_bounds = "deny"
|
||||
redundant_clone = "deny"
|
||||
# We should always use log instead of println
|
||||
print_stdout = "deny"
|
||||
print_stderr = "deny"
|
||||
# not too much we can do to avoid multiple crate versions
|
||||
multiple-crate-versions = "allow"
|
||||
# We use Vec<Range<u64>> in a lot of places and it is very common to use a single range in the vec.
|
||||
single_range_in_vec_init = "allow"
|
||||
large_futures = "deny"
|
||||
disallowed_macros = "deny"
|
||||
@@ -1,22 +0,0 @@
|
||||
# LanceDB patch provenance
|
||||
|
||||
This artifact vendors the Lance 11.0.0-beta.3 Rust workspace from Lance commit
|
||||
`f7d475539cefbd140cc46a828f3d843e68cd10f1`. The complete workspace keeps all mutually coupled
|
||||
Lance crates on one Cargo source identity when `lancedb` consumes the pinned artifact commit.
|
||||
|
||||
The local patch makes AWS credential-family merging atomic before backend selection and teaches
|
||||
the built-in OpenDAL S3 signer to resolve credential-only storage options at request time. This
|
||||
keeps long-lived multipart uploads refreshable without rebuilding a store or changing its outer
|
||||
metadata. Keeping the change inside `AwsStoreProvider` leaves arbitrary registry providers and
|
||||
their complete `ObjectStore` results untouched. Remove this patch when the same behavior is
|
||||
available in the pinned Lance release.
|
||||
|
||||
The LanceDB workspace pins every coupled Lance crate to the immutable repository commit containing
|
||||
this artifact. That durable source survives transitive Git consumption instead of relying on a
|
||||
root `[patch]`, which Cargo ignores when LanceDB itself is used as a dependency.
|
||||
|
||||
The artifact also contains Apache OpenDAL 0.58.1's `opendal` and `opendal-service-s3` crates. The
|
||||
only OpenDAL change adds a direct custom-provider hook alongside its existing credential-chain
|
||||
hook. Lance uses the direct hook so a selected dynamic authority can propagate refresh and
|
||||
validation errors; static or ambient credentials are considered only when that authority returns
|
||||
`Ok(None)`. Remove these copies when upstream OpenDAL exposes an equivalent hook.
|
||||
@@ -1,255 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
This project includes code from Ritchie Vink's Polars project, which is licensed
|
||||
under the MIT license:
|
||||
|
||||
Copyright (c) 2020 Ritchie Vink
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
https://github.com/pola-rs/polars/blob/main/LICENSE
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
This project includes code adapted from the quickwit-oss/bitpacking crate, which
|
||||
is licensed under the MIT license:
|
||||
|
||||
Copyright (c) 2016 Paul Masurel
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
https://github.com/quickwit-oss/bitpacking/blob/main/LICENSE
|
||||
@@ -1,247 +0,0 @@
|
||||
<div align="center">
|
||||
<p align="center">
|
||||
|
||||
<img width="257" alt="Lance Logo" src="https://user-images.githubusercontent.com/917119/199353423-d3e202f7-0269-411d-8ff2-e747e419e492.png">
|
||||
|
||||
**The Open Lakehouse Format for Multimodal AI**<br/>
|
||||
**High-performance vector search, full-text search, random access, and feature engineering capabilities for the lakehouse.**<br/>
|
||||
**Compatible with Pandas, DuckDB, Polars, PyArrow, Ray, Spark, and more integrations on the way.**
|
||||
|
||||
<a href="https://lance.org">Documentation</a> •
|
||||
<a href="https://lance.org/community">Community</a> •
|
||||
<a href="https://discord.gg/lance">Discord</a> •
|
||||
<a href="https://groups.google.com/a/lance.org/g/dev">Mailing List</a>
|
||||
|
||||
[CI]: https://github.com/lance-format/lance/actions/workflows/rust.yml
|
||||
[CI Badge]: https://github.com/lance-format/lance/actions/workflows/rust.yml/badge.svg
|
||||
[Docs]: https://lance.org
|
||||
[Docs Badge]: https://img.shields.io/badge/docs-passing-brightgreen
|
||||
[crates.io]: https://crates.io/crates/lance
|
||||
[crates.io badge]: https://img.shields.io/crates/v/lance.svg
|
||||
[Python versions]: https://pypi.org/project/pylance/
|
||||
[Python versions badge]: https://img.shields.io/pypi/pyversions/pylance
|
||||
|
||||
[![CI Badge]][CI]
|
||||
[![Docs Badge]][Docs]
|
||||
[![crates.io badge]][crates.io]
|
||||
[![Python versions badge]][Python versions]
|
||||
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
Lance is an open lakehouse format for multimodal AI. It contains a file format, table format, and catalog spec that allows you to build a complete lakehouse on top of object storage to power your AI workflows. Lance is perfect for:
|
||||
|
||||
1. Building search engines and feature stores with hybrid search capabilities.
|
||||
2. Large-scale ML training requiring high performance IO and random access.
|
||||
3. Storing, querying, and managing multimodal data including images, videos, audio, text, and embeddings.
|
||||
|
||||
The key features of Lance include:
|
||||
|
||||
* **Expressive hybrid search:** Combine vector similarity search, full-text search (BM25), and SQL analytics on the same dataset with accelerated secondary indices.
|
||||
|
||||
* **Lightning-fast random access:** 100x faster than Parquet or Iceberg for random access without sacrificing scan performance.
|
||||
|
||||
* **Native multimodal data support:** Store images, videos, audio, text, and embeddings in a single unified format with efficient blob encoding and lazy loading.
|
||||
|
||||
* **Data evolution:** Efficiently add columns with backfilled values without full table rewrites, perfect for ML feature engineering.
|
||||
|
||||
* **Zero-copy versioning:** Automatic versioning with ACID transactions, time travel, tags, and branches—no extra infrastructure needed.
|
||||
|
||||
* **Rich ecosystem integrations:** Apache Arrow, Pandas, Polars, DuckDB, Apache Spark, Ray, Trino, Apache Flink, and open catalogs (Apache Polaris, Unity Catalog, Apache Gravitino).
|
||||
|
||||
For more details, see the full [Lance format specification](https://lance.org/format).
|
||||
|
||||
> [!TIP]
|
||||
> Lance is in active development and we welcome contributions. Please see our [contributing guide](https://lance.org/community/contributing/) for more information.
|
||||
|
||||
## File format stability
|
||||
|
||||
Lance releases frequently because the SDKs, integrations, and performance work are moving quickly. This does not mean the Lance file format changes incompatibly in every release. The Lance file format is identified by the `data_storage_version` stored in each dataset, and stable storage versions are a long-term compatibility contract.
|
||||
|
||||
* Once a dataset is written with a stable `data_storage_version`, future Lance releases will continue to support reading that storage version.
|
||||
* SDK and API compatibility is separate from file format compatibility. SDK/API changes follow semantic versioning and are documented in the [migration guide](https://lance.org/guide/migration/).
|
||||
* Older Lance releases may not understand file format versions introduced later. If you run mixed Lance versions, pin `data_storage_version` for deterministic writes.
|
||||
* The `next` file format alias is unstable and should only be used for experimentation, never for production data.
|
||||
|
||||
For production, write data with a stable `data_storage_version`. See the [format versioning guide](https://lance.org/format/file/versioning/) for the current compatibility matrix.
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Installation**
|
||||
|
||||
```shell
|
||||
pip install pylance
|
||||
```
|
||||
|
||||
To install a preview release:
|
||||
|
||||
```shell
|
||||
pip install --pre --extra-index-url https://pypi.fury.io/lance-format pylance
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> Preview releases are released more often than full releases and contain the
|
||||
> latest features and bug fixes. They receive the same level of testing as full releases.
|
||||
> We guarantee they will remain published and available for download for at
|
||||
> least 6 months. When you want to pin to a specific version, prefer a stable release.
|
||||
|
||||
**Converting to Lance**
|
||||
|
||||
```python
|
||||
import lance
|
||||
|
||||
import pandas as pd
|
||||
import pyarrow as pa
|
||||
import pyarrow.dataset
|
||||
|
||||
df = pd.DataFrame({"a": [5], "b": [10]})
|
||||
uri = "/tmp/test.parquet"
|
||||
tbl = pa.Table.from_pandas(df)
|
||||
pa.dataset.write_dataset(tbl, uri, format='parquet')
|
||||
|
||||
parquet = pa.dataset.dataset(uri, format='parquet')
|
||||
lance.write_dataset(parquet, "/tmp/test.lance")
|
||||
```
|
||||
|
||||
**Reading Lance data**
|
||||
```python
|
||||
dataset = lance.dataset("/tmp/test.lance")
|
||||
assert isinstance(dataset, pa.dataset.Dataset)
|
||||
```
|
||||
|
||||
**Pandas**
|
||||
```python
|
||||
df = dataset.to_table().to_pandas()
|
||||
df
|
||||
```
|
||||
|
||||
**DuckDB**
|
||||
```python
|
||||
import duckdb
|
||||
|
||||
# If this segfaults, make sure you have duckdb v0.7+ installed
|
||||
duckdb.query("SELECT * FROM dataset LIMIT 10").to_df()
|
||||
```
|
||||
|
||||
**Vector search**
|
||||
|
||||
Download the sift1m subset
|
||||
|
||||
```shell
|
||||
wget ftp://ftp.irisa.fr/local/texmex/corpus/sift.tar.gz
|
||||
tar -xzf sift.tar.gz
|
||||
```
|
||||
|
||||
Convert it to Lance
|
||||
|
||||
```python
|
||||
import lance
|
||||
from lance.vector import vec_to_table
|
||||
import numpy as np
|
||||
import struct
|
||||
|
||||
nvecs = 1000000
|
||||
ndims = 128
|
||||
with open("sift/sift_base.fvecs", mode="rb") as fobj:
|
||||
buf = fobj.read()
|
||||
data = np.array(struct.unpack("<128000000f", buf[4 : 4 + 4 * nvecs * ndims])).reshape((nvecs, ndims))
|
||||
dd = dict(zip(range(nvecs), data))
|
||||
|
||||
table = vec_to_table(dd)
|
||||
uri = "vec_data.lance"
|
||||
sift1m = lance.write_dataset(table, uri, max_rows_per_group=8192, max_rows_per_file=1024*1024)
|
||||
```
|
||||
|
||||
Build the index
|
||||
|
||||
```python
|
||||
sift1m.create_index("vector",
|
||||
index_type="IVF_PQ",
|
||||
num_partitions=256, # IVF
|
||||
num_sub_vectors=16) # PQ
|
||||
```
|
||||
|
||||
Search the dataset
|
||||
|
||||
```python
|
||||
# Get top 10 similar vectors
|
||||
import duckdb
|
||||
|
||||
dataset = lance.dataset(uri)
|
||||
|
||||
# Sample 100 query vectors. If this segfaults, make sure you have duckdb v0.7+ installed
|
||||
sample = duckdb.query("SELECT vector FROM dataset USING SAMPLE 100").to_df()
|
||||
query_vectors = np.array([np.array(x) for x in sample.vector])
|
||||
|
||||
# Get nearest neighbors for all of them
|
||||
rs = [dataset.to_table(nearest={"column": "vector", "k": 10, "q": q})
|
||||
for q in query_vectors]
|
||||
```
|
||||
|
||||
## Directory structure
|
||||
|
||||
| Directory | Description |
|
||||
|--------------------|--------------------------|
|
||||
| [rust](./rust) | Core Rust implementation |
|
||||
| [python](./python) | Python bindings (PyO3) |
|
||||
| [java](./java) | Java bindings (JNI) |
|
||||
| [docs](./docs) | Documentation source |
|
||||
|
||||
## Benchmarks
|
||||
|
||||
### Vector search
|
||||
|
||||
We used the SIFT dataset to benchmark our results with 1M vectors of 128D
|
||||
|
||||
1. For 100 randomly sampled query vectors, we get <1ms average response time (on a 2023 m2 MacBook Air)
|
||||
|
||||

|
||||
|
||||
2. ANNs are always a trade-off between recall and performance
|
||||
|
||||

|
||||
|
||||
### Vs. parquet
|
||||
|
||||
We create a Lance dataset using the Oxford Pet dataset to do some preliminary performance testing of Lance as compared to Parquet and raw image/XMLs. For analytics queries, Lance is 50-100x better than reading the raw metadata. For batched random access, Lance is 100x better than both parquet and raw files.
|
||||
|
||||

|
||||
|
||||
## Why Lance for AI/ML workflows?
|
||||
|
||||
The machine learning development cycle involves multiple stages:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Collection] --> B[Exploration];
|
||||
B --> C[Analytics];
|
||||
C --> D[Feature Engineer];
|
||||
D --> E[Training];
|
||||
E --> F[Evaluation];
|
||||
F --> C;
|
||||
E --> G[Deployment];
|
||||
G --> H[Monitoring];
|
||||
H --> A;
|
||||
```
|
||||
|
||||
Traditional lakehouse formats were designed for SQL analytics and struggle with AI/ML workloads that require:
|
||||
- **Vector search** for similarity and semantic retrieval
|
||||
- **Fast random access** for sampling and interactive exploration
|
||||
- **Multimodal data** storage (images, videos, audio alongside embeddings)
|
||||
- **Data evolution** for feature engineering without full table rewrites
|
||||
- **Hybrid search** combining vectors, full-text, and SQL predicates
|
||||
|
||||
While existing formats (Parquet, Iceberg, Delta Lake) excel at SQL analytics, they require additional specialized systems for AI capabilities. Lance brings these AI-first features directly into the lakehouse format.
|
||||
|
||||
A comparison of different formats across ML development stages:
|
||||
|
||||
| | Lance | Parquet & ORC | JSON & XML | TFRecord | Database | Warehouse |
|
||||
|---------------------|-------|---------------|------------|----------|----------|-----------|
|
||||
| Analytics | Fast | Fast | Slow | Slow | Decent | Fast |
|
||||
| Feature Engineering | Fast | Fast | Decent | Slow | Decent | Good |
|
||||
| Training | Fast | Decent | Slow | Fast | N/A | N/A |
|
||||
| Exploration | Fast | Slow | Fast | Slow | Fast | Decent |
|
||||
| Infra Support | Rich | Rich | Decent | Limited | Rich | Rich |
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# Protobuf Guidelines
|
||||
|
||||
Also see [root AGENTS.md](../AGENTS.md) for cross-language standards.
|
||||
|
||||
## Compatibility
|
||||
|
||||
- Protobuf schemas that are part of a stable file format or any other stable persisted contract must remain backwards compatible. Never reuse or change their existing field numbers.
|
||||
- Protobuf schemas used exclusively by an unstable file format follow the root file-format stability contract: do not preserve compatibility with prior unstable revisions. Before making a breaking protobuf change, verify that the schema is not shared with a stable format or another persisted contract.
|
||||
|
||||
## Schema Design
|
||||
|
||||
- Use `optional` when you need to distinguish "not set" from "zero value" — `optional` enables presence tracking (`has_*` methods) and maps to `Option<T>` in Rust. Bare proto3 fields have no presence semantics: they always hold a value (defaulting to zero), so you cannot tell if the sender explicitly set them.
|
||||
- Use structured message types (e.g., `BasePath`) instead of plain scalars, and scope fields to operation-specific messages (e.g., `InsertTransaction`) rather than generic top-level ones.
|
||||
- Don't duplicate data across messages — store each fact once and derive relationships. Prefer parallel sequences over maps when keys already exist in another field.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Document the semantic meaning of both present and absent states for `optional` fields — explain when each case applies.
|
||||
- Use precise domain terminology in field descriptions — avoid ambiguous abbreviations or terms that collide with domain concepts.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -1,72 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.pb;
|
||||
|
||||
import "table_identifier.proto";
|
||||
import "table.proto";
|
||||
import "index.proto";
|
||||
|
||||
// Query-time approximation mode for vector search.
|
||||
//
|
||||
// This currently only affects RQ-quantized vector indexes, such as IVF_RQ.
|
||||
// Other index types ignore this setting.
|
||||
enum VectorApproxMode {
|
||||
// Use all RQ bits for query-time scoring with u8-quantized lookup tables.
|
||||
Normal = 0;
|
||||
// Use only one RQ bit for query-time scoring, even for multi-bit indexes.
|
||||
Fast = 1;
|
||||
// Use all RQ bits for query-time scoring with u16-quantized lookup tables
|
||||
// to reduce estimator quantization error.
|
||||
Accurate = 2;
|
||||
}
|
||||
|
||||
// Serialized vector query parameters.
|
||||
message VectorQueryProto {
|
||||
// Query vector as Arrow IPC bytes (supports Float16, Float32, Float64, UInt8, etc.)
|
||||
bytes query_vector_arrow_ipc = 1;
|
||||
string column = 2;
|
||||
uint32 k = 3;
|
||||
optional float lower_bound = 4;
|
||||
optional float upper_bound = 5;
|
||||
optional uint32 minimum_nprobes = 6;
|
||||
optional uint32 maximum_nprobes = 7;
|
||||
optional uint32 ef = 8;
|
||||
optional uint32 refine_factor = 9;
|
||||
// Distance metric type. Absent means None (use the index's default metric).
|
||||
optional lance.index.pb.VectorMetricType metric_type = 10;
|
||||
bool use_index = 11;
|
||||
optional float dist_q_c = 12;
|
||||
optional int32 query_parallelism = 13;
|
||||
// Query-time approximation mode. Currently only affects RQ-quantized vector
|
||||
// indexes, such as IVF_RQ. Other index types ignore this setting.
|
||||
VectorApproxMode approx_mode = 14;
|
||||
}
|
||||
|
||||
// Serializable form of ANNIvfSubIndexExec — the IVF sub-index search node.
|
||||
//
|
||||
// The prefilter child ExecutionPlan is serialized by DataFusion's codec
|
||||
// automatically via children() / with_new_children(). The prefilter_type
|
||||
// field tells the decoder which PreFilterSource variant to use when
|
||||
// reconstructing from the deserialized child inputs.
|
||||
message ANNIvfSubIndexExecProto {
|
||||
enum PreFilterType {
|
||||
NONE = 0;
|
||||
FILTERED_ROW_IDS = 1;
|
||||
SCALAR_INDEX_QUERY = 2;
|
||||
}
|
||||
|
||||
VectorQueryProto query = 1;
|
||||
lance.datafusion.TableIdentifier table = 2;
|
||||
repeated lance.table.IndexMetadata indices = 3;
|
||||
PreFilterType prefilter_type = 4;
|
||||
}
|
||||
|
||||
// Serializable form of ANNIvfPartitionExec — the IVF centroid routing node.
|
||||
message ANNIvfPartitionExecProto {
|
||||
VectorQueryProto query = 1;
|
||||
lance.datafusion.TableIdentifier table = 2;
|
||||
repeated string index_uuids = 3;
|
||||
}
|
||||
@@ -1,347 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.encodings;
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
// This file contains a specification for encodings that can be used
|
||||
// to store and load Arrow data into a Lance file for the 2.0 format. It
|
||||
// has been superseded by encodings21.proto which is used for the 2.1 format.
|
||||
//
|
||||
// # Types
|
||||
//
|
||||
// This file assumes the user wants to load data into Arrow arrays and
|
||||
// explains how to map Arrow arrays into Lance files. Encodings are divided
|
||||
// into "array encoding" (which maps to an Arrow array and may contain multiple
|
||||
// buffers) and "buffer encoding" (which encodes a single buffer of data).
|
||||
//
|
||||
// # Encoding Tree
|
||||
//
|
||||
// Most encodings are layered on top of each other. These form a tree of
|
||||
// encodings with a single root node. To encode an array you will typically
|
||||
// start with the root node and then take the output from that root encoding
|
||||
// and feed it into child encodings. The decoding process works in reverse.
|
||||
//
|
||||
// # Multi-column Encodings
|
||||
//
|
||||
// Some Arrow arrays will map to more than one column of Lance data. For
|
||||
// example, struct arrays and list arrays. This file only contains encodings
|
||||
// for a single column. However, it does describe how multi-column arrays can
|
||||
// be encoded.
|
||||
|
||||
// A pointer to a buffer in a Lance file
|
||||
//
|
||||
// A writer can place a buffer in three different locations. The buffer
|
||||
// can go in the data page, in the column metadata, or in the file metadata.
|
||||
// The writer is free to choose whatever is most appropriate (for example, a dictionary
|
||||
// that is shared across all pages in a column will probably go in the column
|
||||
// metadata). This specification does not dictate where the buffer should go.
|
||||
message Buffer {
|
||||
// The index of the buffer in the collection of buffers
|
||||
uint32 buffer_index = 1;
|
||||
// The collection holding the buffer
|
||||
enum BufferType {
|
||||
// The buffer is stored in the data page itself
|
||||
page = 0;
|
||||
// The buffer is stored in the column metadata
|
||||
column = 1;
|
||||
// The buffer is stored in the file metadata
|
||||
file = 2;
|
||||
};
|
||||
BufferType buffer_type = 2;
|
||||
}
|
||||
|
||||
// An encoding that adds nullability to another array encoding
|
||||
//
|
||||
// This can wrap any array encoding and add nullability information
|
||||
message Nullable {
|
||||
message NoNull {
|
||||
ArrayEncoding values = 1;
|
||||
}
|
||||
message AllNull {}
|
||||
message SomeNull {
|
||||
ArrayEncoding validity = 1;
|
||||
ArrayEncoding values = 2;
|
||||
}
|
||||
oneof nullability {
|
||||
// The array has no nulls and there is a single buffer needed
|
||||
NoNull no_nulls = 1;
|
||||
// The array may have nulls and we need two buffers
|
||||
SomeNull some_nulls = 2;
|
||||
// All values are null (no buffers needed)
|
||||
AllNull all_nulls = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// An array encoding for variable-length list fields
|
||||
message List {
|
||||
// An array containing the offsets into an items array.
|
||||
//
|
||||
// This array will have num_rows items and will never
|
||||
// have nulls.
|
||||
//
|
||||
// If the list at index i is not null then offsets[i] will
|
||||
// contain `base + len(list)` where `base` is defined as:
|
||||
// i == 0: 0
|
||||
// i > 0: (offsets[i-1] % null_offset_adjustment)
|
||||
//
|
||||
// To help understand we can consider the following example list:
|
||||
// [ [A, B], null, [], [C, D, E] ]
|
||||
//
|
||||
// The offsets will be [2, ?, 2, 5]
|
||||
//
|
||||
// If the incoming list at index i IS null then offsets[i] will
|
||||
// contain `base + len(list) + null_offset_adjustment` where `base`
|
||||
// is defined the same as above.
|
||||
//
|
||||
// To complete the above example let's assume that `null_offset_adjustment`
|
||||
// is 7. Then the offsets will be [2, 9, 2, 5]
|
||||
//
|
||||
// If there are no nulls then the offsets we write here are exactly the
|
||||
// same as the offsets in an Arrow list array (except we omit the leading
|
||||
// 0 which is redundant)
|
||||
//
|
||||
// The reason we do this is so that reading a single list at index i only
|
||||
// requires us to load the indices at i and i-1.
|
||||
//
|
||||
// If the offset at index i is greater than `null_offset_adjustment``
|
||||
// then the list at index i is null.
|
||||
//
|
||||
// Otherwise the length of the list is `offsets[i] - base` where
|
||||
// base is defined the same as above.
|
||||
//
|
||||
// Let's consider our example offsets: [2, 9, 2, 5]
|
||||
//
|
||||
// We can take any range of lists and determine how many list items are
|
||||
// referenced by the sublist.
|
||||
//
|
||||
// 0..3: [_, 5] -> items 0..5 (base = 0* and end is 5)
|
||||
// 0..2: [_, 2] -> items 0..2 (base = 0* and end is 2)
|
||||
// 0..1: [_, 9] -> items 0..2 (base = 0* and end is 9 % 7)
|
||||
// 1..3: [2, 5] -> items 2..5 (base = 2 and end is 5)
|
||||
// 1..2: [2, 2] -> items 2..2 (base = 2 and end is 2)
|
||||
// 2..3: [9, 5] -> items 2..5 (base = 9 % 7 and end is 5)
|
||||
//
|
||||
// * When the start of our range is the 0th item the base is always 0 and we only
|
||||
// need to load a single index from disk to determine the range.
|
||||
//
|
||||
// The data type of the offsets array is flexible and does not need
|
||||
// to match the data type of the destination array. Please note that the offsets
|
||||
// array is very likely to be efficiently encoded by bit packing deltas.
|
||||
ArrayEncoding offsets = 1;
|
||||
// If a list is null then we add this value to the offset
|
||||
//
|
||||
// This value must be greater than the length of the items so that
|
||||
// (offset + null_offset_adjustment) is never used by a non-null list.
|
||||
//
|
||||
// Note that this value cannot be equal to the length of the items
|
||||
// because then a page with a single list would store [ X ] and we
|
||||
// couldn't know if that is a null list or a list with X items.
|
||||
//
|
||||
// Therefore, the best choice for this value is 1 + # of items.
|
||||
// Choosing this will maximize the bit packing that we can apply to the offsets.
|
||||
uint64 null_offset_adjustment = 2;
|
||||
// How many items are referenced by these offsets. This is needed in
|
||||
// order to determine which items pages map to this offsets page.
|
||||
uint64 num_items = 3;
|
||||
}
|
||||
|
||||
// An array encoding for fixed-size list fields
|
||||
message FixedSizeList {
|
||||
/// The number of items in each list
|
||||
uint32 dimension = 1;
|
||||
/// True if the list is nullable
|
||||
bool has_validity = 3;
|
||||
/// The items in the list
|
||||
ArrayEncoding items = 2;
|
||||
}
|
||||
|
||||
message Compression {
|
||||
string scheme = 1;
|
||||
optional int32 level = 2;
|
||||
}
|
||||
|
||||
// Fixed width items placed contiguously in a buffer
|
||||
message Flat {
|
||||
// the number of bits per value, must be greater than 0, does
|
||||
// not need to be a multiple of 8
|
||||
uint64 bits_per_value = 1;
|
||||
// the buffer of values
|
||||
Buffer buffer = 2;
|
||||
// The Compression message can specify the compression scheme (e.g. zstd) and any
|
||||
// other information that is needed for decompression.
|
||||
//
|
||||
// If this array is compressed then the bits_per_value refers to the uncompressed
|
||||
// data.
|
||||
Compression compression = 3;
|
||||
}
|
||||
|
||||
// Compression algorithm where all values have a constant value
|
||||
message Constant {
|
||||
// The value (TODO: define encoding for literals?)
|
||||
bytes value = 1;
|
||||
}
|
||||
|
||||
// Items are bitpacked in a buffer
|
||||
message Bitpacked {
|
||||
// the number of bits used for a value in the buffer
|
||||
uint64 compressed_bits_per_value = 1;
|
||||
|
||||
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
||||
uint64 uncompressed_bits_per_value = 2;
|
||||
|
||||
// The items in the list
|
||||
Buffer buffer = 3;
|
||||
|
||||
// Whether or not a sign bit is included in the bitpacked value
|
||||
bool signed = 4;
|
||||
}
|
||||
|
||||
// Items are bitpacked in a buffer
|
||||
message BitpackedForNonNeg {
|
||||
// the number of bits used for a value in the buffer
|
||||
uint64 compressed_bits_per_value = 1;
|
||||
|
||||
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
||||
uint64 uncompressed_bits_per_value = 2;
|
||||
|
||||
// The items in the list
|
||||
Buffer buffer = 3;
|
||||
}
|
||||
|
||||
// Opaque bitpacking variant where the bits per value are stored inline in the chunks themselves
|
||||
message InlineBitpacking {
|
||||
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
||||
uint64 uncompressed_bits_per_value = 2;
|
||||
}
|
||||
|
||||
// Transparent bitpacking variant where the number of bits per value is fixed through the whole buffer
|
||||
message OutOfLineBitpacking {
|
||||
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
||||
uint64 uncompressed_bits_per_value = 2;
|
||||
// The number of compressed bits per value, fixed across the entire buffer
|
||||
uint64 compressed_bits_per_value = 3;
|
||||
}
|
||||
|
||||
// An array encoding for shredded structs that will never be null
|
||||
//
|
||||
// There is no actual data in this column.
|
||||
//
|
||||
// TODO: Struct validity bitmaps will be placed here.
|
||||
message SimpleStruct {}
|
||||
|
||||
// An array encoding for binary fields
|
||||
message Binary {
|
||||
ArrayEncoding indices = 1;
|
||||
ArrayEncoding bytes = 2;
|
||||
uint64 null_adjustment = 3;
|
||||
}
|
||||
|
||||
message Variable {
|
||||
uint32 bits_per_offset = 1;
|
||||
}
|
||||
|
||||
message Fsst {
|
||||
ArrayEncoding binary = 1;
|
||||
bytes symbol_table = 2;
|
||||
}
|
||||
|
||||
// An array encoding for dictionary-encoded fields
|
||||
message Dictionary {
|
||||
ArrayEncoding indices = 1;
|
||||
ArrayEncoding items = 2;
|
||||
uint32 num_dictionary_items = 3;
|
||||
}
|
||||
|
||||
message PackedStruct {
|
||||
repeated ArrayEncoding inner = 1;
|
||||
Buffer buffer = 2;
|
||||
}
|
||||
|
||||
message PackedStructFixedWidthMiniBlock {
|
||||
ArrayEncoding Flat = 1;
|
||||
repeated uint32 bits_per_values = 2;
|
||||
}
|
||||
|
||||
message FixedSizeBinary {
|
||||
ArrayEncoding bytes = 1;
|
||||
uint32 byte_width = 2;
|
||||
}
|
||||
|
||||
message Block {
|
||||
string scheme = 1;
|
||||
}
|
||||
|
||||
// Run-Length Encoding for miniblock format
|
||||
message Rle {
|
||||
// Number of bits per value (8, 16, 32, 64, or 128)
|
||||
uint64 bits_per_value = 1;
|
||||
}
|
||||
|
||||
// Byte Stream Split encoding for floating point values
|
||||
message ByteStreamSplit {
|
||||
// Number of bits per value (32 for float, 64 for double)
|
||||
uint64 bits_per_value = 1;
|
||||
}
|
||||
|
||||
// General miniblock encoding - wraps another miniblock encoding with compression
|
||||
message GeneralMiniBlock {
|
||||
// The inner miniblock encoding (e.g., Rle, Bitpacked, etc.)
|
||||
ArrayEncoding inner = 1;
|
||||
// The compression scheme to apply to the miniblock buffers
|
||||
Compression compression = 2;
|
||||
}
|
||||
|
||||
// Encodings that decode into an Arrow array
|
||||
message ArrayEncoding {
|
||||
oneof array_encoding {
|
||||
Flat flat = 1;
|
||||
Nullable nullable = 2;
|
||||
FixedSizeList fixed_size_list = 3;
|
||||
List list = 4;
|
||||
SimpleStruct struct = 5;
|
||||
Binary binary = 6;
|
||||
Dictionary dictionary = 7;
|
||||
Fsst fsst = 8;
|
||||
PackedStruct packed_struct = 9;
|
||||
Bitpacked bitpacked = 10;
|
||||
FixedSizeBinary fixed_size_binary = 11;
|
||||
BitpackedForNonNeg bitpacked_for_non_neg = 12;
|
||||
Constant constant = 13;
|
||||
InlineBitpacking inline_bitpacking = 14;
|
||||
OutOfLineBitpacking out_of_line_bitpacking = 15;
|
||||
Variable variable = 16;
|
||||
PackedStructFixedWidthMiniBlock packed_struct_fixed_width_mini_block = 17;
|
||||
Block block = 18;
|
||||
Rle rle = 19;
|
||||
GeneralMiniBlock general_mini_block = 20;
|
||||
ByteStreamSplit byte_stream_split = 21;
|
||||
}
|
||||
}
|
||||
|
||||
// Wraps a column with a zone map index that can be used
|
||||
// to apply pushdown filters
|
||||
message ZoneIndex {
|
||||
uint32 rows_per_zone = 1;
|
||||
Buffer zone_map_buffer = 2;
|
||||
ColumnEncoding inner = 3;
|
||||
}
|
||||
|
||||
// Marks a column as blob data. It will contain a packed struct
|
||||
// with fields position and size (u64)
|
||||
message Blob {
|
||||
ColumnEncoding inner = 1;
|
||||
}
|
||||
|
||||
// Encodings that describe a column of values
|
||||
message ColumnEncoding {
|
||||
oneof column_encoding {
|
||||
// No special encoding, just column values
|
||||
google.protobuf.Empty values = 1;
|
||||
ZoneIndex zone_index = 2;
|
||||
Blob blob = 3;
|
||||
}
|
||||
}
|
||||
@@ -1,635 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.encodings21;
|
||||
|
||||
// This file contains a specification for encodings that can be used
|
||||
// to store and load Arrow data into a Lance file for the 2.1 format.
|
||||
//
|
||||
// # Types
|
||||
//
|
||||
// This file assumes the user wants to load data into Arrow arrays and
|
||||
// explains how to map Arrow arrays into Lance files. Encodings are divided
|
||||
// into "structural encodings" (which are used to encode the structure of the
|
||||
// data such as any list or struct layers) and "compressive encodings" (which
|
||||
// are used to compress the actual data values).
|
||||
//
|
||||
// # Standardized Interpretation of Counting Terms
|
||||
//
|
||||
// When working with 2.1 encodings we have a number of different "counting terms" and it can be
|
||||
// difficult to understand what we mean when we are talking about a "number of values". Here is
|
||||
// a standard interpretation of these terms:
|
||||
//
|
||||
// To understand these definitions consider a data type FIXED_SIZE_LIST<LIST<INT32>>.
|
||||
//
|
||||
// A "value" is an abstract term when we aren't being specific.
|
||||
//
|
||||
// - num_rows: This is the highest level counting term. A single row includes everything in the
|
||||
// fixed size list. This is what the user asks for when they asks for a range of rows.
|
||||
// - num_elements: The number of elements is the number of rows multiplied by the dimension of any
|
||||
// fixed size list wrappers. This is what you get when you flatten the FSL layer and
|
||||
// is the starting point for structural encoding. Note that an element can be a list
|
||||
// value or a single primitive value.
|
||||
// - num_items: The number of items is the number of values in the repetition and definition vectors
|
||||
// after everything has been flattened.
|
||||
// - num_visible_items: The number of visible items is the number of items after invisible items
|
||||
// have been removed. Invisible items are rep/def levels that don't correspond to an
|
||||
// actual value.
|
||||
|
||||
|
||||
// # Structural Encodings
|
||||
//
|
||||
// The following message are used to describe the structural encoding of the
|
||||
// data. In this document, we refer to these structural encodings as layouts.
|
||||
|
||||
// Repetition and definition levels are described in more detail elsewhere. As we peel through
|
||||
// the structure of an array we will encounter layers of struct and list. Each of these layers
|
||||
// potentially adds a new level to the repetition and definition levels. This message describes
|
||||
// the meaning of each layer.
|
||||
enum RepDefLayer {
|
||||
// Should never be used, included for debugging purporses and general protobuf best practice
|
||||
REPDEF_UNSPECIFIED = 0;
|
||||
// All values are valid (can be primitive or struct)
|
||||
REPDEF_ALL_VALID_ITEM = 1;
|
||||
// All list values are valid
|
||||
REPDEF_ALL_VALID_LIST = 2;
|
||||
// There are one or more null items (can be primitive or struct)
|
||||
REPDEF_NULLABLE_ITEM = 3;
|
||||
// A list layer with null lists but no empty lists
|
||||
REPDEF_NULLABLE_LIST = 4;
|
||||
// A list layer with empty lists but no null lists
|
||||
REPDEF_EMPTYABLE_LIST = 5;
|
||||
// A list layer with both empty lists and null lists
|
||||
REPDEF_NULL_AND_EMPTY_LIST = 6;
|
||||
}
|
||||
|
||||
// A layout used for pages where the data is small
|
||||
//
|
||||
// In this case we can fit many values into a single disk sector and transposing buffers is
|
||||
// expensive. As a result, we do not transpose the buffers but compress the data into small
|
||||
// chunks (called mini blocks) which are roughly the size of a disk sector.
|
||||
//
|
||||
// The end result is a small amount of read amplification (since we must read an entire page
|
||||
// at a time) but we have more flexibility in compression and do less work per value when
|
||||
// compressing and decompressing in bulk.
|
||||
message MiniBlockLayout {
|
||||
// Description of the compression of repetition levels (e.g. how many bits per rep)
|
||||
//
|
||||
// Optional, if there is no repetition then this field is not present
|
||||
CompressiveEncoding rep_compression = 1;
|
||||
// Description of the compression of definition levels (e.g. how many bits per def)
|
||||
//
|
||||
// Optional, if there is no definition then this field is not present
|
||||
CompressiveEncoding def_compression = 2;
|
||||
// Description of the compression of values
|
||||
CompressiveEncoding value_compression = 3;
|
||||
// Description of the compression of the dictionary data
|
||||
//
|
||||
// Optional, if there is no dictionary then this field is not present
|
||||
CompressiveEncoding dictionary = 4;
|
||||
// Number of items in the dictionary
|
||||
uint64 num_dictionary_items = 5;
|
||||
// The meaning of each repdef layer, used to interpret repdef buffers correctly
|
||||
repeated RepDefLayer layers = 6;
|
||||
// The number of buffers in each mini-block, this is determined by the compression and does
|
||||
// NOT include the repetition or definition buffers (the presence of these buffers can be determined
|
||||
// by looking at the rep_compression and def_compression fields)
|
||||
uint64 num_buffers = 7;
|
||||
// The depth of the repetition index.
|
||||
//
|
||||
// If there is repetition then the depth must be at least 1. If there are many layers
|
||||
// of repetition then deeper repetition indices will support deeper nested random access. For
|
||||
// example, given 5 layers of repetition then the repetition index depth must be at least
|
||||
// 3 to support access like `rows[50][17][3]`.
|
||||
//
|
||||
// We require `repetition_index_depth + 1` u64 values per mini-block to store the repetition
|
||||
// index if the `repetition_index_depth` is greater than 0. The +1 is because we need to store
|
||||
// the number of "leftover items" at the end of the chunk. Otherwise, we wouldn't have any way
|
||||
// to know if the final item in a chunk is valid or not.
|
||||
uint32 repetition_index_depth = 8;
|
||||
// The page already records how many rows are in the page. For mini-block we also need to know how
|
||||
// many "items" are in the page. A row and an item are the same thing unless the page has lists.
|
||||
uint64 num_items = 9;
|
||||
|
||||
// Since Lance 2.2, miniblocks have larger chunk sizes (>= 64KB)
|
||||
bool has_large_chunk = 10;
|
||||
}
|
||||
|
||||
// A layout used for pages where the data is large
|
||||
//
|
||||
// In this case the cost of transposing the data is relatively small (compared to the cost of writing the data)
|
||||
// and so we just zip the buffers together
|
||||
message FullZipLayout {
|
||||
// The number of bits of repetition info (0 if there is no repetition)
|
||||
uint32 bits_rep = 1;
|
||||
// The number of bits of definition info (0 if there is no definition)
|
||||
uint32 bits_def = 2;
|
||||
// The number of bits of value info
|
||||
//
|
||||
// Note: we use bits here (and not bytes) for consistency with other encodings. However, in practice,
|
||||
// there is never a reason to use a bits per value that is not a multiple of 8. The complexity is not
|
||||
// worth the small savings in space since this encoding is typically used with large values already.
|
||||
oneof details {
|
||||
// If this is a fixed width block then we need to have a fixed number of bits per value
|
||||
uint32 bits_per_value = 3;
|
||||
// If this is a variable width block then we need to have a fixed number of bits per offset
|
||||
uint32 bits_per_offset = 4;
|
||||
}
|
||||
// The number of items in the page
|
||||
uint32 num_items = 5;
|
||||
// The number of visible items in the page
|
||||
uint32 num_visible_items = 6;
|
||||
// Description of the compression of values
|
||||
CompressiveEncoding value_compression = 7;
|
||||
// The meaning of each repdef layer, used to interpret repdef buffers correctly
|
||||
repeated RepDefLayer layers = 8;
|
||||
}
|
||||
|
||||
// A layout used for sparse flat or nested pages where Arrow structure is represented directly
|
||||
// in layer-local slot domains instead of as dense repetition / definition events.
|
||||
//
|
||||
// Structural layers are ordered from outer-most to inner-most. Values remain mini-block
|
||||
// compressed and are split into independently readable chunks.
|
||||
message SparseLayout {
|
||||
// Description of the compression of values.
|
||||
CompressiveEncoding value_compression = 1;
|
||||
// Number of value buffers in each mini-block chunk. This does not include structural buffers.
|
||||
uint64 num_buffers = 2;
|
||||
// Number of entries in the equivalent dense repetition / definition stream. This equals
|
||||
// num_visible_items plus one structural placeholder for every list slot without children.
|
||||
// Null leaf slots count as visible items because they still occupy positions in Arrow's
|
||||
// leaf value buffer. For example, a nullable primitive with 100 slots, 30 of them null,
|
||||
// has num_items = num_visible_items = 100.
|
||||
uint64 num_items = 3;
|
||||
// Number of leaf value slots encoded in the value chunks, including null leaf slots.
|
||||
uint64 num_visible_items = 4;
|
||||
// If true, chunk-local value buffer sizes use u32. Otherwise they use u16.
|
||||
bool has_large_chunk = 5;
|
||||
// Structural layers ordered from outer-most to inner-most. This may be empty for a flat,
|
||||
// non-nullable leaf page whose scheduling domain equals num_visible_items.
|
||||
repeated SparseStructuralLayer structural_layers = 6;
|
||||
}
|
||||
|
||||
// A domain is a layer-local integer coordinate space [0, num_slots). A slot is one
|
||||
// element in that space. The outer-most domain is the page's top-level rows; each
|
||||
// layer's child domain is the next layer's parent domain, and the terminal child
|
||||
// domain contains num_visible_items leaf value slots.
|
||||
message SparseStructuralLayer {
|
||||
// Exactly one layer kind is required.
|
||||
oneof layer {
|
||||
SparseValidityLayer validity = 1;
|
||||
SparseListLayer list = 2;
|
||||
SparseFixedSizeListLayer fixed_size_list = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message SparseValidityLayer {
|
||||
// Number of nullable item or struct slots in this layer's parent and child domain.
|
||||
uint64 num_slots = 1;
|
||||
// Validity for the slots in this layer.
|
||||
SparseValiditySet validity = 2;
|
||||
}
|
||||
|
||||
message SparseListLayer {
|
||||
// Number of list, large-list, or map slots in this layer's parent domain.
|
||||
uint64 num_slots = 1;
|
||||
// Number of slots in this layer's child domain.
|
||||
uint64 num_child_slots = 2;
|
||||
// Non-empty parent slots. Valid parent slots absent from this set are empty lists.
|
||||
SparsePositionSet non_empty_positions = 3;
|
||||
// Positive child counts corresponding one-for-one with non_empty_positions.
|
||||
SparseCountSet counts = 4;
|
||||
// Validity for the parent slots in this layer.
|
||||
SparseValiditySet validity = 5;
|
||||
}
|
||||
|
||||
message SparseFixedSizeListLayer {
|
||||
// Number of fixed-size-list slots in this layer's parent domain.
|
||||
uint64 num_slots = 1;
|
||||
// Number of children per parent slot. The child domain has num_slots * dimension slots.
|
||||
uint64 dimension = 2;
|
||||
// Validity for the parent slots in this layer.
|
||||
SparseValiditySet validity = 3;
|
||||
}
|
||||
|
||||
message SparseValiditySet {
|
||||
enum Meaning {
|
||||
SPARSE_VALIDITY_UNSPECIFIED = 0;
|
||||
// Stored positions are null; all other positions are valid.
|
||||
SPARSE_VALIDITY_NULL_POSITIONS = 1;
|
||||
// Stored positions are valid; all other positions are null.
|
||||
SPARSE_VALIDITY_VALID_POSITIONS = 2;
|
||||
}
|
||||
|
||||
Meaning meaning = 1;
|
||||
SparsePositionSet positions = 2;
|
||||
}
|
||||
|
||||
message SparsePositionEmpty {}
|
||||
|
||||
message SparsePositionAll {}
|
||||
|
||||
message SparsePositionRange {
|
||||
uint64 start = 1;
|
||||
uint64 length = 2;
|
||||
}
|
||||
|
||||
message SparsePositionSet {
|
||||
oneof positions {
|
||||
// Delta-compressed u64 positions. Cardinality is num_positions.
|
||||
CompressiveEncoding explicit = 1;
|
||||
// One contiguous, non-empty range.
|
||||
SparsePositionRange range = 2;
|
||||
// Every position in the domain.
|
||||
SparsePositionAll all = 3;
|
||||
// No positions in the domain.
|
||||
SparsePositionEmpty empty = 4;
|
||||
}
|
||||
// Semantic cardinality of this set.
|
||||
uint64 num_positions = 5;
|
||||
}
|
||||
|
||||
message SparseCountEmpty {}
|
||||
|
||||
message SparseCountConstant {
|
||||
// Child count shared by every non-empty list slot.
|
||||
uint64 value = 1;
|
||||
}
|
||||
|
||||
message SparseCountSet {
|
||||
oneof counts {
|
||||
// Compressed u64 child counts. Cardinality comes from the containing position set.
|
||||
CompressiveEncoding explicit = 1;
|
||||
// One positive child count shared by every non-empty list slot.
|
||||
SparseCountConstant constant = 2;
|
||||
// No counts; valid only when there are no non-empty list slots.
|
||||
SparseCountEmpty empty = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// A layout used for pages where all (visible) values are the same scalar value.
|
||||
//
|
||||
// This generalizes the prior AllNullLayout semantics for file_version >= 2.2.
|
||||
//
|
||||
// There may be buffers of repetition and definition information if required in order
|
||||
// to interpret what kind of nulls are present / which items are visible.
|
||||
message ConstantLayout {
|
||||
// The meaning of each repdef layer, used to interpret repdef buffers correctly
|
||||
repeated RepDefLayer layers = 5;
|
||||
|
||||
// Inline fixed-width scalar value bytes.
|
||||
//
|
||||
// This MUST only be used for types where a single non-null element is represented by a single
|
||||
// fixed-width Arrow value buffer (i.e. no offsets buffer, no child data).
|
||||
//
|
||||
// Constraints:
|
||||
// - MUST be absent for an all-null page
|
||||
// - MUST be <= 32 bytes if present
|
||||
optional bytes inline_value = 6;
|
||||
|
||||
// Optional compression algorithm used for the repetition buffer.
|
||||
// If absent, repetition levels are stored as raw u16 values.
|
||||
CompressiveEncoding rep_compression = 7;
|
||||
// Optional compression algorithm used for the definition buffer.
|
||||
// If absent, definition levels are stored as raw u16 values.
|
||||
CompressiveEncoding def_compression = 8;
|
||||
// Number of values in repetition buffer after decompression.
|
||||
uint64 num_rep_values = 9;
|
||||
// Number of values in definition buffer after decompression.
|
||||
uint64 num_def_values = 10;
|
||||
}
|
||||
|
||||
// A layout where large binary data is encoded externally and only
|
||||
// the descriptions (position + size) are placed in the page
|
||||
//
|
||||
// Repdef information is stored in the descriptions. A description with a size of
|
||||
// 0 and a position of 0 is an empty value. A description with a size of 0 and a
|
||||
// non-zero position is a null value and the position is the repdef value.
|
||||
message BlobLayout {
|
||||
// The inner layout used to store the descriptions
|
||||
PageLayout inner_layout = 1;
|
||||
// The meaning of each repdef layer, used to interpret repdef buffers correctly
|
||||
//
|
||||
// The inner layout's repdef layers will always be 1 all valid item layer
|
||||
repeated RepDefLayer layers = 2;
|
||||
}
|
||||
|
||||
// Describes the structural encoding of a page
|
||||
message PageLayout {
|
||||
oneof layout {
|
||||
// A layout used for pages where the data is small
|
||||
MiniBlockLayout mini_block_layout = 1;
|
||||
// A layout used for pages where all (visible) values are the same scalar value or null.
|
||||
ConstantLayout constant_layout = 2;
|
||||
// A layout used for pages where the data is large
|
||||
FullZipLayout full_zip_layout = 3;
|
||||
// A layout where large binary data is encoded externally
|
||||
// and only the descriptions are put in the page
|
||||
BlobLayout blob_layout = 4;
|
||||
// A sparse structural layout. This variant requires file version 2.3 or later.
|
||||
SparseLayout sparse_layout = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// # Compressive Encodings
|
||||
//
|
||||
// These encodings describe how an array is compressed. An encoding may split an
|
||||
// array into multiple buffers. The buffers can then be compressed further (and split
|
||||
// into yet more buffers). The entire process forms a tree of encodings with the root
|
||||
// of the tree being the initial array and the leaves being the final compressed buffers.
|
||||
//
|
||||
// # Data blocks and buffers
|
||||
//
|
||||
// Data blocks are a simplified version of arrays and represent a collection of buffers grouped
|
||||
// with some kind of interpretation. Data blocks are the input and output of compressive encodings.
|
||||
// There are different kinds of data blocks:
|
||||
// - Fixed width data blocks (e.g. u8, u16, ...)
|
||||
// - Variable width data blocks (e.g. strings, binary)
|
||||
// - Struct data blocks (note: this is for packed structs, normal structs are encoded in the structural encoding)
|
||||
//
|
||||
// In addition, leaf encodings may output "buffers". These are fully compressed buffers of data that
|
||||
// are stored in the page and no longer compressed.
|
||||
|
||||
enum CompressionScheme {
|
||||
COMPRESSION_ALGORITHM_UNSPECIFIED = 0;
|
||||
COMPRESSION_ALGORITHM_LZ4 = 1;
|
||||
COMPRESSION_ALGORITHM_ZSTD = 2;
|
||||
}
|
||||
|
||||
// Compression applied to a single buffer of data
|
||||
//
|
||||
// A buffer is the leaf of the compression tree. Unlike data blocks, which can
|
||||
// be further compressed with a variety of techniques, a buffer cannot be understood
|
||||
// in any particular way.
|
||||
//
|
||||
// A general compression scheme may be applied to a buffer. This is something like
|
||||
// zstd, lz4, etc. The entire buffer is compressed as a single unit. If this happens
|
||||
// then any parent encoding becomes opaque, even if it would normally be transparent.
|
||||
//
|
||||
// This is a leaf, no further compression is applied to the data.
|
||||
message BufferCompression {
|
||||
// A general compression scheme to apply to the buffer
|
||||
CompressionScheme scheme = 1;
|
||||
// The compression level
|
||||
//
|
||||
// Optional, if not present a scheme-specific default value will be used.
|
||||
//
|
||||
// Interpretation of this value depends on the compression scheme. Generally, larger
|
||||
// values indicate more compression at the expense of more CPU time.
|
||||
optional int32 level = 2;
|
||||
}
|
||||
|
||||
// Fixed width items placed contiguously in a single buffer
|
||||
//
|
||||
// This is a leaf encoding, there is no compression applied to the data.
|
||||
//
|
||||
// This is a transparent encoding by definition.
|
||||
//
|
||||
// The input is a fixed-width data block.
|
||||
// The output is a single buffer.
|
||||
message Flat {
|
||||
// the number of bits per value, must be greater than 0, does
|
||||
// not need to be a multiple of 8
|
||||
uint64 bits_per_value = 1;
|
||||
// The compression applied to the data
|
||||
optional BufferCompression data = 2;
|
||||
}
|
||||
|
||||
// Variable width items have the values stored in one buffer and the
|
||||
// offsets are output as a data block that may be further compressed.
|
||||
//
|
||||
// This is a partial leaf encoding. Values are not compressed but
|
||||
// the offsets may be further compressed.
|
||||
//
|
||||
// This is a transparent encoding by definition.
|
||||
//
|
||||
// The input is a variable-width data block.
|
||||
// The output is a single fixed-width data block (the offsets) and
|
||||
// a single buffer (the values)
|
||||
message Variable {
|
||||
// Describes how the offsets data block is compressed
|
||||
CompressiveEncoding offsets = 1;
|
||||
// The compression applied to the values
|
||||
optional BufferCompression values = 2;
|
||||
}
|
||||
|
||||
// Compression algorithm where all values have a constant value (encoded in the description)
|
||||
//
|
||||
// This is a leaf encoding, there is no compression applied to the data.
|
||||
//
|
||||
// The input can be any kind of data block.
|
||||
// There is no output.
|
||||
message Constant {
|
||||
// The value (TODO: define encoding for literals?)
|
||||
optional bytes value = 1;
|
||||
}
|
||||
|
||||
// A compression scheme in which a single fixed-width block is "packed" into
|
||||
// a smaller fixed-width block values where each value has fewer bits.
|
||||
//
|
||||
// This is typically done by throwing away the most significant bits of each value when
|
||||
// those bits are all the same.
|
||||
//
|
||||
// In this scheme the number of bits per value is fixed across the entire buffer and stored
|
||||
// in this message.
|
||||
//
|
||||
// This is a transparent encoding.
|
||||
//
|
||||
// The input is a fixed-width data block.
|
||||
// The output is a single fixed-width data block.
|
||||
message OutOfLineBitpacking {
|
||||
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
||||
uint64 uncompressed_bits_per_value = 1;
|
||||
// The compression used to store the bitpacked values data block
|
||||
CompressiveEncoding values = 3;
|
||||
}
|
||||
|
||||
// Bitpacking variant where the bits per value are stored inline in the chunks themselves
|
||||
//
|
||||
// This variation of bitpacking allows for the number of bits per value to change throughout the
|
||||
// buffer, which makes the compression more robust to outliers.
|
||||
//
|
||||
// This is an opaque encoding.
|
||||
//
|
||||
// The input is a fixed-width data block.
|
||||
// The output is a single buffer.
|
||||
message InlineBitpacking {
|
||||
// the number of bits of the uncompressed value. e.g. for a u32, this will be 32
|
||||
uint64 uncompressed_bits_per_value = 1;
|
||||
// The compression applied to the values
|
||||
optional BufferCompression values = 2;
|
||||
}
|
||||
|
||||
// A compression scheme for variable-width data
|
||||
//
|
||||
// A small dictionary (referred to as a "symbol table") is used to compress the values.
|
||||
// In this scheme there is a single symbol table for the entire page and it is stored in the
|
||||
// encoding description itself.
|
||||
//
|
||||
// This is a transparent encoding.
|
||||
//
|
||||
// The input is a variable-width data block.
|
||||
// The output is a single variable-width data block.
|
||||
message Fsst {
|
||||
// The FSST symbol table
|
||||
bytes symbol_table = 1;
|
||||
// The compression used to store the compressed values data block
|
||||
CompressiveEncoding values = 2;
|
||||
}
|
||||
|
||||
// A compression scheme where common values are stored in a dictionary and the values are
|
||||
// encoded as indices into the dictionary.
|
||||
//
|
||||
// This is an opaque encoding unless the dictionary is considered metadata.
|
||||
//
|
||||
// The input is a any kind of data block.
|
||||
// There are two outputs:
|
||||
// - A data block of the same kind as the input (the dictionary)
|
||||
// - A fixed-width data block containing the indices into the dictionary.
|
||||
message Dictionary {
|
||||
// The compression used to store the indices data block
|
||||
CompressiveEncoding indices = 1;
|
||||
// The compression used to store the dictionary items data block
|
||||
CompressiveEncoding items = 2;
|
||||
// The number of items in the dictionary
|
||||
uint32 num_dictionary_items = 3;
|
||||
}
|
||||
|
||||
// A compression scheme where runs of common values are encoded as a single value and a count
|
||||
//
|
||||
// This is an opaque encoding unless the run lengths are considered metadata.
|
||||
//
|
||||
// The input is a single data block of any kind.
|
||||
// There are two outputs:
|
||||
// - A data block of the same kind as the input (the run values)
|
||||
// - A fixed-width data block containing the lengths of the runs
|
||||
message Rle {
|
||||
// The compression used to store the run values data block
|
||||
CompressiveEncoding values = 1;
|
||||
// The compression used to store the run lengths data block
|
||||
CompressiveEncoding run_lengths = 2;
|
||||
}
|
||||
|
||||
// Converts a fixed-size-list of values into a flattened list of values
|
||||
//
|
||||
// This encoding does not actually compress the data, it just flattens out the FSL layers.
|
||||
//
|
||||
// This is a transparent encoding.
|
||||
//
|
||||
// The input is a single block of fixed-width data (with a wide width and few items)
|
||||
// The output is a single block of fixed-width data (with a narrow width and many items)
|
||||
message FixedSizeList {
|
||||
// The number of items in this layer of FSL
|
||||
uint64 items_per_value = 1;
|
||||
// Whether or not there is a validity buffer
|
||||
bool has_validity = 3;
|
||||
// The compression used to store the flattened values data block
|
||||
CompressiveEncoding values = 2;
|
||||
}
|
||||
|
||||
// Packs a struct containing only fixed-width children into a single fixed-width data block
|
||||
//
|
||||
// The children are concatenated row by row and stored as a single fixed-width buffer. This is
|
||||
// the legacy packed struct representation and remains available for backwards compatibility.
|
||||
message PackedStruct {
|
||||
// The number of bits contributed by each child field in the packed row
|
||||
repeated uint64 bits_per_value = 1;
|
||||
// The compression used to store the packed fixed-width values
|
||||
CompressiveEncoding values = 2;
|
||||
}
|
||||
|
||||
// Variable-width packed struct encoding (2.2 extension)
|
||||
//
|
||||
// Each child value is compressed independently before being transposed into
|
||||
// a row-major layout. This preserves per-field compression boundaries at the
|
||||
// cost of disabling mini-block compression. Readers must prefer this field
|
||||
// when present and fall back to the legacy encoding otherwise.
|
||||
message VariablePackedStruct {
|
||||
// Per-field encoding metadata in struct order
|
||||
repeated FieldEncoding fields = 1;
|
||||
|
||||
// Encoding description for a single child field
|
||||
message FieldEncoding {
|
||||
// Compression applied to individual field values before transposition
|
||||
CompressiveEncoding value = 1;
|
||||
oneof layout {
|
||||
// Bit width of each compressed value (when fixed width)
|
||||
uint64 bits_per_value = 2;
|
||||
// Bit width of the length prefix for variable-width compressed values
|
||||
uint64 bits_per_length = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A compression scheme that wraps the underlying data with general compression
|
||||
//
|
||||
// Note: The application of wrapped compression will depend on the layout of the data.
|
||||
// If we apply it to mini-block data then we compress entire mini-blocks. If we apply
|
||||
// it to full-zip data then we compress each value individually.
|
||||
//
|
||||
// Note: Wrapped compression is somewhat unique at the moment as it is applied to the
|
||||
// output of the inner encoding and not the input like all other compressive encodings.
|
||||
//
|
||||
// Note: General compression can usually be applied in two spots. We can apply
|
||||
// it to individual buffers or we can apply it here, to the entire array.
|
||||
//
|
||||
// For example, let's say we are storing mini-blocks of strings and we are using
|
||||
// FSST and bitpacking the offsets. We have something like this...
|
||||
//
|
||||
// WRAPPED(†3) -> FSST -> VARIABLE -(offsets)-> INLINE_BITPACKING -(data)-> FLAT -> BUFFER (†1)
|
||||
// -(data)-> BUFFER (†2)
|
||||
//
|
||||
// General compression can be applied at †1, †2, or †3 (or any combination of these).
|
||||
//
|
||||
// If we apply it at †1 then we apply it just to the bitpacked offsets
|
||||
// If we apply it at †2 then we apply it just to the FSST compressed data
|
||||
// If we apply it at †3 then we apply it to the entire mini-block (both offsets and data)
|
||||
//
|
||||
// The input is a single data block of any kind.
|
||||
// The output is a single data block of the same kind as the input.
|
||||
message General {
|
||||
// The compression to apply to the values
|
||||
BufferCompression compression = 1;
|
||||
// The compression used to store the output data block
|
||||
CompressiveEncoding values = 3;
|
||||
}
|
||||
|
||||
// A compression scheme where fixed-width values are transposed into a series of byte streams
|
||||
//
|
||||
// This is commonly used for floating point values where the upper bits (the mantissa) have a
|
||||
// significantly different meaning than the lower bits. By splitting the values into byte streams
|
||||
// we group the mantissa bits together and the exponent bits together. The end result is typically
|
||||
// more compressible.
|
||||
//
|
||||
// Note that this encoding is mostly useful when combined with other encodings. It does not do any
|
||||
// compression on its own.
|
||||
//
|
||||
// This is an opaque encoding.
|
||||
//
|
||||
// The input is a fixed-width data block
|
||||
// The output is a single fixed-width data block
|
||||
message ByteStreamSplit {
|
||||
// The compression used to store the values
|
||||
CompressiveEncoding values = 1;
|
||||
}
|
||||
|
||||
// An encoding that compresses a data block into buffers
|
||||
message CompressiveEncoding {
|
||||
oneof compression {
|
||||
Flat flat = 1;
|
||||
Variable variable = 2;
|
||||
Constant constant = 3;
|
||||
OutOfLineBitpacking out_of_line_bitpacking = 4;
|
||||
InlineBitpacking inline_bitpacking = 5;
|
||||
Fsst fsst = 6;
|
||||
Dictionary dictionary = 7;
|
||||
Rle rle = 8;
|
||||
ByteStreamSplit byte_stream_split = 9;
|
||||
General general = 10;
|
||||
FixedSizeList fixed_size_list = 11;
|
||||
PackedStruct packed_struct = 12;
|
||||
VariablePackedStruct variable_packed_struct = 13;
|
||||
}
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.file;
|
||||
|
||||
// A file descriptor that describes the contents of a Lance file
|
||||
message FileDescriptor {
|
||||
// The schema of the file
|
||||
Schema schema = 1;
|
||||
// The number of rows in the file
|
||||
uint64 length = 2;
|
||||
}
|
||||
|
||||
// A schema which describes the data type of each of the columns
|
||||
message Schema {
|
||||
// All fields in this file, including the nested fields.
|
||||
repeated lance.file.Field fields = 1;
|
||||
// Schema metadata.
|
||||
map<string, bytes> metadata = 5;
|
||||
}
|
||||
|
||||
// Metadata of one Lance file.
|
||||
message Metadata {
|
||||
// 4 was used for StatisticsMetadata in the past, but has been moved to
|
||||
// prevent a bug in older readers.
|
||||
reserved 4;
|
||||
|
||||
// Position of the manifest in the file. If it is zero, the manifest is stored
|
||||
// externally.
|
||||
uint64 manifest_position = 1;
|
||||
|
||||
// Logical offsets of each chunk group, i.e., number of the rows in each
|
||||
// chunk.
|
||||
repeated int32 batch_offsets = 2;
|
||||
|
||||
// The file position that page table is stored.
|
||||
//
|
||||
// A page table is a matrix of N x M x 2, where N = num_fields, and M =
|
||||
// num_batches. Each cell in the table is a pair of <position:int64,
|
||||
// length:int64> of the page. Both position and length are int64 values. The
|
||||
// <position, length> of all the pages in the same column are then
|
||||
// contiguously stored.
|
||||
//
|
||||
// Every field that is a part of the file will have a run in the page table.
|
||||
// This includes struct columns, which will have a run of length 0 since
|
||||
// they don't store any actual data.
|
||||
//
|
||||
// For example, for the column 5 and batch 4, we have:
|
||||
// ```text
|
||||
// position = page_table[5][4][0];
|
||||
// length = page_table[5][4][1];
|
||||
// ```
|
||||
uint64 page_table_position = 3;
|
||||
|
||||
message StatisticsMetadata {
|
||||
// The schema of the statistics.
|
||||
//
|
||||
// This might be empty, meaning there are no statistics. It also might not
|
||||
// contain statistics for every field.
|
||||
repeated Field schema = 1;
|
||||
|
||||
// The field ids of the statistics leaf fields.
|
||||
//
|
||||
// This plays a similar role to the `fields` field in the DataFile message.
|
||||
// Each of these field ids corresponds to a field in the stats_schema. There
|
||||
// is one per column in the stats page table.
|
||||
repeated int32 fields = 2;
|
||||
|
||||
// The file position of the statistics page table
|
||||
//
|
||||
// The page table is a matrix of N x 2, where N = length of stats_fields.
|
||||
// This is the same layout as the main page table, except there is always
|
||||
// only one batch.
|
||||
//
|
||||
// For example, to get the stats column 5, we have:
|
||||
// ```text
|
||||
// position = stats_page_table[5][0];
|
||||
// length = stats_page_table[5][1];
|
||||
// ```
|
||||
uint64 page_table_position = 3;
|
||||
}
|
||||
|
||||
StatisticsMetadata statistics = 5;
|
||||
} // Metadata
|
||||
|
||||
// Supported encodings.
|
||||
enum Encoding {
|
||||
// Invalid encoding.
|
||||
NONE = 0;
|
||||
// Plain encoding.
|
||||
PLAIN = 1;
|
||||
// Var-length binary encoding.
|
||||
VAR_BINARY = 2;
|
||||
// Dictionary encoding.
|
||||
DICTIONARY = 3;
|
||||
// Run-length encoding.
|
||||
RLE = 4;
|
||||
}
|
||||
|
||||
// Dictionary field metadata
|
||||
message Dictionary {
|
||||
/// The file offset for storing the dictionary value.
|
||||
/// It is only valid if encoding is DICTIONARY.
|
||||
///
|
||||
/// The logic type presents the value type of the column, i.e., string value.
|
||||
int64 offset = 1;
|
||||
|
||||
/// The length of dictionary values.
|
||||
int64 length = 2;
|
||||
}
|
||||
|
||||
// Field metadata for a column.
|
||||
message Field {
|
||||
enum Type {
|
||||
PARENT = 0;
|
||||
REPEATED = 1;
|
||||
LEAF = 2;
|
||||
}
|
||||
Type type = 1;
|
||||
|
||||
// Fully qualified name.
|
||||
string name = 2;
|
||||
/// Field Id.
|
||||
///
|
||||
/// See the comment in `DataFile.fields` for how field ids are assigned.
|
||||
int32 id = 3;
|
||||
/// Parent Field ID. If not set, this is a top-level column.
|
||||
int32 parent_id = 4;
|
||||
|
||||
// Logical types, support parameterized Arrow Type.
|
||||
//
|
||||
// PARENT types will always have logical type "struct".
|
||||
//
|
||||
// REPEATED types may have logical types:
|
||||
// * "list"
|
||||
// * "large_list"
|
||||
// * "list.struct"
|
||||
// * "large_list.struct"
|
||||
// The final two are used if the list values are structs, and therefore the
|
||||
// field is both implicitly REPEATED and PARENT.
|
||||
//
|
||||
// LEAF types may have logical types:
|
||||
// * "null"
|
||||
// * "bool"
|
||||
// * "int8" / "uint8"
|
||||
// * "int16" / "uint16"
|
||||
// * "int32" / "uint32"
|
||||
// * "int64" / "uint64"
|
||||
// * "halffloat" / "float" / "double"
|
||||
// * "string" / "large_string"
|
||||
// * "binary" / "large_binary"
|
||||
// * "date32:day"
|
||||
// * "date64:ms"
|
||||
// * "decimal:128:{precision}:{scale}" / "decimal:256:{precision}:{scale}"
|
||||
// * "time:{unit}" / "timestamp:{unit}" / "duration:{unit}", where unit is
|
||||
// "s", "ms", "us", "ns"
|
||||
// * "dict:{value_type}:{index_type}:false"
|
||||
string logical_type = 5;
|
||||
// If this field is nullable.
|
||||
bool nullable = 6;
|
||||
|
||||
// optional field metadata (e.g. extension type name/parameters)
|
||||
map<string, bytes> metadata = 10;
|
||||
|
||||
bool unenforced_primary_key = 12;
|
||||
|
||||
// Position of this field in the primary key (1-based).
|
||||
// 0 means the field is part of the primary key but uses schema field id for ordering.
|
||||
// When set to a positive value, primary key fields are ordered by this position.
|
||||
uint32 unenforced_primary_key_position = 13;
|
||||
|
||||
// Reserved for future use. Use unenforced_clustering_key_position instead.
|
||||
bool unenforced_clustering_key = 14;
|
||||
|
||||
// Position of this field in the clustering key (1-based).
|
||||
// 0 means the field is not part of the clustering key.
|
||||
uint32 unenforced_clustering_key_position = 15;
|
||||
|
||||
// DEPRECATED ----------------------------------------------------------------
|
||||
|
||||
// Deprecated: Only used in V1 file format. V2 uses variable encodings defined
|
||||
// per page.
|
||||
//
|
||||
// The global encoding to use for this field.
|
||||
Encoding encoding = 7;
|
||||
|
||||
// Deprecated: Only used in V1 file format. V2 dynamically chooses when to
|
||||
// do dictionary encoding and keeps the dictionary in the data files.
|
||||
//
|
||||
// The file offset for storing the dictionary value.
|
||||
// It is only valid if encoding is DICTIONARY.
|
||||
//
|
||||
// The logic type presents the value type of the column, i.e., string value.
|
||||
Dictionary dictionary = 8;
|
||||
|
||||
// Deprecated: optional extension type name, use metadata field
|
||||
// ARROW:extension:name
|
||||
string extension_name = 9;
|
||||
|
||||
// Field number 11 was previously `string storage_class`.
|
||||
// Keep it reserved so older manifests remain compatible while new writers
|
||||
// avoid reusing the slot.
|
||||
reserved 11;
|
||||
reserved "storage_class";
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.file.v2;
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
// # Lance v2.X File Format
|
||||
//
|
||||
// The Lance file format is a barebones format for serializing columnar data
|
||||
// into a file.
|
||||
//
|
||||
// * Each Lance file contains between 0 and 4Gi columns
|
||||
// * Each column contains between 0 and 4Gi pages
|
||||
// * Each page contains between 0 and 2^64 items
|
||||
// * Different pages within a column can have different items counts
|
||||
// * Columns may have up to 2^64 items
|
||||
// * Different columns within a file can have different item counts
|
||||
//
|
||||
// The Lance file format does not have any notion of a type system or schemas.
|
||||
// From the perspective of the file format all data is arbitrary buffers of
|
||||
// bytes with an extensible metadata block to describe the data. It is up to
|
||||
// the user to interpret these bytes meaningfully.
|
||||
//
|
||||
// Data buffers are written to the file first. These data buffers can be
|
||||
// referenced from three different places in the file:
|
||||
//
|
||||
// * Page encodings can reference data buffers. This is the most common way
|
||||
// that actual data is stored.
|
||||
// * Column encodings can reference data buffers. For example, a column encoding
|
||||
// may reference data buffer(s) containing statistics or dictionaries.
|
||||
// * Finally, the global buffer offset table can reference data buffers. This
|
||||
// is useful for storing data that is shared across multiple columns.
|
||||
// This is also useful for global file metadata (e.g. a schema that describes
|
||||
// the file)
|
||||
//
|
||||
// ## File Layout
|
||||
//
|
||||
// Note: the number of buffers (BN) is independent of the number of columns (CN)
|
||||
// and pages.
|
||||
//
|
||||
// Buffers often need to be aligned. 64-byte alignment is common when
|
||||
// working with SIMD operations. 4096-byte alignment is common when
|
||||
// working with direct I/O. In order to ensure these buffers are aligned
|
||||
// writers may need to insert padding before the buffers.
|
||||
//
|
||||
// If direct I/O is required then most (but not all) fields described
|
||||
// below must be sector aligned. We have marked these fields with an
|
||||
// asterisk for clarity. Readers should assume there will be optional
|
||||
// padding inserted before these fields.
|
||||
//
|
||||
// All footer fields are unsigned integers written with little endian
|
||||
// byte order.
|
||||
//
|
||||
// ├──────────────────────────────────┤
|
||||
// | Data Pages |
|
||||
// | Data Buffer 0* |
|
||||
// | ... |
|
||||
// | Data Buffer BN* |
|
||||
// ├──────────────────────────────────┤
|
||||
// | Column Metadatas |
|
||||
// | |A| Column 0 Metadata* |
|
||||
// | Column 1 Metadata* |
|
||||
// | ... |
|
||||
// | Column CN Metadata* |
|
||||
// ├──────────────────────────────────┤
|
||||
// | Column Metadata Offset Table |
|
||||
// | |B| Column 0 Metadata Position* |
|
||||
// | Column 0 Metadata Size |
|
||||
// | ... |
|
||||
// | Column CN Metadata Position |
|
||||
// | Column CN Metadata Size |
|
||||
// ├──────────────────────────────────┤
|
||||
// | Global Buffers Offset Table |
|
||||
// | |C| Global Buffer 0 Position* |
|
||||
// | Global Buffer 0 Size |
|
||||
// | ... |
|
||||
// | Global Buffer GN Position |
|
||||
// | Global Buffer GN Size |
|
||||
// ├──────────────────────────────────┤
|
||||
// | Footer |
|
||||
// | A u64: Offset to column meta 0 |
|
||||
// | B u64: Offset to CMO table |
|
||||
// | C u64: Offset to GBO table |
|
||||
// | u32: Number of global bufs |
|
||||
// | u32: Number of columns |
|
||||
// | u16: Major version |
|
||||
// | u16: Minor version |
|
||||
// | "LANC" |
|
||||
// ├──────────────────────────────────┤
|
||||
//
|
||||
// File Layout-End
|
||||
//
|
||||
// ## Data Pages
|
||||
//
|
||||
// A lot of flexibility is provided in how data is stored. A page's buffers do
|
||||
// not strictly need to be contiguous on the disk. However, it is recommended
|
||||
// that buffers within a page be grouped together for best performance.
|
||||
//
|
||||
// Data pages should be large. The only time a page should be written to disk
|
||||
// is when the writer needs to flush the page to disk because it has accumulated
|
||||
// too much data. Pages are not read in sequential order and if pages are too
|
||||
// small then the seek overhead (or request overhead) will be problematic. We
|
||||
// generally advise that pages be at least 8MB or larger.
|
||||
//
|
||||
// ## Encodings
|
||||
//
|
||||
// Specific encodings are not part of this minimal format. They are provided
|
||||
// by extensions. Readers and writers should be designed so that encodings can
|
||||
// be easily added and removed. Ideally, they should allow for this without
|
||||
// requiring recompilation through some kind of plugin system.
|
||||
|
||||
// The deferred encoding is used to place the encoding itself in a different
|
||||
// part of the file. This is most commonly used to allow encodings to be shared
|
||||
// across different columns. For example, when writing a file with thousands of
|
||||
// columns, where many pages have the exact same encoding, it can be useful
|
||||
// to cut down on the size of the metadata by using a deferred encoding.
|
||||
message DeferredEncoding {
|
||||
// Location of the buffer containing the encoding.
|
||||
//
|
||||
// * If sharing encodings across columns then this will be in a global buffer
|
||||
// * If sharing encodings across pages within a column this could be in a
|
||||
// column metadata buffer.
|
||||
// * This could also be a page buffer if the encoding is not shared, needs
|
||||
// to be written before the file ends, and the encoding is too large to load
|
||||
// unless we first determine the page needs to be read. This combination
|
||||
// seems unusual.
|
||||
uint64 buffer_location = 1;
|
||||
uint64 buffer_length = 2;
|
||||
}
|
||||
|
||||
// The encoding is placed directly in the metadata section
|
||||
message DirectEncoding {
|
||||
// The bytes that make up the encoding embedded directly in the metadata
|
||||
//
|
||||
// This is the most common approach.
|
||||
bytes encoding = 1;
|
||||
}
|
||||
|
||||
// An encoding stores the information needed to decode a column or page
|
||||
//
|
||||
// For example, it could describe if the page is using bit packing, and how many bits
|
||||
// there are in each individual value.
|
||||
//
|
||||
// At the column level it can be used to wrap columns with dictionaries or statistics.
|
||||
message Encoding {
|
||||
oneof location {
|
||||
// The encoding is stored elsewhere and not part of this protobuf message
|
||||
DeferredEncoding indirect = 1;
|
||||
// The encoding is stored within this protobuf message
|
||||
DirectEncoding direct = 2;
|
||||
// There is no encoding information
|
||||
google.protobuf.Empty none = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// ## Metadata
|
||||
|
||||
// Each column has a metadata block that is placed at the end of the file.
|
||||
// These may be read individually to allow for column projection.
|
||||
message ColumnMetadata {
|
||||
|
||||
// This describes a page of column data.
|
||||
message Page {
|
||||
// The file offsets for each of the page buffers
|
||||
//
|
||||
// The number of buffers is variable and depends on the encoding. There
|
||||
// may be zero buffers (e.g. constant encoded data) in which case this
|
||||
// could be empty.
|
||||
repeated uint64 buffer_offsets = 1;
|
||||
// The size (in bytes) of each of the page buffers
|
||||
//
|
||||
// This field will have the same length as `buffer_offsets` and
|
||||
// may be empty.
|
||||
repeated uint64 buffer_sizes = 2;
|
||||
// Logical length (e.g. # rows) of the page
|
||||
uint64 length = 3;
|
||||
// The encoding used to encode the page
|
||||
Encoding encoding = 4;
|
||||
// The priority of the page
|
||||
//
|
||||
// For tabular data this will be the top-level row number of the first row
|
||||
// in the page (and top-level rows should not split across pages).
|
||||
uint64 priority = 5;
|
||||
}
|
||||
// Encoding information about the column itself. This typically describes
|
||||
// how to interpret the column metadata buffers. For example, it could
|
||||
// describe how statistics or dictionaries are stored in the column metadata.
|
||||
Encoding encoding = 1;
|
||||
// The pages in the column
|
||||
repeated Page pages = 2;
|
||||
// The file offsets of each of the column metadata buffers
|
||||
//
|
||||
// There may be zero buffers.
|
||||
repeated uint64 buffer_offsets = 3;
|
||||
// The size (in bytes) of each of the column metadata buffers
|
||||
//
|
||||
// This field will have the same length as `buffer_offsets` and
|
||||
// may be empty.
|
||||
repeated uint64 buffer_sizes = 4;
|
||||
} // Metadata-End
|
||||
|
||||
// ## Where is the rest?
|
||||
//
|
||||
// This file format is extremely minimal. It is a building block for
|
||||
// creating more useful readers and writers and not terribly useful by itself.
|
||||
// Other protobuf files will describe how this can be extended.
|
||||
@@ -1,99 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.datafusion;
|
||||
|
||||
import "table_identifier.proto";
|
||||
|
||||
message U64Range {
|
||||
uint64 start = 1;
|
||||
uint64 end = 2;
|
||||
}
|
||||
|
||||
message ProjectionProto {
|
||||
repeated int32 field_ids = 1;
|
||||
bool with_row_id = 2;
|
||||
bool with_row_addr = 3;
|
||||
bool with_row_last_updated_at_version = 4;
|
||||
bool with_row_created_at_version = 5;
|
||||
BlobHandlingProto blob_handling = 6;
|
||||
}
|
||||
|
||||
message BlobHandlingProto {
|
||||
oneof mode {
|
||||
// All blobs read as binary
|
||||
bool all_binary = 1;
|
||||
// Blobs as descriptions, other binary as binary (default)
|
||||
bool blobs_descriptions = 2;
|
||||
// All binary columns as descriptions
|
||||
bool all_descriptions = 3;
|
||||
// Specific blobs read as binary, rest as descriptions (non-blob binary stays binary)
|
||||
FieldIdSet some_blobs_binary = 4;
|
||||
// Specific columns as binary, all other binary as descriptions
|
||||
FieldIdSet some_binary = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message FieldIdSet {
|
||||
repeated uint32 field_ids = 1;
|
||||
}
|
||||
|
||||
message FilteredReadThreadingModeProto {
|
||||
oneof mode {
|
||||
uint64 one_partition_multiple_threads = 1;
|
||||
uint64 multiple_partitions = 2;
|
||||
}
|
||||
}
|
||||
|
||||
// Serializable form of FilteredReadOptions.
|
||||
message FilteredReadOptionsProto {
|
||||
optional U64Range scan_range_before_filter = 1;
|
||||
optional U64Range scan_range_after_filter = 2;
|
||||
bool with_deleted_rows = 3;
|
||||
optional uint32 batch_size = 4;
|
||||
optional uint64 fragment_readahead = 5;
|
||||
repeated uint64 fragment_ids = 6;
|
||||
ProjectionProto projection = 7;
|
||||
optional bytes refine_filter_substrait = 8;
|
||||
optional bytes full_filter_substrait = 9;
|
||||
FilteredReadThreadingModeProto threading_mode = 10;
|
||||
optional uint64 io_buffer_size_bytes = 11;
|
||||
// Arrow IPC schema for decoding Substrait filters (may be wider than projection).
|
||||
optional bytes filter_schema_ipc = 12;
|
||||
}
|
||||
|
||||
// Serializable form of FilteredReadPlan (planned/distributed mode).
|
||||
// RowAddrTreeMap serialized via its built-in serialize_into/deserialize_from.
|
||||
// Per-fragment filters are Substrait-encoded and deduplicated.
|
||||
message FilteredReadPlanProto {
|
||||
bytes row_addr_tree_map = 1;
|
||||
optional U64Range scan_range_after_filter = 2;
|
||||
// Arrow IPC schema for decoding Substrait filters (matches the schema used at encode time).
|
||||
optional bytes filter_schema_ipc = 3;
|
||||
// Per-fragment filter mapping. Key is fragment id, value is a list index into
|
||||
// filter_expressions. Multiple fragments can share the same list index when
|
||||
// they have the same filter, avoiding duplicate Substrait encoding.
|
||||
map<uint32, uint32> fragment_filter_ids = 4;
|
||||
// Deduplicated Substrait-encoded filter expressions. Each entry is referenced
|
||||
// by one or more values in fragment_filter_ids.
|
||||
repeated bytes filter_expressions = 5;
|
||||
}
|
||||
|
||||
// Top-level wrapper for FilteredReadExec serialization.
|
||||
message FilteredReadExecProto {
|
||||
TableIdentifier table = 1;
|
||||
FilteredReadOptionsProto options = 2;
|
||||
// FilteredRead has two modes
|
||||
// Plan-then-execute (distributed): The planner creates a FilteredReadPlan and sends it to a remote executor.
|
||||
// Plan-and-execute (local): The executor creates the plan itself at execution time.
|
||||
optional FilteredReadPlanProto plan = 3;
|
||||
// Note: FilteredReadExec.index_input (child ExecutionPlan) is NOT serialized here.
|
||||
// DataFusion's PhysicalExtensionCodec handles child plans automatically: it walks
|
||||
// the plan tree via children() / with_new_children(), serializes each node, and
|
||||
// passes deserialized children back as the `inputs` parameter in try_decode.
|
||||
// This means any ExecutionPlan in the tree (including index_input) must also
|
||||
// implement try_encode/try_decode in the PhysicalExtensionCodec.
|
||||
// TODO: implement serialize/deserialize for lance-specific index input ExecutionPlans.
|
||||
}
|
||||
@@ -1,251 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.index.pb;
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
// The type of an index.
|
||||
enum IndexType {
|
||||
// Vector index
|
||||
VECTOR = 0;
|
||||
}
|
||||
|
||||
message Index {
|
||||
// The unique index name in the dataset.
|
||||
string name = 1;
|
||||
|
||||
// Columns to be used to build the index.
|
||||
repeated string columns = 2;
|
||||
|
||||
// The version of the dataset this index was built from.
|
||||
uint64 dataset_version = 3;
|
||||
|
||||
// The [`IndexType`] of the index.
|
||||
IndexType index_type = 4;
|
||||
|
||||
/// Index implementation details.
|
||||
oneof implementation {
|
||||
VectorIndex vector_index = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message Tensor {
|
||||
enum DataType {
|
||||
BFLOAT16 = 0;
|
||||
FLOAT16 = 1;
|
||||
FLOAT32 = 2;
|
||||
FLOAT64 = 3;
|
||||
UINT8 = 4;
|
||||
UINT16 = 5;
|
||||
UINT32 = 6;
|
||||
UINT64 = 7;
|
||||
}
|
||||
|
||||
DataType data_type = 1;
|
||||
|
||||
// Data shape, [dim1, dim2, ...]
|
||||
repeated uint32 shape = 2;
|
||||
|
||||
// Data buffer
|
||||
bytes data = 3;
|
||||
}
|
||||
|
||||
// Inverted Index File Metadata.
|
||||
message IVF {
|
||||
// Centroids of partitions. `dimension * num_partitions` of float32s.
|
||||
//
|
||||
// Deprecated, use centroids_tensor instead.
|
||||
repeated float centroids = 1; // [deprecated = true];
|
||||
|
||||
// File offset of each partition.
|
||||
repeated uint64 offsets = 2;
|
||||
|
||||
// Number of records in the partition.
|
||||
repeated uint32 lengths = 3;
|
||||
|
||||
// Tensor of centroids. `num_partitions * dimension` of float32s.
|
||||
Tensor centroids_tensor = 4;
|
||||
|
||||
// KMeans loss.
|
||||
optional double loss = 5;
|
||||
}
|
||||
|
||||
// Product Quantization.
|
||||
message PQ {
|
||||
// The number of bits to present a centroid.
|
||||
uint32 num_bits = 1;
|
||||
|
||||
// Number of sub vectors.
|
||||
uint32 num_sub_vectors = 2;
|
||||
|
||||
// Vector dimension
|
||||
uint32 dimension = 3;
|
||||
|
||||
// Codebook. `dimension * 2 ^ num_bits` of float32s.
|
||||
repeated float codebook = 4;
|
||||
|
||||
// Tensor of codebook. `2 ^ num_bits * dimension` of floats.
|
||||
Tensor codebook_tensor = 5;
|
||||
}
|
||||
|
||||
// Transform type
|
||||
enum TransformType {
|
||||
OPQ = 0;
|
||||
}
|
||||
|
||||
// A transform matrix to apply to a vector or vectors.
|
||||
message Transform {
|
||||
// The file offset the matrix is stored
|
||||
uint64 position = 1;
|
||||
|
||||
// Data shape of the matrix, [rows, cols].
|
||||
repeated uint32 shape = 2;
|
||||
|
||||
// Transform type.
|
||||
TransformType type = 3;
|
||||
}
|
||||
|
||||
// Flat Index
|
||||
message Flat {}
|
||||
|
||||
// DiskAnn Index
|
||||
message DiskAnn {
|
||||
// Graph spec version
|
||||
uint32 spec = 1;
|
||||
|
||||
// Graph file
|
||||
string filename = 2;
|
||||
|
||||
// r parameter
|
||||
uint32 r = 3;
|
||||
|
||||
// alpha parameter
|
||||
float alpha = 4;
|
||||
|
||||
// L parameter
|
||||
uint32 L = 5;
|
||||
|
||||
/// Entry points to the graph
|
||||
repeated uint64 entries = 6;
|
||||
}
|
||||
|
||||
// One stage in the vector index pipeline.
|
||||
message VectorIndexStage {
|
||||
oneof stage {
|
||||
// Flat index
|
||||
Flat flat = 1;
|
||||
// `IVF` - Inverted File
|
||||
IVF ivf = 2;
|
||||
// Product Quantization
|
||||
PQ pq = 3;
|
||||
// Transformer
|
||||
Transform transform = 4;
|
||||
// DiskANN
|
||||
DiskAnn diskann = 5;
|
||||
}
|
||||
}
|
||||
|
||||
// Metric Type for Vector Index
|
||||
enum VectorMetricType {
|
||||
// L2 (Euclidean) Distance
|
||||
L2 = 0;
|
||||
|
||||
// Cosine Distance
|
||||
Cosine = 1;
|
||||
|
||||
// Dot Product
|
||||
Dot = 2;
|
||||
|
||||
// Hamming Distance
|
||||
Hamming = 3;
|
||||
}
|
||||
|
||||
// Vector Index Metadata
|
||||
message VectorIndex {
|
||||
// Index specification version.
|
||||
uint32 spec_version = 1;
|
||||
|
||||
// Vector dimension;
|
||||
uint32 dimension = 2;
|
||||
|
||||
// Composed vector index stages.
|
||||
//
|
||||
// For example, `IVF_PQ` index type can be expressed as:
|
||||
//
|
||||
// ```text
|
||||
// let stages = vec![Ivf{}, PQ{num_bits: 8, num_sub_vectors: 16}]
|
||||
// ```
|
||||
repeated VectorIndexStage stages = 3;
|
||||
|
||||
// Vector distance metrics type
|
||||
VectorMetricType metric_type = 4;
|
||||
}
|
||||
|
||||
// Details for vector indexes, stored in the manifest's index_details field.
|
||||
message VectorIndexDetails {
|
||||
VectorMetricType metric_type = 1;
|
||||
|
||||
// The target number of vectors per partition.
|
||||
// 0 means unset.
|
||||
uint64 target_partition_size = 2;
|
||||
|
||||
// Optional HNSW index configuration. If set, the index has an HNSW layer.
|
||||
optional HnswParameters hnsw_index_config = 3;
|
||||
|
||||
message ProductQuantization {
|
||||
uint32 num_bits = 1;
|
||||
uint32 num_sub_vectors = 2;
|
||||
}
|
||||
message ScalarQuantization {
|
||||
uint32 num_bits = 1;
|
||||
}
|
||||
message RabitQuantization {
|
||||
enum RotationType {
|
||||
FAST = 0;
|
||||
MATRIX = 1;
|
||||
}
|
||||
uint32 num_bits = 1;
|
||||
RotationType rotation_type = 2;
|
||||
}
|
||||
|
||||
// No quantization; vectors are stored as-is.
|
||||
message FlatCompression {}
|
||||
|
||||
oneof compression {
|
||||
ProductQuantization pq = 4;
|
||||
ScalarQuantization sq = 5;
|
||||
RabitQuantization rq = 6;
|
||||
FlatCompression flat = 8;
|
||||
}
|
||||
|
||||
// Runtime hints: optional build preferences that don't affect index structure.
|
||||
// Keys use reverse-DNS namespacing (e.g., "lance.ivf.max_iters", "lancedb.accelerator").
|
||||
// Unrecognized keys must be silently ignored by all runtimes.
|
||||
map<string, string> runtime_hints = 9;
|
||||
}
|
||||
|
||||
// Hierarchical Navigable Small World (HNSW) parameters, used as an optional configuration for IVF indexes.
|
||||
message HnswParameters {
|
||||
// The maximum number of outgoing edges per node in the HNSW graph. Higher values
|
||||
// means more connections, better recall, but more memory and slower builds.
|
||||
// Referred to as "M" in the HNSW literature.
|
||||
uint32 max_connections = 1;
|
||||
// "construction exploration factor": The size of the dynamic list used during
|
||||
// index construction.
|
||||
uint32 construction_ef = 2;
|
||||
// The maximum number of levels in the HNSW graph.
|
||||
uint32 max_level = 3;
|
||||
}
|
||||
|
||||
message JsonIndexDetails {
|
||||
string path = 1;
|
||||
google.protobuf.Any target_details = 2;
|
||||
}
|
||||
message BloomFilterIndexDetails {}
|
||||
|
||||
message RTreeIndexDetails {}
|
||||
|
||||
message FMIndexDetails {}
|
||||
@@ -1,104 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.table;
|
||||
|
||||
// NOTE: Do *NOT* add new index details here. Add them to the index.proto file instead.
|
||||
// This file is in the lance.table package namespace while the index.proto file is in the
|
||||
// lance.index package namespace.
|
||||
//
|
||||
// These are only here for forward compatibility. Older versions of Lance expect btree indexes
|
||||
// to have lance.table in the package namespace.
|
||||
//
|
||||
// If you need to modify these messages (e.g. to add new fields to btree or bitmap) then
|
||||
// it is ok to modify them here.
|
||||
|
||||
// Currently many of these are empty messages because all needed details are either hard-coded (e.g.
|
||||
// filenames) or stored in the index itself. However, we may want to add more details in the
|
||||
// future, in particular we can add details that may be useful for planning queries (e.g. don't
|
||||
// force us to load the index until we know we can make use of it)
|
||||
|
||||
message BTreeIndexDetails {}
|
||||
message BitmapIndexDetails {}
|
||||
message LabelListIndexDetails {}
|
||||
message NGramIndexDetails {}
|
||||
message ZoneMapIndexDetails {
|
||||
// Number of rows per zone. Optional for backwards compatibility: absent on
|
||||
// datasets written before this field was added. When absent, no seed writer
|
||||
// is created for the index.
|
||||
optional uint64 rows_per_zone = 1;
|
||||
// Whether seed-based incremental updates are enabled for this index.
|
||||
// On-disk semantics: absent means seeds are disabled (old datasets written
|
||||
// before this field was added). Present false means explicitly disabled.
|
||||
// Present true means seeds are enabled: the index will embed per-fragment
|
||||
// seed buffers in data files and harvest them during incremental updates
|
||||
// to skip full column scans.
|
||||
// Creation-time default: index creation code sets this to true for
|
||||
// variable-length types (strings, binary) and fixed-width types wider than
|
||||
// 8 bytes, and to false for narrow fixed-width types (e.g. Int64, Float64).
|
||||
optional bool use_seeds = 2;
|
||||
// Whether this index tracks exact null row addresses in a separate bitmap.
|
||||
// Absent or false means legacy format: null positions are not tracked and
|
||||
// IS NULL searches fall back to approximate zone-level statistics. Present
|
||||
// true means IS NULL is exact and IS NOT NULL can be answered without a
|
||||
// full scan.
|
||||
optional bool has_null_bitmap = 3;
|
||||
}
|
||||
message InvertedIndexDetails {
|
||||
enum DocumentGranularity {
|
||||
ROW = 0;
|
||||
LIST_ELEMENT = 1;
|
||||
}
|
||||
|
||||
message CodeTokenizerConfig {
|
||||
// Split one lexical identifier into subwords, e.g. getUserName ->
|
||||
// get/user/name.
|
||||
bool split_identifiers = 1;
|
||||
// Split identifier subwords across letter/number boundaries, e.g.
|
||||
// HTML2JSON -> html/2/json. An absent value uses the code tokenizer default;
|
||||
// a present value records the explicit index-time choice.
|
||||
optional bool split_on_numerics = 2;
|
||||
// Keep the complete lexical identifier in addition to subwords, e.g.
|
||||
// user_name plus user/name. An absent value uses the code tokenizer default;
|
||||
// a present value records the explicit index-time choice.
|
||||
optional bool preserve_original = 3;
|
||||
// Index operator tokens such as "::", "->", and "!=". Operators are not
|
||||
// indexed by default because they are often high-frequency noise.
|
||||
bool index_operators = 4;
|
||||
}
|
||||
|
||||
// Lexical tokenizer used after document-level text extraction. This is an
|
||||
// implementation component such as "simple", "icu", "ngram", or "code".
|
||||
// Input-time analyzer profiles are expanded into this field and the concrete
|
||||
// options below before these details are persisted.
|
||||
// Marking this field as optional as old versions of the index store blank details and we
|
||||
// need to make sure we have a proper optional field to detect this.
|
||||
optional string base_tokenizer = 1;
|
||||
string language = 2;
|
||||
bool with_position = 3;
|
||||
optional uint32 max_token_length = 4;
|
||||
bool lower_case = 5;
|
||||
bool stem = 6;
|
||||
bool remove_stop_words = 7;
|
||||
bool ascii_folding = 8;
|
||||
uint32 min_ngram_length = 9;
|
||||
uint32 max_ngram_length = 10;
|
||||
bool prefix_only = 11;
|
||||
// Number of documents per compressed posting block. An absent value means
|
||||
// the index predates this field and must use the legacy block size of 128.
|
||||
// A present value records the block size used by the index; 256 is valid
|
||||
// with format versions 3 and 4.
|
||||
optional uint32 block_size = 12;
|
||||
// Options for base_tokenizer = "code". Presence records the code tokenizer
|
||||
// configuration used to build the index; absence means there is no
|
||||
// code-specific configuration to apply.
|
||||
CodeTokenizerConfig code_config = 13;
|
||||
// The logical FTS document boundary. The protobuf default preserves the
|
||||
// legacy row-document behavior when this field is absent.
|
||||
DocumentGranularity document_granularity = 14;
|
||||
// The posting-list payload format. This is separate from index_version,
|
||||
// which identifies the overall inverted-index layout.
|
||||
optional uint32 posting_format_version = 15;
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
@@ -1,113 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.table;
|
||||
// TODO: what would it take to store this in a LanceV2 file?
|
||||
// Or would flatbuffers be better for this?
|
||||
|
||||
/// A sequence of row IDs. This is split up into one or more segments,
|
||||
/// each of which can be encoded in different ways. The encodings are optimized
|
||||
/// for values that are sorted, which will often be the case with row ids.
|
||||
/// They also have optimized forms depending on how sparse the values are.
|
||||
message RowIdSequence {
|
||||
repeated U64Segment segments = 1;
|
||||
}
|
||||
|
||||
/// Different ways to encode a sequence of u64 values.
|
||||
message U64Segment {
|
||||
/// A range of u64 values.
|
||||
message Range {
|
||||
/// The start of the range, inclusive.
|
||||
uint64 start = 1;
|
||||
/// The end of the range, exclusive.
|
||||
uint64 end = 2;
|
||||
}
|
||||
|
||||
/// A range of u64 values with holes.
|
||||
message RangeWithHoles {
|
||||
/// The start of the range, inclusive.
|
||||
uint64 start = 1;
|
||||
/// The end of the range, exclusive.
|
||||
uint64 end = 2;
|
||||
/// The holes in the range, as a sorted array of values;
|
||||
/// Binary search can be used to check whether a value is a hole and should
|
||||
/// be skipped. This can also be used to count the number of holes before a
|
||||
/// given value, if you need to find the logical offset of a value in the
|
||||
/// segment.
|
||||
EncodedU64Array holes = 3;
|
||||
}
|
||||
|
||||
/// A range of u64 values with a bitmap.
|
||||
message RangeWithBitmap {
|
||||
/// The start of the range, inclusive.
|
||||
uint64 start = 1;
|
||||
/// The end of the range, exclusive.
|
||||
uint64 end = 2;
|
||||
/// A bitmap of the values in the range. The bitmap is a sequence of bytes,
|
||||
/// where each byte represents 8 values. The first byte represents values
|
||||
/// start to start + 7, the second byte represents values start + 8 to
|
||||
/// start + 15, and so on. The most significant bit of each byte represents
|
||||
/// the first value in the range, and the least significant bit represents
|
||||
/// the last value in the range. If the bit is set, the value is in the
|
||||
/// range; if it is not set, the value is not in the range.
|
||||
bytes bitmap = 3;
|
||||
}
|
||||
|
||||
oneof segment {
|
||||
/// When the values are sorted and contiguous.
|
||||
Range range = 1;
|
||||
/// When the values are sorted but have a few gaps.
|
||||
RangeWithHoles range_with_holes = 2;
|
||||
/// When the values are sorted but have many gaps.
|
||||
RangeWithBitmap range_with_bitmap = 3;
|
||||
/// When the values are sorted but are sparse.
|
||||
EncodedU64Array sorted_array = 4;
|
||||
/// A general array of values, which is not sorted.
|
||||
EncodedU64Array array = 5;
|
||||
}
|
||||
} // RowIdSegment
|
||||
|
||||
/// A basic bitpacked array of u64 values.
|
||||
message EncodedU64Array {
|
||||
message U16Array {
|
||||
uint64 base = 1;
|
||||
/// The deltas are stored as 16-bit unsigned integers.
|
||||
/// (protobuf doesn't support 16-bit integers, so we use bytes instead)
|
||||
bytes offsets = 2;
|
||||
}
|
||||
|
||||
message U32Array {
|
||||
uint64 base = 1;
|
||||
/// The deltas are stored as 32-bit unsigned integers.
|
||||
/// (we use bytes instead of uint32 to avoid overhead of varint encoding)
|
||||
bytes offsets = 2;
|
||||
}
|
||||
|
||||
message U64Array {
|
||||
/// (We use bytes instead of uint64 to avoid overhead of varint encoding)
|
||||
bytes values = 2;
|
||||
}
|
||||
|
||||
oneof array {
|
||||
U16Array u16_array = 1;
|
||||
U32Array u32_array = 2;
|
||||
U64Array u64_array = 3;
|
||||
}
|
||||
}
|
||||
|
||||
/// A sequence of dataset versions. Similar to RowIdSequence but tracks
|
||||
/// version runs. It uses RLE (Run-Length Encoding) to efficiently
|
||||
// represent consecutive rows with the same version.
|
||||
message RowDatasetVersionSequence {
|
||||
repeated RowDatasetVersionRun runs = 1;
|
||||
}
|
||||
|
||||
/// A run of rows with the same version.
|
||||
message RowDatasetVersionRun {
|
||||
/// The number of consecutive rows with the same version.
|
||||
U64Segment span = 1;
|
||||
|
||||
uint64 version = 2;
|
||||
}
|
||||
@@ -1,805 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.table;
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "file.proto";
|
||||
|
||||
/*
|
||||
|
||||
Format:
|
||||
|
||||
+----------------------------------------+
|
||||
| Encoded Column 0, Chunk 0 |
|
||||
...
|
||||
| Encoded Column M, Chunk N - 1 |
|
||||
| Encoded Column M, Chunk N |
|
||||
| Indices ... |
|
||||
| Chunk Position (M x N x 8) |
|
||||
| Manifest (Optional) |
|
||||
| Metadata |
|
||||
| i64: metadata position |
|
||||
| MAJOR_VERSION | MINOR_VERSION | "LANC" |
|
||||
+----------------------------------------+
|
||||
*/
|
||||
|
||||
// UUID type. encoded as 16 bytes.
|
||||
message UUID {
|
||||
bytes uuid = 1;
|
||||
}
|
||||
|
||||
// Manifest is a global section shared between all the files.
|
||||
message Manifest {
|
||||
// All fields of the dataset, including the nested fields.
|
||||
repeated lance.file.Field fields = 1;
|
||||
|
||||
// Schema metadata.
|
||||
map<string, bytes> schema_metadata = 5;
|
||||
|
||||
// Fragments of the dataset.
|
||||
repeated DataFragment fragments = 2;
|
||||
|
||||
// Snapshot version number.
|
||||
uint64 version = 3;
|
||||
|
||||
// The file position of the version auxiliary data.
|
||||
// * It is not inheritable between versions.
|
||||
// * It is not loaded by default during query.
|
||||
uint64 version_aux_data = 4;
|
||||
|
||||
message WriterVersion {
|
||||
// The name of the library that created this file.
|
||||
string library = 1;
|
||||
// The version of the library that created this file. Because we cannot assume
|
||||
// that the library is semantically versioned, this is a string. However, if it
|
||||
// is semantically versioned, it should be a valid semver string without any 'v'
|
||||
// prefix. For example: `2.0.0`, `2.0.0-rc.1`.
|
||||
//
|
||||
// For forward compatibility with older readers, when writing new manifests this
|
||||
// field should contain only the core version (major.minor.patch) without any
|
||||
// prerelease or build metadata. The prerelease/build info should be stored in
|
||||
// the separate prerelease and build_metadata fields instead.
|
||||
string version = 2;
|
||||
// Optional semver prerelease identifier.
|
||||
//
|
||||
// This field stores the prerelease portion of a semantic version separately
|
||||
// from the core version number. For example, if the full version is "2.0.0-rc.1",
|
||||
// the version field would contain "2.0.0" and prerelease would contain "rc.1".
|
||||
//
|
||||
// This separation ensures forward compatibility: older readers can parse the
|
||||
// clean version field without errors, while newer readers can reconstruct the
|
||||
// full semantic version by combining version, prerelease, and build_metadata.
|
||||
//
|
||||
// If absent, the version field is used as-is.
|
||||
optional string prerelease = 3;
|
||||
// Optional semver build metadata.
|
||||
//
|
||||
// This field stores the build metadata portion of a semantic version separately
|
||||
// from the core version number. For example, if the full version is
|
||||
// "2.0.0-rc.1+build.123", the version field would contain "2.0.0", prerelease
|
||||
// would contain "rc.1", and build_metadata would contain "build.123".
|
||||
//
|
||||
// If absent, no build metadata is present.
|
||||
optional string build_metadata = 4;
|
||||
}
|
||||
|
||||
// The version of the writer that created this file.
|
||||
//
|
||||
// This information may be used to detect whether the file may have known bugs
|
||||
// associated with that writer.
|
||||
WriterVersion writer_version = 13;
|
||||
|
||||
// If present, the file position of the index metadata.
|
||||
optional uint64 index_section = 6;
|
||||
|
||||
// Version creation Timestamp, UTC timezone
|
||||
google.protobuf.Timestamp timestamp = 7;
|
||||
|
||||
// Optional version tag
|
||||
string tag = 8;
|
||||
|
||||
// Feature flags for readers.
|
||||
//
|
||||
// A bitmap of flags that indicate which features are required to be able to
|
||||
// read the table. If a reader does not recognize a flag that is set, it
|
||||
// should not attempt to read the dataset.
|
||||
//
|
||||
// Known flags:
|
||||
// * 1 << 0: deletion files are present
|
||||
// * 1 << 1: row ids are stable and stored as part of the fragment metadata.
|
||||
// * 1 << 2: use v2 format (deprecated)
|
||||
// * 1 << 3: table config is present
|
||||
// * 1 << 4: dataset uses multiple base paths
|
||||
// * 1 << 5: transaction file writes are disabled
|
||||
// * 1 << 6: data overlay files are present (see DataOverlayFile). Readers that do
|
||||
// not understand overlays must refuse the dataset, since ignoring an overlay
|
||||
// would silently return stale base values.
|
||||
uint64 reader_feature_flags = 9;
|
||||
|
||||
// Feature flags for writers.
|
||||
//
|
||||
// A bitmap of flags that indicate which features must be used when writing to the
|
||||
// dataset. If a writer does not recognize a flag that is set, it should not attempt to
|
||||
// write to the dataset.
|
||||
//
|
||||
// The flag identities are the same as for reader_feature_flags, but the values of
|
||||
// reader_feature_flags and writer_feature_flags are not required to be identical.
|
||||
uint64 writer_feature_flags = 10;
|
||||
|
||||
// The highest fragment ID that has been used so far.
|
||||
//
|
||||
// This ID is not guaranteed to be present in the current version, but it may
|
||||
// have been used in previous versions.
|
||||
//
|
||||
// For a single fragment, will be zero. For no fragments, will be absent.
|
||||
optional uint32 max_fragment_id = 11;
|
||||
|
||||
// Path to the transaction file, relative to `{root}/_transactions`. The file at that
|
||||
// location contains a wire-format serialized Transaction message representing the
|
||||
// transaction that created this version.
|
||||
//
|
||||
// This string field "transaction_file" may be empty if no transaction file was written.
|
||||
//
|
||||
// The path format is "{read_version}-{uuid}.txn" where {read_version} is the version of
|
||||
// the table the transaction read from (serialized to decimal with no padding digits),
|
||||
// and {uuid} is a hyphen-separated UUID.
|
||||
string transaction_file = 12;
|
||||
|
||||
// The file position of the transaction content. None if transaction is empty
|
||||
// This transaction content begins with the transaction content length as u32
|
||||
// If the transaction proto message has a length of `len`, the message ends at `len` + 4
|
||||
optional uint64 transaction_section = 21;
|
||||
|
||||
// The next unused row id. If zero, then the table does not have any rows.
|
||||
//
|
||||
// This is only used if the "stable_row_ids" feature flag is set.
|
||||
uint64 next_row_id = 14;
|
||||
|
||||
message DataStorageFormat {
|
||||
// The format of the data files (e.g. "lance")
|
||||
string file_format = 1;
|
||||
// The max format version of the data files. The format of the version can vary by
|
||||
// file_format and is not required to follow semver.
|
||||
//
|
||||
// Every file in this version of the dataset has the same file_format version.
|
||||
string version = 2;
|
||||
}
|
||||
|
||||
// The data storage format
|
||||
//
|
||||
// This specifies what format is used to store the data files.
|
||||
DataStorageFormat data_format = 15;
|
||||
|
||||
// Table config.
|
||||
//
|
||||
// Keys with the prefix "lance." are reserved for the Lance library. Other
|
||||
// libraries may wish to similarly prefix their configuration keys
|
||||
// appropriately.
|
||||
map<string, string> config = 16;
|
||||
|
||||
// Metadata associated with the table.
|
||||
//
|
||||
// This is a key-value map that can be used to store arbitrary metadata
|
||||
// associated with the table.
|
||||
//
|
||||
// This is different than configuration, which is used to tell libraries how
|
||||
// to read, write, or manage the table.
|
||||
//
|
||||
// This is different than schema metadata, which is used to describe the
|
||||
// data itself and is attached to the output schema of scans.
|
||||
map<string, string> table_metadata = 19;
|
||||
|
||||
// Field number 17 (`blob_dataset_version`) was used for a secondary blob dataset.
|
||||
reserved 17;
|
||||
reserved "blob_dataset_version";
|
||||
|
||||
// The base paths of data files.
|
||||
//
|
||||
// This is used to determine the base path of a data file. In common cases data file paths are under current dataset base path.
|
||||
// But for shallow cloning, importing file and other multi-tier storage cases, the actual data files could be outside of the current dataset.
|
||||
// This field is used with the `base_id` in `lance.file.File` and `lance.file.DeletionFile`.
|
||||
//
|
||||
// For example, if we have a dataset with base path `s3://bucket/dataset`, we have a DataFile with base_id 0, we get the actual data file path by:
|
||||
// base_paths[id = 0] + /data/ + file.path
|
||||
// the key(a.k.a index) starts from 0, increased by 1 for each new base path.
|
||||
repeated BasePath base_paths = 18;
|
||||
|
||||
// The branch of the dataset. None means main branch.
|
||||
optional string branch = 20;
|
||||
} // Manifest
|
||||
|
||||
// external dataset base path
|
||||
message BasePath {
|
||||
uint32 id = 1;
|
||||
// This is an alias name of the base path, it is optional.
|
||||
// When we use shallow clone and the target version is a tag, the tag name will be set here.
|
||||
optional string name = 2;
|
||||
// Flag indicating whether this path is a dataset root path or file directory:
|
||||
// - true: Path is a dataset root (actual files under subdirectories like `data`, '_deletions')
|
||||
// - false: Path is a direct file directory (scenario like importing files)
|
||||
bool is_dataset_root = 3;
|
||||
// Note: This absolute path will be directly used by Path:parse(),
|
||||
string path = 4;
|
||||
}
|
||||
|
||||
// Auxiliary Data attached to a version.
|
||||
// Only load on-demand.
|
||||
message VersionAuxData {
|
||||
// key-value metadata.
|
||||
map<string, bytes> metadata = 3;
|
||||
}
|
||||
|
||||
// Metadata describing an index.
|
||||
message IndexMetadata {
|
||||
// Unique ID of an index. It is unique across all the dataset versions.
|
||||
UUID uuid = 1;
|
||||
|
||||
// The columns to build the index. These refer to file.Field.id.
|
||||
repeated int32 fields = 2;
|
||||
|
||||
// Index name. Must be unique within one dataset version.
|
||||
string name = 3;
|
||||
|
||||
// The version of the dataset this index was built from.
|
||||
uint64 dataset_version = 4;
|
||||
|
||||
// A bitmap of the included fragment ids.
|
||||
//
|
||||
// This may by used to determine how much of the dataset is covered by the
|
||||
// index. This information can be retrieved from the dataset by looking at
|
||||
// the dataset at `dataset_version`. However, since the old version may be
|
||||
// deleted while the index is still in use, this information is also stored
|
||||
// in the index.
|
||||
//
|
||||
// The bitmap is stored as a 32-bit Roaring bitmap.
|
||||
bytes fragment_bitmap = 5;
|
||||
|
||||
// Details, specific to the index type, which are needed to load / interpret the index
|
||||
//
|
||||
// Indices should avoid putting large amounts of information in this field, as it will
|
||||
// bloat the manifest.
|
||||
//
|
||||
// Indexes are plugins, and so the format of the details message is flexible and not fully
|
||||
// defined by the table format. However, there are some conventions that should be followed:
|
||||
//
|
||||
// - When Lance APIs refer to indexes they will use the type URL of the index details as the
|
||||
// identifier for the index type. If a user provides a simple string identifier like
|
||||
// "btree" then it will be converted to "/lance.table.BTreeIndexDetails"
|
||||
// - Type URLs comparisons are case-insensitive. Thereform an index must have a unique type
|
||||
// URL ignoring case.
|
||||
google.protobuf.Any index_details = 6;
|
||||
|
||||
// The minimum lance version that this index is compatible with.
|
||||
optional int32 index_version = 7;
|
||||
|
||||
// Timestamp when the index was created (UTC timestamp in milliseconds since epoch)
|
||||
//
|
||||
// This field is optional for backward compatibility. For existing indices created before
|
||||
// this field was added, this will be None/null.
|
||||
optional uint64 created_at = 8;
|
||||
|
||||
// The base path index of the data file. Used when the file is imported or referred from another dataset.
|
||||
// Lance use it as key of the base_paths field in Manifest to determine the actual base path of the data file.
|
||||
optional uint32 base_id = 9;
|
||||
|
||||
// List of files and their sizes for this index segment.
|
||||
// This enables skipping HEAD calls when opening indices and allows reporting
|
||||
// of index sizes without extra IO.
|
||||
// If this is empty, the index files sizes are unknown.
|
||||
repeated IndexFile files = 10;
|
||||
}
|
||||
|
||||
// Metadata about a single file within an index segment.
|
||||
message IndexFile {
|
||||
// Path relative to the index directory (e.g., "index.idx", "auxiliary.idx")
|
||||
string path = 1;
|
||||
// Size of the file in bytes
|
||||
uint64 size_bytes = 2;
|
||||
}
|
||||
|
||||
// Index Section, containing a list of index metadata for one dataset version.
|
||||
message IndexSection {
|
||||
repeated IndexMetadata indices = 1;
|
||||
}
|
||||
|
||||
// A DataFragment is a set of files which represent the different columns of the same
|
||||
// rows. If column exists in the schema of a dataset, but the file for that column does
|
||||
// not exist within a DataFragment of that dataset, that column consists entirely of
|
||||
// nulls.
|
||||
message DataFragment {
|
||||
// The ID of a DataFragment is unique within a dataset.
|
||||
uint64 id = 1;
|
||||
|
||||
repeated DataFile files = 2;
|
||||
|
||||
// Optional overlay files for this fragment, which supply new values for a
|
||||
// subset of cells without rewriting the base data files. This MUST be empty
|
||||
// if the data overlay files feature flag (64) is not set in the manifest.
|
||||
//
|
||||
// Order is significant: a later entry is newer than an earlier one. When two
|
||||
// overlays cover the same (offset, field) and share a `committed_version`, the
|
||||
// later entry wins. See DataOverlayFile for the full resolution rules.
|
||||
repeated DataOverlayFile overlays = 11;
|
||||
|
||||
// File that indicates which rows, if any, should be considered deleted.
|
||||
DeletionFile deletion_file = 3;
|
||||
|
||||
// TODO: What's the simplest way we can allow an inline tombstone bitmap?
|
||||
|
||||
// A serialized RowIdSequence message (see rowids.proto).
|
||||
//
|
||||
// These are the row ids for the fragment, in order of the rows as they appear.
|
||||
// That is, if a fragment has 3 rows, and the row ids are [1, 42, 3], then the
|
||||
// first row is row 1, the second row is row 42, and the third row is row 3.
|
||||
oneof row_id_sequence {
|
||||
// If small (< 200KB), the row ids are stored inline.
|
||||
bytes inline_row_ids = 5;
|
||||
// Otherwise, stored as part of a file.
|
||||
ExternalFile external_row_ids = 6;
|
||||
} // row_id_sequence
|
||||
|
||||
oneof last_updated_at_version_sequence {
|
||||
// If small (< 200KB), the row latest updated versions are stored inline.
|
||||
bytes inline_last_updated_at_versions = 7;
|
||||
// Otherwise, stored as part of a file.
|
||||
ExternalFile external_last_updated_at_versions = 8;
|
||||
} // last_updated_at_version_sequence
|
||||
|
||||
oneof created_at_version_sequence {
|
||||
// If small (< 200KB), the row created at versions are stored inline.
|
||||
bytes inline_created_at_versions = 9;
|
||||
// Otherwise, stored as part of a file.
|
||||
ExternalFile external_created_at_versions = 10;
|
||||
} // created_at_version_sequence
|
||||
|
||||
// Number of original rows in the fragment, this includes rows that are now marked with
|
||||
// deletion tombstones. To compute the current number of rows, subtract
|
||||
// `deletion_file.num_deleted_rows` from this value.
|
||||
uint64 physical_rows = 4;
|
||||
}
|
||||
|
||||
message DataFile {
|
||||
// Path to the root relative to the dataset's URI.
|
||||
string path = 1;
|
||||
// The ids of the fields/columns in this file.
|
||||
//
|
||||
// When a DataFile object is created in memory, every value in fields is assigned -1 by
|
||||
// default. An object with a value in fields of -1 must not be stored to disk. -2 is
|
||||
// used for "tombstoned", meaning a field that is no longer in use. This is often
|
||||
// because the original field id was reassigned to a different data file.
|
||||
//
|
||||
// In Lance v1 IDs are assigned based on position in the file, offset by the max
|
||||
// existing field id in the table (if any already). So when a fragment is first created
|
||||
// with one file of N columns, the field ids will be 1, 2, ..., N. If a second fragment
|
||||
// is created with M columns, the field ids will be N+1, N+2, ..., N+M.
|
||||
//
|
||||
// In Lance v1 there is one field for each field in the input schema, this includes
|
||||
// nested fields (both struct and list). Fixed size list fields have only a single
|
||||
// field id (these are not considered nested fields in Lance v1).
|
||||
//
|
||||
// This allows column indices to be calculated from field IDs and the input schema.
|
||||
//
|
||||
// In Lance v2 the field IDs generally follow the same pattern but there is no
|
||||
// way to calculate the column index from the field ID. This is because a given
|
||||
// field could be encoded in many different ways, some of which occupy a different
|
||||
// number of columns. For example, a struct field could be encoded into N + 1 columns
|
||||
// or it could be encoded into a single packed column. To determine column indices
|
||||
// the column_indices property should be used instead.
|
||||
//
|
||||
// In Lance v1 these ids must be sorted but might not always be contiguous.
|
||||
repeated int32 fields = 2;
|
||||
// The top-level column indices for each field in the file.
|
||||
//
|
||||
// If the data file is version 1 then this property will be empty
|
||||
//
|
||||
// Otherwise there must be one entry for each field in `fields`.
|
||||
//
|
||||
// Some fields may not correspond to a top-level column in the file. In these cases
|
||||
// the index will -1.
|
||||
//
|
||||
// For example, consider the schema:
|
||||
//
|
||||
// - dimension: packed-struct (0):
|
||||
// - x: u32 (1)
|
||||
// - y: u32 (2)
|
||||
// - path: `list<u32>` (3)
|
||||
// - embedding: `fsl<768>` (4)
|
||||
// - fp64
|
||||
// - borders: `fsl<4>` (5)
|
||||
// - simple-struct (6)
|
||||
// - margin: fp64 (7)
|
||||
// - padding: fp64 (8)
|
||||
//
|
||||
// One possible column indices array could be:
|
||||
// [0, -1, -1, 1, 3, 4, 5, 6, 7]
|
||||
//
|
||||
// This reflects quite a few phenomenon:
|
||||
// - The packed struct is encoded into a single column and there is no top-level column
|
||||
// for the x or y fields
|
||||
// - The variable sized list is encoded into two columns
|
||||
// - The embedding is encoded into a single column (common for FSL of primitive) and there
|
||||
// is not "FSL column"
|
||||
// - The borders field actually does have an "FSL column"
|
||||
//
|
||||
// The column indices table may not have duplicates (other than -1)
|
||||
repeated int32 column_indices = 3;
|
||||
// The major file version used to create the file
|
||||
uint32 file_major_version = 4;
|
||||
// The minor file version used to create the file
|
||||
//
|
||||
// If both `file_major_version` and `file_minor_version` are set to 0,
|
||||
// then this is a version 0.1 or version 0.2 file.
|
||||
uint32 file_minor_version = 5;
|
||||
|
||||
// The known size of the file on disk in bytes.
|
||||
//
|
||||
// This is used to quickly find the footer of the file.
|
||||
//
|
||||
// When this is zero, it should be interpreted as "unknown".
|
||||
uint64 file_size_bytes = 6;
|
||||
|
||||
// The base path index of the data file. Used when the file is imported or referred from another dataset.
|
||||
// Lance use it as key of the base_paths field in Manifest to determine the actual base path of the data file.
|
||||
optional uint32 base_id = 7;
|
||||
} // DataFile
|
||||
|
||||
// An overlay file supplies new values for a subset of (row offset, field) cells
|
||||
// within a fragment, without rewriting the fragment's base data files. It is
|
||||
// used for efficient updates when only a small fraction of rows and/or columns
|
||||
// change.
|
||||
//
|
||||
// On read, a cell is resolved by consulting the fragment's overlays from newest
|
||||
// to oldest: the first overlay that covers that (offset, field) wins; if none
|
||||
// cover it, the value falls through to the base data file. Because deletions
|
||||
// take precedence over overlays, an overlay value for an offset that is also
|
||||
// marked deleted is dead and is ignored.
|
||||
//
|
||||
// The overlay's data file does NOT store a row-offset key column. Within a value
|
||||
// column, the position of a covered offset's value is the rank (0-based count of
|
||||
// set bits below it) of that offset within the field's coverage bitmap. Because
|
||||
// fields may cover different offset sets, the value columns of a single overlay
|
||||
// data file may have different lengths (which the Lance file format permits).
|
||||
message DataOverlayFile {
|
||||
// The data file storing the overlay's new cell values, one value column per
|
||||
// field in `data_file.fields`. No row-offset key column is stored.
|
||||
DataFile data_file = 1;
|
||||
|
||||
// Which (offset, field) cells this overlay provides values for.
|
||||
oneof coverage {
|
||||
// A single 32-bit Roaring bitmap of physical row offsets that applies to
|
||||
// every field in `data_file.fields` (a "dense" / rectangular overlay).
|
||||
// Every covered offset has a value for every field. This is the common case
|
||||
// for a plain UPDATE, where one SET list is applied to one set of rows.
|
||||
bytes shared_offset_bitmap = 2;
|
||||
// Per-field coverage for a "sparse" overlay, used when different fields cover
|
||||
// different offset sets (e.g. a MERGE with multiple WHEN MATCHED branches).
|
||||
FieldCoverage field_coverage = 4;
|
||||
}
|
||||
|
||||
// The dataset version at which this overlay became effective: the version of
|
||||
// the commit that introduced it, NOT the version it was read from. It is
|
||||
// stamped at commit time and re-stamped if the commit is retried, in the same
|
||||
// way as the created-at / last-updated-at version sequences.
|
||||
//
|
||||
// This drives two orderings:
|
||||
// * Versus index builds: an index whose `dataset_version` >= this value
|
||||
// already incorporates this overlay. Otherwise the overlay's covered cells
|
||||
// are excluded from index results for the affected fields and re-evaluated
|
||||
// against their current values (see the Data Overlay Files specification).
|
||||
// * Versus other overlays: when two overlays cover the same (offset, field),
|
||||
// the one with the higher `committed_version` wins. Overlays that share a
|
||||
// `committed_version` are ordered by their position in
|
||||
// `DataFragment.overlays`, where a later entry is newer and wins.
|
||||
uint64 committed_version = 3;
|
||||
}
|
||||
|
||||
// Per-field coverage for a sparse overlay.
|
||||
message FieldCoverage {
|
||||
// One entry per field in the overlay's `data_file.fields`, in the same order.
|
||||
// Each is a 32-bit Roaring bitmap of the physical row offsets covered for that
|
||||
// field. An offset present in a field's bitmap but mapped to a NULL value
|
||||
// means the cell is overridden to NULL (distinct from an offset that is absent,
|
||||
// which falls through to the base data file).
|
||||
repeated bytes offset_bitmaps = 1;
|
||||
}
|
||||
|
||||
// Deletion File
|
||||
//
|
||||
// The path of the deletion file is constructed as:
|
||||
// {root}/_deletions/{fragment_id}-{read_version}-{id}.{extension}
|
||||
// where {extension} depends on DeletionFileType.
|
||||
message DeletionFile {
|
||||
// Type of deletion file, intended as a way to increase efficiency of the storage of deleted row
|
||||
// offsets. If there are sparsely deleted rows, then ARROW_ARRAY is the most efficient. If there
|
||||
// are densely deleted rows, then BITMAP is the most efficient.
|
||||
enum DeletionFileType {
|
||||
// A single Int32Array of deleted row offsets, stored as an Arrow IPC file with one batch and
|
||||
// one column. Has a .arrow extension.
|
||||
ARROW_ARRAY = 0;
|
||||
// A Roaring Bitmap of deleted row offsets. Has a .bin extension.
|
||||
BITMAP = 1;
|
||||
}
|
||||
|
||||
// Type of deletion file.
|
||||
DeletionFileType file_type = 1;
|
||||
// The version of the dataset this deletion file was built from.
|
||||
uint64 read_version = 2;
|
||||
// An opaque id used to differentiate this file from others written by concurrent
|
||||
// writers.
|
||||
uint64 id = 3;
|
||||
// The number of rows that are marked as deleted.
|
||||
uint64 num_deleted_rows = 4;
|
||||
// The base path index of the deletion file. Used when the file is imported or referred from another
|
||||
// dataset. Lance uses it as key of the base_paths field in Manifest to determine the actual base
|
||||
// path of the deletion file.
|
||||
optional uint32 base_id = 7;
|
||||
} // DeletionFile
|
||||
|
||||
message ExternalFile {
|
||||
// Path to the file, relative to the root of the table.
|
||||
string path = 1;
|
||||
// The byte offset in the file where the data starts.
|
||||
uint64 offset = 2;
|
||||
// The size of the data in the file, in bytes.
|
||||
uint64 size = 3;
|
||||
}
|
||||
|
||||
// VectorIndexDetails and HnswParameters (formerly HnswIndexDetails) moved to index.proto
|
||||
|
||||
message FragmentReuseIndexDetails {
|
||||
|
||||
oneof content {
|
||||
// if < 200KB, store the content inline, otherwise store the InlineContent bytes in external file
|
||||
InlineContent inline = 1;
|
||||
ExternalFile external = 2;
|
||||
}
|
||||
|
||||
message InlineContent {
|
||||
repeated Version versions = 1;
|
||||
}
|
||||
|
||||
message FragmentDigest {
|
||||
uint64 id = 1;
|
||||
|
||||
uint64 physical_rows = 2;
|
||||
|
||||
uint64 num_deleted_rows = 3;
|
||||
}
|
||||
|
||||
// A summarized version of the RewriteGroup information in a Rewrite transaction
|
||||
message Group {
|
||||
// A roaring treemap of the changed row addresses.
|
||||
// When combined with the old fragment IDs and new fragment IDs,
|
||||
// it can recover the full mapping of old row addresses to either new row addresses or deleted.
|
||||
// this mapping can then be used to remap indexes or satisfy index queries for the new unindexed fragments.
|
||||
bytes changed_row_addrs = 1;
|
||||
|
||||
repeated FragmentDigest old_fragments = 2;
|
||||
|
||||
repeated FragmentDigest new_fragments = 3;
|
||||
}
|
||||
|
||||
message Version {
|
||||
// The dataset_version at the time the index adds this version entry
|
||||
uint64 dataset_version = 1;
|
||||
|
||||
repeated Group groups = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MemWAL Index Types
|
||||
// ============================================================================
|
||||
|
||||
// Lifecycle status of a WAL shard. Drives drop-table two-phase commit:
|
||||
// a SEALED shard refuses new writer claims (reversible) until the drop
|
||||
// commits (the shard dir is deleted) or rolls back (status -> ACTIVE).
|
||||
enum ShardStatus {
|
||||
// Normal: the shard accepts writer claims.
|
||||
ACTIVE = 0;
|
||||
// A drop is in flight: claims are refused. Reversible to ACTIVE.
|
||||
SEALED = 1;
|
||||
}
|
||||
|
||||
// Shard manifest containing epoch-based fencing and WAL state.
|
||||
// Each shard has exactly one active writer at any time.
|
||||
message ShardManifest {
|
||||
// Shard identifier (UUID v4).
|
||||
UUID shard_id = 11;
|
||||
|
||||
// Manifest version number.
|
||||
// Matches the version encoded in the filename.
|
||||
uint64 version = 1;
|
||||
|
||||
// Shard spec ID this shard was created with.
|
||||
// Set at shard creation and immutable thereafter.
|
||||
// A value of 0 indicates a manually-created shard not governed by any spec.
|
||||
uint32 shard_spec_id = 10;
|
||||
|
||||
// Computed shard field values as raw Arrow scalar bytes, keyed by shard
|
||||
// field id. The byte encoding follows Arrow's little-endian convention:
|
||||
// int32 is 4 LE bytes, utf8 is raw UTF-8 bytes, etc. The receiver looks
|
||||
// up the result_type from the ShardingSpec to interpret each value.
|
||||
repeated ShardFieldEntry shard_field_entries = 14;
|
||||
|
||||
// Writer fencing token - monotonically increasing.
|
||||
// A writer must increment this when claiming the shard.
|
||||
uint64 writer_epoch = 2;
|
||||
|
||||
// The most recent WAL entry position that has been flushed to a MemTable.
|
||||
// During recovery, replay starts from replay_after_wal_entry_position + 1.
|
||||
// WAL positions are 1-based, so the default value 0 unambiguously means
|
||||
// "no flush has ever stamped this shard" and recovery replays from 1.
|
||||
uint64 replay_after_wal_entry_position = 3;
|
||||
|
||||
// The most recent WAL entry position observed at the time the manifest was
|
||||
// updated. WAL positions are 1-based; default 0 means no entry has been
|
||||
// written yet. This is a hint, not authoritative - recovery must list
|
||||
// files to find actual state.
|
||||
uint64 wal_entry_position_last_seen = 4;
|
||||
|
||||
// Generation to assign to the next SSTable (incremented after each MemTable flush).
|
||||
uint64 current_generation = 6;
|
||||
|
||||
// Field 7 removed: compaction progress lives in
|
||||
// MemWalIndexDetails.compacted_sstables.
|
||||
|
||||
// List of SSTables created by flushing MemTables and their directory paths.
|
||||
repeated SsTable sstables = 8;
|
||||
|
||||
// Lifecycle status. Default ACTIVE; SEALED marks an in-flight drop
|
||||
// (drop-table 2PC). A SEALED manifest refuses claims at claim_epoch.
|
||||
ShardStatus status = 15;
|
||||
}
|
||||
|
||||
// A shard field value stored as raw Arrow scalar bytes.
|
||||
message ShardFieldEntry {
|
||||
// Shard field id (matches ShardingField.field_id in the ShardingSpec).
|
||||
string field_id = 1;
|
||||
|
||||
// Raw Arrow scalar value bytes in little-endian encoding.
|
||||
// The data type is determined by the result_type of the matching ShardingField.
|
||||
bytes value = 2;
|
||||
}
|
||||
|
||||
// An SSTable: the immutable result of flushing a MemTable, stored as a Lance dataset.
|
||||
message SsTable {
|
||||
// Generation number identifying this SSTable.
|
||||
uint64 generation = 1;
|
||||
|
||||
// Directory name relative to the shard directory.
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
// A pointer to the latest SSTable compacted for a shard.
|
||||
message CompactedSsTable {
|
||||
// Shard identifier (UUID v4).
|
||||
UUID shard_id = 1;
|
||||
|
||||
// Generation of the latest SSTable compacted into the base table for this shard.
|
||||
uint64 generation = 2;
|
||||
}
|
||||
|
||||
// Tracks which compacted SSTable generation a base table index has been rebuilt to cover.
|
||||
// Used to determine whether to read from SSTable indexes or base table.
|
||||
message IndexCatchupProgress {
|
||||
// Name of the base table index (must match an entry in maintained_indexes).
|
||||
string index_name = 1;
|
||||
|
||||
// Per-shard progress: the generation up to which this index covers.
|
||||
// If a shard is not present, the index is assumed to be fully caught up
|
||||
// (i.e., caught_up_generation >= compacted_generation for that shard).
|
||||
repeated CompactedSsTable caught_up_generations = 2;
|
||||
}
|
||||
|
||||
// Index details for MemWAL Index, stored in IndexMetadata.index_details.
|
||||
// This is the centralized structure for all MemWAL metadata:
|
||||
// - Configuration (sharding specs, indexes to maintain)
|
||||
// - SSTable compaction progress
|
||||
// - Shard state snapshots
|
||||
//
|
||||
// Writers read this index to get configuration before writing.
|
||||
// Readers may use shard snapshots in this index as a point-in-time
|
||||
// optimization. Readers that need the latest shard set should list shard
|
||||
// directories in storage and read each shard's latest manifest.
|
||||
// A background process updates the index periodically to keep shard snapshots current.
|
||||
//
|
||||
// Shard snapshots are stored as a Lance file with one row per shard.
|
||||
// The schema records shard discovery fields. Full mutable shard state remains
|
||||
// authoritative in the shard manifest files.
|
||||
// shard_id: utf8
|
||||
// shard_spec_id: uint32
|
||||
// shard_field_{field_id}: typed per the matching ShardingField.result_type
|
||||
message MemWalIndexDetails {
|
||||
// Snapshot timestamp (Unix timestamp in milliseconds).
|
||||
int64 snapshot_ts_millis = 1;
|
||||
|
||||
// Number of shards in the snapshot.
|
||||
// Used to determine storage format without reading the snapshot data.
|
||||
uint32 num_shards = 2;
|
||||
|
||||
// Inline shard snapshots for small shard counts.
|
||||
// When num_shards <= threshold (implementation-defined, e.g., 100),
|
||||
// snapshots are stored inline as serialized bytes.
|
||||
// Format: Lance file bytes with the shard snapshot schema.
|
||||
optional bytes inline_snapshots = 3;
|
||||
|
||||
// Sharding specs defining how to derive shard identifiers.
|
||||
// This configuration determines how rows are partitioned into shards.
|
||||
repeated ShardingSpec sharding_specs = 7;
|
||||
|
||||
// Indexes from the base table to maintain in MemTables.
|
||||
// These are index names referencing indexes defined on the base table.
|
||||
// The primary key btree index is always maintained implicitly and
|
||||
// should not be listed here.
|
||||
//
|
||||
// For vector indexes, MemTables inherit quantization parameters (PQ codebook,
|
||||
// SQ params) from the base table index to ensure distance comparability.
|
||||
repeated string maintained_indexes = 8;
|
||||
|
||||
// Latest SSTable compacted into the base table for each shard.
|
||||
// This is updated atomically with merge-insert data commits, enabling
|
||||
// conflict resolution when multiple compactors operate concurrently.
|
||||
//
|
||||
// Note: This is separate from shard snapshots because:
|
||||
// 1. compacted_sstables is updated by compactors (atomic with data commit)
|
||||
// 2. shard snapshots are updated by background index builder
|
||||
repeated CompactedSsTable compacted_sstables = 9;
|
||||
|
||||
// Per-index catchup progress tracking.
|
||||
// When data is compacted into the base table, base table indexes are rebuilt
|
||||
// asynchronously. This field tracks which generation each index covers.
|
||||
//
|
||||
// For indexed queries, if an index's caught_up_generation < compacted_generation,
|
||||
// readers should use SSTable indexes for the gap instead of
|
||||
// scanning unindexed data in the base table.
|
||||
//
|
||||
// If an index is not present in this list, it is assumed to be fully caught up.
|
||||
repeated IndexCatchupProgress index_catchup = 10;
|
||||
|
||||
// Default ShardWriter configuration values for this MemWAL index.
|
||||
//
|
||||
// A free-form string map persisted so that every writer — across
|
||||
// processes and restarts — starts from the same default writer
|
||||
// configuration. These are defaults only: an individual writer may
|
||||
// still override any value at runtime in its own ShardWriterConfig
|
||||
// (which is not persisted).
|
||||
map<string, string> writer_config_defaults = 11;
|
||||
}
|
||||
|
||||
// Sharding spec definition.
|
||||
message ShardingSpec {
|
||||
// Unique identifier for this spec within the index.
|
||||
// IDs are never reused.
|
||||
uint32 spec_id = 1;
|
||||
|
||||
// Sharding field definitions that determine how to compute shard identifiers.
|
||||
repeated ShardingField fields = 2;
|
||||
}
|
||||
|
||||
// Sharding field definition.
|
||||
message ShardingField {
|
||||
// Unique string identifier for this shard field.
|
||||
string field_id = 1;
|
||||
|
||||
// Field IDs referencing source columns in the schema.
|
||||
repeated int32 source_ids = 2;
|
||||
|
||||
// Well-known shard transform name (e.g., "identity", "year", "bucket").
|
||||
// Mutually exclusive with expression.
|
||||
optional string transform = 3;
|
||||
|
||||
// DataFusion SQL expression for custom logic.
|
||||
// Mutually exclusive with transform.
|
||||
optional string expression = 4;
|
||||
|
||||
// Output type of the shard value (Arrow type name).
|
||||
string result_type = 5;
|
||||
|
||||
// Transform parameters (e.g., num_buckets for bucket transform).
|
||||
map<string, string> parameters = 6;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package lance.datafusion;
|
||||
|
||||
// Identifies a Lance dataset for remote reconstruction.
|
||||
//
|
||||
// Two modes:
|
||||
// 1. uri + serialized_manifest (fast): remote executor skips manifest read.
|
||||
// 2. uri + version + etag (lightweight): remote executor loads manifest from storage.
|
||||
message TableIdentifier {
|
||||
string uri = 1;
|
||||
uint64 version = 2;
|
||||
optional string manifest_etag = 3;
|
||||
optional bytes serialized_manifest = 4;
|
||||
map<string, string> storage_options = 5;
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
import "file.proto";
|
||||
import "table.proto";
|
||||
import "google/protobuf/any.proto";
|
||||
|
||||
package lance.table;
|
||||
|
||||
// A transaction represents the changes to a dataset.
|
||||
//
|
||||
// This has two purposes:
|
||||
// 1. When retrying a commit, the transaction can be used to re-build an updated
|
||||
// manifest.
|
||||
// 2. When there's a conflict, this can be used to determine whether the other
|
||||
// transaction is compatible with this one.
|
||||
message Transaction {
|
||||
// The version of the dataset this transaction was built from.
|
||||
//
|
||||
// For example, for a delete transaction this means the version of the dataset
|
||||
// that was read from while evaluating the deletion predicate.
|
||||
uint64 read_version = 1;
|
||||
|
||||
// The UUID that unique identifies a transaction.
|
||||
string uuid = 2;
|
||||
|
||||
// Optional version tag.
|
||||
string tag = 3;
|
||||
|
||||
// Optional properties for the transaction
|
||||
// __lance_commit_message is a reserved key
|
||||
map<string, string> transaction_properties = 4;
|
||||
|
||||
// Add new rows to the dataset.
|
||||
message Append {
|
||||
// The new fragments to append.
|
||||
//
|
||||
// Fragment IDs are not yet assigned.
|
||||
repeated DataFragment fragments = 1;
|
||||
}
|
||||
|
||||
// Mark rows as deleted.
|
||||
message Delete {
|
||||
// The fragments to update
|
||||
//
|
||||
// The fragment IDs will match existing fragments in the dataset.
|
||||
repeated DataFragment updated_fragments = 1;
|
||||
// The fragments to delete entirely.
|
||||
repeated uint64 deleted_fragment_ids = 2;
|
||||
// The predicate that was evaluated
|
||||
//
|
||||
// This may be used to determine whether the delete would have affected
|
||||
// files written by a concurrent transaction.
|
||||
string predicate = 3;
|
||||
}
|
||||
|
||||
// Create or overwrite the entire dataset.
|
||||
message Overwrite {
|
||||
// The new fragments
|
||||
//
|
||||
// Fragment IDs are not yet assigned.
|
||||
repeated DataFragment fragments = 1;
|
||||
// The new schema
|
||||
repeated lance.file.Field schema = 2;
|
||||
// Schema metadata.
|
||||
map<string, bytes> schema_metadata = 3;
|
||||
// Key-value pairs to merge with existing config.
|
||||
map<string, string> config_upsert_values = 4;
|
||||
// The base paths to be added for the initial dataset creation
|
||||
repeated BasePath initial_bases = 5;
|
||||
}
|
||||
|
||||
// Add or replace a new secondary index.
|
||||
//
|
||||
// This is also used to remove an index (we are replacing it with nothing)
|
||||
//
|
||||
// - new_indices: the modified indices, empty if dropping indices only
|
||||
// - removed_indices: the indices that are being replaced
|
||||
message CreateIndex {
|
||||
repeated IndexMetadata new_indices = 1;
|
||||
repeated IndexMetadata removed_indices = 2;
|
||||
}
|
||||
|
||||
// An operation that rewrites but does not change the data in the table. These
|
||||
// kinds of operations just rearrange data.
|
||||
message Rewrite {
|
||||
// The old fragments that are being replaced
|
||||
//
|
||||
// DEPRECATED: use groups instead.
|
||||
//
|
||||
// These should all have existing fragment IDs.
|
||||
repeated DataFragment old_fragments = 1;
|
||||
// The new fragments
|
||||
//
|
||||
// DEPRECATED: use groups instead.
|
||||
//
|
||||
// These fragments IDs are not yet assigned.
|
||||
repeated DataFragment new_fragments = 2;
|
||||
|
||||
// During a rewrite an index may be rewritten. We only serialize the UUID
|
||||
// since a rewrite should not change the other index parameters.
|
||||
message RewrittenIndex {
|
||||
// The id of the index that will be replaced
|
||||
UUID old_id = 1;
|
||||
// the id of the new index
|
||||
UUID new_id = 2;
|
||||
// the new index details
|
||||
google.protobuf.Any new_index_details = 3;
|
||||
// the version of the new index
|
||||
uint32 new_index_version = 4;
|
||||
// Files in the new index with their sizes.
|
||||
// Empty if file sizes are not available (e.g. older writers).
|
||||
repeated IndexFile new_index_files = 5;
|
||||
}
|
||||
|
||||
// A group of rewrite files that are all part of the same rewrite.
|
||||
message RewriteGroup {
|
||||
// The old fragment that is being replaced
|
||||
//
|
||||
// This should have an existing fragment ID.
|
||||
repeated DataFragment old_fragments = 1;
|
||||
// The new fragment
|
||||
//
|
||||
// The ID should have been reserved by an earlier
|
||||
// reserve operation
|
||||
repeated DataFragment new_fragments = 2;
|
||||
}
|
||||
|
||||
// Groups of files that have been rewritten
|
||||
repeated RewriteGroup groups = 3;
|
||||
// Indices that have been rewritten
|
||||
repeated RewrittenIndex rewritten_indices = 4;
|
||||
}
|
||||
|
||||
// An operation that merges in a new column, altering the schema.
|
||||
message Merge {
|
||||
// The updated fragments
|
||||
//
|
||||
// These should all have existing fragment IDs.
|
||||
repeated DataFragment fragments = 1;
|
||||
// The new schema
|
||||
repeated lance.file.Field schema = 2;
|
||||
// Schema metadata.
|
||||
map<string, bytes> schema_metadata = 3;
|
||||
}
|
||||
|
||||
// An operation that projects a subset of columns, altering the schema.
|
||||
message Project {
|
||||
// The new schema
|
||||
repeated lance.file.Field schema = 1;
|
||||
}
|
||||
|
||||
// An operation that restores a dataset to a previous version.
|
||||
message Restore {
|
||||
// The version to restore to
|
||||
uint64 version = 1;
|
||||
}
|
||||
|
||||
// An operation that reserves fragment ids for future use in
|
||||
// a rewrite operation.
|
||||
message ReserveFragments {
|
||||
uint32 num_fragments = 1;
|
||||
}
|
||||
|
||||
// An operation that clones a dataset.
|
||||
message Clone {
|
||||
// - true: Performs a metadata-only clone (copies manifest without data files).
|
||||
// The cloned dataset references original data through `base_paths`,
|
||||
// suitable for experimental scenarios or rapid metadata migration.
|
||||
// - false: Performs a full deep clone using the underlying object storage's native
|
||||
// copy API (e.g., S3 CopyObject, GCS rewrite). This leverages server-side
|
||||
// bulk copy operations to bypass download/upload bottlenecks, achieving
|
||||
// near-linear speedup for large datasets (typically 3-10x faster than
|
||||
// manual file transfers). The operation maintains atomicity and data
|
||||
// integrity guarantees provided by the storage backend.
|
||||
bool is_shallow = 1;
|
||||
// the reference name in the source dataset
|
||||
// in most cases it should be the branch or tag name in the source dataset
|
||||
optional string ref_name = 2;
|
||||
// the version of the source dataset for cloning
|
||||
uint64 ref_version = 3;
|
||||
// the absolute base path of the source dataset for cloning
|
||||
string ref_path = 4;
|
||||
// if the target dataset is a branch, this is the branch name of the target dataset
|
||||
optional string branch_name = 5;
|
||||
}
|
||||
|
||||
// Exact set of key hashes for conflict detection.
|
||||
// Used when the number of inserted rows is small.
|
||||
message ExactKeySetFilter {
|
||||
// 64-bit hashes of the inserted row keys.
|
||||
repeated uint64 key_hashes = 1;
|
||||
}
|
||||
|
||||
// Bloom filter for key existence tests.
|
||||
// Used when the number of rows is large.
|
||||
message BloomFilter {
|
||||
// Bitset backing the bloom filter (SBBF format).
|
||||
bytes bitmap = 1;
|
||||
// Number of bits in the bitmap.
|
||||
uint32 num_bits = 2;
|
||||
// Number of items the filter was sized for.
|
||||
// Used for intersection validation (filters with different sizes cannot be compared).
|
||||
// Default: 8192
|
||||
uint64 number_of_items = 3;
|
||||
// False positive probability the filter was sized for.
|
||||
// Used for intersection validation (filters with different parameters cannot be compared).
|
||||
// Default: 0.00057
|
||||
double probability = 4;
|
||||
}
|
||||
|
||||
// A filter for checking key existence in set of rows inserted by a merge insert operation.
|
||||
// Only created when the merge insert's ON columns match the schema's unenforced primary key.
|
||||
// The presence of this filter indicates strict primary key conflict detection should be used.
|
||||
// Can use either an exact set (for small row counts) or a Bloom filter (for large row counts).
|
||||
message KeyExistenceFilter {
|
||||
// Field IDs of columns participating in the key (must match unenforced primary key).
|
||||
repeated int32 field_ids = 1;
|
||||
// The underlying data structure storing the key hashes.
|
||||
oneof data {
|
||||
// Exact set of key hashes (used for small number of rows).
|
||||
ExactKeySetFilter exact = 2;
|
||||
// Bloom filter (used for large number of rows).
|
||||
BloomFilter bloom = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Serialized as sorted distinct local physical row offsets within the fragment (0-based).
|
||||
message UInt32List {
|
||||
repeated uint32 values = 1;
|
||||
}
|
||||
|
||||
// An operation that updates rows but does not add or remove rows.
|
||||
message Update {
|
||||
// The fragments that have been removed. These are fragments where all rows
|
||||
// have been updated and moved to a new fragment.
|
||||
repeated uint64 removed_fragment_ids = 1;
|
||||
// The fragments that have been updated.
|
||||
repeated DataFragment updated_fragments = 2;
|
||||
// The new fragments where updated rows have been moved to.
|
||||
repeated DataFragment new_fragments = 3;
|
||||
// The ids of the fields that have been modified.
|
||||
repeated uint32 fields_modified = 4;
|
||||
/// SSTables to mark as compacted after this transaction.
|
||||
repeated CompactedSsTable compacted_sstables = 5;
|
||||
/// The fields that used to judge whether to preserve the new frag's id into
|
||||
/// the frag bitmap of the specified indices.
|
||||
repeated uint32 fields_for_preserving_frag_bitmap = 6;
|
||||
// The mode of update
|
||||
UpdateMode update_mode = 7;
|
||||
// Filter for checking existence of keys in newly inserted rows, used for conflict detection.
|
||||
// Only tracks keys from INSERT operations during merge insert, not updates.
|
||||
optional KeyExistenceFilter inserted_rows = 8;
|
||||
// Per-fragment physical row offsets that matched an update_columns hash join (RewriteColumns).
|
||||
map<uint64, UInt32List> updated_fragment_offsets = 9;
|
||||
}
|
||||
|
||||
// The mode of update operation
|
||||
enum UpdateMode {
|
||||
|
||||
/// rows are deleted in current fragments and rewritten in new fragments.
|
||||
/// This is most optimal when the majority of columns are being rewritten
|
||||
/// or only a few rows are being updated.
|
||||
REWRITE_ROWS = 0;
|
||||
|
||||
/// within each fragment, columns are fully rewritten and inserted as new data files.
|
||||
/// Old versions of columns are tombstoned. This is most optimal when most rows are affected
|
||||
/// but a small subset of columns are affected.
|
||||
REWRITE_COLUMNS = 1;
|
||||
}
|
||||
|
||||
// An entry for a map update. If value is not set, the key will be removed from the map.
|
||||
message UpdateMapEntry {
|
||||
// The key of the map entry to update.
|
||||
string key = 1;
|
||||
// The value to set for the key.
|
||||
optional string value = 2;
|
||||
}
|
||||
|
||||
message UpdateMap {
|
||||
repeated UpdateMapEntry update_entries = 1;
|
||||
// If true, the map will be replaced entirely with the new entries.
|
||||
// If false, the new entries will be merged with the existing map.
|
||||
bool replace = 2;
|
||||
}
|
||||
|
||||
// An operation that updates the table config, table metadata, schema metadata,
|
||||
// or field metadata.
|
||||
message UpdateConfig {
|
||||
UpdateMap config_updates = 6;
|
||||
UpdateMap table_metadata_updates = 7;
|
||||
UpdateMap schema_metadata_updates = 8;
|
||||
map<int32, UpdateMap> field_metadata_updates = 9;
|
||||
|
||||
// Deprecated -------------------------------
|
||||
map<string, string> upsert_values = 1;
|
||||
repeated string delete_keys = 2;
|
||||
map<string, string> schema_metadata = 3;
|
||||
map<uint32, FieldMetadataUpdate> field_metadata = 4;
|
||||
|
||||
message FieldMetadataUpdate {
|
||||
map<string, string> metadata = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message DataReplacementGroup {
|
||||
uint64 fragment_id = 1;
|
||||
DataFile new_file = 2;
|
||||
}
|
||||
|
||||
// An operation that replaces the data in a region of the table with new data.
|
||||
message DataReplacement {
|
||||
repeated DataReplacementGroup replacements = 1;
|
||||
}
|
||||
|
||||
// Overlay files to append to a single fragment, in order (the last entry is
|
||||
// newest). The overlays are appended to the fragment's existing `overlays`
|
||||
// list; they do not replace it, so overlays written by concurrent commits are
|
||||
// preserved.
|
||||
message DataOverlayGroup {
|
||||
uint64 fragment_id = 1;
|
||||
// Each DataOverlayFile.committed_version is left 0 by the writer and stamped
|
||||
// to the new dataset version at commit time (re-stamped on retry), in the
|
||||
// same way as the created-at / last-updated-at version sequences. The fields
|
||||
// touched are read from each overlay's `data_file.fields`.
|
||||
repeated DataOverlayFile overlays = 2;
|
||||
}
|
||||
|
||||
// Attach overlay files to fragments, supplying new values for a subset of
|
||||
// (row offset, field) cells without rewriting the fragments' base data files.
|
||||
// See the DataOverlayFile message in table.proto for resolution, coverage, and
|
||||
// versioning rules, and the Data Overlay Files and Transactions specifications
|
||||
// for the (intentionally permissive) conflict semantics.
|
||||
message DataOverlay {
|
||||
repeated DataOverlayGroup groups = 1;
|
||||
}
|
||||
|
||||
// Update SSTable compaction progress in the MemWAL index.
|
||||
// This operation is used during merge-insert to atomically record which
|
||||
// SSTables have been compacted into the base table.
|
||||
message UpdateMemWalState {
|
||||
// SSTables being marked as compacted.
|
||||
repeated CompactedSsTable compacted_sstables = 1;
|
||||
}
|
||||
|
||||
// An operation that updates base paths in the dataset.
|
||||
message UpdateBases {
|
||||
// The new base paths to add to the manifest.
|
||||
repeated BasePath new_bases = 1;
|
||||
}
|
||||
|
||||
// The operation of this transaction.
|
||||
oneof operation {
|
||||
Append append = 100;
|
||||
Delete delete = 101;
|
||||
Overwrite overwrite = 102;
|
||||
CreateIndex create_index = 103;
|
||||
Rewrite rewrite = 104;
|
||||
Merge merge = 105;
|
||||
Restore restore = 106;
|
||||
ReserveFragments reserve_fragments = 107;
|
||||
Update update = 108;
|
||||
Project project = 109;
|
||||
UpdateConfig update_config = 110;
|
||||
DataReplacement data_replacement = 111;
|
||||
UpdateMemWalState update_mem_wal_state = 112;
|
||||
Clone clone = 113;
|
||||
UpdateBases update_bases = 114;
|
||||
DataOverlay data_overlay = 115;
|
||||
}
|
||||
|
||||
// Fields 200/202 (`blob_append` / `blob_overwrite`) previously represented blob dataset ops.
|
||||
reserved 200, 202;
|
||||
reserved "blob_append", "blob_overwrite";
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
.env
|
||||
@@ -1,90 +0,0 @@
|
||||
# Rust Guidelines
|
||||
|
||||
Also see [root AGENTS.md](../AGENTS.md) for cross-language standards.
|
||||
|
||||
## Code Style
|
||||
|
||||
- Use `Vec::with_capacity()` when size is known or estimable — prefer over-estimating capacity to multiple reallocations.
|
||||
- Wrap large or expensive-to-clone struct fields (maps, protobuf metadata, schemas) in `Arc<T>` to avoid deep copies.
|
||||
- Use `Box::pin(...)` or `.boxed()` but never both — `.boxed()` already returns `Pin<Box<...>>`.
|
||||
- Remove dead code instead of adding `#[allow(dead_code)]`. Delete unused constants instead of reducing visibility.
|
||||
- Use `column_by_name()` for `RecordBatch` column access in production code; use `batch["column_name"]` in tests.
|
||||
- Use `PrimitiveArray::<T>::from(vec)` (zero-copy) instead of `from_iter_values(vec)` for Vec-to-PrimitiveArray conversion.
|
||||
- Implement `Default` trait on config/options structs instead of standalone `default_*()` helpers.
|
||||
- Place `#[cfg(test)] mod tests` as a single block at the bottom of each file — no production code after it.
|
||||
- Place `use` imports at the top of the file, not inline within function bodies.
|
||||
- Extract substantial new logic (bin packing, scheduling) into dedicated submodules instead of inlining into large files.
|
||||
- Delete obsolete internal (`pub(crate)` / private) methods in the same PR that introduces their replacements. For public API methods, follow the deprecation path in root AGENTS.md instead.
|
||||
- Choose log levels by audience: `debug!` for routine/high-frequency ops, `info!` for infrequent operator-visible state changes, `warn!` for unexpected conditions.
|
||||
|
||||
## Concurrency
|
||||
|
||||
- The closure passed to `spawn_cpu()` must only consume CPU and return — it must **never** wait on anything: **no channels** (blocking send/recv), **no I/O**, **no locks**, and no `block_on`/`.blocking_*`. The CPU pool can collapse to a single worker in resource-constrained environments (`<= 3` CPUs), so a parked closure can deadlock the whole pool with a silent 0% hang. Keep the waiting in surrounding async code and hand only the pure-CPU work to `spawn_cpu()`. Only dispatch substantial work (rule of thumb: ~100µs+ of CPU); below that the pool overhead outweighs the benefit and the work is better left inline. See the doc comment on `spawn_cpu` for the rationale.
|
||||
|
||||
## API Design
|
||||
|
||||
- Use `with_`-prefixed builder methods for optional config (e.g., `MyStruct::new(required).with_option(v)`) — don't create separate constructor variants.
|
||||
- For public APIs, prefer `Into<T>` or `AsRef<T>` trait bounds for flexible inputs.
|
||||
- Prefer `pub(crate)` over `pub` for crate-internal items. Use `pub use` re-exports for the actual public API surface.
|
||||
- Use enums instead of magic numbers for format versions, variant types, and discriminators — leverage exhaustive `match`.
|
||||
- Use strongly-typed structs instead of `HashMap<String, String>` in APIs — convert to strings only at serialization boundaries.
|
||||
- Keep `RowAddr` (physical fragment+offset) and `RowId` (stable logical identifier) as distinct types — never raw `u64` for both.
|
||||
- Use `RowAddress` from `lance-core/src/utils/address.rs` instead of raw bitwise operations on row addresses.
|
||||
- Use `RowAddrTreeMap`/`RoaringBitmap` instead of `Vec<Range<u64>>` for physical row selections.
|
||||
- Use logical row counts (`num_rows()`) instead of `physical_rows` for user-facing metrics — subtract deletions.
|
||||
- Keep traits minimal — only core abstraction methods. Move helpers to standalone functions and config to struct fields.
|
||||
- Get column/field types from schema metadata — never materialize data rows just to inspect types.
|
||||
- Use stable, versioned serialization formats for persistent storage (e.g., index files) — avoid unstable cross-version formats.
|
||||
- Use Arrow's type-safe access (`ArrayAccessor` trait bounds, `as_*_array` helpers) instead of `arrow::compute::cast` + `downcast_ref`. Prefer `_opt` variants (e.g., `as_string_opt`) unless the data type has already been verified.
|
||||
- In `lance-io/`, use single-syscall writes for local filesystem I/O — don't reuse cloud multipart upload machinery.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Never use `.unwrap()`, `.expect()`, `panic!()`, or `assert!()` in library code for fallible operations — use `?` with `Result` and proper error types. Reserve `.unwrap()` for tests only.
|
||||
- Avoid bare `.unwrap()`; use `if let`, `match`, `let ... else`, `?`, or combinators. Never `.is_none()` followed by `.unwrap()`. If unavoidable, use `.expect("reason")`.
|
||||
- Return `LanceError::NotSupported` instead of `todo!()` or `unimplemented!()` for unsupported code paths. Test with `Result::Err` assertions, not `#[should_panic]`.
|
||||
- Match `Error` variant to root cause: `Error::invalid_input` for caller data issues, `Error::corrupt_file` for format/integrity issues, `Error::not_found` for missing resources, `Error::io` for I/O failures.
|
||||
- Include full context in error messages — variable names, values, sizes, types, indices. Not generic messages like `"Invalid chunk size"`.
|
||||
- Use `checked_add`/`checked_mul` instead of `wrapping_add`/`wrapping_mul` for counters and IDs — return an error on overflow.
|
||||
- Prefer `debug_assert!` over `assert!` for non-safety invariants; reserve `assert!` for conditions preventing data corruption. Always include descriptive messages.
|
||||
- Don't silently guard against impossible conditions — use `debug_assert!`, return an explicit error, or remove the check.
|
||||
- Log warnings on best-effort/cleanup failures instead of silently swallowing or propagating errors.
|
||||
- Log warnings for silent no-ops (skipped operations); omit warnings before errors since the error message is sufficient.
|
||||
- Avoid `unwrap_or(default)` on map lookups for required config params — use `.ok_or_else(|| Error::...)` and verify key names match between serialization and deserialization.
|
||||
- Advance all parallel iterators before any `continue` branches — early exits that skip `.next()` calls cause misalignment.
|
||||
- Bind `iter.next()` with `let Some(x) = iter.next() else { ... }` — never call `.next()` twice to check-then-use.
|
||||
|
||||
## Naming
|
||||
|
||||
- Reserve `_`-prefixed names for truly unused bindings — if a variable is read, drop the underscore.
|
||||
- Prefix boolean variables with `is_` or `has_` instead of ambiguous `with_` or bare adjectives.
|
||||
- Name booleans so `false` (zero/`Default::default()`) is the desired default — use `disable_*` instead of `enable_*` when the feature should be on by default.
|
||||
- Name functions to match their actual scope — e.g., `handle_partition_system_columns` not `handle_system_columns` if only a subset is handled.
|
||||
|
||||
## Testing
|
||||
|
||||
- Use `record_batch!()` from `arrow_array` to construct `RecordBatch` in tests instead of manual Schema/Arc/try_new boilerplate.
|
||||
- Use `gen_batch()` builder API (`.col()`, `.into_reader_rows()`) for test data setup instead of manual Arrow construction.
|
||||
- Use `.try_into_batch()` instead of `.try_into_stream().try_collect()` for scanner results in tests.
|
||||
- Use plain `"memory://"` URIs in tests — no atomic counters or unique suffixes needed.
|
||||
- Assert on both error variant (`assert!(matches!(error, ErrorType::Variant { .. }))`) and message content — don't just check `is_err()`.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Add doc comments to public API elements that convey semantic meaning, valid values, and effects — don't restate type signatures.
|
||||
- Document enum variant doc comments with behavioral semantics, not just labels. For numeric parameters, state whether it's an id, count, index, etc.
|
||||
- Add doc comments to magic constants, thresholds, and non-obvious transformation functions — explain what the value represents and why it was chosen.
|
||||
- Comment fallback/guard code paths with when they trigger and why they exist.
|
||||
- Ensure doc comments match actual semantics — distinguish mutates-in-place (`&mut self`) from returns-new-value.
|
||||
- Use explicit forward-looking language (`TODO`, `FIXME`) in comments to distinguish current behavior from planned changes.
|
||||
- Document the semantic meaning of both present and absent states for `Option<T>` fields.
|
||||
- Use precise domain terminology — avoid ambiguous abbreviations (e.g., "FIXED" vs "fixed-width") or incorrect terms (e.g., "fields" when meaning "fragments").
|
||||
|
||||
## lance-encoding
|
||||
|
||||
Performance-critical encoding/decoding paths have additional requirements:
|
||||
|
||||
- Hoist loop-invariant conditionals out of hot loops — branch once outside, then use separate loop bodies or monomorphized variants.
|
||||
- Pre-allocate single contiguous buffers. Default to `buf.resize(len, 0)` for safe initialization; reserve `Vec::with_capacity` + `unsafe { set_len() }` for measured hot paths only, with a `// SAFETY:` comment explaining why the buffer will be fully initialized before read (e.g., immediately followed by `read_exact`).
|
||||
- Use `spawn_cpu()` only at the async-to-CPU boundary (e.g., FSST, decompression, batch materialization) — never nest redundant `spawn_cpu()` calls.
|
||||
- Use `expect_next()` and similar utility methods instead of inlining `None`-checks with error returns.
|
||||
@@ -1 +0,0 @@
|
||||
AGENTS.md
|
||||
@@ -1,41 +0,0 @@
|
||||
# Contributing to Rust
|
||||
|
||||
To format and lint Rust code:
|
||||
|
||||
```bash
|
||||
cargo fmt --all
|
||||
cargo clippy --all-features --tests --benches
|
||||
```
|
||||
|
||||
## Core Format
|
||||
|
||||
The core format is implemented in Rust under the `rust` directory. Once you've setup Rust you can build the core format with:
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
This builds the debug build. For the optimized release build:
|
||||
|
||||
```bash
|
||||
cargo build -r
|
||||
```
|
||||
|
||||
To run the Rust unit tests:
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
If you're working on a performance related feature, benchmarks can be run via:
|
||||
|
||||
```bash
|
||||
cargo bench
|
||||
```
|
||||
|
||||
If you want detailed logging and full backtraces, set the following environment variables.
|
||||
More details can be found [here](../docs/src/guide/performance.md#logging).
|
||||
|
||||
```bash
|
||||
LANCE_LOG=info RUST_BACKTRACE=FULL <cargo-commands>
|
||||
```
|
||||
@@ -1,2 +0,0 @@
|
||||
# Lance Rust Workspace
|
||||
Where core rust code lance lives
|
||||
@@ -1,31 +0,0 @@
|
||||
[package]
|
||||
name = "lance-arrow-scalar"
|
||||
version = "58.0.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Arrow scalar type with Ord, Hash, and Eq support"
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
# Note: this is a core crate and we should aim to keep this dependency list
|
||||
# as minimal as possible.
|
||||
arrow-array = { workspace = true }
|
||||
arrow-buffer = { workspace = true }
|
||||
arrow-cast = { workspace = true }
|
||||
arrow-data = { workspace = true }
|
||||
arrow-row = { workspace = true }
|
||||
arrow-schema = { workspace = true }
|
||||
half = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
arrow-ord = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,57 +0,0 @@
|
||||
# lance-arrow-scalar
|
||||
|
||||
A scalar type backed by Apache Arrow arrays with `Ord`, `Hash`, and `Eq` support.
|
||||
|
||||
## Overview
|
||||
|
||||
`ArrowScalar` wraps a single-element Arrow array and provides comparison and hashing operations by leveraging Apache Arrow's `OwnedRow` representation. This ensures:
|
||||
|
||||
- **Correct total ordering** for all Arrow types
|
||||
- **Proper NaN handling** for floating-point values
|
||||
- **Consistent null ordering**
|
||||
- **O(1) comparisons** via cached row bytes
|
||||
|
||||
## Features
|
||||
|
||||
- `Eq`, `Ord`, and `Hash` traits for Arrow scalar values
|
||||
- Support for all Arrow data types
|
||||
- Serde serialization/deserialization support
|
||||
- Zero-copy conversion from Arrow arrays
|
||||
|
||||
## Usage
|
||||
|
||||
Add to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
lance-arrow-scalar = "57.0.0"
|
||||
```
|
||||
|
||||
Then use in your code:
|
||||
|
||||
```rust
|
||||
use lance_arrow_scalar::ArrowScalar;
|
||||
|
||||
// Create from primitive types
|
||||
let a = ArrowScalar::from(42i32);
|
||||
let b = ArrowScalar::from(100i32);
|
||||
assert!(a < b);
|
||||
|
||||
// Create from strings
|
||||
let s1 = ArrowScalar::from("hello");
|
||||
let s2 = ArrowScalar::from("world");
|
||||
assert!(s1 < s2);
|
||||
|
||||
// Use in collections
|
||||
use std::collections::HashMap;
|
||||
let mut map = HashMap::new();
|
||||
map.insert(ArrowScalar::from("key"), ArrowScalar::from(123));
|
||||
```
|
||||
|
||||
## Cross-Type Comparison
|
||||
|
||||
Comparing scalars of different data types produces an arbitrary but consistent ordering based on the underlying row bytes. This allows scalars to be used as keys in sorted collections regardless of type, though the ordering across types is not semantically meaningful.
|
||||
|
||||
## Implementation Details
|
||||
|
||||
Comparisons and hashing are delegated to [`arrow_row::OwnedRow`], which provides efficient byte-level operations. The row representation is cached at construction time, making all comparison and hashing operations O(1).
|
||||
@@ -1,108 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::*;
|
||||
use half::f16;
|
||||
|
||||
use crate::ArrowScalar;
|
||||
|
||||
macro_rules! impl_from_primitive {
|
||||
($native_ty:ty, $array_ty:ty) => {
|
||||
impl From<$native_ty> for ArrowScalar {
|
||||
fn from(value: $native_ty) -> Self {
|
||||
let array: ArrayRef = Arc::new(<$array_ty>::from(vec![value]));
|
||||
Self::try_from_array(array).expect("single-element primitive array is always valid")
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_from_primitive!(i8, Int8Array);
|
||||
impl_from_primitive!(i16, Int16Array);
|
||||
impl_from_primitive!(i32, Int32Array);
|
||||
impl_from_primitive!(i64, Int64Array);
|
||||
impl_from_primitive!(u8, UInt8Array);
|
||||
impl_from_primitive!(u16, UInt16Array);
|
||||
impl_from_primitive!(u32, UInt32Array);
|
||||
impl_from_primitive!(u64, UInt64Array);
|
||||
impl_from_primitive!(f32, Float32Array);
|
||||
impl_from_primitive!(f64, Float64Array);
|
||||
|
||||
impl From<bool> for ArrowScalar {
|
||||
fn from(value: bool) -> Self {
|
||||
let array: ArrayRef = Arc::new(BooleanArray::from(vec![value]));
|
||||
Self::try_from_array(array).expect("single-element boolean array is always valid")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f16> for ArrowScalar {
|
||||
fn from(value: f16) -> Self {
|
||||
let array: ArrayRef = Arc::new(Float16Array::from(vec![value]));
|
||||
Self::try_from_array(array).expect("single-element f16 array is always valid")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ArrowScalar {
|
||||
fn from(value: &str) -> Self {
|
||||
let array: ArrayRef = Arc::new(StringArray::from(vec![value]));
|
||||
Self::try_from_array(array).expect("single-element string array is always valid")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ArrowScalar {
|
||||
fn from(value: String) -> Self {
|
||||
Self::from(value.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for ArrowScalar {
|
||||
fn from(value: &[u8]) -> Self {
|
||||
let array: ArrayRef = Arc::new(BinaryArray::from_vec(vec![value]));
|
||||
Self::try_from_array(array).expect("single-element binary array is always valid")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_from_primitives() {
|
||||
let s = ArrowScalar::from(42i32);
|
||||
assert!(!s.is_null());
|
||||
assert_eq!(format!("{s}"), "42");
|
||||
|
||||
let s = ArrowScalar::from(1.5f64);
|
||||
assert!(!s.is_null());
|
||||
|
||||
let s = ArrowScalar::from(true);
|
||||
assert_eq!(format!("{s}"), "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_string_types() {
|
||||
let s = ArrowScalar::from("hello");
|
||||
assert_eq!(format!("{s}"), "hello");
|
||||
|
||||
let s = ArrowScalar::from(String::from("world"));
|
||||
assert_eq!(format!("{s}"), "world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_binary() {
|
||||
let bytes: &[u8] = &[0xDE, 0xAD];
|
||||
let s = ArrowScalar::from(bytes);
|
||||
assert!(!s.is_null());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_f16() {
|
||||
let s = ArrowScalar::from(f16::from_f32(1.5));
|
||||
assert!(!s.is_null());
|
||||
}
|
||||
}
|
||||
@@ -1,624 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
//! A scalar type backed by a single-element Arrow array with [`Ord`], [`Hash`],
|
||||
//! and [`Eq`] support.
|
||||
//!
|
||||
//! Comparisons and hashing are delegated to [`arrow_row::OwnedRow`], which
|
||||
//! provides a correct total ordering for all Arrow types (including proper NaN
|
||||
//! handling for floats and null ordering).
|
||||
|
||||
mod convert;
|
||||
pub mod serde;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::cast::AsArray;
|
||||
use arrow_array::types::{Float16Type, Float32Type, Float64Type};
|
||||
use arrow_array::{ArrayRef, make_array, new_null_array};
|
||||
use arrow_cast::display::ArrayFormatter;
|
||||
use arrow_data::transform::MutableArrayData;
|
||||
use arrow_row::{OwnedRow, RowConverter, SortField};
|
||||
use arrow_schema::{ArrowError, DataType};
|
||||
|
||||
type Result<T> = std::result::Result<T, ArrowError>;
|
||||
|
||||
/// A scalar value backed by a length-1 Arrow array.
|
||||
///
|
||||
/// `ArrowScalar` provides [`Eq`], [`Ord`], and [`Hash`] by caching an
|
||||
/// [`OwnedRow`] at construction time. This means comparisons and hashing are
|
||||
/// O(1) row-byte operations rather than per-type dispatch.
|
||||
///
|
||||
/// # Cross-type comparison
|
||||
///
|
||||
/// Comparing scalars of different data types produces an arbitrary but
|
||||
/// consistent ordering based on the underlying row bytes. This is intentional
|
||||
/// — it allows scalars to be used as keys in sorted collections regardless of
|
||||
/// type, but the ordering across types is not semantically meaningful.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use lance_arrow_scalar::ArrowScalar;
|
||||
///
|
||||
/// let a = ArrowScalar::from(1i32);
|
||||
/// let b = ArrowScalar::from(2i32);
|
||||
/// assert!(a < b);
|
||||
///
|
||||
/// let c = ArrowScalar::from("hello");
|
||||
/// assert_eq!(c, ArrowScalar::from("hello"));
|
||||
/// ```
|
||||
pub struct ArrowScalar {
|
||||
array: ArrayRef,
|
||||
row: OwnedRow,
|
||||
}
|
||||
|
||||
impl ArrowScalar {
|
||||
/// Create a scalar by extracting the element at `offset` from `array`.
|
||||
pub fn try_new(array: &ArrayRef, offset: usize) -> Result<Self> {
|
||||
if offset >= array.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Scalar index out of bounds".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let data = array.to_data();
|
||||
let mut mutable = MutableArrayData::new(vec![&data], true, 1);
|
||||
mutable.extend(0, offset, offset + 1);
|
||||
let single = make_array(mutable.freeze());
|
||||
Self::try_from_array(single)
|
||||
}
|
||||
|
||||
/// Create a scalar from a length-1 array.
|
||||
pub fn try_from_array(array: ArrayRef) -> Result<Self> {
|
||||
if array.len() != 1 {
|
||||
return Err(ArrowError::InvalidArgumentError(format!(
|
||||
"ArrowScalar requires a length-1 array, got length {}",
|
||||
array.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let row = Self::compute_row(&array)?;
|
||||
Ok(Self { array, row })
|
||||
}
|
||||
|
||||
/// Create a null scalar of the given data type.
|
||||
pub fn new_null(data_type: &DataType) -> Result<Self> {
|
||||
Self::try_from_array(new_null_array(data_type, 1))
|
||||
}
|
||||
|
||||
fn compute_row(array: &ArrayRef) -> Result<OwnedRow> {
|
||||
let sort_field = SortField::new(array.data_type().clone());
|
||||
let converter = RowConverter::new(vec![sort_field])?;
|
||||
let rows = converter.convert_columns(&[Arc::clone(array)])?;
|
||||
Ok(rows.row(0).owned())
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying length-1 array.
|
||||
pub fn as_array(&self) -> &ArrayRef {
|
||||
&self.array
|
||||
}
|
||||
|
||||
/// Returns the data type of this scalar.
|
||||
pub fn data_type(&self) -> &DataType {
|
||||
self.array.data_type()
|
||||
}
|
||||
|
||||
/// Returns `true` if this scalar is null.
|
||||
pub fn is_null(&self) -> bool {
|
||||
self.array.null_count() == 1
|
||||
}
|
||||
|
||||
/// Returns `true` if this scalar is a non-null floating-point NaN.
|
||||
///
|
||||
/// ```
|
||||
/// use lance_arrow_scalar::ArrowScalar;
|
||||
///
|
||||
/// assert!(ArrowScalar::from(f32::NAN).is_nan());
|
||||
/// assert!(!ArrowScalar::from(1.0f32).is_nan());
|
||||
/// assert!(!ArrowScalar::from(1i32).is_nan());
|
||||
/// ```
|
||||
pub fn is_nan(&self) -> bool {
|
||||
if self.is_null() {
|
||||
return false;
|
||||
}
|
||||
|
||||
match self.data_type() {
|
||||
DataType::Float16 => self.array.as_primitive::<Float16Type>().value(0).is_nan(),
|
||||
DataType::Float32 => self.array.as_primitive::<Float32Type>().value(0).is_nan(),
|
||||
DataType::Float64 => self.array.as_primitive::<Float64Type>().value(0).is_nan(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ArrowScalar {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.row == other.row
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ArrowScalar {}
|
||||
|
||||
impl PartialOrd for ArrowScalar {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for ArrowScalar {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
self.row.cmp(&other.row)
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ArrowScalar {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.row.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ArrowScalar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.is_null() {
|
||||
return write!(f, "null");
|
||||
}
|
||||
let formatter =
|
||||
ArrayFormatter::try_new(&self.array, &Default::default()).map_err(|_| fmt::Error)?;
|
||||
write!(f, "{}", formatter.value(0))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ArrowScalar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "ArrowScalar({}: {})", self.data_type(), self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ArrowScalar {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
array: Arc::clone(&self.array),
|
||||
row: self.row.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::*;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_try_new_extracts_element() {
|
||||
let array: ArrayRef = Arc::new(Int32Array::from(vec![10, 20, 30]));
|
||||
let s = ArrowScalar::try_new(&array, 1).unwrap();
|
||||
assert_eq!(format!("{s}"), "20");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_new_out_of_bounds() {
|
||||
let array: ArrayRef = Arc::new(Int32Array::from(vec![1]));
|
||||
assert!(ArrowScalar::try_new(&array, 5).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_from_array_wrong_length() {
|
||||
let array: ArrayRef = Arc::new(Int32Array::from(vec![1, 2]));
|
||||
assert!(ArrowScalar::try_from_array(array).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_equality() {
|
||||
let a = ArrowScalar::from(42i32);
|
||||
let b = ArrowScalar::from(42i32);
|
||||
let c = ArrowScalar::from(99i32);
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ordering() {
|
||||
let a = ArrowScalar::from(1i32);
|
||||
let b = ArrowScalar::from(2i32);
|
||||
let c = ArrowScalar::from(3i32);
|
||||
assert!(a < b);
|
||||
assert!(b < c);
|
||||
assert_eq!(a.cmp(&a), Ordering::Equal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hash_consistent_with_eq() {
|
||||
use std::hash::DefaultHasher;
|
||||
|
||||
let a = ArrowScalar::from(42i32);
|
||||
let b = ArrowScalar::from(42i32);
|
||||
let hash_a = {
|
||||
let mut h = DefaultHasher::new();
|
||||
a.hash(&mut h);
|
||||
h.finish()
|
||||
};
|
||||
let hash_b = {
|
||||
let mut h = DefaultHasher::new();
|
||||
b.hash(&mut h);
|
||||
h.finish()
|
||||
};
|
||||
assert_eq!(hash_a, hash_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_hashset() {
|
||||
let mut set = HashSet::new();
|
||||
set.insert(ArrowScalar::from(1i32));
|
||||
set.insert(ArrowScalar::from(2i32));
|
||||
set.insert(ArrowScalar::from(1i32));
|
||||
assert_eq!(set.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_in_btreeset() {
|
||||
let mut set = BTreeSet::new();
|
||||
set.insert(ArrowScalar::from(3i32));
|
||||
set.insert(ArrowScalar::from(1i32));
|
||||
set.insert(ArrowScalar::from(2i32));
|
||||
let values: Vec<_> = set.iter().map(|s| format!("{s}")).collect();
|
||||
assert_eq!(values, vec!["1", "2", "3"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_scalar() {
|
||||
let array: ArrayRef = Arc::new(Int32Array::from(vec![None]));
|
||||
let s = ArrowScalar::try_from_array(array).unwrap();
|
||||
assert!(s.is_null());
|
||||
assert_eq!(format!("{s}"), "null");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_sorts_first() {
|
||||
let null_scalar = {
|
||||
let array: ArrayRef = Arc::new(Int32Array::from(vec![None]));
|
||||
ArrowScalar::try_from_array(array).unwrap()
|
||||
};
|
||||
let value_scalar = ArrowScalar::from(0i32);
|
||||
assert!(null_scalar < value_scalar);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::float_nan(
|
||||
ArrowScalar::from(f64::NAN),
|
||||
ArrowScalar::from(f64::INFINITY),
|
||||
Ordering::Greater
|
||||
)]
|
||||
#[case::float_normal(ArrowScalar::from(1.0f64), ArrowScalar::from(2.0f64), Ordering::Less)]
|
||||
fn test_float_ordering(
|
||||
#[case] a: ArrowScalar,
|
||||
#[case] b: ArrowScalar,
|
||||
#[case] expected: Ordering,
|
||||
) {
|
||||
assert_eq!(a.cmp(&b), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::float16_nan(ArrowScalar::from(half::f16::NAN), true)]
|
||||
#[case::float32_nan(ArrowScalar::from(f32::NAN), true)]
|
||||
#[case::float64_nan(ArrowScalar::from(f64::NAN), true)]
|
||||
#[case::float64_finite(ArrowScalar::from(1.0f64), false)]
|
||||
#[case::int32(ArrowScalar::from(1i32), false)]
|
||||
fn test_is_nan(#[case] scalar: ArrowScalar, #[case] expected: bool) {
|
||||
assert_eq!(scalar.is_nan(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_is_not_nan() {
|
||||
let array: ArrayRef = Arc::new(Float64Array::from(vec![None]));
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
assert!(!scalar.is_nan());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display_string() {
|
||||
let s = ArrowScalar::from("hello world");
|
||||
assert_eq!(format!("{s}"), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_debug() {
|
||||
let s = ArrowScalar::from(42i32);
|
||||
let debug = format!("{s:?}");
|
||||
assert!(debug.contains("ArrowScalar"));
|
||||
assert!(debug.contains("42"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone() {
|
||||
let a = ArrowScalar::from(42i32);
|
||||
let b = a.clone();
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_type() {
|
||||
let s = ArrowScalar::from(42i32);
|
||||
assert_eq!(s.data_type(), &DataType::Int32);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean_roundtrip() {
|
||||
let t = ArrowScalar::from(true);
|
||||
let f = ArrowScalar::from(false);
|
||||
assert_eq!(t.data_type(), &DataType::Boolean);
|
||||
assert!(!t.is_null());
|
||||
assert_eq!(format!("{t}"), "true");
|
||||
assert_eq!(format!("{f}"), "false");
|
||||
|
||||
// Extract from multi-element array
|
||||
let array: ArrayRef = Arc::new(BooleanArray::from(vec![true, false, true]));
|
||||
let s = ArrowScalar::try_new(&array, 1).unwrap();
|
||||
assert_eq!(format!("{s}"), "false");
|
||||
assert_eq!(s.data_type(), &DataType::Boolean);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean_equality_and_ordering() {
|
||||
let t1 = ArrowScalar::from(true);
|
||||
let t2 = ArrowScalar::from(true);
|
||||
let f1 = ArrowScalar::from(false);
|
||||
assert_eq!(t1, t2);
|
||||
assert_ne!(t1, f1);
|
||||
// false < true in arrow row encoding
|
||||
assert!(f1 < t1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean_null() {
|
||||
let array: ArrayRef = Arc::new(BooleanArray::from(vec![None]));
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
assert!(scalar.is_null());
|
||||
assert_eq!(scalar.data_type(), &DataType::Boolean);
|
||||
assert_eq!(format!("{scalar}"), "null");
|
||||
|
||||
// null sorts before false
|
||||
let f = ArrowScalar::from(false);
|
||||
assert!(scalar < f);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_view_roundtrip() {
|
||||
let array: ArrayRef = Arc::new(StringViewArray::from(vec![
|
||||
"hello world, this is a long string view",
|
||||
]));
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
assert_eq!(scalar.data_type(), &DataType::Utf8View);
|
||||
assert!(!scalar.is_null());
|
||||
assert_eq!(
|
||||
format!("{scalar}"),
|
||||
"hello world, this is a long string view"
|
||||
);
|
||||
|
||||
// Extract from multi-element array
|
||||
let array: ArrayRef = Arc::new(StringViewArray::from(vec!["alpha", "beta", "gamma"]));
|
||||
let s = ArrowScalar::try_new(&array, 1).unwrap();
|
||||
assert_eq!(format!("{s}"), "beta");
|
||||
assert_eq!(s.data_type(), &DataType::Utf8View);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_view_roundtrip() {
|
||||
let values: Vec<&[u8]> = vec![b"\xDE\xAD\xBE\xEF"];
|
||||
let array: ArrayRef = Arc::new(BinaryViewArray::from(values));
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
assert_eq!(scalar.data_type(), &DataType::BinaryView);
|
||||
assert!(!scalar.is_null());
|
||||
|
||||
// Extract from multi-element array
|
||||
let values: Vec<&[u8]> = vec![b"aaa", b"bbb", b"ccc"];
|
||||
let array: ArrayRef = Arc::new(BinaryViewArray::from(values));
|
||||
let s = ArrowScalar::try_new(&array, 2).unwrap();
|
||||
assert_eq!(s.data_type(), &DataType::BinaryView);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_view_equality_and_ordering() {
|
||||
let mk = |s: &str| {
|
||||
let array: ArrayRef = Arc::new(StringViewArray::from(vec![s]));
|
||||
ArrowScalar::try_from_array(array).unwrap()
|
||||
};
|
||||
let a = mk("apple");
|
||||
let b = mk("apple");
|
||||
let c = mk("banana");
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, c);
|
||||
assert!(a < c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_view_equality_and_ordering() {
|
||||
let mk = |b: &[u8]| {
|
||||
let values: Vec<&[u8]> = vec![b];
|
||||
let array: ArrayRef = Arc::new(BinaryViewArray::from(values));
|
||||
ArrowScalar::try_from_array(array).unwrap()
|
||||
};
|
||||
let a = mk(b"\x01\x02");
|
||||
let b = mk(b"\x01\x02");
|
||||
let c = mk(b"\x01\x03");
|
||||
assert_eq!(a, b);
|
||||
assert_ne!(a, c);
|
||||
assert!(a < c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_view_in_collections() {
|
||||
let mk = |s: &str| {
|
||||
let array: ArrayRef = Arc::new(StringViewArray::from(vec![s]));
|
||||
ArrowScalar::try_from_array(array).unwrap()
|
||||
};
|
||||
|
||||
let mut hset = HashSet::new();
|
||||
hset.insert(mk("foo"));
|
||||
hset.insert(mk("bar"));
|
||||
hset.insert(mk("foo"));
|
||||
assert_eq!(hset.len(), 2);
|
||||
|
||||
let mut bset = BTreeSet::new();
|
||||
bset.insert(mk("cherry"));
|
||||
bset.insert(mk("apple"));
|
||||
bset.insert(mk("banana"));
|
||||
let sorted: Vec<_> = bset.iter().map(|s| format!("{s}")).collect();
|
||||
assert_eq!(sorted, vec!["apple", "banana", "cherry"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_view_null() {
|
||||
let array: ArrayRef = Arc::new(StringViewArray::from(vec![Option::<&str>::None]));
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
assert!(scalar.is_null());
|
||||
assert_eq!(scalar.data_type(), &DataType::Utf8View);
|
||||
assert_eq!(format!("{scalar}"), "null");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_view_null() {
|
||||
let array: ArrayRef = Arc::new(BinaryViewArray::from(vec![Option::<&[u8]>::None]));
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
assert!(scalar.is_null());
|
||||
assert_eq!(scalar.data_type(), &DataType::BinaryView);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cross_type_comparison_is_consistent() {
|
||||
let int_scalar = ArrowScalar::from(42i32);
|
||||
let str_scalar = ArrowScalar::from("hello");
|
||||
// The ordering is arbitrary but must be consistent
|
||||
let ord1 = int_scalar.cmp(&str_scalar);
|
||||
let ord2 = int_scalar.cmp(&str_scalar);
|
||||
assert_eq!(ord1, ord2);
|
||||
// And the reverse should be opposite
|
||||
assert_eq!(str_scalar.cmp(&int_scalar), ord1.reverse());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod prop_tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::*;
|
||||
use arrow_ord::sort::sort;
|
||||
use arrow_schema::SortOptions;
|
||||
use proptest::prelude::*;
|
||||
|
||||
use super::ArrowScalar;
|
||||
|
||||
/// Generate an arbitrary Arrow array of a randomly chosen type, including
|
||||
/// nulls. Covers primitives, booleans, string/binary types and their view
|
||||
/// variants.
|
||||
fn arbitrary_array() -> BoxedStrategy<ArrayRef> {
|
||||
let len = 0..=100usize;
|
||||
|
||||
prop_oneof![
|
||||
// --- integer types ---
|
||||
proptest::collection::vec(proptest::option::of(any::<i8>()), len.clone())
|
||||
.prop_map(|v| Arc::new(Int8Array::from(v)) as ArrayRef),
|
||||
proptest::collection::vec(proptest::option::of(any::<i16>()), len.clone())
|
||||
.prop_map(|v| Arc::new(Int16Array::from(v)) as ArrayRef),
|
||||
proptest::collection::vec(proptest::option::of(any::<i32>()), len.clone())
|
||||
.prop_map(|v| Arc::new(Int32Array::from(v)) as ArrayRef),
|
||||
proptest::collection::vec(proptest::option::of(any::<i64>()), len.clone())
|
||||
.prop_map(|v| Arc::new(Int64Array::from(v)) as ArrayRef),
|
||||
proptest::collection::vec(proptest::option::of(any::<u8>()), len.clone())
|
||||
.prop_map(|v| Arc::new(UInt8Array::from(v)) as ArrayRef),
|
||||
proptest::collection::vec(proptest::option::of(any::<u16>()), len.clone())
|
||||
.prop_map(|v| Arc::new(UInt16Array::from(v)) as ArrayRef),
|
||||
proptest::collection::vec(proptest::option::of(any::<u32>()), len.clone())
|
||||
.prop_map(|v| Arc::new(UInt32Array::from(v)) as ArrayRef),
|
||||
proptest::collection::vec(proptest::option::of(any::<u64>()), len.clone())
|
||||
.prop_map(|v| Arc::new(UInt64Array::from(v)) as ArrayRef),
|
||||
// --- float types ---
|
||||
proptest::collection::vec(proptest::option::of(any::<f32>()), len.clone())
|
||||
.prop_map(|v| Arc::new(Float32Array::from(v)) as ArrayRef),
|
||||
proptest::collection::vec(proptest::option::of(any::<f64>()), len.clone())
|
||||
.prop_map(|v| Arc::new(Float64Array::from(v)) as ArrayRef),
|
||||
// --- boolean ---
|
||||
proptest::collection::vec(proptest::option::of(any::<bool>()), len.clone())
|
||||
.prop_map(|v| Arc::new(BooleanArray::from(v)) as ArrayRef),
|
||||
// --- string types ---
|
||||
proptest::collection::vec(proptest::option::of(any::<String>()), len.clone()).prop_map(
|
||||
|v| {
|
||||
let refs: Vec<Option<&str>> = v.iter().map(|o| o.as_deref()).collect();
|
||||
Arc::new(StringArray::from(refs)) as ArrayRef
|
||||
}
|
||||
),
|
||||
proptest::collection::vec(proptest::option::of(any::<String>()), len.clone()).prop_map(
|
||||
|v| {
|
||||
let refs: Vec<Option<&str>> = v.iter().map(|o| o.as_deref()).collect();
|
||||
Arc::new(LargeStringArray::from(refs)) as ArrayRef
|
||||
}
|
||||
),
|
||||
proptest::collection::vec(proptest::option::of(any::<String>()), len.clone()).prop_map(
|
||||
|v| {
|
||||
let refs: Vec<Option<&str>> = v.iter().map(|o| o.as_deref()).collect();
|
||||
Arc::new(StringViewArray::from(refs)) as ArrayRef
|
||||
}
|
||||
),
|
||||
// --- binary types ---
|
||||
proptest::collection::vec(
|
||||
proptest::option::of(proptest::collection::vec(any::<u8>(), 0..50)),
|
||||
len.clone(),
|
||||
)
|
||||
.prop_map(|v| {
|
||||
let refs: Vec<Option<&[u8]>> = v.iter().map(|o| o.as_deref()).collect();
|
||||
Arc::new(BinaryArray::from(refs)) as ArrayRef
|
||||
}),
|
||||
proptest::collection::vec(
|
||||
proptest::option::of(proptest::collection::vec(any::<u8>(), 0..50)),
|
||||
len.clone(),
|
||||
)
|
||||
.prop_map(|v| {
|
||||
let refs: Vec<Option<&[u8]>> = v.iter().map(|o| o.as_deref()).collect();
|
||||
Arc::new(LargeBinaryArray::from(refs)) as ArrayRef
|
||||
}),
|
||||
proptest::collection::vec(
|
||||
proptest::option::of(proptest::collection::vec(any::<u8>(), 0..50)),
|
||||
len,
|
||||
)
|
||||
.prop_map(|v| {
|
||||
let refs: Vec<Option<&[u8]>> = v.iter().map(|o| o.as_deref()).collect();
|
||||
Arc::new(BinaryViewArray::from(refs)) as ArrayRef
|
||||
}),
|
||||
]
|
||||
.boxed()
|
||||
}
|
||||
|
||||
proptest::proptest! {
|
||||
#[test]
|
||||
fn sorted_array_produces_sorted_scalars(array in arbitrary_array()) {
|
||||
let sorted = sort(
|
||||
&array,
|
||||
Some(SortOptions { descending: false, nulls_first: true }),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let scalars: Vec<ArrowScalar> = (0..sorted.len())
|
||||
.map(|i| ArrowScalar::try_new(&sorted, i).unwrap())
|
||||
.collect();
|
||||
|
||||
for i in 1..scalars.len() {
|
||||
prop_assert!(
|
||||
scalars[i - 1] <= scalars[i],
|
||||
"scalar[{}] ({:?}) should be <= scalar[{}] ({:?})",
|
||||
i - 1, scalars[i - 1], i, scalars[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,561 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The Lance Authors
|
||||
|
||||
//! Binary serialization for [`ArrowScalar`].
|
||||
//!
|
||||
//! Default format (with type prefix):
|
||||
//! ```text
|
||||
//! | varint: format_string_len | raw: format_string_bytes |
|
||||
//! | varint: null_flag (0 = non-null, 1 = null) |
|
||||
//! | varint: num_buffers | (only if non-null)
|
||||
//! | varint: buffer_0_len | ... | varint: buffer_{n-1}_len | (only if non-null)
|
||||
//! | raw: buffer_0 bytes | ... | raw: buffer_{n-1} bytes | (only if non-null)
|
||||
//! ```
|
||||
//!
|
||||
//! The format string uses the
|
||||
//! [Arrow C Data Interface](https://arrow.apache.org/docs/format/CDataInterface.html#data-type-description-format-strings)
|
||||
//! encoding. Use [`EncodeOptions`] / [`DecodeOptions`] to omit the type prefix
|
||||
//! when the caller already knows the data type.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::make_array;
|
||||
use arrow_buffer::Buffer;
|
||||
use arrow_data::ArrayDataBuilder;
|
||||
use arrow_schema::{ArrowError, DataType, IntervalUnit, TimeUnit};
|
||||
|
||||
use crate::ArrowScalar;
|
||||
|
||||
type Result<T> = std::result::Result<T, ArrowError>;
|
||||
|
||||
/// Options for [`ArrowScalar::encode_with_options`].
|
||||
pub struct EncodeOptions {
|
||||
/// When `true` (the default), the Arrow C Data Interface format string
|
||||
/// for the scalar's data type is prepended as a varint-length-prefixed
|
||||
/// UTF-8 string. Set to `false` to omit the type prefix (the caller
|
||||
/// must then supply the `DataType` at decode time).
|
||||
pub include_data_type: bool,
|
||||
}
|
||||
|
||||
impl Default for EncodeOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
include_data_type: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Options for [`ArrowScalar::decode_with_options`].
|
||||
#[derive(Default)]
|
||||
pub struct DecodeOptions<'a> {
|
||||
/// When `Some`, the data type is taken from this value and the encoded
|
||||
/// bytes are assumed to contain no type prefix. When `None` (the
|
||||
/// default), the data type is read from the encoded format-string prefix.
|
||||
pub data_type: Option<&'a DataType>,
|
||||
}
|
||||
|
||||
/// Encode a `u64` as a variable-length integer (LEB128).
|
||||
///
|
||||
/// Values below 128 use a single byte; the maximum encoding is 10 bytes.
|
||||
pub fn encode_varint(out: &mut Vec<u8>, mut value: u64) {
|
||||
loop {
|
||||
let byte = (value & 0x7F) as u8;
|
||||
value >>= 7;
|
||||
if value == 0 {
|
||||
out.push(byte);
|
||||
return;
|
||||
}
|
||||
out.push(byte | 0x80);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode a variable-length integer (LEB128) from `buf` at the given `offset`.
|
||||
///
|
||||
/// On success, `offset` is advanced past the consumed bytes.
|
||||
pub fn decode_varint(buf: &[u8], offset: &mut usize) -> Result<u64> {
|
||||
let mut result: u64 = 0;
|
||||
let mut shift = 0u32;
|
||||
loop {
|
||||
if *offset >= buf.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid varint: unexpected EOF".to_string(),
|
||||
));
|
||||
}
|
||||
let byte = buf[*offset];
|
||||
*offset += 1;
|
||||
|
||||
result |= u64::from(byte & 0x7F) << shift;
|
||||
if byte & 0x80 == 0 {
|
||||
return Ok(result);
|
||||
}
|
||||
shift += 7;
|
||||
if shift >= 64 {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid varint: too many bytes".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a [`DataType`] to its Arrow C Data Interface format string.
|
||||
///
|
||||
/// Only non-nested types are supported (nested types are already rejected by
|
||||
/// [`ArrowScalar::encode`]).
|
||||
fn data_type_to_format_string(dtype: &DataType) -> Result<Cow<'static, str>> {
|
||||
match dtype {
|
||||
DataType::Null => Ok("n".into()),
|
||||
DataType::Boolean => Ok("b".into()),
|
||||
DataType::Int8 => Ok("c".into()),
|
||||
DataType::UInt8 => Ok("C".into()),
|
||||
DataType::Int16 => Ok("s".into()),
|
||||
DataType::UInt16 => Ok("S".into()),
|
||||
DataType::Int32 => Ok("i".into()),
|
||||
DataType::UInt32 => Ok("I".into()),
|
||||
DataType::Int64 => Ok("l".into()),
|
||||
DataType::UInt64 => Ok("L".into()),
|
||||
DataType::Float16 => Ok("e".into()),
|
||||
DataType::Float32 => Ok("f".into()),
|
||||
DataType::Float64 => Ok("g".into()),
|
||||
DataType::Binary => Ok("z".into()),
|
||||
DataType::LargeBinary => Ok("Z".into()),
|
||||
DataType::Utf8 => Ok("u".into()),
|
||||
DataType::LargeUtf8 => Ok("U".into()),
|
||||
DataType::BinaryView => Ok("vz".into()),
|
||||
DataType::Utf8View => Ok("vu".into()),
|
||||
DataType::FixedSizeBinary(n) => Ok(Cow::Owned(format!("w:{n}"))),
|
||||
DataType::Decimal32(p, s) => Ok(Cow::Owned(format!("d:{p},{s},32"))),
|
||||
DataType::Decimal64(p, s) => Ok(Cow::Owned(format!("d:{p},{s},64"))),
|
||||
DataType::Decimal128(p, s) => Ok(Cow::Owned(format!("d:{p},{s}"))),
|
||||
DataType::Decimal256(p, s) => Ok(Cow::Owned(format!("d:{p},{s},256"))),
|
||||
DataType::Date32 => Ok("tdD".into()),
|
||||
DataType::Date64 => Ok("tdm".into()),
|
||||
DataType::Time32(TimeUnit::Second) => Ok("tts".into()),
|
||||
DataType::Time32(TimeUnit::Millisecond) => Ok("ttm".into()),
|
||||
DataType::Time64(TimeUnit::Microsecond) => Ok("ttu".into()),
|
||||
DataType::Time64(TimeUnit::Nanosecond) => Ok("ttn".into()),
|
||||
DataType::Timestamp(TimeUnit::Second, None) => Ok("tss:".into()),
|
||||
DataType::Timestamp(TimeUnit::Millisecond, None) => Ok("tsm:".into()),
|
||||
DataType::Timestamp(TimeUnit::Microsecond, None) => Ok("tsu:".into()),
|
||||
DataType::Timestamp(TimeUnit::Nanosecond, None) => Ok("tsn:".into()),
|
||||
DataType::Timestamp(TimeUnit::Second, Some(tz)) => Ok(Cow::Owned(format!("tss:{tz}"))),
|
||||
DataType::Timestamp(TimeUnit::Millisecond, Some(tz)) => Ok(Cow::Owned(format!("tsm:{tz}"))),
|
||||
DataType::Timestamp(TimeUnit::Microsecond, Some(tz)) => Ok(Cow::Owned(format!("tsu:{tz}"))),
|
||||
DataType::Timestamp(TimeUnit::Nanosecond, Some(tz)) => Ok(Cow::Owned(format!("tsn:{tz}"))),
|
||||
DataType::Duration(TimeUnit::Second) => Ok("tDs".into()),
|
||||
DataType::Duration(TimeUnit::Millisecond) => Ok("tDm".into()),
|
||||
DataType::Duration(TimeUnit::Microsecond) => Ok("tDu".into()),
|
||||
DataType::Duration(TimeUnit::Nanosecond) => Ok("tDn".into()),
|
||||
DataType::Interval(IntervalUnit::YearMonth) => Ok("tiM".into()),
|
||||
DataType::Interval(IntervalUnit::DayTime) => Ok("tiD".into()),
|
||||
DataType::Interval(IntervalUnit::MonthDayNano) => Ok("tin".into()),
|
||||
other => Err(ArrowError::InvalidArgumentError(format!(
|
||||
"Cannot encode data type as format string: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse an Arrow C Data Interface format string back to a [`DataType`].
|
||||
///
|
||||
/// Only non-nested types are supported.
|
||||
fn format_string_to_data_type(fmt: &str) -> Result<DataType> {
|
||||
match fmt {
|
||||
"n" => Ok(DataType::Null),
|
||||
"b" => Ok(DataType::Boolean),
|
||||
"c" => Ok(DataType::Int8),
|
||||
"C" => Ok(DataType::UInt8),
|
||||
"s" => Ok(DataType::Int16),
|
||||
"S" => Ok(DataType::UInt16),
|
||||
"i" => Ok(DataType::Int32),
|
||||
"I" => Ok(DataType::UInt32),
|
||||
"l" => Ok(DataType::Int64),
|
||||
"L" => Ok(DataType::UInt64),
|
||||
"e" => Ok(DataType::Float16),
|
||||
"f" => Ok(DataType::Float32),
|
||||
"g" => Ok(DataType::Float64),
|
||||
"z" => Ok(DataType::Binary),
|
||||
"Z" => Ok(DataType::LargeBinary),
|
||||
"u" => Ok(DataType::Utf8),
|
||||
"U" => Ok(DataType::LargeUtf8),
|
||||
"vz" => Ok(DataType::BinaryView),
|
||||
"vu" => Ok(DataType::Utf8View),
|
||||
"tdD" => Ok(DataType::Date32),
|
||||
"tdm" => Ok(DataType::Date64),
|
||||
"tts" => Ok(DataType::Time32(TimeUnit::Second)),
|
||||
"ttm" => Ok(DataType::Time32(TimeUnit::Millisecond)),
|
||||
"ttu" => Ok(DataType::Time64(TimeUnit::Microsecond)),
|
||||
"ttn" => Ok(DataType::Time64(TimeUnit::Nanosecond)),
|
||||
"tDs" => Ok(DataType::Duration(TimeUnit::Second)),
|
||||
"tDm" => Ok(DataType::Duration(TimeUnit::Millisecond)),
|
||||
"tDu" => Ok(DataType::Duration(TimeUnit::Microsecond)),
|
||||
"tDn" => Ok(DataType::Duration(TimeUnit::Nanosecond)),
|
||||
"tiM" => Ok(DataType::Interval(IntervalUnit::YearMonth)),
|
||||
"tiD" => Ok(DataType::Interval(IntervalUnit::DayTime)),
|
||||
"tin" => Ok(DataType::Interval(IntervalUnit::MonthDayNano)),
|
||||
other => {
|
||||
let parts: Vec<&str> = other.splitn(2, ':').collect();
|
||||
match parts.as_slice() {
|
||||
["w", num_bytes] => {
|
||||
let n = num_bytes.parse::<i32>().map_err(|_| {
|
||||
ArrowError::InvalidArgumentError(
|
||||
"FixedSizeBinary requires an integer byte count".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(DataType::FixedSizeBinary(n))
|
||||
}
|
||||
["d", extra] => {
|
||||
let dec_parts: Vec<&str> = extra.splitn(3, ',').collect();
|
||||
match dec_parts.as_slice() {
|
||||
[precision, scale] => {
|
||||
let p = precision.parse::<u8>().map_err(|_| {
|
||||
ArrowError::InvalidArgumentError(
|
||||
"Decimal requires an integer precision".to_string(),
|
||||
)
|
||||
})?;
|
||||
let s = scale.parse::<i8>().map_err(|_| {
|
||||
ArrowError::InvalidArgumentError(
|
||||
"Decimal requires an integer scale".to_string(),
|
||||
)
|
||||
})?;
|
||||
Ok(DataType::Decimal128(p, s))
|
||||
}
|
||||
[precision, scale, bits] => {
|
||||
let p = precision.parse::<u8>().map_err(|_| {
|
||||
ArrowError::InvalidArgumentError(
|
||||
"Decimal requires an integer precision".to_string(),
|
||||
)
|
||||
})?;
|
||||
let s = scale.parse::<i8>().map_err(|_| {
|
||||
ArrowError::InvalidArgumentError(
|
||||
"Decimal requires an integer scale".to_string(),
|
||||
)
|
||||
})?;
|
||||
match *bits {
|
||||
"32" => Ok(DataType::Decimal32(p, s)),
|
||||
"64" => Ok(DataType::Decimal64(p, s)),
|
||||
"128" => Ok(DataType::Decimal128(p, s)),
|
||||
"256" => Ok(DataType::Decimal256(p, s)),
|
||||
_ => Err(ArrowError::InvalidArgumentError(format!(
|
||||
"Unsupported decimal bit width: {bits}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
_ => Err(ArrowError::InvalidArgumentError(format!(
|
||||
"Invalid decimal format string: d:{extra}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
["tss", ""] => Ok(DataType::Timestamp(TimeUnit::Second, None)),
|
||||
["tsm", ""] => Ok(DataType::Timestamp(TimeUnit::Millisecond, None)),
|
||||
["tsu", ""] => Ok(DataType::Timestamp(TimeUnit::Microsecond, None)),
|
||||
["tsn", ""] => Ok(DataType::Timestamp(TimeUnit::Nanosecond, None)),
|
||||
["tss", tz] => Ok(DataType::Timestamp(TimeUnit::Second, Some(Arc::from(*tz)))),
|
||||
["tsm", tz] => Ok(DataType::Timestamp(
|
||||
TimeUnit::Millisecond,
|
||||
Some(Arc::from(*tz)),
|
||||
)),
|
||||
["tsu", tz] => Ok(DataType::Timestamp(
|
||||
TimeUnit::Microsecond,
|
||||
Some(Arc::from(*tz)),
|
||||
)),
|
||||
["tsn", tz] => Ok(DataType::Timestamp(
|
||||
TimeUnit::Nanosecond,
|
||||
Some(Arc::from(*tz)),
|
||||
)),
|
||||
_ => Err(ArrowError::InvalidArgumentError(format!(
|
||||
"Unsupported format string: {other:?}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ArrowScalar {
|
||||
/// Serialize this scalar to a self-describing binary representation.
|
||||
///
|
||||
/// The data type is encoded as a format-string prefix so that
|
||||
/// [`decode`](Self::decode) can reconstruct the scalar without external
|
||||
/// type information. Use [`encode_with_options`](Self::encode_with_options)
|
||||
/// to omit the prefix when the caller already knows the type.
|
||||
///
|
||||
/// Only non-nested scalars are supported. Null scalars are encoded as a
|
||||
/// null flag with no buffer data.
|
||||
pub fn encode(&self) -> Result<Vec<u8>> {
|
||||
self.encode_with_options(&EncodeOptions::default())
|
||||
}
|
||||
|
||||
/// Serialize this scalar with the given [`EncodeOptions`].
|
||||
pub fn encode_with_options(&self, options: &EncodeOptions) -> Result<Vec<u8>> {
|
||||
let array = self.as_array();
|
||||
let data = array.to_data();
|
||||
if !data.child_data().is_empty() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Cannot encode nested scalar".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(64);
|
||||
|
||||
if options.include_data_type {
|
||||
let fmt = data_type_to_format_string(array.data_type())?;
|
||||
encode_varint(&mut out, fmt.len() as u64);
|
||||
out.extend_from_slice(fmt.as_bytes());
|
||||
}
|
||||
|
||||
if self.is_null() {
|
||||
encode_varint(&mut out, 1); // null_flag = 1
|
||||
} else {
|
||||
encode_varint(&mut out, 0); // null_flag = 0
|
||||
let buffers = data.buffers();
|
||||
encode_varint(&mut out, buffers.len() as u64);
|
||||
for b in buffers {
|
||||
encode_varint(&mut out, b.len() as u64);
|
||||
}
|
||||
for b in buffers {
|
||||
out.extend_from_slice(b.as_slice());
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Deserialize a scalar from the self-describing binary representation
|
||||
/// produced by [`encode`](Self::encode).
|
||||
///
|
||||
/// The data type is read from the format-string prefix in the encoded
|
||||
/// bytes. Use [`decode_with_options`](Self::decode_with_options) to supply
|
||||
/// the type externally when the prefix was omitted at encode time.
|
||||
pub fn decode(buf: &[u8]) -> Result<Self> {
|
||||
Self::decode_with_options(buf, &DecodeOptions::default())
|
||||
}
|
||||
|
||||
/// Deserialize a scalar with the given [`DecodeOptions`].
|
||||
pub fn decode_with_options(buf: &[u8], options: &DecodeOptions) -> Result<Self> {
|
||||
let mut offset = 0;
|
||||
|
||||
let data_type = match options.data_type {
|
||||
Some(dt) => dt.clone(),
|
||||
None => {
|
||||
let fmt_len = decode_varint(buf, &mut offset)? as usize;
|
||||
if offset + fmt_len > buf.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid scalar buffer: unexpected EOF reading format string".to_string(),
|
||||
));
|
||||
}
|
||||
let fmt_str = std::str::from_utf8(&buf[offset..offset + fmt_len]).map_err(|e| {
|
||||
ArrowError::InvalidArgumentError(format!(
|
||||
"Invalid format string: not valid UTF-8: {e}"
|
||||
))
|
||||
})?;
|
||||
offset += fmt_len;
|
||||
format_string_to_data_type(fmt_str)?
|
||||
}
|
||||
};
|
||||
|
||||
let null_flag = decode_varint(buf, &mut offset)?;
|
||||
if null_flag == 1 {
|
||||
if offset != buf.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid scalar buffer: trailing bytes after null flag".to_string(),
|
||||
));
|
||||
}
|
||||
return Self::new_null(&data_type);
|
||||
}
|
||||
|
||||
let num_buffers = decode_varint(buf, &mut offset)? as usize;
|
||||
|
||||
let mut buffer_lens = Vec::with_capacity(num_buffers);
|
||||
for _ in 0..num_buffers {
|
||||
buffer_lens.push(decode_varint(buf, &mut offset)? as usize);
|
||||
}
|
||||
|
||||
let mut buffers = Vec::with_capacity(num_buffers);
|
||||
for len in &buffer_lens {
|
||||
if offset + len > buf.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid scalar buffer: unexpected EOF".to_string(),
|
||||
));
|
||||
}
|
||||
buffers.push(Buffer::from_vec(buf[offset..offset + len].to_vec()));
|
||||
offset += len;
|
||||
}
|
||||
|
||||
if offset != buf.len() {
|
||||
return Err(ArrowError::InvalidArgumentError(
|
||||
"Invalid scalar buffer: trailing bytes".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut builder = ArrayDataBuilder::new(data_type).len(1).null_count(0);
|
||||
for b in buffers {
|
||||
builder = builder.add_buffer(b);
|
||||
}
|
||||
let array = make_array(builder.build()?);
|
||||
Self::try_from_array(array)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use arrow_array::{
|
||||
ArrayRef, BinaryViewArray, Int32Array, StringArray, StringViewArray,
|
||||
TimestampMicrosecondArray,
|
||||
};
|
||||
use arrow_schema::DataType;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::ArrowScalar;
|
||||
|
||||
#[test]
|
||||
fn test_varint_roundtrip() {
|
||||
for value in [0u64, 1, 127, 128, 16383, 16384, u64::MAX] {
|
||||
let mut buf = Vec::new();
|
||||
encode_varint(&mut buf, value);
|
||||
let mut offset = 0;
|
||||
let decoded = decode_varint(&buf, &mut offset).unwrap();
|
||||
assert_eq!(decoded, value);
|
||||
assert_eq!(offset, buf.len());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_varint_small_is_one_byte() {
|
||||
let mut buf = Vec::new();
|
||||
encode_varint(&mut buf, 42);
|
||||
assert_eq!(buf.len(), 1);
|
||||
assert_eq!(buf[0], 42);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::int32(Arc::new(Int32Array::from(vec![42])) as ArrayRef)]
|
||||
#[case::string(Arc::new(StringArray::from(vec!["hello"])) as ArrayRef)]
|
||||
#[case::string_view(Arc::new(StringViewArray::from(vec!["hello world, long string view"])) as ArrayRef)]
|
||||
#[case::binary_view(Arc::new(BinaryViewArray::from(vec![b"\xDE\xAD\xBE\xEF".as_ref()])) as ArrayRef)]
|
||||
fn test_encode_decode_roundtrip(#[case] array: ArrayRef) {
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
let encoded = scalar.encode().unwrap();
|
||||
let decoded = ArrowScalar::decode(&encoded).unwrap();
|
||||
assert_eq!(scalar, decoded);
|
||||
assert_eq!(scalar.data_type(), decoded.data_type());
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::int32(Arc::new(Int32Array::from(vec![42])) as ArrayRef, DataType::Int32)]
|
||||
#[case::string(Arc::new(StringArray::from(vec!["hello"])) as ArrayRef, DataType::Utf8)]
|
||||
#[case::string_view(Arc::new(StringViewArray::from(vec!["hello view"])) as ArrayRef, DataType::Utf8View)]
|
||||
#[case::binary_view(Arc::new(BinaryViewArray::from(vec![b"\xCA\xFE".as_ref()])) as ArrayRef, DataType::BinaryView)]
|
||||
fn test_encode_decode_without_type_prefix(#[case] array: ArrayRef, #[case] dt: DataType) {
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
let opts = EncodeOptions {
|
||||
include_data_type: false,
|
||||
};
|
||||
let encoded = scalar.encode_with_options(&opts).unwrap();
|
||||
let decode_opts = DecodeOptions {
|
||||
data_type: Some(&dt),
|
||||
};
|
||||
let decoded = ArrowScalar::decode_with_options(&encoded, &decode_opts).unwrap();
|
||||
assert_eq!(scalar, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_encode_decode_roundtrip() {
|
||||
let array: ArrayRef = Arc::new(Int32Array::from(vec![None]));
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
assert!(scalar.is_null());
|
||||
let encoded = scalar.encode().unwrap();
|
||||
let decoded = ArrowScalar::decode(&encoded).unwrap();
|
||||
assert!(decoded.is_null());
|
||||
assert_eq!(decoded.data_type(), &DataType::Int32);
|
||||
assert_eq!(scalar, decoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_null_encode_decode_without_type_prefix() {
|
||||
let array: ArrayRef = Arc::new(StringArray::from(vec![Option::<&str>::None]));
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
let opts = EncodeOptions {
|
||||
include_data_type: false,
|
||||
};
|
||||
let encoded = scalar.encode_with_options(&opts).unwrap();
|
||||
let decode_opts = DecodeOptions {
|
||||
data_type: Some(&DataType::Utf8),
|
||||
};
|
||||
let decoded = ArrowScalar::decode_with_options(&encoded, &decode_opts).unwrap();
|
||||
assert!(decoded.is_null());
|
||||
assert_eq!(decoded.data_type(), &DataType::Utf8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_trailing_bytes() {
|
||||
let scalar = ArrowScalar::from(42i32);
|
||||
let mut encoded = scalar.encode().unwrap();
|
||||
encoded.push(0xFF);
|
||||
assert!(ArrowScalar::decode(&encoded).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoded_bytes_contain_format_prefix() {
|
||||
let scalar = ArrowScalar::from(42i32);
|
||||
let encoded = scalar.encode().unwrap();
|
||||
// First byte is varint length of format string "i" (length 1)
|
||||
assert_eq!(encoded[0], 1);
|
||||
// Second byte is the format string itself
|
||||
assert_eq!(encoded[1], b'i');
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::null(DataType::Null, "n")]
|
||||
#[case::boolean(DataType::Boolean, "b")]
|
||||
#[case::int8(DataType::Int8, "c")]
|
||||
#[case::uint8(DataType::UInt8, "C")]
|
||||
#[case::int16(DataType::Int16, "s")]
|
||||
#[case::uint16(DataType::UInt16, "S")]
|
||||
#[case::int32(DataType::Int32, "i")]
|
||||
#[case::uint32(DataType::UInt32, "I")]
|
||||
#[case::int64(DataType::Int64, "l")]
|
||||
#[case::uint64(DataType::UInt64, "L")]
|
||||
#[case::float16(DataType::Float16, "e")]
|
||||
#[case::float32(DataType::Float32, "f")]
|
||||
#[case::float64(DataType::Float64, "g")]
|
||||
#[case::binary(DataType::Binary, "z")]
|
||||
#[case::large_binary(DataType::LargeBinary, "Z")]
|
||||
#[case::utf8(DataType::Utf8, "u")]
|
||||
#[case::large_utf8(DataType::LargeUtf8, "U")]
|
||||
#[case::binary_view(DataType::BinaryView, "vz")]
|
||||
#[case::utf8_view(DataType::Utf8View, "vu")]
|
||||
#[case::date32(DataType::Date32, "tdD")]
|
||||
#[case::date64(DataType::Date64, "tdm")]
|
||||
#[case::fixed_size_binary(DataType::FixedSizeBinary(16), "w:16")]
|
||||
#[case::decimal128(DataType::Decimal128(10, 2), "d:10,2")]
|
||||
#[case::decimal256(DataType::Decimal256(38, 10), "d:38,10,256")]
|
||||
#[case::timestamp_us_utc(
|
||||
DataType::Timestamp(TimeUnit::Microsecond, Some(Arc::from("UTC"))),
|
||||
"tsu:UTC"
|
||||
)]
|
||||
#[case::timestamp_ns_none(DataType::Timestamp(TimeUnit::Nanosecond, None), "tsn:")]
|
||||
#[case::duration_s(DataType::Duration(TimeUnit::Second), "tDs")]
|
||||
#[case::interval_ym(DataType::Interval(IntervalUnit::YearMonth), "tiM")]
|
||||
fn test_format_string_roundtrip(#[case] dt: DataType, #[case] expected_fmt: &str) {
|
||||
let fmt = data_type_to_format_string(&dt).unwrap();
|
||||
assert_eq!(fmt.as_ref(), expected_fmt);
|
||||
let roundtripped = format_string_to_data_type(&fmt).unwrap();
|
||||
assert_eq!(roundtripped, dt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_timestamp_with_tz_roundtrip() {
|
||||
let array: ArrayRef = Arc::new(
|
||||
TimestampMicrosecondArray::from(vec![1_000_000]).with_timezone("America/New_York"),
|
||||
);
|
||||
let scalar = ArrowScalar::try_from_array(array).unwrap();
|
||||
let encoded = scalar.encode().unwrap();
|
||||
let decoded = ArrowScalar::decode(&encoded).unwrap();
|
||||
assert_eq!(scalar, decoded);
|
||||
assert_eq!(scalar.data_type(), decoded.data_type());
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
[package]
|
||||
name = "lance-arrow-stats"
|
||||
version = "58.0.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
repository.workspace = true
|
||||
description = "Statistics accumulator for Arrow arrays (min, max, null_count, nan_count)"
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
arrow-array = { workspace = true }
|
||||
arrow-schema = { workspace = true }
|
||||
lance-arrow-scalar = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
arrow-select = { workspace = true }
|
||||
proptest = { workspace = true }
|
||||
rstest = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,62 +0,0 @@
|
||||
# lance-arrow-stats
|
||||
|
||||
Statistics accumulator for [Apache Arrow](https://arrow.apache.org/) arrays.
|
||||
|
||||
Computes min, max, null count, NaN count, and buffer memory usage over one or
|
||||
more batches of Arrow data. Designed for use in Lance's columnar storage layer
|
||||
where page-level statistics drive predicate pushdown and query planning.
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use arrow_array::{Int32Array, ArrayRef};
|
||||
use lance_arrow_stats::StatisticsAccumulator;
|
||||
use arrow_schema::DataType;
|
||||
use std::sync::Arc;
|
||||
|
||||
let mut acc = StatisticsAccumulator::new(&DataType::Int32);
|
||||
|
||||
let batch: ArrayRef = Arc::new(Int32Array::from(vec![Some(3), None, Some(1), Some(4)]));
|
||||
acc.update(&batch).unwrap();
|
||||
|
||||
let stats = acc.finish();
|
||||
assert_eq!(stats.null_count, 1);
|
||||
```
|
||||
|
||||
## Tracked Statistics
|
||||
|
||||
| Statistic | Description |
|
||||
| --------------- | -------------------------------------------------------- |
|
||||
| `min` | Minimum non-null, non-NaN value (`ArrowScalar`) |
|
||||
| `max` | Maximum non-null, non-NaN value (`ArrowScalar`) |
|
||||
| `null_count` | Total number of null values |
|
||||
| `nan_count` | Total NaN values (float and float-list types only) |
|
||||
| `item_nulls` | Null items inside list entries (list types only) |
|
||||
| `buffer_memory` | Total Arrow buffer memory in bytes |
|
||||
|
||||
## Supported Types
|
||||
|
||||
- **Numeric** — Int8–Int64, UInt8–UInt64, Float16/32/64
|
||||
- **Temporal** — Date32/64, Time32/64, Timestamp, Duration
|
||||
- **Boolean**
|
||||
- **String** — Utf8, LargeUtf8
|
||||
- **Binary** — Binary, LargeBinary
|
||||
- **List** — List, LargeList, FixedSizeList (computes stats over items)
|
||||
|
||||
Dictionary, run-end encoded, and view types are accepted but min/max will be
|
||||
`None`.
|
||||
|
||||
## Merging
|
||||
|
||||
Accumulators of the same data type can be merged, which is useful for combining
|
||||
statistics computed in parallel across different pages or files:
|
||||
|
||||
```rust
|
||||
use lance_arrow_stats::StatisticsAccumulator;
|
||||
use arrow_schema::DataType;
|
||||
|
||||
let mut a = StatisticsAccumulator::new(&DataType::Float32);
|
||||
let mut b = StatisticsAccumulator::new(&DataType::Float32);
|
||||
// ... update each with different batches ...
|
||||
a.merge(&b).unwrap();
|
||||
```
|
||||
@@ -1,8 +0,0 @@
|
||||
# Seeds for failure cases proptest has generated in the past. It is
|
||||
# automatically read and these particular cases re-run before any
|
||||
# novel cases are generated.
|
||||
#
|
||||
# It is recommended to check this file in to source control so that
|
||||
# everyone who runs the test benefits from these saved cases.
|
||||
cc 81b0445f36fa8f491c1fb3162f51b61c8be140d5b2a1e792c42b4bdb7f1b6a62 # shrinks to values = [0.0, -0.0]
|
||||
cc 8651fce939497f33c6dafd842937d95965af97833bfbbd10df30d5ea00dbd07d # shrinks to values = [Some(0.0), Some(-0.0)]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user