mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-30 09:58:20 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1825f6df9e | |||
| 6a1dacb1fe | |||
| 62dea8acd8 | |||
| ae9e8e8f8d | |||
| 36e44ab7a9 | |||
| feccabd739 | |||
| 1f3093a51f |
@@ -1,222 +0,0 @@
|
||||
name: Check doc links
|
||||
|
||||
# Checking external links is inherently noisy: third-party sites rate-limit
|
||||
# automated clients, reject non-browser user agents, and go down temporarily.
|
||||
# Blocking pull requests on that trades a lot of false failures for very little
|
||||
# signal, so this runs on a schedule and reports findings in a single tracking
|
||||
# issue instead of failing anyone's build.
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 7 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
# The report lives in one repository-global issue, so runs must not overlap: a
|
||||
# lookup racing a create produces duplicate issues, and a healthy run closing
|
||||
# the issue while a failing run only rewrites its body would leave a broken
|
||||
# report closed. The group is deliberately ref-independent so that a manual
|
||||
# dispatch serializes against the scheduled run.
|
||||
concurrency:
|
||||
group: docs-link-check
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions: {}
|
||||
|
||||
env:
|
||||
REPORT_TITLE: "Docs link checker report"
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
name: Scan links
|
||||
runs-on: ubuntu-24.04
|
||||
# lychee-action is pinned by SHA, but its wrapper downloads the lychee
|
||||
# release tarball at run time without verifying a digest, and hands the
|
||||
# resulting binary a GitHub token. Release assets remain replaceable, so
|
||||
# that binary is confined to a job whose token can only read public
|
||||
# content; everything that writes runs in the report job below.
|
||||
permissions:
|
||||
contents: read
|
||||
outputs:
|
||||
exit_code: ${{ steps.lychee.outputs.exit_code }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
# workflow_dispatch can run from any ref, but the report is
|
||||
# repository-global. Always measure the default branch so a manual
|
||||
# run from a topic branch cannot close a report that main warrants,
|
||||
# or overwrite it with branch-only findings.
|
||||
ref: ${{ github.event.repository.default_branch }}
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check links
|
||||
id: lychee
|
||||
uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
|
||||
with:
|
||||
# Restricted to http(s) on purpose. Much of docs/src is generated
|
||||
# API reference (the js/ tree comes from `npm run docs` in nodejs)
|
||||
# and the hand-written pages use mkdocstrings cross-references and
|
||||
# nav-relative paths that only resolve in the site mkdocs builds,
|
||||
# not in this checkout, so relative links would be reported as
|
||||
# broken on every run.
|
||||
args: >-
|
||||
--scheme https
|
||||
--scheme http
|
||||
--no-progress
|
||||
--max-retries 3
|
||||
--timeout 20
|
||||
'docs/src/**/*.md'
|
||||
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.
|
||||
fail: false
|
||||
|
||||
- name: Validate report
|
||||
# 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
|
||||
env:
|
||||
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
|
||||
|
||||
- name: Upload report
|
||||
if: steps.lychee.outputs.exit_code == 2
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: link-report
|
||||
path: ./lychee/out.json
|
||||
retention-days: 7
|
||||
|
||||
report:
|
||||
name: Update report issue
|
||||
needs: scan
|
||||
runs-on: ubuntu-24.04
|
||||
# Deliberately no checkout: this job needs the report artifact and the
|
||||
# issues API, not the repository contents.
|
||||
permissions:
|
||||
issues: write
|
||||
env:
|
||||
EXIT_CODE: ${{ needs.scan.outputs.exit_code }}
|
||||
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:
|
||||
# the issue action applies labels in a separate call after creating the
|
||||
# issue, so a label filter misses a half-created report, and this
|
||||
# repository has far more open issues than one listing page holds.
|
||||
# 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.
|
||||
run: |
|
||||
match=$(gh issue list --repo "$GITHUB_REPOSITORY" --state all \
|
||||
--search "in:title \"$REPORT_TITLE\" author:app/github-actions" \
|
||||
--limit 50 --json number,title,state \
|
||||
--jq "[.[] | select(.title == \"$REPORT_TITLE\")] | sort_by(.number) | first // empty")
|
||||
echo "number=$(jq -r '.number // empty' <<<"$match")" >> "$GITHUB_OUTPUT"
|
||||
echo "state=$(jq -r '.state // empty' <<<"$match")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download report
|
||||
if: env.EXIT_CODE == 2
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: link-report
|
||||
path: ./lychee
|
||||
|
||||
- name: Compose report
|
||||
if: env.EXIT_CODE == 2
|
||||
run: |
|
||||
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
||||
{
|
||||
echo "Broken documentation links found by [\`$GITHUB_WORKFLOW\`]($run_url)."
|
||||
echo
|
||||
echo "This issue is rewritten by every scheduled run and closed automatically once all links resolve."
|
||||
echo
|
||||
echo "Entries can be false positives: some sites rate-limit or block automated clients while working fine in a browser. Confirm before editing the docs, and add persistent offenders to \`--exclude\` in \`.github/workflows/docs-link-check.yml\`."
|
||||
echo
|
||||
# Timeouts are reported alongside errors: entries land in
|
||||
# timeout_map with a status text instead of an HTTP code.
|
||||
jq -r '
|
||||
"\(.errors) of \(.total) links failed, \(.timeouts) timed out.",
|
||||
"",
|
||||
([(.error_map | to_entries[]), (.timeout_map | to_entries[])]
|
||||
| group_by(.key)[] |
|
||||
"### Errors in \(.[0].key)",
|
||||
"",
|
||||
(map(.value[])[] | "* [\(.status.code // .status.text // "ERR")] <\(.url)> — \(.status.details // .status.text // "unknown error")"),
|
||||
"")
|
||||
' ./lychee/out.json
|
||||
} > ./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'
|
||||
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)."
|
||||
|
||||
- name: Report broken links
|
||||
if: env.EXIT_CODE == 2
|
||||
uses: peter-evans/create-issue-from-file@fca9117c27cdc29c6c4db3b86c48e4115a786710 # v6.0.0
|
||||
with:
|
||||
# Empty on the first failing run, which creates the issue; afterwards
|
||||
# the same issue is updated in place.
|
||||
issue-number: ${{ steps.report.outputs.number }}
|
||||
title: ${{ env.REPORT_TITLE }}
|
||||
content-filepath: ./lychee/issue.md
|
||||
labels: documentation
|
||||
|
||||
- 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'
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ steps.report.outputs.number }}
|
||||
run: |
|
||||
run_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"
|
||||
gh issue close "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" \
|
||||
--comment "All documentation links resolved in [the latest run]($run_url)."
|
||||
@@ -296,18 +296,16 @@ jobs:
|
||||
cargo update -p aws-types --precise 1.3.9
|
||||
cargo update -p aws-sigv4 --precise 1.3.5
|
||||
cargo update -p aws-credential-types --precise 1.2.8
|
||||
# aws-smithy-checksums must stay at or above 0.63.13: OpenDAL's S3
|
||||
# service needs crc-fast ~1.9, and older releases pin it to ~1.3.
|
||||
cargo update -p aws-smithy-checksums --precise 0.63.13
|
||||
cargo update -p aws-smithy-checksums --precise 0.63.9
|
||||
cargo update -p aws-smithy-runtime --precise 1.9.3
|
||||
cargo update -p aws-smithy-http --precise 0.62.6
|
||||
cargo update -p aws-smithy-eventstream --precise 0.60.14
|
||||
cargo update -p aws-smithy-http --precise 0.62.4
|
||||
cargo update -p aws-smithy-eventstream --precise 0.60.12
|
||||
cargo update -p aws-smithy-http-client --precise 1.1.3
|
||||
cargo update -p aws-smithy-observability --precise 0.1.4
|
||||
cargo update -p aws-smithy-query --precise 0.60.8
|
||||
cargo update -p aws-smithy-runtime-api --precise 1.9.3
|
||||
cargo update -p aws-smithy-async --precise 1.2.7
|
||||
cargo update -p aws-smithy-types --precise 1.3.6
|
||||
cargo update -p aws-smithy-runtime-api --precise 1.9.1
|
||||
cargo update -p aws-smithy-async --precise 1.2.6
|
||||
cargo update -p aws-smithy-types --precise 1.3.5
|
||||
cargo update -p aws-smithy-xml --precise 0.60.11
|
||||
cargo update -p home --precise 0.5.9
|
||||
- name: cargo +${{ matrix.msrv }} check
|
||||
|
||||
Generated
+233
-257
File diff suppressed because it is too large
Load Diff
+15
-15
@@ -13,20 +13,20 @@ categories = ["database-implementations"]
|
||||
rust-version = "1.91.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
lance = { "version" = "=11.0.0-beta.2", default-features = false, "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-core = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datagen = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-file = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-io = { "version" = "=11.0.0-beta.2", default-features = false, "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-index = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-linalg = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace-impls = { "version" = "=11.0.0-beta.2", default-features = false, "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-table = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-testing = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datafusion = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-encoding = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-arrow = { "version" = "=11.0.0-beta.2", "tag" = "v11.0.0-beta.2", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance = { "version" = "=10.1.0-beta.1", default-features = false, "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-core = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datagen = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-file = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-io = { "version" = "=10.1.0-beta.1", default-features = false, "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-index = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-linalg = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-namespace-impls = { "version" = "=10.1.0-beta.1", default-features = false, "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-table = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-testing = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-datafusion = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-encoding = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
lance-arrow = { "version" = "=10.1.0-beta.1", "tag" = "v10.1.0-beta.1", "git" = "https://github.com/lance-format/lance.git" }
|
||||
ahash = "0.8"
|
||||
# Note that this one does not include pyarrow
|
||||
arrow = { version = "58.0.0", optional = false }
|
||||
@@ -52,7 +52,7 @@ env_logger = "0.11"
|
||||
half = { "version" = "2.7.1", default-features = false, features = [
|
||||
"num-traits",
|
||||
] }
|
||||
futures = "0.3"
|
||||
futures = "0"
|
||||
log = "0.4"
|
||||
metrics = "0.24"
|
||||
metrics-util = "0.19"
|
||||
|
||||
@@ -31,7 +31,7 @@ is also an [asynchronous API client](#connections-asynchronous).
|
||||
## Namespaces (Synchronous)
|
||||
|
||||
A namespace-backed connection resolves tables through a
|
||||
[Lance namespace](https://lance-format.github.io/lance-namespace/) service instead of
|
||||
[Lance namespace](https://lancedb.github.io/lance-namespace/) service instead of
|
||||
listing a storage directory.
|
||||
|
||||
::: lancedb.connect_namespace
|
||||
|
||||
+1
-1
@@ -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.2</lance-core.version>
|
||||
<lance-core.version>10.1.0-beta.1</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>
|
||||
|
||||
@@ -11,11 +11,8 @@ import {
|
||||
Float16,
|
||||
Float32,
|
||||
Float64,
|
||||
Int32,
|
||||
Schema,
|
||||
Utf8,
|
||||
fromDataToBuffer,
|
||||
tableFromIPC,
|
||||
} from "../lancedb/arrow";
|
||||
import { EmbeddingFunction, LanceSchema } from "../lancedb/embedding";
|
||||
import { getRegistry, register } from "../lancedb/embedding/registry";
|
||||
@@ -187,63 +184,6 @@ describe("embedding functions", () => {
|
||||
const vector0 = JSON.parse(JSON.stringify(arr[0].vector));
|
||||
expect(vector0).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("should append generated vectors to a non-nullable schema", async () => {
|
||||
@register("non_nullable_schema_test")
|
||||
class MockEmbeddingFunction extends EmbeddingFunction<string> {
|
||||
ndims() {
|
||||
return 3;
|
||||
}
|
||||
embeddingDataType(): Float {
|
||||
return new Float64();
|
||||
}
|
||||
async computeSourceEmbeddings(data: string[]) {
|
||||
return data.map(() => [1, 2, 3]);
|
||||
}
|
||||
}
|
||||
|
||||
const schema = new Schema([
|
||||
new Field("id", new Int32()),
|
||||
new Field("text", new Utf8()),
|
||||
new Field("type", new Utf8()),
|
||||
new Field(
|
||||
"vector",
|
||||
new FixedSizeList(3, new Field("item", new Float64())),
|
||||
),
|
||||
]);
|
||||
const func = new MockEmbeddingFunction();
|
||||
const db = await connect(tmpDir.name);
|
||||
const table = await db.createEmptyTable("test_non_nullable", schema, {
|
||||
embeddingFunction: {
|
||||
function: func,
|
||||
sourceColumn: "text",
|
||||
},
|
||||
});
|
||||
|
||||
const data = [
|
||||
{ id: 1, text: "Carrot", type: "vegetable" },
|
||||
{ id: 2, text: "Apple", type: "fruit" },
|
||||
];
|
||||
const buffer = await fromDataToBuffer(
|
||||
data,
|
||||
undefined,
|
||||
await table.schema(),
|
||||
);
|
||||
const generatedTable = tableFromIPC(buffer);
|
||||
const vectorField = generatedTable.schema.fields.find(
|
||||
(field) => field.name === "vector",
|
||||
);
|
||||
expect(vectorField?.nullable).toBe(false);
|
||||
|
||||
await table.add(data);
|
||||
|
||||
const rows = await table.query().toArray();
|
||||
expect(rows).toHaveLength(2);
|
||||
for (const row of rows) {
|
||||
expect([...row.vector]).toEqual([1, 2, 3]);
|
||||
}
|
||||
});
|
||||
|
||||
it("should error when appending to a table with an unregistered embedding function", async () => {
|
||||
@register("mock")
|
||||
class MockEmbeddingFunction extends EmbeddingFunction<string> {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
import packageJson = require("../package.json");
|
||||
|
||||
describe("package metadata", () => {
|
||||
it("requires Node.js type declarations compatible with the runtime", () => {
|
||||
expect(packageJson.engines.node).toBe(">= 18");
|
||||
expect(packageJson.peerDependencies["@types/node"]).toBe(">=18");
|
||||
expect(packageJson.peerDependenciesMeta["@types/node"]).toEqual({
|
||||
optional: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -110,81 +110,6 @@ describe("Query outputSchema", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Search pagination", () => {
|
||||
let tmpDir: tmp.DirResult;
|
||||
let table: Table;
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
||||
const db = await connect(tmpDir.name);
|
||||
const schema = new Schema([
|
||||
new Field("id", new Int64(), false),
|
||||
new Field("text", new Utf8(), false),
|
||||
new Field(
|
||||
"vector",
|
||||
new FixedSizeList(2, new Field("item", new Float32())),
|
||||
false,
|
||||
),
|
||||
]);
|
||||
const data = makeArrowTable(
|
||||
[
|
||||
{ id: 1n, text: "common", vector: [0, 0] },
|
||||
{ id: 2n, text: "common common", vector: [1, 1] },
|
||||
{ id: 3n, text: "common common common", vector: [2, 2] },
|
||||
{ id: 4n, text: "common common common common", vector: [3, 3] },
|
||||
],
|
||||
{ schema },
|
||||
);
|
||||
table = await db.createTable("test", data);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
tmpDir.removeCallback();
|
||||
});
|
||||
|
||||
it("applies offset after the vector search limit", async () => {
|
||||
const allResults = await table
|
||||
.vectorSearch([0, 0])
|
||||
.select(["id"])
|
||||
.limit(4)
|
||||
.toArray();
|
||||
const secondPage = await table
|
||||
.vectorSearch([0, 0])
|
||||
.select(["id"])
|
||||
.limit(2)
|
||||
.offset(2)
|
||||
.toArray();
|
||||
|
||||
expect(allResults).toHaveLength(4);
|
||||
expect(secondPage).toHaveLength(2);
|
||||
expect(secondPage.map((row) => row.id)).toEqual(
|
||||
allResults.slice(2, 4).map((row) => row.id),
|
||||
);
|
||||
});
|
||||
|
||||
it("applies offset after the full-text search limit", async () => {
|
||||
await table.createIndex("text", { config: Index.fts() });
|
||||
|
||||
const allResults = await table
|
||||
.search("common", "fts")
|
||||
.select(["id"])
|
||||
.limit(4)
|
||||
.toArray();
|
||||
const secondPage = await table
|
||||
.search("common", "fts")
|
||||
.select(["id"])
|
||||
.limit(2)
|
||||
.offset(2)
|
||||
.toArray();
|
||||
|
||||
expect(allResults).toHaveLength(4);
|
||||
expect(secondPage).toHaveLength(2);
|
||||
expect(secondPage.map((row) => row.id)).toEqual(
|
||||
allResults.slice(2, 4).map((row) => row.id),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Query orderBy", () => {
|
||||
let tmpDir: tmp.DirResult;
|
||||
let table: Table;
|
||||
|
||||
Generated
-6
@@ -55,13 +55,7 @@
|
||||
"openai": "4.29.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18",
|
||||
"apache-arrow": ">=15.0.0 <=18.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-crypto/crc32": {
|
||||
|
||||
@@ -101,12 +101,6 @@
|
||||
"openai": "4.29.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18",
|
||||
"apache-arrow": ">=15.0.0 <=18.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ tests = [
|
||||
"pytest-asyncio>=0.21",
|
||||
"duckdb>=0.9.0",
|
||||
"pytz>=2023.3",
|
||||
"polars>=0.19, <=1.32.3",
|
||||
"polars>=0.19, <=1.3.0",
|
||||
"pyarrow<25",
|
||||
"pyarrow-stubs>=16.0",
|
||||
"pylance==9.0.0rc1",
|
||||
@@ -140,7 +140,6 @@ include = [
|
||||
"python/lancedb/remote/errors.py",
|
||||
"python/lancedb/embeddings/__init__.py",
|
||||
"python/lancedb/_lancedb.pyi",
|
||||
"python/type_tests/connect.py",
|
||||
]
|
||||
exclude = ["python/tests/"]
|
||||
pythonVersion = "3.13"
|
||||
|
||||
@@ -804,7 +804,7 @@ class LanceDBConnection(DBConnection):
|
||||
"manifest_enabled": self._manifest_enabled,
|
||||
"namespace_client_properties": self._namespace_client_properties,
|
||||
"read_consistency_interval_seconds": (
|
||||
rci.total_seconds() if rci else None
|
||||
rci.total_seconds() if rci is not None else None
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -183,11 +183,7 @@ class EmbeddingFunction(BaseModel, ABC):
|
||||
def VectorField(self, **kwargs):
|
||||
"""
|
||||
Creates a pydantic Field that can automatically annotate
|
||||
the target vector column for this embedding function.
|
||||
|
||||
The field can be annotated as ``list[float]`` for compatibility with
|
||||
static type checkers. LanceDB will infer the fixed vector dimension from
|
||||
this embedding function.
|
||||
the target vector column for this embedding function
|
||||
"""
|
||||
return Field(json_schema_extra={"vector_column_for": self}, **kwargs)
|
||||
|
||||
|
||||
@@ -482,6 +482,16 @@ class LanceNamespaceDBConnection(DBConnection):
|
||||
def serialize(self) -> str:
|
||||
import json
|
||||
|
||||
if (
|
||||
self._namespace_client_impl is None
|
||||
or self._namespace_client_properties is None
|
||||
):
|
||||
raise ValueError(
|
||||
"Cannot serialize a namespace connection constructed from an "
|
||||
"opaque namespace client. Pass namespace_client_impl and "
|
||||
"namespace_client_properties when constructing the connection."
|
||||
)
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
"connection_type": "namespace",
|
||||
@@ -493,7 +503,7 @@ class LanceNamespaceDBConnection(DBConnection):
|
||||
"storage_options": self.storage_options or None,
|
||||
"read_consistency_interval_seconds": (
|
||||
self.read_consistency_interval.total_seconds()
|
||||
if self.read_consistency_interval
|
||||
if self.read_consistency_interval is not None
|
||||
else None
|
||||
),
|
||||
}
|
||||
@@ -569,6 +579,7 @@ class LanceNamespaceDBConnection(DBConnection):
|
||||
self,
|
||||
name,
|
||||
namespace_path=namespace_path,
|
||||
storage_options=storage_options,
|
||||
namespace_client=self._namespace_client,
|
||||
pushdown_operations=self._namespace_client_pushdown_operations,
|
||||
route_pushdown_to_rust=self._route_pushdown_to_rust,
|
||||
@@ -607,6 +618,8 @@ class LanceNamespaceDBConnection(DBConnection):
|
||||
self,
|
||||
name,
|
||||
namespace_path=namespace_path,
|
||||
storage_options=storage_options,
|
||||
index_cache_size=index_cache_size,
|
||||
namespace_client=self._namespace_client,
|
||||
pushdown_operations=self._namespace_client_pushdown_operations,
|
||||
route_pushdown_to_rust=self._route_pushdown_to_rust,
|
||||
@@ -899,10 +912,13 @@ class LanceNamespaceDBConnection(DBConnection):
|
||||
self,
|
||||
name,
|
||||
namespace_path=namespace_path,
|
||||
storage_options=storage_options,
|
||||
index_cache_size=index_cache_size,
|
||||
location=table_uri,
|
||||
namespace_client=namespace_client,
|
||||
managed_versioning=managed_versioning,
|
||||
pushdown_operations=self._namespace_client_pushdown_operations,
|
||||
route_pushdown_to_rust=self._route_pushdown_to_rust,
|
||||
_async=async_table,
|
||||
)
|
||||
|
||||
|
||||
@@ -591,6 +591,13 @@ class Permutation:
|
||||
then the first split will be used.
|
||||
"""
|
||||
assert base_table is not None, "base_table is required"
|
||||
# A PyTorch fork worker may construct its Permutation lazily from a
|
||||
# table opened in the parent process. Reopen that table before the
|
||||
# Rust reader clones its object-store clients and connection pools.
|
||||
if hasattr(base_table, "_ensure_open"):
|
||||
base_table._ensure_open()
|
||||
if permutation_table is not None and hasattr(permutation_table, "_ensure_open"):
|
||||
permutation_table._ensure_open()
|
||||
if split is not None:
|
||||
if permutation_table is None:
|
||||
raise ValueError(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -99,17 +99,6 @@ def Vector(
|
||||
... pa.field("url", pa.utf8(), False),
|
||||
... pa.field("embeddings", pa.list_(pa.float32(), 768))
|
||||
... ])
|
||||
|
||||
Notes
|
||||
-----
|
||||
``Vector`` creates a type dynamically, so calls such as ``Vector(768)`` are
|
||||
not valid static type annotations. For an embedding field, use the standard
|
||||
``list[float]`` annotation when running mypy; ``VectorField`` supplies the
|
||||
fixed dimension to LanceDB::
|
||||
|
||||
class MyModel(LanceModel):
|
||||
text: str = embeddings.SourceField()
|
||||
vector: list[float] = embeddings.VectorField()
|
||||
"""
|
||||
|
||||
# TODO: make a public parameterized type.
|
||||
@@ -164,16 +153,6 @@ def Vector(
|
||||
return FixedSizeList
|
||||
|
||||
|
||||
def _raise_bare_vector_error(*_args):
|
||||
raise TypeError("Vector must be parameterized with a dimension, e.g. Vector(128).")
|
||||
|
||||
|
||||
# Pydantic v1 and v2 otherwise treat the bare Vector factory as a field validator
|
||||
# and inspect its signature, which produces misleading errors about internal types.
|
||||
setattr(Vector, "__get_validators__", _raise_bare_vector_error)
|
||||
setattr(Vector, "__get_pydantic_core_schema__", _raise_bare_vector_error)
|
||||
|
||||
|
||||
def MultiVector(
|
||||
dim: int, value_type: pa.DataType = pa.float32(), nullable: bool = True
|
||||
) -> Type:
|
||||
@@ -390,10 +369,6 @@ def _unwrap_optional_annotation(annotation: Any) -> Any | None:
|
||||
|
||||
def _pydantic_to_arrow_type(field: FieldInfo) -> pa.DataType:
|
||||
"""Convert a Pydantic FieldInfo to Arrow DataType"""
|
||||
embedding_vector_type = _embedding_vector_to_arrow_type(field)
|
||||
if embedding_vector_type is not None:
|
||||
return embedding_vector_type
|
||||
|
||||
unwrapped = _unwrap_optional_annotation(field.annotation)
|
||||
if unwrapped is not None:
|
||||
return _pydantic_type_to_arrow_type(unwrapped, field)
|
||||
@@ -407,32 +382,8 @@ def _pydantic_to_arrow_type(field: FieldInfo) -> pa.DataType:
|
||||
return _pydantic_type_to_arrow_type(field.annotation, field)
|
||||
|
||||
|
||||
def _embedding_vector_to_arrow_type(field: FieldInfo) -> pa.DataType | None:
|
||||
"""Infer a fixed-size vector type from ``VectorField`` metadata."""
|
||||
if not _is_embedding_vector_annotation(field):
|
||||
return None
|
||||
|
||||
function = get_extras(field, "vector_column_for")
|
||||
return pa.list_(pa.float32(), function.ndims())
|
||||
|
||||
|
||||
def _is_embedding_vector_annotation(field: FieldInfo) -> bool:
|
||||
if get_extras(field, "vector_column_for") is None:
|
||||
return False
|
||||
|
||||
annotation = _unwrap_optional_annotation(field.annotation)
|
||||
if annotation is None:
|
||||
annotation = field.annotation
|
||||
|
||||
origin = getattr(annotation, "__origin__", None)
|
||||
args = getattr(annotation, "__args__", ())
|
||||
return origin is list and args == (float,)
|
||||
|
||||
|
||||
def is_nullable(field: FieldInfo) -> bool:
|
||||
"""Check if a Pydantic FieldInfo is nullable."""
|
||||
if _is_embedding_vector_annotation(field):
|
||||
return True
|
||||
if _unwrap_optional_annotation(field.annotation) is not None:
|
||||
return True
|
||||
if isinstance(field.annotation, (_GenericAlias, GenericAlias)):
|
||||
|
||||
+306
-32
@@ -6,6 +6,8 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import inspect
|
||||
import deprecation
|
||||
import os
|
||||
import threading
|
||||
import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
@@ -108,11 +110,6 @@ def _should_push_down_query_table(
|
||||
return namespace_client is not None and "QueryTable" in pushdown_operations
|
||||
|
||||
|
||||
def _polars_predicate_pushdown_barrier(frame: Any) -> Any:
|
||||
"""Return a Polars frame unchanged while blocking predicate pushdown."""
|
||||
return frame
|
||||
|
||||
|
||||
_MODEL_BACKED_TOKENIZER_PREFIXES = ("jieba", "lindera")
|
||||
_MODEL_BACKED_TOKENIZER_ERRORS = (
|
||||
"unknown base tokenizer",
|
||||
@@ -168,7 +165,7 @@ def _maybe_add_fts_error_note(
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .db import LanceDBConnection
|
||||
from .db import DBConnection, LanceDBConnection
|
||||
from ._lancedb import (
|
||||
Table as LanceDBTable,
|
||||
OptimizeStats,
|
||||
@@ -869,18 +866,12 @@ class Table(ABC):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def to_polars(self, **kwargs) -> "pl.LazyFrame":
|
||||
"""Return the table as a Polars LazyFrame.
|
||||
|
||||
Note
|
||||
----
|
||||
The Polars streaming engine is not supported because it does not currently
|
||||
implement Python PyArrow dataset scans. Use the default engine when collecting
|
||||
this LazyFrame.
|
||||
def to_polars(self, **kwargs) -> "pl.DataFrame":
|
||||
"""Return the table as a polars.DataFrame.
|
||||
|
||||
Returns
|
||||
-------
|
||||
polars.LazyFrame
|
||||
polars.DataFrame
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -2116,6 +2107,23 @@ class Table(ABC):
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
class _LanceTableReopenState:
|
||||
"""Process-independent coordinates for reopening a native table."""
|
||||
|
||||
connection_state: Optional[str]
|
||||
can_reopen_after_fork: bool
|
||||
fork_reopen_error: Optional[str]
|
||||
name: str
|
||||
namespace_path: List[str]
|
||||
storage_options: Optional[Dict[str, str]]
|
||||
index_cache_size: Optional[int]
|
||||
location: Optional[str]
|
||||
managed_versioning: Optional[bool]
|
||||
branch: Optional[str]
|
||||
checkout_version: Optional[int]
|
||||
|
||||
|
||||
class LanceTable(Table):
|
||||
"""
|
||||
A table in a LanceDB database.
|
||||
@@ -2150,7 +2158,10 @@ class LanceTable(Table):
|
||||
namespace_path = []
|
||||
self._conn = connection
|
||||
self._namespace_path = namespace_path
|
||||
self._storage_options = storage_options
|
||||
self._index_cache_size = index_cache_size
|
||||
self._location = location # Store location for use in _dataset_path
|
||||
self._managed_versioning = managed_versioning
|
||||
self._namespace_client = namespace_client
|
||||
self._pushdown_operations = pushdown_operations or set()
|
||||
# When the connection built the namespace client natively (e.g. an
|
||||
@@ -2175,9 +2186,206 @@ class LanceTable(Table):
|
||||
managed_versioning=managed_versioning,
|
||||
)
|
||||
)
|
||||
self._initialize_reopen_state(name)
|
||||
|
||||
def _initialize_reopen_state(self, name: str) -> None:
|
||||
"""Capture the state needed to replace inherited native handles."""
|
||||
self._name = name
|
||||
self._pid = os.getpid()
|
||||
self._native_state_guard = (self._pid, threading.RLock())
|
||||
|
||||
# A native table owns object-store clients and connection pools. Those
|
||||
# handles must not be used after fork, so retain a process-independent
|
||||
# connection description while it is still safe to inspect the parent
|
||||
# connection. Connections without reconstructible metadata are not
|
||||
# safe to reuse in a forked child, so retain a clear diagnostic rather
|
||||
# than advertising them as reopenable based on JSON encoding alone.
|
||||
try:
|
||||
connection_uri: Optional[str] = self._conn.uri
|
||||
except Exception:
|
||||
connection_uri = None
|
||||
|
||||
fork_reopen_error: Optional[str] = None
|
||||
try:
|
||||
connection_state: Optional[str] = self._conn.serialize()
|
||||
can_reopen_after_fork = connection_uri is not None and not (
|
||||
connection_uri.startswith("memory://")
|
||||
)
|
||||
except Exception as error:
|
||||
connection_state = None
|
||||
can_reopen_after_fork = False
|
||||
if connection_uri is not None and not connection_uri.startswith(
|
||||
"memory://"
|
||||
):
|
||||
fork_reopen_error = (
|
||||
f"Cannot reopen table {name!r} in a forked process: {error}"
|
||||
)
|
||||
|
||||
self._reopen_state = _LanceTableReopenState(
|
||||
connection_state=connection_state,
|
||||
can_reopen_after_fork=can_reopen_after_fork,
|
||||
fork_reopen_error=fork_reopen_error,
|
||||
name=name,
|
||||
namespace_path=list(self._namespace_path),
|
||||
storage_options=(
|
||||
dict(self._storage_options)
|
||||
if self._storage_options is not None
|
||||
else None
|
||||
),
|
||||
index_cache_size=self._index_cache_size,
|
||||
location=self._location,
|
||||
managed_versioning=self._managed_versioning,
|
||||
branch=self._table.current_branch(),
|
||||
checkout_version=None,
|
||||
)
|
||||
|
||||
@property
|
||||
def _connection_state(self) -> Optional[str]:
|
||||
"""Serialized connection retained for worker reconstruction."""
|
||||
return self._reopen_state.connection_state
|
||||
|
||||
@property
|
||||
def _can_reopen_after_fork(self) -> bool:
|
||||
return self._reopen_state.can_reopen_after_fork
|
||||
|
||||
@property
|
||||
def _branch(self) -> Optional[str]:
|
||||
state = getattr(self, "_reopen_state", None)
|
||||
if state is not None:
|
||||
return state.branch
|
||||
return getattr(self, "_legacy_branch", None)
|
||||
|
||||
@_branch.setter
|
||||
def _branch(self, value: Optional[str]) -> None:
|
||||
state = getattr(self, "_reopen_state", None)
|
||||
if state is not None:
|
||||
state.branch = value
|
||||
else:
|
||||
self._legacy_branch = value
|
||||
|
||||
@property
|
||||
def _checkout_version(self) -> Optional[int]:
|
||||
state = getattr(self, "_reopen_state", None)
|
||||
if state is not None:
|
||||
return state.checkout_version
|
||||
return getattr(self, "_legacy_checkout_version", None)
|
||||
|
||||
@_checkout_version.setter
|
||||
def _checkout_version(self, value: Optional[int]) -> None:
|
||||
state = getattr(self, "_reopen_state", None)
|
||||
if state is not None:
|
||||
state.checkout_version = value
|
||||
else:
|
||||
self._legacy_checkout_version = value
|
||||
|
||||
def _native_state_lock(self):
|
||||
"""Return the per-process lock coordinating native mode and reopen state."""
|
||||
pid = os.getpid()
|
||||
guard = getattr(self, "_native_state_guard", None)
|
||||
if guard is None:
|
||||
candidate = (pid, threading.RLock())
|
||||
guard = self.__dict__.setdefault("_native_state_guard", candidate)
|
||||
elif guard[0] != pid:
|
||||
# A lock inherited while another parent thread held it cannot be
|
||||
# safely acquired in the child. Child state starts single-threaded,
|
||||
# so replace it before coordinating the first reopen.
|
||||
guard = (pid, threading.RLock())
|
||||
self._native_state_guard = guard
|
||||
return guard[1]
|
||||
|
||||
@classmethod
|
||||
def _open_from_reopen_state(
|
||||
cls,
|
||||
connection: "DBConnection",
|
||||
state: "_LanceTableReopenState",
|
||||
) -> "LanceTable":
|
||||
"""Open a table from its complete process-independent descriptor."""
|
||||
async_connection = getattr(connection, "_conn", None)
|
||||
if async_connection is None:
|
||||
async_connection = connection._inner
|
||||
|
||||
namespace_client = getattr(connection, "_namespace_client", None)
|
||||
async_table = LOOP.run(
|
||||
async_connection.open_table(
|
||||
state.name,
|
||||
namespace_path=state.namespace_path,
|
||||
storage_options=state.storage_options,
|
||||
index_cache_size=state.index_cache_size,
|
||||
location=state.location,
|
||||
namespace_client=namespace_client,
|
||||
managed_versioning=state.managed_versioning,
|
||||
)
|
||||
)
|
||||
table = cls(
|
||||
connection,
|
||||
state.name,
|
||||
namespace_path=state.namespace_path,
|
||||
storage_options=state.storage_options,
|
||||
index_cache_size=state.index_cache_size,
|
||||
location=state.location,
|
||||
namespace_client=namespace_client,
|
||||
managed_versioning=state.managed_versioning,
|
||||
pushdown_operations=getattr(
|
||||
connection, "_namespace_client_pushdown_operations", None
|
||||
),
|
||||
route_pushdown_to_rust=getattr(
|
||||
connection, "_route_pushdown_to_rust", False
|
||||
),
|
||||
_async=async_table,
|
||||
)
|
||||
if state.branch is not None:
|
||||
table = table.branches.checkout(state.branch, state.checkout_version)
|
||||
elif state.checkout_version is not None:
|
||||
table.checkout(state.checkout_version)
|
||||
return table
|
||||
|
||||
def _ensure_open(self) -> None:
|
||||
"""Reopen native table handles inherited from another process."""
|
||||
with self._native_state_lock():
|
||||
pid = os.getpid()
|
||||
if getattr(self, "_pid", pid) == pid:
|
||||
return
|
||||
|
||||
state = getattr(self, "_reopen_state", None)
|
||||
fork_reopen_error = getattr(state, "fork_reopen_error", None)
|
||||
if fork_reopen_error is not None:
|
||||
raise RuntimeError(fork_reopen_error)
|
||||
if (
|
||||
state is None
|
||||
or not state.can_reopen_after_fork
|
||||
or state.connection_state is None
|
||||
):
|
||||
# In-memory and opaque Rust-only connections cannot be recreated
|
||||
# from connection metadata. Their local handles retain the prior
|
||||
# best-effort fork behavior.
|
||||
self._pid = pid
|
||||
return
|
||||
|
||||
from lancedb import deserialize_conn
|
||||
|
||||
connection = deserialize_conn(state.connection_state, for_worker=True)
|
||||
reopened = self._open_from_reopen_state(
|
||||
connection,
|
||||
state,
|
||||
)
|
||||
|
||||
# Keep this Python object stable because user datasets commonly retain
|
||||
# it across fork. Replace every process-bound component with the fresh
|
||||
# child's equivalent.
|
||||
self._conn = reopened._conn
|
||||
self._table = reopened._table
|
||||
self._namespace_client = reopened._namespace_client
|
||||
self._pushdown_operations = reopened._pushdown_operations
|
||||
self._route_pushdown_to_rust = reopened._route_pushdown_to_rust
|
||||
self._reopen_state = reopened._reopen_state
|
||||
self._pid = pid
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
if hasattr(self, "_name"):
|
||||
return self._name
|
||||
# Preserve compatibility with lightweight / legacy instances that
|
||||
# were constructed without running ``LanceTable.__init__``.
|
||||
return self._table.name
|
||||
|
||||
@property
|
||||
@@ -2394,18 +2602,75 @@ class LanceTable(Table):
|
||||
def _wrap_branch_handle(
|
||||
self, async_table: "AsyncTable", version: Optional[int] = None
|
||||
) -> "LanceTable":
|
||||
# version is unused locally: the pin already lives on async_table and a
|
||||
# local handle is not reopened via a serialized connection.
|
||||
return LanceTable(
|
||||
table = LanceTable(
|
||||
self._conn,
|
||||
async_table.name,
|
||||
namespace_path=self._namespace_path,
|
||||
storage_options=self._storage_options,
|
||||
index_cache_size=self._index_cache_size,
|
||||
namespace_client=self._namespace_client,
|
||||
pushdown_operations=self._pushdown_operations,
|
||||
route_pushdown_to_rust=self._route_pushdown_to_rust,
|
||||
location=self._location,
|
||||
managed_versioning=self._managed_versioning,
|
||||
_async=async_table,
|
||||
)
|
||||
table._checkout_version = version
|
||||
return table
|
||||
|
||||
def _resolve_checkout_version(self, version: Union[int, str]) -> int:
|
||||
if isinstance(version, int):
|
||||
return version
|
||||
try:
|
||||
return self.tags.get_version(version)
|
||||
except RuntimeError as err:
|
||||
# Native checkout historically exposes an unknown tag as ValueError.
|
||||
# Preserve that contract while resolving tags before mutating the table.
|
||||
if "Ref not found" in str(err) and "does not exist" in str(err):
|
||||
raise ValueError(str(err)) from err
|
||||
raise
|
||||
|
||||
async def _commit_native_state(
|
||||
self,
|
||||
transition,
|
||||
version: Optional[int],
|
||||
started: threading.Event,
|
||||
finished: threading.Event,
|
||||
):
|
||||
"""Commit a native transition and its fork coordinate as one task."""
|
||||
started.set()
|
||||
try:
|
||||
task = asyncio.ensure_future(transition)
|
||||
try:
|
||||
result = await asyncio.shield(task)
|
||||
except asyncio.CancelledError:
|
||||
# BackgroundEventLoop cancels its submitted task when the
|
||||
# waiting caller is interrupted. Let an already-started native
|
||||
# transition reach its authoritative terminal state before the
|
||||
# per-table boundary is released.
|
||||
result = await task
|
||||
self._checkout_version = version
|
||||
raise
|
||||
self._checkout_version = version
|
||||
return result
|
||||
finally:
|
||||
finished.set()
|
||||
|
||||
def _run_native_state_transition(self, transition, version: Optional[int]):
|
||||
started = threading.Event()
|
||||
finished = threading.Event()
|
||||
try:
|
||||
return LOOP.run(
|
||||
self._commit_native_state(transition, version, started, finished)
|
||||
)
|
||||
except BaseException:
|
||||
if started.is_set():
|
||||
while not finished.is_set():
|
||||
try:
|
||||
finished.wait()
|
||||
except BaseException: # noqa: PERF203
|
||||
continue
|
||||
raise
|
||||
|
||||
def checkout(self, version: Union[int, str]):
|
||||
"""Checkout a version of the table. This is an in-place operation.
|
||||
@@ -2443,7 +2708,14 @@ class LanceTable(Table):
|
||||
vector type
|
||||
0 [1.1, 0.9] vector
|
||||
"""
|
||||
LOOP.run(self._table.checkout(version))
|
||||
# Resolve tags before mutating the native handle. This leaves the live
|
||||
# handle and reopen descriptor aligned if tag lookup fails, and avoids a
|
||||
# second fallible version lookup after checkout succeeds.
|
||||
with self._native_state_lock():
|
||||
resolved_version = self._resolve_checkout_version(version)
|
||||
self._run_native_state_transition(
|
||||
self._table.checkout(resolved_version), resolved_version
|
||||
)
|
||||
|
||||
def checkout_latest(self):
|
||||
"""Checkout the latest version of the table. This is an in-place operation.
|
||||
@@ -2451,7 +2723,8 @@ class LanceTable(Table):
|
||||
The table will be set back into standard mode, and will track the latest
|
||||
version of the table.
|
||||
"""
|
||||
LOOP.run(self._table.checkout_latest())
|
||||
with self._native_state_lock():
|
||||
self._run_native_state_transition(self._table.checkout_latest(), None)
|
||||
|
||||
def restore(self, version: Optional[Union[int, str]] = None):
|
||||
"""Restore a version of the table. This is an in-place operation.
|
||||
@@ -2497,9 +2770,13 @@ class LanceTable(Table):
|
||||
>>> len(table.list_versions())
|
||||
4
|
||||
"""
|
||||
if version is not None:
|
||||
LOOP.run(self._table.checkout(version))
|
||||
LOOP.run(self._table.restore())
|
||||
with self._native_state_lock():
|
||||
if version is not None:
|
||||
resolved_version = self._resolve_checkout_version(version)
|
||||
self._run_native_state_transition(
|
||||
self._table.checkout(resolved_version), resolved_version
|
||||
)
|
||||
self._run_native_state_transition(self._table.restore(), None)
|
||||
|
||||
def count_rows(self, filter: Optional[str] = None) -> int:
|
||||
return LOOP.run(self._table.count_rows(filter))
|
||||
@@ -2580,9 +2857,6 @@ class LanceTable(Table):
|
||||
2. Currently we've disabled push-down of the filters from polars
|
||||
because polars pushdown into pyarrow uses pyarrow compute
|
||||
expressions rather than SQl strings (which LanceDB supports)
|
||||
3. The Polars streaming engine is not supported because it does not
|
||||
currently implement Python PyArrow dataset scans. Use the default
|
||||
engine when collecting this LazyFrame.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -2591,12 +2865,8 @@ class LanceTable(Table):
|
||||
from lancedb.integrations.pyarrow import PyarrowDatasetAdapter
|
||||
|
||||
dataset = PyarrowDatasetAdapter(self)
|
||||
# Polars 1.32's non-PyArrow callback path passes batch_size twice. Keep
|
||||
# the compatible PyArrow path, but block predicates because this adapter
|
||||
# cannot translate PyArrow expressions into LanceDB filters.
|
||||
return pl.scan_pyarrow_dataset(dataset, batch_size=batch_size).map_batches(
|
||||
_polars_predicate_pushdown_barrier,
|
||||
predicate_pushdown=False,
|
||||
return pl.scan_pyarrow_dataset(
|
||||
dataset, allow_pyarrow_filter=False, batch_size=batch_size
|
||||
)
|
||||
|
||||
# New unified API overload
|
||||
@@ -3617,7 +3887,9 @@ class LanceTable(Table):
|
||||
self = cls.__new__(cls)
|
||||
self._conn = db
|
||||
self._namespace_path = namespace_path
|
||||
self._index_cache_size = None
|
||||
self._location = location
|
||||
self._managed_versioning = None
|
||||
self._namespace_client = namespace_client
|
||||
self._pushdown_operations = pushdown_operations or set()
|
||||
self._route_pushdown_to_rust = route_pushdown_to_rust
|
||||
@@ -3645,6 +3917,7 @@ class LanceTable(Table):
|
||||
enable_v2_manifest_paths
|
||||
)
|
||||
|
||||
self._storage_options = storage_options
|
||||
self._table = LOOP.run(
|
||||
self._conn._conn.create_table(
|
||||
name,
|
||||
@@ -3661,6 +3934,7 @@ class LanceTable(Table):
|
||||
namespace_client=namespace_client,
|
||||
)
|
||||
)
|
||||
self._initialize_reopen_state(name)
|
||||
return self
|
||||
|
||||
def delete(self, where: Union[str, Expr]) -> DeleteResult:
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
|
||||
import json
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from importlib import resources
|
||||
import os
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -19,10 +19,6 @@ from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError
|
||||
from lancedb.pydantic import LanceModel, Vector
|
||||
|
||||
|
||||
def test_package_includes_pep_561_marker():
|
||||
assert resources.files(lancedb).joinpath("py.typed").is_file()
|
||||
|
||||
|
||||
def test_basic(tmp_path):
|
||||
db = lancedb.connect(tmp_path)
|
||||
|
||||
@@ -106,6 +102,17 @@ def test_read_consistency_interval_does_not_use_background_loop(tmp_path, monkey
|
||||
assert db_from_inner.read_consistency_interval == consistency_interval
|
||||
|
||||
|
||||
def test_serialize_preserves_zero_read_consistency_interval(tmp_path):
|
||||
db = lancedb.connect(tmp_path, read_consistency_interval=timedelta(0))
|
||||
table = db.create_table("items", pa.table({"x": [1]}))
|
||||
|
||||
encoded = json.loads(table._connection_state)
|
||||
assert encoded["read_consistency_interval_seconds"] == 0.0
|
||||
|
||||
restored = lancedb.deserialize_conn(table._connection_state)
|
||||
assert restored.read_consistency_interval == timedelta(0)
|
||||
|
||||
|
||||
def test_ingest_pd(tmp_path):
|
||||
db = lancedb.connect(tmp_path)
|
||||
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
import tempfile
|
||||
import shutil
|
||||
import importlib
|
||||
import multiprocessing as mp
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
import pytest
|
||||
import pyarrow as pa
|
||||
import lancedb
|
||||
from lance_namespace import connect as namespace_connect
|
||||
from lance_namespace.errors import NamespaceNotEmptyError, TableNotFoundError
|
||||
from lancedb.namespace import _MAX_QUERY_K
|
||||
from lancedb.table import AsyncTable, LanceTable
|
||||
@@ -72,6 +76,16 @@ def _namespace_lance_table(namespace_client: _NamespaceClient) -> LanceTable:
|
||||
return table
|
||||
|
||||
|
||||
def _direct_namespace_fork_child(table, result_queue):
|
||||
from lancedb.permutation import Permutation
|
||||
|
||||
try:
|
||||
permutation = Permutation.identity(table)
|
||||
result_queue.put(("ok", permutation.num_rows))
|
||||
except Exception as error:
|
||||
result_queue.put((type(error).__name__, str(error)))
|
||||
|
||||
|
||||
class TestNamespaceConnection:
|
||||
"""Test namespace-based LanceDB connection using DirectoryNamespace."""
|
||||
|
||||
@@ -419,7 +433,95 @@ class TestNamespaceConnection:
|
||||
pa.field("vector", pa.list_(pa.float32(), 2)),
|
||||
]
|
||||
)
|
||||
db.create_table("test_table", schema=schema, storage_options=table_opts)
|
||||
created = db.create_table(
|
||||
"test_table", schema=schema, storage_options=table_opts
|
||||
)
|
||||
assert created._storage_options == table_opts
|
||||
|
||||
opened = db.open_table(
|
||||
"test_table",
|
||||
storage_options={"allow_http": "true"},
|
||||
index_cache_size=17,
|
||||
)
|
||||
assert opened._storage_options == {"allow_http": "true"}
|
||||
assert opened._index_cache_size == 17
|
||||
opened._pid = -1
|
||||
opened._ensure_open()
|
||||
assert opened.count_rows() == 0
|
||||
|
||||
def test_serialize_preserves_zero_read_consistency_interval(self):
|
||||
db = lancedb.connect_namespace(
|
||||
"dir",
|
||||
{"root": self.temp_dir},
|
||||
read_consistency_interval=timedelta(0),
|
||||
)
|
||||
|
||||
restored = lancedb.deserialize_conn(db.serialize())
|
||||
assert restored.read_consistency_interval == timedelta(0)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform != "linux",
|
||||
reason="fork() is only supported safely for this test on Linux",
|
||||
)
|
||||
def test_direct_namespace_with_descriptor_reopens_after_fork(self):
|
||||
properties = {"root": self.temp_dir}
|
||||
namespace = namespace_connect("dir", properties)
|
||||
db = lancedb.LanceNamespaceDBConnection(
|
||||
namespace,
|
||||
namespace_client_impl="dir",
|
||||
namespace_client_properties=properties,
|
||||
)
|
||||
table = db.create_table("items", pa.table({"id": [1]}))
|
||||
|
||||
ctx = mp.get_context("fork")
|
||||
result_queue = ctx.Queue()
|
||||
process = ctx.Process(
|
||||
target=_direct_namespace_fork_child,
|
||||
args=(table, result_queue),
|
||||
)
|
||||
process.start()
|
||||
process.join(10)
|
||||
|
||||
if process.is_alive():
|
||||
process.terminate()
|
||||
process.join(5)
|
||||
pytest.fail("Direct namespace table hung while reopening after fork")
|
||||
|
||||
assert process.exitcode == 0
|
||||
assert result_queue.get(timeout=2) == ("ok", 1)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform != "linux",
|
||||
reason="fork() is only supported safely for this test on Linux",
|
||||
)
|
||||
def test_opaque_direct_namespace_reports_unsupported_fork(self):
|
||||
namespace = namespace_connect("dir", {"root": self.temp_dir})
|
||||
db = lancedb.LanceNamespaceDBConnection(namespace)
|
||||
table = db.create_table("items", pa.table({"id": [1]}))
|
||||
|
||||
with pytest.raises(ValueError, match="opaque namespace client"):
|
||||
db.serialize()
|
||||
assert not table._can_reopen_after_fork
|
||||
|
||||
ctx = mp.get_context("fork")
|
||||
result_queue = ctx.Queue()
|
||||
process = ctx.Process(
|
||||
target=_direct_namespace_fork_child,
|
||||
args=(table, result_queue),
|
||||
)
|
||||
process.start()
|
||||
process.join(10)
|
||||
|
||||
if process.is_alive():
|
||||
process.terminate()
|
||||
process.join(5)
|
||||
pytest.fail("Opaque namespace table hung after fork")
|
||||
|
||||
assert process.exitcode == 0
|
||||
error_type, message = result_queue.get(timeout=2)
|
||||
assert error_type == "RuntimeError"
|
||||
assert "Cannot reopen table 'items' in a forked process" in message
|
||||
assert "namespace_client_impl and namespace_client_properties" in message
|
||||
|
||||
def test_namespace_operations(self):
|
||||
"""Test namespace management operations."""
|
||||
|
||||
@@ -9,7 +9,6 @@ from typing import List, Optional, Tuple
|
||||
import pyarrow as pa
|
||||
import pydantic
|
||||
import pytest
|
||||
from lancedb.conftest import MockTextEmbeddingFunction
|
||||
from lancedb.pydantic import (
|
||||
PYDANTIC_VERSION,
|
||||
LanceModel,
|
||||
@@ -416,36 +415,6 @@ def test_nullable_vector():
|
||||
assert schema == pa.schema([pa.field("vec", pa.list_(pa.float32(), 16), True)])
|
||||
|
||||
|
||||
def test_bare_vector_raises_clear_error():
|
||||
namespace = {
|
||||
"__name__": "test_model_without_pyarrow",
|
||||
"LanceModel": LanceModel,
|
||||
"Vector": Vector,
|
||||
}
|
||||
|
||||
with pytest.raises(TypeError, match=r"Vector must be parameterized.*Vector\(128\)"):
|
||||
exec("class TestModel(LanceModel):\n vector: Vector", namespace)
|
||||
|
||||
|
||||
def test_embedding_vector_list_annotation():
|
||||
embedding = MockTextEmbeddingFunction.create()
|
||||
|
||||
class StaticTypingModel(LanceModel):
|
||||
text: str = embedding.SourceField()
|
||||
vector: list[float] = embedding.VectorField()
|
||||
|
||||
schema = pydantic_to_schema(StaticTypingModel)
|
||||
assert schema == pa.schema(
|
||||
[
|
||||
pa.field("text", pa.utf8(), False),
|
||||
pa.field("vector", pa.list_(pa.float32(), embedding.ndims()), True),
|
||||
]
|
||||
)
|
||||
|
||||
model = StaticTypingModel(text="hello", vector=[0.0] * embedding.ndims())
|
||||
assert model.vector == [0.0] * embedding.ndims()
|
||||
|
||||
|
||||
def test_fixed_size_list_field():
|
||||
class TestModel(pydantic.BaseModel):
|
||||
vec: Vector(16)
|
||||
|
||||
@@ -570,15 +570,6 @@ def test_query_builder(table):
|
||||
assert all(np.array(rs[0]["vector"]) == [1, 2])
|
||||
|
||||
|
||||
def test_query_multiple_vectors(table):
|
||||
results = table.search([np.array([1, 2]), np.array([4, 5])]).limit(1).to_list()
|
||||
|
||||
assert len(results) == 2
|
||||
results_by_query = {result["query_index"]: result for result in results}
|
||||
assert results_by_query[0]["id"] == 1
|
||||
assert results_by_query[1]["id"] == 2
|
||||
|
||||
|
||||
def test_with_row_id(table: lancedb.table.Table):
|
||||
rs = table.search().with_row_id(True).to_arrow()
|
||||
assert "_rowid" in rs.column_names
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
|
||||
import asyncio
|
||||
import ctypes
|
||||
import gc
|
||||
import os
|
||||
@@ -9,7 +10,7 @@ import sys
|
||||
import threading
|
||||
import warnings
|
||||
import weakref
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from concurrent.futures import CancelledError, ThreadPoolExecutor
|
||||
from datetime import date, datetime, timedelta
|
||||
from time import sleep
|
||||
from typing import List
|
||||
@@ -929,7 +930,6 @@ def test_polars(mem_db: DBConnection):
|
||||
|
||||
# enter table to polars dataframe
|
||||
result = table.to_polars()
|
||||
assert isinstance(result, pl.LazyFrame)
|
||||
assert np.allclose(result.collect()["vector"].to_list(), data["vector"])
|
||||
|
||||
# make sure filtering isn't broken
|
||||
@@ -1846,27 +1846,6 @@ def test_add_with_empty_fixed_size_list_drops_bad_rows(mem_db: DBConnection):
|
||||
assert np.allclose(data["embedding"].to_pylist()[0], np.array([0.1] * 16))
|
||||
|
||||
|
||||
def test_add_nullable_fixed_size_list_with_none(mem_db: DBConnection):
|
||||
"""Regression test for issue #2340."""
|
||||
table = mem_db.create_table(
|
||||
"test_nullable_fixed_size_list",
|
||||
schema=pa.schema(
|
||||
[
|
||||
pa.field("id", pa.string()),
|
||||
pa.field("feature", pa.list_(pa.float32(), 256)),
|
||||
pa.field("tags", pa.list_(pa.string())),
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
table.add([{"id": "1", "feature": None, "tags": ["tag1", "tag2"]}])
|
||||
|
||||
result = table.to_arrow()
|
||||
assert result.to_pylist() == [
|
||||
{"id": "1", "feature": None, "tags": ["tag1", "tag2"]}
|
||||
]
|
||||
|
||||
|
||||
def test_add_nullable_struct_with_none(mem_db: DBConnection):
|
||||
"""Regression test for issue #2654: a nullable struct column whose
|
||||
first batch contains only None values must not crash in
|
||||
@@ -2167,6 +2146,260 @@ def test_restore(mem_db: DBConnection):
|
||||
table.restore(0)
|
||||
|
||||
|
||||
def test_restore_tracks_checkout_when_restore_fails():
|
||||
class FailingRestore:
|
||||
def __init__(self):
|
||||
self.live_version = None
|
||||
|
||||
async def checkout(self, version):
|
||||
self.live_version = version
|
||||
|
||||
async def restore(self):
|
||||
raise RuntimeError("injected restore failure")
|
||||
|
||||
inner = FailingRestore()
|
||||
table = LanceTable.__new__(LanceTable)
|
||||
table._table = inner
|
||||
table._checkout_version = None
|
||||
|
||||
with pytest.raises(RuntimeError, match="injected restore failure"):
|
||||
table.restore(7)
|
||||
|
||||
assert table._checkout_version == inner.live_version
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("operation", "expected_descriptor", "expected_restore_calls"),
|
||||
[("checkout", 11, 0), ("restore", None, 1)],
|
||||
)
|
||||
def test_string_tag_resolves_before_checkout(
|
||||
operation, expected_descriptor, expected_restore_calls
|
||||
):
|
||||
class Tags:
|
||||
async def get_version(self, tag):
|
||||
assert tag == "tag-v1"
|
||||
return 11
|
||||
|
||||
class NoPostCheckoutVersionLookup:
|
||||
def __init__(self):
|
||||
self.tags = Tags()
|
||||
self.checkout_versions = []
|
||||
self.restore_calls = 0
|
||||
|
||||
async def checkout(self, version):
|
||||
self.checkout_versions.append(version)
|
||||
|
||||
async def version(self):
|
||||
raise RuntimeError("post-checkout version lookup must not run")
|
||||
|
||||
async def restore(self):
|
||||
self.restore_calls += 1
|
||||
|
||||
inner = NoPostCheckoutVersionLookup()
|
||||
table = LanceTable.__new__(LanceTable)
|
||||
table._table = inner
|
||||
table._checkout_version = 3
|
||||
|
||||
getattr(table, operation)("tag-v1")
|
||||
|
||||
assert inner.checkout_versions == [11]
|
||||
assert table._checkout_version == expected_descriptor
|
||||
assert inner.restore_calls == expected_restore_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["checkout", "restore"])
|
||||
def test_string_tag_resolution_failure_does_not_mutate_handle(operation):
|
||||
class FailingTags:
|
||||
async def get_version(self, tag):
|
||||
assert tag == "missing-tag"
|
||||
raise RuntimeError("injected tag lookup failure")
|
||||
|
||||
class UnchangedTable:
|
||||
def __init__(self):
|
||||
self.tags = FailingTags()
|
||||
self.checkout_calls = 0
|
||||
self.restore_calls = 0
|
||||
|
||||
async def checkout(self, version):
|
||||
self.checkout_calls += 1
|
||||
|
||||
async def restore(self):
|
||||
self.restore_calls += 1
|
||||
|
||||
inner = UnchangedTable()
|
||||
table = LanceTable.__new__(LanceTable)
|
||||
table._table = inner
|
||||
table._checkout_version = 3
|
||||
|
||||
with pytest.raises(RuntimeError, match="injected tag lookup failure"):
|
||||
getattr(table, operation)("missing-tag")
|
||||
|
||||
assert table._checkout_version == 3
|
||||
assert inner.checkout_calls == 0
|
||||
assert inner.restore_calls == 0
|
||||
|
||||
|
||||
def test_native_state_transitions_are_serialized(monkeypatch):
|
||||
from lancedb.background_loop import LOOP
|
||||
|
||||
class Inner:
|
||||
def __init__(self):
|
||||
self.live_version = None
|
||||
|
||||
async def checkout(self, version):
|
||||
self.live_version = version
|
||||
|
||||
inner = Inner()
|
||||
table = LanceTable.__new__(LanceTable)
|
||||
table._table = inner
|
||||
table._checkout_version = None
|
||||
|
||||
first_native_done = threading.Event()
|
||||
release_first_call = threading.Event()
|
||||
second_call_started = threading.Event()
|
||||
second_call_done = threading.Event()
|
||||
errors = []
|
||||
original_run = LOOP.run
|
||||
|
||||
def delayed_delivery(awaitable):
|
||||
result = original_run(awaitable)
|
||||
if threading.current_thread().name == "checkout-1":
|
||||
first_native_done.set()
|
||||
assert release_first_call.wait(5)
|
||||
return result
|
||||
|
||||
monkeypatch.setattr(LOOP, "run", delayed_delivery)
|
||||
|
||||
def checkout(version):
|
||||
if version == 2:
|
||||
second_call_started.set()
|
||||
try:
|
||||
table.checkout(version)
|
||||
except BaseException as err:
|
||||
errors.append(err)
|
||||
finally:
|
||||
if version == 2:
|
||||
second_call_done.set()
|
||||
|
||||
first = threading.Thread(target=checkout, args=(1,), name="checkout-1")
|
||||
first.start()
|
||||
assert first_native_done.wait(5)
|
||||
|
||||
second = threading.Thread(target=checkout, args=(2,), name="checkout-2")
|
||||
second.start()
|
||||
assert second_call_started.wait(5)
|
||||
assert not second_call_done.wait(0.1)
|
||||
|
||||
release_first_call.set()
|
||||
first.join(5)
|
||||
second.join(5)
|
||||
|
||||
assert not first.is_alive()
|
||||
assert not second.is_alive()
|
||||
assert errors == []
|
||||
assert inner.live_version == 2
|
||||
assert table._checkout_version == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("operation", "args", "initial_version", "expected_version"),
|
||||
[
|
||||
("checkout", (11,), 3, 11),
|
||||
("checkout_latest", (), 3, None),
|
||||
("restore", (), 11, None),
|
||||
],
|
||||
)
|
||||
def test_native_state_commits_before_success_delivery(
|
||||
monkeypatch, operation, args, initial_version, expected_version
|
||||
):
|
||||
from lancedb.background_loop import LOOP
|
||||
|
||||
class Inner:
|
||||
def __init__(self, live_version):
|
||||
self.live_version = live_version
|
||||
|
||||
async def checkout(self, version):
|
||||
self.live_version = version
|
||||
|
||||
async def checkout_latest(self):
|
||||
self.live_version = None
|
||||
|
||||
async def restore(self):
|
||||
self.live_version = None
|
||||
|
||||
inner = Inner(initial_version)
|
||||
table = LanceTable.__new__(LanceTable)
|
||||
table._table = inner
|
||||
table._checkout_version = initial_version
|
||||
|
||||
original_run = LOOP.run
|
||||
|
||||
def success_then_interrupt(awaitable):
|
||||
original_run(awaitable)
|
||||
raise KeyboardInterrupt("injected after native success")
|
||||
|
||||
monkeypatch.setattr(LOOP, "run", success_then_interrupt)
|
||||
|
||||
with pytest.raises(KeyboardInterrupt, match="injected after native success"):
|
||||
getattr(table, operation)(*args)
|
||||
|
||||
assert inner.live_version == expected_version
|
||||
assert table._checkout_version == expected_version
|
||||
|
||||
|
||||
def test_native_state_waits_for_cancelled_delivery(monkeypatch):
|
||||
from lancedb.background_loop import LOOP
|
||||
|
||||
class Inner:
|
||||
def __init__(self):
|
||||
self.live_version = 3
|
||||
|
||||
async def checkout(self, version):
|
||||
await asyncio.sleep(0.01)
|
||||
self.live_version = version
|
||||
|
||||
inner = Inner()
|
||||
table = LanceTable.__new__(LanceTable)
|
||||
table._table = inner
|
||||
table._checkout_version = 3
|
||||
|
||||
original_run = LOOP.run
|
||||
|
||||
def cancel_while_running(awaitable):
|
||||
async def cancel_after_start():
|
||||
task = asyncio.create_task(awaitable)
|
||||
await asyncio.sleep(0)
|
||||
task.cancel()
|
||||
return await task
|
||||
|
||||
return original_run(cancel_after_start())
|
||||
|
||||
monkeypatch.setattr(LOOP, "run", cancel_while_running)
|
||||
|
||||
with pytest.raises(CancelledError):
|
||||
table.checkout(11)
|
||||
|
||||
assert inner.live_version == 11
|
||||
assert table._checkout_version == 11
|
||||
|
||||
|
||||
def test_reopen_preserves_explicit_table_location(tmp_path):
|
||||
db = lancedb.connect(tmp_path / "db")
|
||||
location = str(tmp_path / "physical-table")
|
||||
table = LanceTable.create(
|
||||
db,
|
||||
"items",
|
||||
pa.table({"x": [1]}),
|
||||
location=location,
|
||||
)
|
||||
|
||||
table._pid = -1
|
||||
table._ensure_open()
|
||||
|
||||
assert table.count_rows() == 1
|
||||
assert table._location == location
|
||||
|
||||
|
||||
def test_restore_with_tags(mem_db: DBConnection):
|
||||
table = mem_db.create_table(
|
||||
"my_table",
|
||||
@@ -2218,45 +2451,6 @@ def test_merge(tmp_db: DBConnection, tmp_path):
|
||||
table.merge(other_dataset, left_on="id")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("storage_version", ["legacy", "stable"])
|
||||
def test_search_after_merge(tmp_path, storage_version):
|
||||
pytest.importorskip("lance")
|
||||
pd = pytest.importorskip("pandas")
|
||||
|
||||
db = lancedb.connect(
|
||||
tmp_path,
|
||||
storage_options={"new_table_data_storage_version": storage_version},
|
||||
)
|
||||
rng = np.random.default_rng(42)
|
||||
row_count = 512
|
||||
vectors = rng.standard_normal((row_count, 8)).astype(np.float32)
|
||||
table = db.create_table(
|
||||
"search_after_merge",
|
||||
data=pd.DataFrame(
|
||||
{
|
||||
"id": [str(i) for i in range(row_count)],
|
||||
"vector": list(vectors),
|
||||
}
|
||||
),
|
||||
)
|
||||
table.create_index("vector", config=IvfPq(num_partitions=1, num_sub_vectors=2))
|
||||
|
||||
links = pd.DataFrame(
|
||||
{
|
||||
"id": [str(i) for i in range(row_count // 2)],
|
||||
"link": [f"https://example.com/{i}" for i in range(row_count // 2)],
|
||||
}
|
||||
)
|
||||
table.merge(links, left_on="id")
|
||||
|
||||
query = table.search(vectors[-1]).refine_factor(50).limit(10)
|
||||
assert "ANN" in query.explain_plan(verbose=True)
|
||||
|
||||
result = query.to_arrow()
|
||||
links_by_id = dict(zip(result["id"].to_pylist(), result["link"].to_pylist()))
|
||||
assert links_by_id[str(row_count - 1)] is None
|
||||
|
||||
|
||||
def test_delete(mem_db: DBConnection):
|
||||
table = mem_db.create_table(
|
||||
"my_table",
|
||||
@@ -2799,40 +2993,15 @@ def test_create_with_embedding_function(mem_db: DBConnection):
|
||||
assert actual == expected
|
||||
|
||||
|
||||
def test_create_f16_table_from_arrow_data(mem_db: DBConnection):
|
||||
dimension = 32
|
||||
num_rows = 512
|
||||
values = pa.array(
|
||||
np.random.default_rng(42)
|
||||
.standard_normal(num_rows * dimension)
|
||||
.astype(np.float16)
|
||||
)
|
||||
df = pa.table(
|
||||
{
|
||||
"text": [f"s-{i}" for i in range(num_rows)],
|
||||
"vector": pa.FixedSizeListArray.from_arrays(values, dimension),
|
||||
}
|
||||
)
|
||||
table = mem_db.create_table("f16_tbl", data=df)
|
||||
assert table.schema.field("vector").type == pa.list_(pa.float16(), dimension)
|
||||
table.create_index(num_partitions=2, num_sub_vectors=2)
|
||||
|
||||
query = df["vector"][2].as_py()
|
||||
expected = table.search(query).limit(2).to_arrow()
|
||||
|
||||
assert "s-2" in expected["text"].to_pylist()
|
||||
|
||||
|
||||
def test_create_f16_table(mem_db: DBConnection):
|
||||
class MyTable(LanceModel):
|
||||
text: str
|
||||
vector: Vector(32, value_type=pa.float16())
|
||||
|
||||
rng = np.random.default_rng(42)
|
||||
df = pa.table(
|
||||
{
|
||||
"text": [f"s-{i}" for i in range(512)],
|
||||
"vector": [rng.standard_normal(32).astype(np.float16) for _ in range(512)],
|
||||
"vector": [np.random.randn(32).astype(np.float16) for _ in range(512)],
|
||||
}
|
||||
)
|
||||
table = mem_db.create_table(
|
||||
|
||||
@@ -342,6 +342,42 @@ def _multiworker_dataloader_target(db_uri: str, result_queue):
|
||||
result_queue.put(count)
|
||||
|
||||
|
||||
class _LazyPermutationDataset(torch.utils.data.Dataset):
|
||||
"""Match applications that create their Permutation inside a fork worker."""
|
||||
|
||||
def __init__(self, table):
|
||||
self._table = table
|
||||
self._permutation = None
|
||||
self._length = table.count_rows()
|
||||
|
||||
def __len__(self):
|
||||
return self._length
|
||||
|
||||
def __getitems__(self, indices):
|
||||
if self._permutation is None:
|
||||
inherited_connection = self._table._conn
|
||||
self._permutation = Permutation.identity(self._table)
|
||||
if self._table._conn is inherited_connection:
|
||||
raise RuntimeError("Permutation reused a connection inherited by fork")
|
||||
return self._permutation.__getitems__(indices)
|
||||
|
||||
|
||||
def _lazy_multiworker_dataloader_target(db_uri: str, result_queue):
|
||||
table = lancedb.connect(db_uri).open_table("test_table")
|
||||
dataset = _LazyPermutationDataset(table)
|
||||
dataloader = torch.utils.data.DataLoader(
|
||||
dataset,
|
||||
batch_size=10,
|
||||
num_workers=2,
|
||||
multiprocessing_context="fork",
|
||||
)
|
||||
count = 0
|
||||
for batch in dataloader:
|
||||
assert batch["a"].size(0) == 10
|
||||
count += 1
|
||||
result_queue.put(count)
|
||||
|
||||
|
||||
def _remote_multiworker_dataloader_target(port: int, result_queue):
|
||||
import lancedb
|
||||
from lancedb.permutation import Permutation
|
||||
@@ -410,6 +446,46 @@ def test_permutation_dataloader_fork_workers(tmp_path):
|
||||
assert queue.get() == 100
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform != "linux",
|
||||
reason=(
|
||||
"fork() is unavailable on Windows and unsafe on macOS "
|
||||
"(Apple frameworks/TLS are not fork-safe)"
|
||||
),
|
||||
)
|
||||
def test_lazy_permutation_reopens_inherited_table_in_fork_worker(tmp_path):
|
||||
"""A lazily built Permutation must not reuse an inherited table client.
|
||||
|
||||
Object-store table handles contain HTTP connection pools that are unsafe
|
||||
after fork. The local table makes the handle replacement deterministic
|
||||
without requiring an S3 service in the unit-test environment.
|
||||
"""
|
||||
db_uri = str(tmp_path / "db")
|
||||
db = lancedb.connect(db_uri)
|
||||
db.create_table("test_table", pa.table({"a": list(range(1000))}))
|
||||
|
||||
ctx = mp.get_context("spawn")
|
||||
queue = ctx.Queue()
|
||||
proc = ctx.Process(
|
||||
target=_lazy_multiworker_dataloader_target,
|
||||
args=(db_uri, queue),
|
||||
)
|
||||
proc.start()
|
||||
proc.join(timeout=30)
|
||||
|
||||
if proc.is_alive():
|
||||
proc.terminate()
|
||||
proc.join(timeout=5)
|
||||
if proc.is_alive():
|
||||
proc.kill()
|
||||
proc.join()
|
||||
pytest.fail("Lazy Permutation hung in a fork-based DataLoader worker")
|
||||
|
||||
assert proc.exitcode == 0, f"child exited with code {proc.exitcode}"
|
||||
assert not queue.empty(), "child produced no batches"
|
||||
assert queue.get() == 100
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.platform != "linux",
|
||||
reason=(
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
||||
|
||||
from typing import assert_type
|
||||
|
||||
import lancedb
|
||||
from lancedb import AsyncConnection, DBConnection
|
||||
|
||||
|
||||
def check_connect_type() -> None:
|
||||
assert_type(lancedb.connect("memory://"), DBConnection)
|
||||
|
||||
|
||||
async def check_connect_async_type() -> None:
|
||||
assert_type(await lancedb.connect_async("memory://"), AsyncConnection)
|
||||
Generated
+1
-1
@@ -1998,7 +1998,7 @@ requires-dist = [
|
||||
{ name = "pillow", marker = "extra == 'clip'", specifier = ">=12.1.1" },
|
||||
{ name = "pillow", marker = "extra == 'embeddings'", specifier = ">=12.1.1" },
|
||||
{ name = "pillow", marker = "extra == 'siglip'", specifier = ">=12.1.1" },
|
||||
{ name = "polars", marker = "extra == 'tests'", specifier = ">=0.19,<=1.32.3" },
|
||||
{ name = "polars", marker = "extra == 'tests'", specifier = ">=0.19,<=1.3.0" },
|
||||
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.5.0" },
|
||||
{ name = "pyarrow", specifier = ">=16" },
|
||||
{ name = "pyarrow", marker = "extra == 'tests'", specifier = "<25" },
|
||||
|
||||
@@ -49,8 +49,8 @@ lance-namespace = { workspace = true }
|
||||
lance-namespace-impls = { workspace = true }
|
||||
metrics = { workspace = true, optional = true }
|
||||
metrics-util = { workspace = true, optional = true }
|
||||
# Pin the GooseFS SDK to the version required by Lance's OpenDAL dependency.
|
||||
goosefs-sdk = { version = "=0.1.9", optional = true }
|
||||
# Pin the transitive GooseFS SDK until the 0.1.6 compile break is fixed upstream.
|
||||
goosefs-sdk = { version = "=0.1.5", optional = true }
|
||||
moka = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
tokio = { version = "1.23", features = ["rt-multi-thread", "sync"] }
|
||||
|
||||
@@ -17,7 +17,7 @@ use arrow_array::builder::LargeBinaryBuilder;
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
use lance::dataset::{BlobRangeRequest as LanceBlobRangeRequest, Dataset, WriteParams};
|
||||
use lance_arrow::FieldExt;
|
||||
use lance_file::version::LanceFileVersion;
|
||||
use lance_encoding::version::LanceFileVersion;
|
||||
use lance_io::object_store::ObjectStore;
|
||||
use object_store::path::Path;
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ use crate::remote::{
|
||||
db::{OPT_REMOTE_API_KEY, OPT_REMOTE_HOST_OVERRIDE, OPT_REMOTE_REGION},
|
||||
};
|
||||
use lance::io::ObjectStoreParams;
|
||||
pub use lance_file::version::LanceFileVersion;
|
||||
pub use lance_encoding::version::LanceFileVersion;
|
||||
#[cfg(feature = "remote")]
|
||||
use lance_io::object_store::StorageOptions;
|
||||
use lance_io::object_store::{StorageOptionsAccessor, StorageOptionsProvider};
|
||||
|
||||
@@ -12,7 +12,7 @@ use lance::dataset::refs::Ref;
|
||||
use lance::dataset::{ReadParams, WriteMode, builder::DatasetBuilder};
|
||||
use lance::io::{ObjectStore, ObjectStoreParams, WrappingObjectStore};
|
||||
use lance_datafusion::utils::StreamingWriteSource;
|
||||
use lance_file::version::LanceFileVersion;
|
||||
use lance_encoding::version::LanceFileVersion;
|
||||
use lance_io::object_store::{StorageOptionsAccessor, StorageOptionsProvider};
|
||||
use lance_table::io::commit::commit_handler_from_url;
|
||||
use object_store::local::LocalFileSystem;
|
||||
@@ -1294,11 +1294,9 @@ mod tests {
|
||||
use crate::connection::ConnectRequest;
|
||||
use crate::data::scannable::Scannable;
|
||||
use crate::database::{CreateTableMode, CreateTableRequest};
|
||||
use crate::query::QueryRequest;
|
||||
use crate::table::{AnyQuery, WriteOptions};
|
||||
use crate::table::WriteOptions;
|
||||
use arrow_array::{Int32Array, RecordBatch, StringArray};
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
use futures::TryStreamExt;
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
@@ -1440,94 +1438,6 @@ mod tests {
|
||||
assert!(after_open.hits >= before_open.hits + 3);
|
||||
}
|
||||
|
||||
/// Regression test for https://github.com/lancedb/lancedb/issues/3197.
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn test_open_table_follows_hugging_face_symlinks() {
|
||||
let (tempdir, db) = setup_database().await;
|
||||
let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int32, false)]));
|
||||
db.create_table(CreateTableRequest {
|
||||
name: "test".to_string(),
|
||||
namespace_path: vec![],
|
||||
data: Box::new(
|
||||
RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![1, 2, 3]))])
|
||||
.unwrap(),
|
||||
) as Box<dyn Scannable>,
|
||||
mode: CreateTableMode::Create,
|
||||
write_options: Default::default(),
|
||||
location: None,
|
||||
namespace_client: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let table_dir = tempdir.path().join("test.lance");
|
||||
let versions_dir = table_dir.join("_versions");
|
||||
let manifest_path = std::fs::read_dir(&versions_dir)
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.find(|path| path.extension().is_some_and(|ext| ext == "manifest"))
|
||||
.unwrap();
|
||||
let data_path = std::fs::read_dir(table_dir.join("data"))
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.find(|path| path.extension().is_some_and(|ext| ext == "lance"))
|
||||
.unwrap();
|
||||
|
||||
// Hugging Face snapshots keep dataset objects in a separate blob directory and
|
||||
// expose them through relative symlinks.
|
||||
let blobs_dir = tempdir.path().join("blobs");
|
||||
std::fs::create_dir(&blobs_dir).unwrap();
|
||||
let manifest_blob = "9b603c63d0e692e05d58be25605f2f2064cc781e5ff94fe983a405059547b816";
|
||||
let data_blob = "be64f20e5723bd0a27cfdbdb41cf7d6fad94cd572a71973b717fb8340f4310c5";
|
||||
std::fs::rename(&manifest_path, blobs_dir.join(manifest_blob)).unwrap();
|
||||
std::fs::rename(&data_path, blobs_dir.join(data_blob)).unwrap();
|
||||
std::os::unix::fs::symlink(Path::new("../../blobs").join(manifest_blob), &manifest_path)
|
||||
.unwrap();
|
||||
std::os::unix::fs::symlink(Path::new("../../blobs").join(data_blob), &data_path).unwrap();
|
||||
let symlink_len = std::fs::symlink_metadata(&manifest_path).unwrap().len();
|
||||
let target_len = std::fs::metadata(&manifest_path).unwrap().len();
|
||||
assert_ne!(symlink_len, target_len);
|
||||
|
||||
drop(db);
|
||||
let db = ListingDatabase::connect_with_options(&ConnectRequest {
|
||||
uri: tempdir.path().to_str().unwrap().to_string(),
|
||||
#[cfg(feature = "remote")]
|
||||
client_config: Default::default(),
|
||||
options: Default::default(),
|
||||
namespace_client_properties: Default::default(),
|
||||
manifest_enabled: false,
|
||||
read_consistency_interval: None,
|
||||
session: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let table = db
|
||||
.open_table(OpenTableRequest {
|
||||
name: "test".to_string(),
|
||||
namespace_path: vec![],
|
||||
index_cache_size: None,
|
||||
lance_read_params: None,
|
||||
location: None,
|
||||
namespace_client: None,
|
||||
managed_versioning: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let batches = table
|
||||
.query(
|
||||
&AnyQuery::Query(QueryRequest::default()),
|
||||
Default::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(batches.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_clone_table_basic() {
|
||||
let (_tempdir, db) = setup_database().await;
|
||||
@@ -2432,7 +2342,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_table_uri() {
|
||||
let (_tempdir, mut db) = setup_database().await;
|
||||
let (_tempdir, db) = setup_database().await;
|
||||
|
||||
let mut pb = PathBuf::new();
|
||||
pb.push(db.uri.clone());
|
||||
@@ -2441,18 +2351,6 @@ mod tests {
|
||||
let expected = pb.to_str().unwrap();
|
||||
let uri = db.table_uri("test").ok().unwrap();
|
||||
assert_eq!(uri, expected);
|
||||
|
||||
// URI paths always use forward slashes, even on Windows. Using
|
||||
// `Path::join` here used to produce `az://container/prefix\\test.lance`,
|
||||
// which Azure treated as a different object from the table returned by
|
||||
// `table_names` (https://github.com/lancedb/lancedb/issues/1072).
|
||||
for base_uri in ["az://container/prefix", "az://container/prefix/"] {
|
||||
db.uri = base_uri.to_string();
|
||||
assert_eq!(
|
||||
db.table_uri("test").unwrap(),
|
||||
"az://container/prefix/test.lance"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Regression: connecting via a URL-style URI (which goes through
|
||||
|
||||
@@ -201,7 +201,7 @@ impl LanceNamespaceDatabase {
|
||||
&self,
|
||||
request: &DbCreateTableRequest,
|
||||
) -> Result<(
|
||||
Option<lance_file::version::LanceFileVersion>,
|
||||
Option<lance_encoding::version::LanceFileVersion>,
|
||||
Option<bool>,
|
||||
Option<bool>,
|
||||
)> {
|
||||
@@ -214,7 +214,7 @@ impl LanceNamespaceDatabase {
|
||||
|
||||
let storage_version_override = storage_options
|
||||
.and_then(|opts| opts.get(OPT_NEW_TABLE_STORAGE_VERSION))
|
||||
.map(|s| s.parse::<lance_file::version::LanceFileVersion>())
|
||||
.map(|s| s.parse::<lance_encoding::version::LanceFileVersion>())
|
||||
.transpose()?;
|
||||
|
||||
let v2_manifest_override = storage_options
|
||||
|
||||
@@ -169,12 +169,6 @@ impl From<DataFusionError> for Error {
|
||||
|
||||
impl From<lance::Error> for Error {
|
||||
fn from(source: lance::Error) -> Self {
|
||||
if has_unsupported_local_filesystem_source(&source) {
|
||||
return Self::NotSupported {
|
||||
message: "the filesystem does not support an operation required for safe Lance commits (such as atomic rename). Object-storage mounts such as Mountpoint for Amazon S3 are not supported; use the native object-store URI (for example, s3://bucket/path) instead".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
// Try to unwrap external errors that were wrapped by lance
|
||||
match source {
|
||||
lance::Error::Wrapped { error, .. } => Self::from_box_error(error),
|
||||
@@ -187,27 +181,6 @@ impl From<lance::Error> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
fn has_unsupported_local_filesystem_source(error: &(dyn std::error::Error + 'static)) -> bool {
|
||||
let mut current = Some(error);
|
||||
let mut is_local_filesystem = false;
|
||||
let mut is_unsupported = false;
|
||||
while let Some(error) = current {
|
||||
is_local_filesystem |= error
|
||||
.downcast_ref::<object_store::Error>()
|
||||
.is_some_and(|error| {
|
||||
matches!(error, object_store::Error::Generic { store, .. } if *store == "LocalFileSystem")
|
||||
});
|
||||
is_unsupported |= error
|
||||
.downcast_ref::<std::io::Error>()
|
||||
.is_some_and(|error| error.kind() == std::io::ErrorKind::Unsupported);
|
||||
if is_local_filesystem && is_unsupported {
|
||||
return true;
|
||||
}
|
||||
current = error.source();
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
impl Error {
|
||||
fn from_box_error(mut source: Box<dyn std::error::Error + Send + Sync>) -> Self {
|
||||
source = match source.downcast::<Self>() {
|
||||
@@ -297,46 +270,3 @@ impl From<candle_core::Error> for Error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unsupported_filesystem_operations_have_actionable_error() {
|
||||
let object_store_error = object_store::Error::Generic {
|
||||
store: "LocalFileSystem",
|
||||
source: Box::new(std::io::Error::from(std::io::ErrorKind::Unsupported)),
|
||||
};
|
||||
let lance_error = lance::Error::io_source(Box::new(object_store_error));
|
||||
|
||||
let error = Error::from(lance_error);
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
Error::NotSupported { message }
|
||||
if message.contains("Mountpoint for Amazon S3")
|
||||
&& message.contains("s3://bucket/path")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_io_errors_remain_lance_errors() {
|
||||
let object_store_error = object_store::Error::Generic {
|
||||
store: "LocalFileSystem",
|
||||
source: Box::new(std::io::Error::from(std::io::ErrorKind::PermissionDenied)),
|
||||
};
|
||||
let lance_error = lance::Error::io_source(Box::new(object_store_error));
|
||||
|
||||
assert!(matches!(Error::from(lance_error), Error::Lance { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_non_filesystem_errors_remain_lance_errors() {
|
||||
let lance_error = lance::Error::io_source(Box::new(std::io::Error::from(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
)));
|
||||
|
||||
assert!(matches!(Error::from(lance_error), Error::Lance { .. }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2942,7 +2942,7 @@ impl<S: HttpSend> BaseTable for RemoteTable<S> {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct MergeInsertRequest {
|
||||
pub(crate) struct MergeInsertRequest {
|
||||
on: String,
|
||||
when_matched_update_all: bool,
|
||||
when_matched_update_all_filt: Option<String>,
|
||||
@@ -5907,18 +5907,16 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Positions are relative to the first retained token, so dropping the
|
||||
// leading "hello" stop word does not shift the remaining tokens.
|
||||
assert_eq!(
|
||||
tokens,
|
||||
vec![
|
||||
FtsToken {
|
||||
text: "こんにちは".to_string(),
|
||||
position: 0,
|
||||
position: 1,
|
||||
},
|
||||
FtsToken {
|
||||
text: "世界".to_string(),
|
||||
position: 1,
|
||||
position: 2,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
@@ -90,7 +90,7 @@ struct RemoteBlobState {
|
||||
|
||||
/// Seekable Cloud blob handle over HTTP Range.
|
||||
#[derive(Debug)]
|
||||
pub struct RemoteBlobFile {
|
||||
pub(crate) struct RemoteBlobFile {
|
||||
requester: Arc<dyn BlobRangeRequester>,
|
||||
state: Mutex<RemoteBlobState>,
|
||||
closed: AtomicBool,
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::table::{AddResult, MergeResult};
|
||||
/// same Arrow-IPC streaming body and error side-channel; only the target
|
||||
/// endpoint, query parameters, and parsed result type differ.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WriteOp {
|
||||
pub(crate) enum WriteOp {
|
||||
/// `add`: stream to `/v1/table/{id}/insert/`, optionally overwriting.
|
||||
Insert { overwrite: bool },
|
||||
/// `merge_insert`: stream to `/v1/table/{id}/merge_insert/` with the merge
|
||||
@@ -49,7 +49,7 @@ pub enum WriteOp {
|
||||
/// The parsed server response for a completed write, discriminated by the
|
||||
/// operation that produced it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WriteResult {
|
||||
pub(crate) enum WriteResult {
|
||||
Add(AddResult),
|
||||
Merge(MergeResult),
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use arrow_array::{
|
||||
use arrow_schema::{DataType, Field, Fields, Schema};
|
||||
use futures::TryStreamExt;
|
||||
use lance::Dataset;
|
||||
use lance_file::version::LanceFileVersion;
|
||||
use lance_encoding::version::LanceFileVersion;
|
||||
use lancedb::{
|
||||
Connection, Error, Result, Table,
|
||||
blob::{BlobRangeRequest, blob},
|
||||
|
||||
Reference in New Issue
Block a user