Compare commits

...
Author SHA1 Message Date
ChilePiquin 477a49cecb test: update create-index default contract coverage 2026-09-03 15:52:46 -07:00
ChilePiquin bd86cc5aa6 feat: default create_index to create-only 2026-09-03 14:24:27 -07:00
2779b75d0d fix(node): resolve remaining pnpm audit findings (#4073)
`pnpm audit` in `nodejs/` reported a number of vulnerable transitive
dependencies. Most were resolved by `pnpm audit --fix`, which bumped the
affected packages in the lockfile; the `minimumReleaseAgeExclude`
additions in `pnpm-workspace.yaml` are its bookkeeping, exempting the
specific patched versions from the repository's 24-hour hold on newly
published packages. Two findings needed handling by hand, because the
vulnerable package could not simply be moved to a newer release in
place.

`@opentelemetry/sdk-metrics` 1.30.1 pins `@opentelemetry/core` to its
own exact version, and the 1.x line is end-of-life, so
GHSA-8988-4f7v-96qf (unbounded memory allocation in W3C Baggage
propagation) has no fix available on 1.x. This PR moves the dependency
to 2.x, which brings in a patched `@opentelemetry/core`. It is a
dev-only dependency with a single consumer, `__test__/otel.test.ts`, and
the parts of the API that test uses are unchanged between 1.x and 2.x.

`@huggingface/transformers` pins `sharp: ^0.33.5`, and no released
version of transformers has moved past `^0.34.5` — every version in
those ranges inherits the libvips CVEs in GHSA-f88m-g3jw-g9cj, so there
is no upstream release to upgrade to. This PR adds a pnpm `overrides`
entry pinning sharp to the patched `^0.35.4` line instead.

`pnpm audit` now reports no known vulnerabilities.

## Not included

The sharp override only applies to this repository's own dependency
tree, since pnpm overrides are not published to npm. Anyone installing
`@lancedb/lancedb` together with the optional
`@huggingface/transformers` still resolves sharp 0.33.5, and will until
transformers itself moves to sharp 0.35. Practical exposure there is
low: the CVEs require decoding untrusted images, and LanceDB's
transformers embedding function is text-only.

`nodejs/examples/` is a separate install with its own lockfile and is
untouched here. It pins `sharp: "0.33.5"` directly and `pnpm audit`
reports 19 findings against it. Bumping sharp there is more involved
than it looks, because sharp 0.35 requires Node >= 20.9 while the
examples tests run on the Node 18/20 CI matrix, so it is left for
separate work.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Xuanwo <github@xuanwo.io>
2026-09-03 07:31:03 +08:00
lancedb-gatefixer[bot] 0d19a6c546 fix: support nested-list FTS indexing (#4059)
## Summary

- validate native FTS fields against the recursively resolved terminal
text leaf
- preserve canonical public paths and list depth for Lance
document-boundary handling
- cover async nested-list index creation and deepest-list `_doc_index`
search coordinates

## Root cause

The FTS resolver recursively found the terminal text field but returned
the outer list field. Native validation therefore rejected
`List(List(Utf8))` before Lance could create a list-element index.

## Validation

- `cargo test --quiet --features remote -p lancedb
test_nested_list_fts_uses_deepest_document_coordinates -- --nocapture`
- `cargo test --quiet --features remote -p lancedb
test_execute_async_validates_fts_input_before_starting_job`
- `cargo test --quiet --features remote -p lancedb
test_public_fts_field_path_prefers_exact_case`
- `cargo check --quiet --features remote --tests --examples`
- `cargo clippy --quiet --features remote --tests --examples`
- `cargo fmt --all -- --check`

Fixes #4058

<!-- lance-gatekeeper-fix:v1 agent=a4e48f49d12b2b6de7f10adb1c5bf5f5
generation=1 -->

Co-authored-by: Gatefixer <313497061+lancedb-gatefixer[bot]@users.noreply.github.com>
2026-09-02 17:51:21 +08:00
Lance Release c0f33f8627 Bump version: 0.39.0-beta.0 → 0.39.0-beta.1 2026-09-02 05:28:44 +00:00
31 changed files with 861 additions and 535 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
[tool.bumpversion]
current_version = "0.39.0-beta.0"
current_version = "0.39.0-beta.1"
parse = """(?x)
(?P<major>0|[1-9]\\d*)\\.
(?P<minor>0|[1-9]\\d*)\\.
Generated
+3 -3
View File
@@ -5403,7 +5403,7 @@ dependencies = [
[[package]]
name = "lancedb"
version = "0.39.0-beta.0"
version = "0.39.0-beta.1"
dependencies = [
"ahash",
"anyhow",
@@ -5491,7 +5491,7 @@ dependencies = [
[[package]]
name = "lancedb-nodejs"
version = "0.39.0-beta.0"
version = "0.39.0-beta.1"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -5516,7 +5516,7 @@ dependencies = [
[[package]]
name = "lancedb-python"
version = "0.39.0-beta.0"
version = "0.39.0-beta.1"
dependencies = [
"arrow",
"async-trait",
+1 -1
View File
@@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`:
<dependency>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-core</artifactId>
<version>0.39.0-beta.0</version>
<version>0.39.0-beta.1</version>
</dependency>
```
+1 -1
View File
@@ -50,7 +50,7 @@ If this is false, and another index already exists on the same columns
and the same name, then an error will be returned. This is true even if
that index is out of date.
The default is true
The default is false
***
+1 -1
View File
@@ -8,7 +8,7 @@
<parent>
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.39.0-beta.0</version>
<version>0.39.0-beta.1</version>
<relativePath>../pom.xml</relativePath>
</parent>
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.lancedb</groupId>
<artifactId>lancedb-parent</artifactId>
<version>0.39.0-beta.0</version>
<version>0.39.0-beta.1</version>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>LanceDB Java SDK Parent POM</description>
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "lancedb-nodejs"
edition.workspace = true
version = "0.39.0-beta.0"
version = "0.39.0-beta.1"
publish = false
license.workspace = true
description.workspace = true
+1 -5
View File
@@ -1526,11 +1526,7 @@ describe("When creating an index", () => {
it("should allow me to replace (or not) an existing index", async () => {
await tbl.createIndex("id");
// Default is replace=true
await tbl.createIndex("id");
await expect(tbl.createIndex("id", { replace: false })).rejects.toThrow(
"already exists",
);
await expect(tbl.createIndex("id")).rejects.toThrow("already exists");
await tbl.createIndex("id", { replace: true });
});
+1 -1
View File
@@ -841,7 +841,7 @@ export interface IndexOptions {
* and the same name, then an error will be returned. This is true even if
* that index is out of date.
*
* The default is true
* The default is false
*/
replace?: boolean;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-darwin-arm64",
"version": "0.39.0-beta.0",
"version": "0.39.0-beta.1",
"os": ["darwin"],
"cpu": ["arm64"],
"main": "lancedb.darwin-arm64.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-arm64-gnu",
"version": "0.39.0-beta.0",
"version": "0.39.0-beta.1",
"os": ["linux"],
"cpu": ["arm64"],
"main": "lancedb.linux-arm64-gnu.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-arm64-musl",
"version": "0.39.0-beta.0",
"version": "0.39.0-beta.1",
"os": ["linux"],
"cpu": ["arm64"],
"main": "lancedb.linux-arm64-musl.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-x64-gnu",
"version": "0.39.0-beta.0",
"version": "0.39.0-beta.1",
"os": ["linux"],
"cpu": ["x64"],
"main": "lancedb.linux-x64-gnu.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-linux-x64-musl",
"version": "0.39.0-beta.0",
"version": "0.39.0-beta.1",
"os": ["linux"],
"cpu": ["x64"],
"main": "lancedb.linux-x64-musl.node",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-win32-arm64-msvc",
"version": "0.39.0-beta.0",
"version": "0.39.0-beta.1",
"os": [
"win32"
],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@lancedb/lancedb-win32-x64-msvc",
"version": "0.39.0-beta.0",
"version": "0.39.0-beta.1",
"os": ["win32"],
"cpu": ["x64"],
"main": "lancedb.win32-x64-msvc.node",
+3 -3
View File
@@ -11,7 +11,7 @@
"ann"
],
"private": false,
"version": "0.39.0-beta.0",
"version": "0.39.0-beta.1",
"main": "dist/index.js",
"exports": {
".": "./dist/index.js",
@@ -44,7 +44,7 @@
"@biomejs/biome": "^1.7.3",
"@jest/globals": "^29.7.0",
"@napi-rs/cli": "3.7.0",
"@opentelemetry/sdk-metrics": "^1.30.0",
"@opentelemetry/sdk-metrics": "^2.10.0",
"@types/axios": "^0.14.0",
"@types/jest": "^29.1.2",
"@types/node": "22.7.4",
@@ -56,7 +56,7 @@
"eslint": "^8.57.0",
"jest": "^29.7.0",
"shx": "^0.3.4",
"tmp": "^0.2.3",
"tmp": "^0.2.7",
"ts-jest": "^29.1.2",
"typedoc": "0.26.4",
"typedoc-plugin-markdown": "4.2.1",
+593 -437
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -16,3 +16,41 @@ allowBuilds:
onnxruntime-node: true
protobufjs: true
sharp: true
minimumReleaseAgeExclude:
- protobufjs@7.5.8
- tmp@0.2.6
- form-data@4.0.6
- tar@7.5.16
- markdown-it@14.1.2
- linkify-it@5.0.1
- js-yaml@3.15.0
- js-yaml@4.1.2
- protobufjs@7.6.1
- protobufjs@7.6.3
- '@babel/core@7.29.1'
- axios@1.18.0
- brace-expansion@2.1.2
- brace-expansion@1.1.16
- js-yaml@4.3.0
- tar@7.5.18
- tar@7.5.19
- tar@7.5.17
- protobufjs@7.6.5
- linkify-it@5.0.2
- sharp@0.35.0
- brace-expansion@1.1.17
- brace-expansion@2.1.3
- brace-expansion@2.1.4
- brace-expansion@1.1.18
- js-yaml@3.15.1
- js-yaml@4.3.1
- tar@7.5.21
- '@opentelemetry/core@2.8.0'
# @huggingface/transformers pins sharp ^0.33.5 and no released version has moved
# past ^0.34.5, all of which inherit the libvips CVEs in GHSA-f88m-g3jw-g9cj.
# Force the patched line. sharp is only reached by transformers' image pipeline,
# which LanceDB's text embedding function never uses.
overrides:
sharp: ^0.35.4
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "lancedb-python"
version = "0.39.0-beta.0"
version = "0.39.0-beta.1"
publish = false
edition.workspace = true
description = "Python bindings for LanceDB"
+4 -12
View File
@@ -402,6 +402,7 @@ class RemoteTable(Table):
/,
*,
config: IndexConfigType,
replace: bool = ...,
wait_timeout: Optional[timedelta] = ...,
name: Optional[str] = ...,
train: bool = ...,
@@ -416,7 +417,7 @@ class RemoteTable(Table):
index_cache_size: Optional[int] = ...,
num_partitions: Optional[int] = ...,
num_sub_vectors: Optional[int] = ...,
replace: Optional[bool] = ...,
replace: bool = ...,
accelerator: Optional[str] = ...,
index_type: Literal[
"VECTOR", "IVF_FLAT", "IVF_SQ", "IVF_PQ", "IVF_HNSW_SQ", "IVF_HNSW_PQ"
@@ -435,7 +436,7 @@ class RemoteTable(Table):
index_cache_size: Optional[int] = None,
num_partitions: Optional[int] = None,
num_sub_vectors: Optional[int] = None,
replace: Optional[bool] = None,
replace: bool = False,
accelerator: Optional[str] = None,
index_type="vector",
wait_timeout: Optional[timedelta] = None,
@@ -479,7 +480,6 @@ class RemoteTable(Table):
vector_column_name,
accelerator,
index_cache_size,
replace,
)
if is_legacy:
@@ -503,12 +503,6 @@ class RemoteTable(Table):
"If you have 100M+ vectors to index,"
"please contact us at contact@lancedb.com"
)
if replace is not None:
logging.warning(
"replace is not supported on LanceDB cloud."
"Existing indexes will always be replaced."
)
idx_type = index_type.upper()
if idx_type == "VECTOR" or idx_type == "IVF_PQ":
config = IvfPq(
@@ -561,7 +555,7 @@ class RemoteTable(Table):
column: str,
*,
config: IndexConfigType,
replace: Optional[bool] = None,
replace: bool = False,
wait_timeout: Optional[timedelta] = None,
name: Optional[str] = None,
train: bool = True,
@@ -593,7 +587,6 @@ class RemoteTable(Table):
vector_column_name: str,
accelerator: Optional[str],
index_cache_size: Optional[int],
replace: Optional[bool],
) -> bool:
"""Detect if this is a legacy create_index call."""
if config is not None:
@@ -605,7 +598,6 @@ class RemoteTable(Table):
num_sub_vectors,
accelerator,
index_cache_size,
replace,
)
):
return True
+13 -13
View File
@@ -1132,7 +1132,7 @@ class Table(ABC):
num_partitions: Optional[int] = None,
num_sub_vectors: Optional[int] = None,
vector_column_name: str = VECTOR_COLUMN_NAME,
replace: bool = True,
replace: bool = False,
accelerator: Optional[str] = None,
index_cache_size: Optional[int] = None,
*,
@@ -1166,7 +1166,7 @@ class Table(ABC):
The index configuration object. If provided, uses the new unified API.
Can be one of: IvfFlat, IvfPq, IvfSq, IvfRq, HnswPq, HnswSq,
BTree, Bitmap, LabelList, Fm, FTS.
replace : bool, default True
replace : bool, default False
Whether to replace an existing index on this column.
wait_timeout : timedelta, optional
Timeout to wait for async indexing to complete.
@@ -1198,7 +1198,7 @@ class Table(ABC):
column: str,
*,
config: IndexConfigType,
replace: Optional[bool] = None,
replace: bool = False,
wait_timeout: Optional[timedelta] = None,
name: Optional[str] = None,
train: bool = True,
@@ -1260,7 +1260,7 @@ class Table(ABC):
self,
column: str,
*,
replace: bool = True,
replace: bool = False,
index_type: ScalarIndexType = "BTREE",
wait_timeout: Optional[timedelta] = None,
name: Optional[str] = None,
@@ -1272,7 +1272,7 @@ class Table(ABC):
column : str
The column to be indexed. Must be a boolean, integer, float,
or string column.
replace : bool, default True
replace : bool, default False
Replace the existing index if it exists.
index_type: Literal["BTREE", "BITMAP", "LABEL_LIST"], default "BTREE"
The type of index to create.
@@ -2988,7 +2988,7 @@ class LanceTable(Table):
num_partitions: Optional[int] = None,
num_sub_vectors: Optional[int] = None,
vector_column_name: str = VECTOR_COLUMN_NAME,
replace: bool = True,
replace: bool = False,
accelerator: Optional[str] = None,
index_cache_size: Optional[int] = None,
num_bits: int = 8,
@@ -3030,7 +3030,7 @@ class LanceTable(Table):
The index configuration object. If provided, uses the new unified API.
Can be one of: IvfFlat, IvfPq, IvfSq, IvfRq, HnswPq, HnswSq,
BTree, Bitmap, LabelList, Fm, FTS.
replace : bool, default True
replace : bool, default False
Whether to replace an existing index on this column.
wait_timeout : timedelta, optional
Timeout to wait for async indexing to complete.
@@ -3169,7 +3169,7 @@ class LanceTable(Table):
column: str,
*,
config: IndexConfigType,
replace: Optional[bool] = None,
replace: bool = False,
wait_timeout: Optional[timedelta] = None,
name: Optional[str] = None,
train: bool = True,
@@ -3424,7 +3424,7 @@ class LanceTable(Table):
self,
column: str,
*,
replace: bool = True,
replace: bool = False,
index_type: ScalarIndexType = "BTREE",
name: Optional[str] = None,
):
@@ -5313,7 +5313,7 @@ class AsyncTable:
self,
column: str,
*,
replace: Optional[bool] = None,
replace: bool = False,
config: Optional[
Union[
IvfFlat,
@@ -5344,14 +5344,14 @@ class AsyncTable:
----------
column: str
The column to index.
replace: bool, default True
replace: bool, default False
Whether to replace the existing index
If this is false, and another index already exists on the same columns
and the same name, then an error will be returned. This is true even if
that index is out of date.
The default is True
The default is False
config: default None
For advanced configuration you can specify the type of index you would
like to create. You can also specify index-specific parameters when
@@ -5409,7 +5409,7 @@ class AsyncTable:
self,
column: str,
*,
replace: Optional[bool] = None,
replace: bool = False,
config: Optional[
Union[
IvfFlat,
+10 -2
View File
@@ -97,6 +97,9 @@ async def test_create_index_async_returns_done_job(some_table: AsyncTable):
async def test_create_scalar_index(some_table: AsyncTable):
# Can create
await some_table.create_index("id")
# Can't recreate by default
with pytest.raises(RuntimeError, match="already exists"):
await some_table.create_index("id")
# Can recreate if replace=True
await some_table.create_index("id", replace=True)
indices = await some_table.list_indices()
@@ -110,7 +113,7 @@ async def test_create_scalar_index(some_table: AsyncTable):
with pytest.raises(RuntimeError, match="already exists"):
await some_table.create_index("id", replace=False)
# can also specify index type
await some_table.create_index("id", config=BTree())
await some_table.create_index("id", config=BTree(), replace=True)
await some_table.drop_index("id_idx")
indices = await some_table.list_indices()
@@ -351,13 +354,18 @@ async def test_full_text_search_index(some_table: AsyncTable):
async def test_create_vector_index(some_table: AsyncTable):
# Can create
await some_table.create_index("vector")
# Can't recreate by default
with pytest.raises(RuntimeError, match="already exists"):
await some_table.create_index("vector")
# Can recreate if replace=True
await some_table.create_index("vector", replace=True)
# Can't recreate if replace=False
with pytest.raises(RuntimeError, match="already exists"):
await some_table.create_index("vector", replace=False)
# Can also specify index type
await some_table.create_index("vector", config=IvfPq(num_partitions=100))
await some_table.create_index(
"vector", config=IvfPq(num_partitions=100), replace=True
)
indices = await some_table.list_indices()
assert len(indices) == 1
assert indices[0].index_type == "IvfPq"
+1 -1
View File
@@ -834,7 +834,7 @@ def test_table_create_indices():
vector_req = received_requests[2]
assert "name" in vector_req
assert vector_req["name"] == "custom_vector_idx"
assert "replace" not in vector_req
assert vector_req["replace"] is False
table.wait_for_index(["custom_scalar_idx"], timedelta(seconds=2))
table.wait_for_index(
+8 -8
View File
@@ -1600,7 +1600,7 @@ def test_create_index_method(mock_create_index, mem_db: DBConnection):
)
mock_create_index.assert_called_with(
"my_vector",
replace=True,
replace=False,
config=expected_config,
wait_timeout=None,
name=None,
@@ -1620,7 +1620,7 @@ def test_create_index_method(mock_create_index, mem_db: DBConnection):
)
mock_create_index.assert_called_with(
"my_vector",
replace=True,
replace=False,
config=expected_config,
wait_timeout=None,
name=None,
@@ -1646,7 +1646,7 @@ def test_create_index_name_and_train_parameters(
expected_config = IvfPq() # Default config
mock_create_index.assert_called_with(
"vector",
replace=True,
replace=False,
config=expected_config,
wait_timeout=None,
name="my_custom_index",
@@ -1657,7 +1657,7 @@ def test_create_index_name_and_train_parameters(
table.create_index(vector_column_name="vector", train=False)
mock_create_index.assert_called_with(
"vector",
replace=True,
replace=False,
config=expected_config,
wait_timeout=None,
name=None,
@@ -1668,7 +1668,7 @@ def test_create_index_name_and_train_parameters(
table.create_index(vector_column_name="vector", name="my_index_name", train=True)
mock_create_index.assert_called_with(
"vector",
replace=True,
replace=False,
config=expected_config,
wait_timeout=None,
name="my_index_name",
@@ -1705,7 +1705,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
table.create_index("vector", config=IvfPq(distance_type="l2"))
mock_create_index.assert_called_with(
"vector",
replace=True,
replace=False,
config=IvfPq(distance_type="l2"),
wait_timeout=None,
name=None,
@@ -1716,7 +1716,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
table.create_index("category", config=BTree())
mock_create_index.assert_called_with(
"category",
replace=True,
replace=False,
config=BTree(),
wait_timeout=None,
name=None,
@@ -1727,7 +1727,7 @@ def test_create_index_new_api(mock_create_index, mem_db: DBConnection):
table.create_index("text", config=FTS(with_position=True))
mock_create_index.assert_called_with(
"text",
replace=True,
replace=False,
config=FTS(with_position=True),
wait_timeout=None,
name=None,
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "lancedb"
version = "0.39.0-beta.0"
version = "0.39.0-beta.1"
edition.workspace = true
description = "LanceDB: A serverless, low-latency vector database for AI applications"
license.workspace = true
+2 -2
View File
@@ -200,14 +200,14 @@ impl IndexBuilder {
parent,
index,
columns,
replace: true,
replace: false,
train: true,
wait_timeout: None,
name: None,
}
}
/// Whether to replace the existing index, the default is `true`.
/// Whether to replace the existing index, the default is `false`.
///
/// If this is false, and another index already exists on the same columns
/// and the same name, then an error will be returned. This is true even if
+35 -17
View File
@@ -40,6 +40,7 @@ impl TerminalResult {
}
}
#[cfg(feature = "remote")]
pub(crate) fn remote(value: Option<Value>, request_id: String) -> Self {
Self {
value,
@@ -47,30 +48,46 @@ impl TerminalResult {
}
}
#[cfg(feature = "remote")]
pub(crate) fn value(&self) -> Option<&Value> {
self.value.as_ref()
}
fn decode<T: DeserializeOwned>(self) -> Result<T> {
let value = self.value.ok_or_else(|| match &self.request_id {
Some(request_id) => Error::Http {
source: "successful typed job response did not contain a result".into(),
request_id: request_id.clone(),
status_code: None,
},
None => Error::Runtime {
let value = self.value.ok_or_else(|| {
#[cfg(feature = "remote")]
if let Some(request_id) = &self.request_id {
return Error::Http {
source: "successful typed job response did not contain a result".into(),
request_id: request_id.clone(),
status_code: None,
};
}
Error::Runtime {
message: "successful typed job did not contain a result".to_string(),
},
}
})?;
serde_json::from_value(value).map_err(|error| match self.request_id {
Some(request_id) => Error::Http {
source: format!("failed to parse typed job result: {error}").into(),
request_id,
status_code: None,
},
None => Error::Runtime {
message: format!("failed to parse typed job result: {error}"),
},
serde_json::from_value(value).map_err(|error| {
#[cfg(feature = "remote")]
{
match self.request_id {
Some(request_id) => Error::Http {
source: format!("failed to parse typed job result: {error}").into(),
request_id,
status_code: None,
},
None => Error::Runtime {
message: format!("failed to parse typed job result: {error}"),
},
}
}
#[cfg(not(feature = "remote"))]
{
let _ = self.request_id;
Error::Runtime {
message: format!("failed to parse typed job result: {error}"),
}
}
})
}
}
@@ -117,6 +134,7 @@ impl Job<()> {
}
}
#[cfg(feature = "remote")]
pub(crate) fn new(handle: Box<dyn JobHandle>) -> Self {
Self {
inner: JobInner::Handle {
+49 -7
View File
@@ -524,13 +524,10 @@ impl<S: HttpSend> RemoteTable<S> {
_ => resolve_arrow_field_path(&schema, &column)?,
};
let mut body = serde_json::json!({
"column": canonical_column
"column": canonical_column,
"replace": index.replace,
});
if !index.replace {
body["replace"] = false.into();
}
// Add name parameter if provided (for backwards compatibility, only include if Some)
if let Some(ref name) = index.name {
body["name"] = serde_json::Value::String(name.clone());
@@ -6309,6 +6306,7 @@ mod tests {
let mut expected_body = expected_body.clone();
expected_body["column"] = "a".into();
expected_body[INDEX_TYPE_KEY] = index_type.into();
expected_body["replace"] = false.into();
assert_eq!(body, expected_body);
@@ -6327,7 +6325,7 @@ mod tests {
}
#[tokio::test]
async fn test_create_index_forwards_replace_false_on_existing_route() {
async fn test_create_index_forwards_default_replace_false_on_existing_route() {
let table = Table::new_with_handler("my_table", move |request| {
assert_eq!(request.method(), "POST");
match request.url().path() {
@@ -6354,7 +6352,40 @@ mod tests {
table
.create_index(&["a"], Index::BTree(Default::default()))
.replace(false)
.execute()
.await
.unwrap();
}
#[tokio::test]
async fn test_create_index_forwards_explicit_replace_true_on_existing_route() {
let table = Table::new_with_handler("my_table", move |request| {
assert_eq!(request.method(), "POST");
match request.url().path() {
"/v1/table/my_table/describe/" => {
let schema = Schema::new(vec![Field::new("a", DataType::Int32, false)]);
http::Response::builder()
.status(200)
.body(describe_response(&schema))
.unwrap()
}
"/v1/table/my_table/create_index/" => {
let body = request.body().unwrap().as_bytes().unwrap();
let body: serde_json::Value = serde_json::from_slice(body).unwrap();
assert_eq!(body["replace"], json!(true));
http::Response::builder()
.status(200)
.body("{}".to_string())
.unwrap()
}
path => panic!("Unexpected path: {}", path),
}
});
table
.create_index(&["a"], Index::BTree(Default::default()))
.replace(true)
.execute()
.await
.unwrap();
@@ -6585,38 +6616,46 @@ mod tests {
json!({
"column": "rowId",
"index_type": "BTREE",
"replace": false,
}),
json!({
"column": "`row-id`",
"index_type": "BTREE",
"replace": false,
}),
json!({
"column": "userId",
"index_type": "BTREE",
"replace": false,
}),
json!({
"column": "MetaData.userId",
"index_type": "BTREE",
"replace": false,
}),
json!({
"column": "metadata.user_id",
"index_type": "BTREE",
"replace": false,
}),
json!({
"column": "image.embedding",
"index_type": "IVF_PQ",
"metric_type": "l2",
"replace": false,
}),
{
let mut body = serde_json::to_value(InvertedIndexParams::default()).unwrap();
body["column"] = "payload.text".into();
body["index_type"] = "FTS".into();
body["replace"] = false.into();
body
},
{
let mut body = serde_json::to_value(InvertedIndexParams::default()).unwrap();
body["column"] = "docs.content".into();
body["index_type"] = "FTS".into();
body["replace"] = false.into();
body
},
{
@@ -6624,15 +6663,18 @@ mod tests {
body["column"] = "docs.content".into();
body["index_type"] = "FTS".into();
body["document_granularity"] = "list_element".into();
body["replace"] = false.into();
body
},
json!({
"column": "`meta-data`.`user-id`",
"index_type": "BTREE",
"replace": false,
}),
json!({
"column": "literal.`a.b`",
"index_type": "BTREE",
"replace": false,
}),
]);
let request_idx = Arc::new(AtomicUsize::new(0));
+80 -5
View File
@@ -133,7 +133,7 @@ impl NativeTable {
),
});
}
(resolved.canonical_path, resolved.field)
(resolved.canonical_path, resolved.terminal_field)
} else {
Self::resolve_index_field(dataset.schema(), &opts.columns[0])?
};
@@ -439,7 +439,8 @@ mod tests {
use arrow_array::record_batch;
use arrow_array::{
Array, ArrayRef, BinaryArray, BooleanArray, FixedSizeListArray, Float32Array, Int32Array,
LargeBinaryArray, LargeStringArray, RecordBatch, StringArray, StructArray,
LargeBinaryArray, LargeStringArray, ListArray, RecordBatch, StringArray, StructArray,
UInt32Array,
};
use arrow_data::ArrayDataBuilder;
use arrow_schema::{DataType, Field, Schema};
@@ -458,6 +459,7 @@ mod tests {
use crate::query::{ExecutableQuery, QueryBase};
use crate::table::optimize::{CompactionOptions, OptimizeAction};
use lance_index::scalar::FullTextSearchQuery;
use lance_index::scalar::inverted::query::{FtsQuery, MatchQuery};
fn create_fixed_size_list<T: Array>(
values: T,
@@ -599,6 +601,80 @@ mod tests {
assert!(invalid_granularity.is_err());
}
#[tokio::test]
async fn test_nested_list_fts_uses_deepest_document_coordinates() {
let conn = connect("memory://").execute().await.unwrap();
let mut docs = ListBuilder::new(ListBuilder::new(StringBuilder::new()));
docs.values().values().append_value("alpha");
docs.values().values().append_value("beta");
docs.values().append(true);
docs.values().values().append_value("gamma");
docs.values().values().append_value("alpha delta");
docs.values().append(true);
docs.append(true);
docs.values().append(true);
docs.values().values().append_value("alpha");
docs.values().append(true);
docs.append(true);
let batch = RecordBatch::try_from_iter(vec![
("id", Arc::new(Int32Array::from(vec![0, 1])) as ArrayRef),
("docs", Arc::new(docs.finish()) as ArrayRef),
])
.unwrap();
let table = conn.create_table("nested", batch).execute().await.unwrap();
let job = table
.create_index(
&["docs"],
Index::FTS(
FtsIndexBuilder::default()
.document_granularity(DocumentGranularity::ListElement),
),
)
.execute_async()
.await
.unwrap();
job.wait().await.unwrap();
let query = FullTextSearchQuery::new_query(FtsQuery::Match(
MatchQuery::new("alpha".to_string())
.with_column(Some("docs".to_string()))
.with_document_granularity(DocumentGranularity::ListElement),
));
let batches = table
.query()
.full_text_search(query)
.limit(10)
.execute()
.await
.unwrap()
.try_collect::<Vec<_>>()
.await
.unwrap();
let mut hits = Vec::new();
for batch in batches {
let ids = batch["id"].as_any().downcast_ref::<Int32Array>().unwrap();
let coordinates = batch["_doc_index"]
.as_any()
.downcast_ref::<ListArray>()
.unwrap();
for row in 0..batch.num_rows() {
let coordinate = coordinates.value(row);
let coordinate = coordinate.as_any().downcast_ref::<UInt32Array>().unwrap();
hits.push((ids.value(row), coordinate.values().to_vec()));
}
}
hits.sort_unstable();
assert_eq!(
hits,
vec![(0, vec![0, 0]), (0, vec![1, 1]), (1, vec![1, 0])]
);
}
/// Concurrent waiters, and a wait issued after the job settled, all
/// succeed once the build does.
#[tokio::test]
@@ -649,12 +725,11 @@ mod tests {
.await
.unwrap();
// Rebuilding the same index without replace fails once the build
// Rebuilding the same index without explicit replace fails once the build
// starts, so the failure reaches the job rather than execute_async.
let job = Arc::new(
table
.create_index(&["id"], Index::BTree(BTreeIndexBuilder::default()))
.replace(false)
.execute_async()
.await
.unwrap(),
@@ -693,7 +768,6 @@ mod tests {
let job = table
.create_index(&["id"], Index::BTree(BTreeIndexBuilder::default()))
.replace(false)
.execute_async()
.await
.unwrap();
@@ -1030,6 +1104,7 @@ mod tests {
// Can also specify btree
table
.create_index(&["i"], Index::BTree(BTreeIndexBuilder::default()))
.replace(true)
.execute()
.await
.unwrap();
+5 -4
View File
@@ -227,7 +227,7 @@ pub(crate) fn resolve_arrow_field_path(schema: &Schema, column: &str) -> Result<
pub(crate) struct ResolvedFtsField {
pub canonical_path: String,
pub field: Field,
pub terminal_field: Field,
pub list_depth: usize,
}
@@ -309,7 +309,7 @@ pub(crate) fn resolve_lance_fts_field_path(
);
Ok(ResolvedFtsField {
canonical_path,
field: Field::from(field),
terminal_field: Field::from(terminal),
list_depth,
})
}
@@ -375,7 +375,7 @@ pub(crate) fn resolve_arrow_fts_field_path(
message: format!("Invalid schema: {}", e),
})?;
let resolved = resolve_lance_fts_field_path(&lance_schema, column)?;
Ok((resolved.canonical_path, resolved.field))
Ok((resolved.canonical_path, resolved.terminal_field))
}
pub fn supported_btree_data_type(dtype: &DataType) -> bool {
@@ -647,8 +647,9 @@ mod tests {
Field::new("docs", text_list(), true),
]);
let (path, _) = resolve_arrow_fts_field_path(&schema, "docs.content").unwrap();
let (path, field) = resolve_arrow_fts_field_path(&schema, "docs.content").unwrap();
assert_eq!(path, "docs.content");
assert_eq!(field.data_type(), &DataType::Utf8);
let lance_schema = lance_core::datatypes::Schema::try_from(&schema).unwrap();
let field_id = lance_schema