mirror of
https://github.com/lancedb/lancedb.git
synced 2026-08-27 08:28:28 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d24b2dcacc | |||
| 2deccf21cf | |||
| ead4d27bfc | |||
| 5153e5a023 |
+1
-1
@@ -1,5 +1,5 @@
|
||||
[tool.bumpversion]
|
||||
current_version = "0.38.0-beta.10"
|
||||
current_version = "0.38.0-beta.11"
|
||||
parse = """(?x)
|
||||
(?P<major>0|[1-9]\\d*)\\.
|
||||
(?P<minor>0|[1-9]\\d*)\\.
|
||||
|
||||
Generated
+3
-3
@@ -5402,7 +5402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb"
|
||||
version = "0.38.0-beta.10"
|
||||
version = "0.38.0-beta.11"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"anyhow",
|
||||
@@ -5490,7 +5490,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb-nodejs"
|
||||
version = "0.38.0-beta.10"
|
||||
version = "0.38.0-beta.11"
|
||||
dependencies = [
|
||||
"arrow-array",
|
||||
"arrow-buffer",
|
||||
@@ -5515,7 +5515,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lancedb-python"
|
||||
version = "0.38.0-beta.10"
|
||||
version = "0.38.0-beta.11"
|
||||
dependencies = [
|
||||
"arrow",
|
||||
"async-trait",
|
||||
|
||||
@@ -14,7 +14,7 @@ Add the following dependency to your `pom.xml`:
|
||||
<dependency>
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-core</artifactId>
|
||||
<version>0.38.0-beta.10</version>
|
||||
<version>0.38.0-beta.11</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<parent>
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-parent</artifactId>
|
||||
<version>0.38.0-beta.10</version>
|
||||
<version>0.38.0-beta.11</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.lancedb</groupId>
|
||||
<artifactId>lancedb-parent</artifactId>
|
||||
<version>0.38.0-beta.10</version>
|
||||
<version>0.38.0-beta.11</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>${project.artifactId}</name>
|
||||
<description>LanceDB Java SDK Parent POM</description>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "lancedb-nodejs"
|
||||
edition.workspace = true
|
||||
version = "0.38.0-beta.10"
|
||||
version = "0.38.0-beta.11"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
description.workspace = true
|
||||
|
||||
@@ -6,6 +6,9 @@ import * as arrow17 from "apache-arrow-17";
|
||||
import * as arrow18 from "apache-arrow-18";
|
||||
|
||||
import {
|
||||
Field as CurrentField,
|
||||
LargeBinary as CurrentLargeBinary,
|
||||
Schema as CurrentSchema,
|
||||
Vector as CurrentVector,
|
||||
convertToTable,
|
||||
tableFromIPC as currentTableFromIPC,
|
||||
@@ -36,6 +39,24 @@ function sampleRecords(): Array<Record<string, any>> {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
it("preserves field metadata from a provided schema", async function () {
|
||||
const jsonMetadata = new Map([["ARROW:extension:name", "lance.json"]]);
|
||||
const schema = new CurrentSchema([
|
||||
new CurrentField("meta", new CurrentLargeBinary(), true, jsonMetadata),
|
||||
]);
|
||||
|
||||
const table = makeArrowTable(
|
||||
[{ meta: Buffer.from(JSON.stringify({ source: "test" })) }],
|
||||
{ schema },
|
||||
);
|
||||
|
||||
expect(table.schema.fields[0].metadata).toEqual(jsonMetadata);
|
||||
|
||||
const roundTripped = currentTableFromIPC(await fromTableToBuffer(table));
|
||||
expect(roundTripped.schema.fields[0].metadata).toEqual(jsonMetadata);
|
||||
});
|
||||
|
||||
describe.each([arrow15, arrow16, arrow17, arrow18])(
|
||||
"Arrow",
|
||||
(
|
||||
|
||||
@@ -187,6 +187,58 @@ describe("embedding functions", () => {
|
||||
const vector0 = JSON.parse(JSON.stringify(arr[0].vector));
|
||||
expect(vector0).toEqual([1, 2, 3]);
|
||||
});
|
||||
it("should append multiple Python embeddings with the same alias", async () => {
|
||||
@register("python-mock")
|
||||
// biome-ignore lint/correctness/noUnusedVariables: the decorator registers this class
|
||||
class MockEmbeddingFunction extends EmbeddingFunction<string> {
|
||||
ndims() {
|
||||
return 3;
|
||||
}
|
||||
embeddingDataType(): Float {
|
||||
return new Float32();
|
||||
}
|
||||
async computeQueryEmbeddings(_data: string) {
|
||||
return [1, 2, 3];
|
||||
}
|
||||
async computeSourceEmbeddings(data: string[]) {
|
||||
return data.map((value) =>
|
||||
value === "hello world" ? [1, 2, 3] : [4, 5, 6],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const metadata = new Map([
|
||||
[
|
||||
"embedding_functions",
|
||||
'[{"source_column":"text1","vector_column":"vector1","name":"python-mock","model":{}},{"source_column":"text2","vector_column":"vector2","name":"python-mock","model":{}}]',
|
||||
],
|
||||
]);
|
||||
const schema = new Schema(
|
||||
[
|
||||
new Field("text1", new Utf8(), true),
|
||||
new Field("text2", new Utf8(), true),
|
||||
new Field(
|
||||
"vector1",
|
||||
new FixedSizeList(3, new Field("item", new Float32(), true)),
|
||||
true,
|
||||
),
|
||||
new Field(
|
||||
"vector2",
|
||||
new FixedSizeList(3, new Field("item", new Float32(), true)),
|
||||
true,
|
||||
),
|
||||
],
|
||||
metadata,
|
||||
);
|
||||
|
||||
const db = await connect(tmpDir.name);
|
||||
const table = await db.createEmptyTable("test", schema);
|
||||
await table.add([{ text1: "hello world", text2: "goodbye world" }]);
|
||||
|
||||
const rows = await table.query().toArray();
|
||||
expect(JSON.parse(JSON.stringify(rows[0].vector1))).toEqual([1, 2, 3]);
|
||||
expect(JSON.parse(JSON.stringify(rows[0].vector2))).toEqual([4, 5, 6]);
|
||||
});
|
||||
|
||||
it("should append generated vectors to a non-nullable schema", async () => {
|
||||
@register("non_nullable_schema_test")
|
||||
|
||||
@@ -3561,6 +3561,27 @@ describe("when creating an empty table", () => {
|
||||
expect((actualSchema.fields[1].type as Float64).precision).toBe(2);
|
||||
});
|
||||
|
||||
it("can add and query JSON data", async () => {
|
||||
const schema = new Schema([
|
||||
new Field("id", new Int32(), true),
|
||||
new Field(
|
||||
"meta",
|
||||
new Utf8(),
|
||||
true,
|
||||
new Map([["ARROW:extension:name", "arrow.json"]]),
|
||||
),
|
||||
]);
|
||||
const table = await con.createEmptyTable("json", schema);
|
||||
const meta = JSON.stringify({ x: 1 });
|
||||
|
||||
await table.add([{ id: 1, meta }]);
|
||||
|
||||
const rows = await table.query().toArray();
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0].id).toBe(1);
|
||||
expect(rows[0].meta).toBe(meta);
|
||||
});
|
||||
|
||||
it("can create an empty table from schema that specifies field types by name", async () => {
|
||||
const schemaLike = {
|
||||
fields: [
|
||||
|
||||
@@ -406,10 +406,11 @@ function matchingFields(fields: Field[], tree: FieldTree): Field[] {
|
||||
field.name,
|
||||
new Struct(matchingFields(struct.children, value)),
|
||||
field.nullable,
|
||||
field.metadata,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
matches.push(new Field(field.name, value as DataType, field.nullable));
|
||||
matches.push(field);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-darwin-arm64",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"os": ["darwin"],
|
||||
"cpu": ["arm64"],
|
||||
"main": "lancedb.darwin-arm64.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-linux-arm64-gnu",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"],
|
||||
"main": "lancedb.linux-arm64-gnu.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-linux-arm64-musl",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"os": ["linux"],
|
||||
"cpu": ["arm64"],
|
||||
"main": "lancedb.linux-arm64-musl.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-linux-x64-gnu",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"],
|
||||
"main": "lancedb.linux-x64-gnu.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-linux-x64-musl",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"os": ["linux"],
|
||||
"cpu": ["x64"],
|
||||
"main": "lancedb.linux-x64-musl.node",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-win32-arm64-msvc",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb-win32-x64-msvc",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"os": ["win32"],
|
||||
"cpu": ["x64"],
|
||||
"main": "lancedb.win32-x64-msvc.node",
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@lancedb/lancedb",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@lancedb/lancedb",
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"cpu": [
|
||||
"x64",
|
||||
"arm64"
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"ann"
|
||||
],
|
||||
"private": false,
|
||||
"version": "0.38.0-beta.10",
|
||||
"version": "0.38.0-beta.11",
|
||||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lancedb-python"
|
||||
version = "0.38.0-beta.10"
|
||||
version = "0.38.0-beta.11"
|
||||
publish = false
|
||||
edition.workspace = true
|
||||
description = "Python bindings for LanceDB"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lancedb"
|
||||
version = "0.38.0-beta.10"
|
||||
version = "0.38.0-beta.11"
|
||||
edition.workspace = true
|
||||
description = "LanceDB: A serverless, low-latency vector database for AI applications"
|
||||
license.workspace = true
|
||||
|
||||
@@ -17,6 +17,7 @@ use arrow::array::{AsArray, FixedSizeListBuilder, Float32Builder};
|
||||
use arrow::datatypes::{Float32Type, UInt8Type};
|
||||
use arrow_array::Array;
|
||||
use arrow_schema::{DataType, Schema};
|
||||
use datafusion_common::{Column, DataFusionError, SchemaError};
|
||||
use datafusion_physical_plan::ExecutionPlan;
|
||||
use datafusion_physical_plan::projection::ProjectionExec;
|
||||
use datafusion_physical_plan::repartition::RepartitionExec;
|
||||
@@ -191,7 +192,7 @@ pub async fn create_plan(
|
||||
if query.query_vector.len() > 1 {
|
||||
if column.is_none() {
|
||||
// Infer a vector column with the same dimension of the query vector.
|
||||
let arrow_schema = Schema::from(ds_ref.schema());
|
||||
let arrow_schema = Schema::from(schema);
|
||||
column = Some(default_vector_column(
|
||||
&arrow_schema,
|
||||
Some(query.query_vector[0].len() as i32),
|
||||
@@ -268,7 +269,7 @@ pub async fn create_plan(
|
||||
let column = if let Some(col) = column {
|
||||
col
|
||||
} else {
|
||||
let arrow_schema = Schema::from(ds_ref.schema());
|
||||
let arrow_schema = Schema::from(schema);
|
||||
default_vector_column(&arrow_schema, Some(query_vector.len() as i32))?
|
||||
};
|
||||
|
||||
@@ -374,7 +375,97 @@ pub async fn create_plan(
|
||||
scanner.order_by(Some(order_by.clone()))?;
|
||||
}
|
||||
|
||||
Ok(scanner.create_plan().await?)
|
||||
scanner
|
||||
.create_plan()
|
||||
.await
|
||||
.map_err(|error| enrich_lance_field_not_found(error, schema))
|
||||
}
|
||||
|
||||
/// Replace DataFusion's top-level field candidates with qualified leaf paths.
|
||||
///
|
||||
/// DataFusion resolves nested fields but its `FieldNotFound` error only lists the
|
||||
/// top-level Arrow fields. This makes a missing leaf look unavailable even when it
|
||||
/// exists below a struct. Keep every other Lance/DataFusion error unchanged and
|
||||
/// enrich only this one schema error at the LanceDB query boundary.
|
||||
fn enrich_lance_field_not_found(
|
||||
error: lance::Error,
|
||||
schema: &lance_core::datatypes::Schema,
|
||||
) -> Error {
|
||||
let Some(field) = find_missing_field(&error) else {
|
||||
return error.into();
|
||||
};
|
||||
field_not_found_error(field, &Schema::from(schema))
|
||||
}
|
||||
|
||||
fn field_not_found_diagnostic(
|
||||
error: &(dyn std::error::Error + 'static),
|
||||
schema: &Schema,
|
||||
) -> Option<Error> {
|
||||
let field = find_missing_field(error)?;
|
||||
Some(field_not_found_error(field, schema))
|
||||
}
|
||||
|
||||
fn field_not_found_error(field: &Column, schema: &Schema) -> Error {
|
||||
let valid_fields = leaf_field_paths(schema);
|
||||
let mut message = format!("Schema error: No field named {}", field.quoted_flat_name());
|
||||
if !valid_fields.is_empty() {
|
||||
message.push_str(". Valid fields are ");
|
||||
message.push_str(&valid_fields.join(", "));
|
||||
}
|
||||
message.push('.');
|
||||
|
||||
Error::InvalidInput { message }
|
||||
}
|
||||
|
||||
fn find_missing_field<'a>(error: &'a (dyn std::error::Error + 'static)) -> Option<&'a Column> {
|
||||
if let Some(DataFusionError::SchemaError(schema_error, _)) =
|
||||
error.downcast_ref::<DataFusionError>()
|
||||
&& let SchemaError::FieldNotFound { field, .. } = schema_error.as_ref()
|
||||
{
|
||||
return Some(field);
|
||||
}
|
||||
|
||||
error.source().and_then(find_missing_field)
|
||||
}
|
||||
|
||||
fn leaf_field_paths(schema: &Schema) -> Vec<String> {
|
||||
fn format_segment(segment: &str) -> String {
|
||||
// Quote every segment instead of maintaining a SQL keyword list. Bare
|
||||
// lowercase names such as `true` can be parsed as expressions rather
|
||||
// than identifiers, while backticks preserve all field names in both
|
||||
// local SQL parsers.
|
||||
format!("`{}`", segment.replace('`', "``"))
|
||||
}
|
||||
|
||||
fn visit(fields: &arrow_schema::Fields, path: &mut Vec<String>, paths: &mut Vec<String>) {
|
||||
for field in fields {
|
||||
// Neither local planner can address an empty field-path segment,
|
||||
// even when it is backtick-quoted. Do not advertise leaves beneath
|
||||
// such a segment as valid filter fields.
|
||||
if field.name().is_empty() {
|
||||
continue;
|
||||
}
|
||||
path.push(field.name().clone());
|
||||
match field.data_type() {
|
||||
DataType::Struct(children) if !children.is_empty() => {
|
||||
visit(children, path, paths);
|
||||
}
|
||||
_ => {
|
||||
paths.push(
|
||||
path.iter()
|
||||
.map(|segment| format_segment(segment))
|
||||
.collect::<Vec<_>>()
|
||||
.join("."),
|
||||
);
|
||||
}
|
||||
}
|
||||
path.pop();
|
||||
}
|
||||
}
|
||||
|
||||
let mut paths = Vec::new();
|
||||
visit(schema.fields(), &mut Vec::new(), &mut paths);
|
||||
paths
|
||||
}
|
||||
|
||||
//Helper functions below
|
||||
@@ -734,7 +825,10 @@ async fn parse_arrow_ipc_response(bytes: bytes::Bytes) -> Result<DatasetRecordBa
|
||||
#[cfg(test)]
|
||||
#[allow(deprecated)]
|
||||
mod tests {
|
||||
use arrow_array::{ArrayRef, FixedSizeListArray, Float32Array};
|
||||
use arrow_array::{
|
||||
ArrayRef, FixedSizeListArray, Float32Array, Int32Array, RecordBatch, StringArray,
|
||||
StructArray,
|
||||
};
|
||||
use futures::TryStreamExt;
|
||||
use lance_arrow::FixedSizeListArrayExt;
|
||||
use std::sync::{
|
||||
@@ -743,7 +837,7 @@ mod tests {
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::query::{QueryExecutionOptions, QueryRequest};
|
||||
use crate::query::{ExecutableQuery, QueryBase, QueryExecutionOptions, QueryRequest};
|
||||
use crate::table::BaseTable;
|
||||
|
||||
fn fixed_size_list_array(values: Vec<f32>, dimension: i32) -> FixedSizeListArray {
|
||||
@@ -884,7 +978,6 @@ mod tests {
|
||||
async fn test_execute_query_local_routing() {
|
||||
use crate::connect;
|
||||
use crate::table::query::execute_query;
|
||||
use arrow_array::{Int32Array, RecordBatch};
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
|
||||
let conn = connect("memory://").execute().await.unwrap();
|
||||
@@ -924,6 +1017,164 @@ mod tests {
|
||||
assert_eq!(count, 2); // 4 and 5
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_missing_filter_field_lists_nested_fields_in_local_planners() {
|
||||
use crate::connect;
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
|
||||
let conn = connect("memory://").execute().await.unwrap();
|
||||
let metadata = Arc::new(StructArray::from(vec![
|
||||
(
|
||||
Arc::new(Field::new("year", DataType::Int32, false)),
|
||||
Arc::new(Int32Array::from(vec![2024])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
Arc::new(Field::new("genre", DataType::Utf8, false)),
|
||||
Arc::new(StringArray::from(vec!["fiction"])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
Arc::new(Field::new("Title", DataType::Int32, false)),
|
||||
Arc::new(Int32Array::from(vec![7])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
Arc::new(Field::new("true", DataType::Int32, false)),
|
||||
Arc::new(Int32Array::from(vec![8])) as ArrayRef,
|
||||
),
|
||||
(
|
||||
Arc::new(Field::new("", DataType::Int32, false)),
|
||||
Arc::new(Int32Array::from(vec![10])) as ArrayRef,
|
||||
),
|
||||
]));
|
||||
let vector = Arc::new(fixed_size_list_array(vec![0.0, 1.0], 2));
|
||||
let schema = Arc::new(Schema::new(vec![
|
||||
Field::new("id", DataType::Int32, false),
|
||||
Field::new("vector", vector.data_type().clone(), false),
|
||||
Field::new("content", DataType::Utf8, false),
|
||||
Field::new("metadata", metadata.data_type().clone(), false),
|
||||
]));
|
||||
let batch = RecordBatch::try_new(
|
||||
schema,
|
||||
vec![
|
||||
Arc::new(Int32Array::from(vec![1])),
|
||||
vector,
|
||||
Arc::new(StringArray::from(vec!["example"])),
|
||||
metadata,
|
||||
],
|
||||
)
|
||||
.unwrap();
|
||||
let table = conn
|
||||
.create_table("nested_error", batch)
|
||||
.execute()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let error = table
|
||||
.query()
|
||||
.only_if("year = 2024")
|
||||
.execute()
|
||||
.await
|
||||
.err()
|
||||
.expect("query should reject the unqualified nested field");
|
||||
let case_sensitive_path = "`metadata`.`Title`";
|
||||
let keyword_path = "`metadata`.`true`";
|
||||
let expected = format!(
|
||||
"No field named year. Valid fields are `id`, `vector`, `content`, `metadata`.`year`, `metadata`.`genre`, {case_sensitive_path}, {keyword_path}."
|
||||
);
|
||||
|
||||
assert!(
|
||||
error.to_string().contains(&expected),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
for (path, value) in [(case_sensitive_path, 7), (keyword_path, 8)] {
|
||||
table
|
||||
.query()
|
||||
.only_if(format!("{path} = {value}"))
|
||||
.execute()
|
||||
.await
|
||||
.expect("the path advertised by the diagnostic should be reusable");
|
||||
}
|
||||
|
||||
table.set_unenforced_primary_key(["id"]).await.unwrap();
|
||||
table
|
||||
.set_lsm_write_spec(crate::table::LsmWriteSpec::unsharded())
|
||||
.await
|
||||
.unwrap();
|
||||
let lsm_error = table
|
||||
.query()
|
||||
.only_if("year = 2024")
|
||||
.execute()
|
||||
.await
|
||||
.err()
|
||||
.expect("LSM query should reject the unqualified nested field");
|
||||
|
||||
assert!(
|
||||
lsm_error.to_string().contains(&expected),
|
||||
"unexpected LSM error: {lsm_error}"
|
||||
);
|
||||
for (path, value) in [(case_sensitive_path, 7), (keyword_path, 8)] {
|
||||
table
|
||||
.query()
|
||||
.only_if(format!("{path} = {value}"))
|
||||
.execute()
|
||||
.await
|
||||
.expect("the path advertised by the diagnostic should be reusable in LSM queries");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_leaf_field_paths_preserve_arbitrary_depth() {
|
||||
use arrow_schema::{DataType, Field, Schema};
|
||||
|
||||
fn nested_field(path: &[&str]) -> Field {
|
||||
let mut segments = path.iter().rev();
|
||||
let mut field = Field::new(
|
||||
*segments.next().expect("path must have a leaf"),
|
||||
DataType::Int32,
|
||||
false,
|
||||
);
|
||||
for segment in segments {
|
||||
field = Field::new(*segment, DataType::Struct(vec![field].into()), false);
|
||||
}
|
||||
field
|
||||
}
|
||||
|
||||
let schema = Schema::new(vec![
|
||||
nested_field(&["a", "b", "c", "d", "e"]),
|
||||
nested_field(&["metadata", "child.with.dot"]),
|
||||
nested_field(&["metadata", "Title"]),
|
||||
nested_field(&["metadata", "123child"]),
|
||||
nested_field(&["metadata", "child`tick"]),
|
||||
nested_field(&["metadata", ""]),
|
||||
nested_field(&["", "child"]),
|
||||
]);
|
||||
|
||||
assert_eq!(
|
||||
leaf_field_paths(&schema),
|
||||
vec![
|
||||
"`a`.`b`.`c`.`d`.`e`",
|
||||
"`metadata`.`child.with.dot`",
|
||||
"`metadata`.`Title`",
|
||||
"`metadata`.`123child`",
|
||||
"`metadata`.`child``tick`",
|
||||
]
|
||||
);
|
||||
|
||||
let source = DataFusionError::SchemaError(
|
||||
Box::new(SchemaError::FieldNotFound {
|
||||
field: Box::new(Column::from_name("missing")),
|
||||
valid_fields: Vec::new(),
|
||||
}),
|
||||
Box::new(None),
|
||||
);
|
||||
let error = field_not_found_diagnostic(&source, &schema).unwrap();
|
||||
assert!(
|
||||
error.to_string().contains(
|
||||
"Valid fields are `a`.`b`.`c`.`d`.`e`, `metadata`.`child.with.dot`, `metadata`.`Title`, `metadata`.`123child`, `metadata`.`child``tick`"
|
||||
),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct CountingNamespaceClient {
|
||||
query_table_calls: AtomicUsize,
|
||||
|
||||
@@ -27,6 +27,8 @@ use std::sync::Arc;
|
||||
|
||||
use arrow_array::Array;
|
||||
use arrow_schema::{DataType, Schema as ArrowSchema};
|
||||
use datafusion::common::{DataFusionError, ToDFSchema};
|
||||
use datafusion::prelude::SessionContext;
|
||||
use datafusion_physical_plan::expressions::Column;
|
||||
use datafusion_physical_plan::projection::ProjectionExec;
|
||||
use datafusion_physical_plan::{ExecutionPlan, PhysicalExpr};
|
||||
@@ -391,7 +393,21 @@ fn base_scanner(
|
||||
}
|
||||
if let Some(filter) = &query.base.filter {
|
||||
scanner = match filter {
|
||||
QueryFilter::Sql(sql) => scanner.filter(sql)?,
|
||||
QueryFilter::Sql(sql) => {
|
||||
// Parse here instead of inside `LsmScanner::filter` so the typed
|
||||
// DataFusion `FieldNotFound` error is still available for the
|
||||
// same nested-field enrichment used by the ordinary scanner.
|
||||
let schema = ArrowSchema::from(dataset.schema());
|
||||
let df_schema = schema.clone().to_dfschema().map_err(|error| {
|
||||
enrich_filter_error(error, &schema, "Failed to create DFSchema")
|
||||
})?;
|
||||
let expr = SessionContext::new()
|
||||
.parse_sql_expr(sql, &df_schema)
|
||||
.map_err(|error| {
|
||||
enrich_filter_error(error, &schema, "Failed to parse filter expression")
|
||||
})?;
|
||||
scanner.filter_expr(expr)
|
||||
}
|
||||
QueryFilter::Datafusion(expr) => scanner.filter_expr(expr.clone()),
|
||||
QueryFilter::Substrait(_) => {
|
||||
return Err(Error::NotSupported {
|
||||
@@ -403,6 +419,12 @@ fn base_scanner(
|
||||
Ok(scanner)
|
||||
}
|
||||
|
||||
fn enrich_filter_error(error: DataFusionError, schema: &ArrowSchema, context: &str) -> Error {
|
||||
super::field_not_found_diagnostic(&error, schema).unwrap_or_else(|| Error::InvalidInput {
|
||||
message: format!("{context}: {error}"),
|
||||
})
|
||||
}
|
||||
|
||||
/// Plain scan: filter / projection / limit over base ∪ SSTables ∪ in-memory.
|
||||
/// The plain scan applies limit and offset inside the planner.
|
||||
async fn plain_plan(
|
||||
|
||||
Reference in New Issue
Block a user