mirror of
https://github.com/lancedb/lancedb.git
synced 2026-10-12 14:52:26 +00:00
Updates the Rust workspace Lance dependencies and Java lance-core from 14.0.0-beta.1 to [v14.0.0-beta.6](https://github.com/lance-format/lance/releases/tag/v14.0.0-beta.6), and refreshes Cargo.lock. No compatibility fixes were required. Validation passed: `cargo clippy --quiet --workspace --tests --all-features -- -D warnings`, `cargo fmt --all --quiet`, and `git diff --check`. --------- Co-authored-by: Daniel Rammer <hamersaw@protonmail.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
530 lines
16 KiB
TypeScript
530 lines
16 KiB
TypeScript
// SPDX-License-Identifier: Apache-2.0
|
|
// SPDX-FileCopyrightText: Copyright The LanceDB Authors
|
|
|
|
import * as tmp from "tmp";
|
|
|
|
import { type Table, connect } from "../lancedb";
|
|
import {
|
|
Field,
|
|
FixedSizeList,
|
|
Float32,
|
|
Int64,
|
|
Schema,
|
|
Utf8,
|
|
makeArrowTable,
|
|
} from "../lancedb/arrow";
|
|
import { Index } from "../lancedb/indices";
|
|
|
|
describe("Query outputSchema", () => {
|
|
let tmpDir: tmp.DirResult;
|
|
let table: Table;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
|
const db = await connect(tmpDir.name);
|
|
|
|
// Create table with explicit schema to ensure proper types
|
|
const schema = new Schema([
|
|
new Field("a", new Int64(), true),
|
|
new Field("text", new Utf8(), true),
|
|
new Field(
|
|
"vec",
|
|
new FixedSizeList(2, new Field("item", new Float32())),
|
|
true,
|
|
),
|
|
]);
|
|
|
|
const data = makeArrowTable(
|
|
[
|
|
{ a: 1n, text: "foo", vec: [1, 2] },
|
|
{ a: 2n, text: "bar", vec: [3, 4] },
|
|
{ a: 3n, text: "baz", vec: [5, 6] },
|
|
],
|
|
{ schema },
|
|
);
|
|
table = await db.createTable("test", data);
|
|
});
|
|
|
|
afterEach(() => {
|
|
tmpDir.removeCallback();
|
|
});
|
|
|
|
it("should return schema for plain query", async () => {
|
|
const schema = await table.query().outputSchema();
|
|
|
|
expect(schema.fields.length).toBe(3);
|
|
expect(schema.fields.map((f) => f.name)).toEqual(["a", "text", "vec"]);
|
|
expect(schema.fields[0].type.toString()).toBe("Int64");
|
|
expect(schema.fields[1].type.toString()).toBe("Utf8");
|
|
});
|
|
|
|
it("should return schema with dynamic projection", async () => {
|
|
const schema = await table.query().select({ bl: "a * 2" }).outputSchema();
|
|
|
|
expect(schema.fields.length).toBe(1);
|
|
expect(schema.fields[0].name).toBe("bl");
|
|
expect(schema.fields[0].type.toString()).toBe("Int64");
|
|
});
|
|
|
|
it("should return schema for vector search with _distance column", async () => {
|
|
const schema = await table
|
|
.vectorSearch([1, 2])
|
|
.select(["a"])
|
|
.outputSchema();
|
|
|
|
expect(schema.fields.length).toBe(2);
|
|
expect(schema.fields.map((f) => f.name)).toEqual(["a", "_distance"]);
|
|
expect(schema.fields[0].type.toString()).toBe("Int64");
|
|
expect(schema.fields[1].type.toString()).toBe("Float32");
|
|
});
|
|
|
|
it("should return schema for FTS search", async () => {
|
|
await table.createIndex("text", { config: Index.fts() });
|
|
|
|
const schema = await table
|
|
.search("foo", "fts")
|
|
.select(["a"])
|
|
.outputSchema();
|
|
|
|
// FTS search includes _score column in addition to selected columns
|
|
expect(schema.fields.length).toBe(2);
|
|
expect(schema.fields.map((f) => f.name)).toContain("a");
|
|
expect(schema.fields.map((f) => f.name)).toContain("_score");
|
|
const aField = schema.fields.find((f) => f.name === "a");
|
|
expect(aField?.type.toString()).toBe("Int64");
|
|
});
|
|
|
|
it("should return schema for take query", async () => {
|
|
const schema = await table.takeOffsets([0]).select(["text"]).outputSchema();
|
|
|
|
expect(schema.fields.length).toBe(1);
|
|
expect(schema.fields[0].name).toBe("text");
|
|
expect(schema.fields[0].type.toString()).toBe("Utf8");
|
|
});
|
|
|
|
it("should return full schema when no select is specified", async () => {
|
|
const schema = await table.query().outputSchema();
|
|
|
|
// Should return all columns
|
|
expect(schema.fields.length).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe("Query empty results", () => {
|
|
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(),
|
|
true,
|
|
new Map([["description", "searchable text"]]),
|
|
),
|
|
new Field(
|
|
"vector",
|
|
new FixedSizeList(2, new Field("item", new Float32())),
|
|
true,
|
|
),
|
|
],
|
|
new Map([["source", "empty-query-regression"]]),
|
|
);
|
|
table = await db.createTable(
|
|
"test",
|
|
makeArrowTable([{ id: 1n, text: "hello", vector: [1, 2] }], { schema }),
|
|
);
|
|
});
|
|
|
|
afterEach(() => {
|
|
tmpDir.removeCallback();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "filtered scan",
|
|
query: (table: Table) => table.query().where("id < 0"),
|
|
fields: ["id", "text", "vector"],
|
|
},
|
|
{
|
|
name: "zero limit",
|
|
query: (table: Table) => table.query().limit(0),
|
|
fields: ["id", "text", "vector"],
|
|
},
|
|
{
|
|
name: "column projection",
|
|
query: (table: Table) =>
|
|
table.query().where("id < 0").select(["text", "id"]),
|
|
fields: ["text", "id"],
|
|
},
|
|
{
|
|
name: "dynamic projection",
|
|
query: (table: Table) =>
|
|
table.query().where("id < 0").select({ doubled: "id * 2" }),
|
|
fields: ["doubled"],
|
|
},
|
|
{
|
|
name: "filtered vector search",
|
|
query: (table: Table) =>
|
|
table.search([0.5, 0.5]).where("id < 0").limit(3),
|
|
fields: ["id", "text", "vector", "_distance"],
|
|
},
|
|
{
|
|
name: "vector projection with row id",
|
|
query: (table: Table) =>
|
|
table
|
|
.vectorSearch([0.5, 0.5])
|
|
.where("id < 0")
|
|
.select(["id"])
|
|
.withRowId(),
|
|
fields: ["id", "_distance", "_rowid"],
|
|
},
|
|
{
|
|
name: "fast search without an index",
|
|
query: (table: Table) => table.search([0.5, 0.5]).fastSearch(),
|
|
fields: ["id", "text", "vector", "_distance"],
|
|
},
|
|
{
|
|
name: "take with no offsets",
|
|
query: (table: Table) => table.takeOffsets([]).select(["text"]),
|
|
fields: ["text"],
|
|
},
|
|
])("preserves the output schema for $name", async ({ query, fields }) => {
|
|
const result = await query(table).toArrow({ maxBatchLength: 1 });
|
|
|
|
expect(result.numRows).toBe(0);
|
|
expect(result.schema.fields.map((field) => field.name)).toEqual(fields);
|
|
for (const name of fields) {
|
|
expect(result.getChild(name)).not.toBeNull();
|
|
expect(result.getChild(name)?.length).toBe(0);
|
|
}
|
|
});
|
|
|
|
it("preserves types, nullability, and metadata", async () => {
|
|
const result = await table.query().where("id < 0").toArrow();
|
|
const schema = await table.schema();
|
|
|
|
expect(result.schema).toEqual(schema);
|
|
expect(result.getChild("id")?.type.toString()).toBe("Int64");
|
|
expect(result.schema.fields[0].nullable).toBe(false);
|
|
expect(result.schema.fields[1].metadata.get("description")).toBe(
|
|
"searchable text",
|
|
);
|
|
expect(result.schema.metadata.get("source")).toBe("empty-query-regression");
|
|
});
|
|
|
|
it("preserves the score column for full-text search with no matches", async () => {
|
|
await table.createIndex("text", { config: Index.fts() });
|
|
const result = await table
|
|
.search("missing", "fts")
|
|
.select(["id"])
|
|
.toArrow();
|
|
|
|
expect(result.numRows).toBe(0);
|
|
expect(result.schema.fields.map((field) => field.name)).toEqual([
|
|
"id",
|
|
"_score",
|
|
]);
|
|
expect(result.getChild("_score")?.type.toString()).toBe("Float32");
|
|
});
|
|
});
|
|
|
|
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;
|
|
|
|
beforeEach(async () => {
|
|
tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
|
const db = await connect(tmpDir.name);
|
|
|
|
// Create table with numeric data for sorting
|
|
const schema = new Schema([
|
|
new Field("id", new Int64(), true),
|
|
new Field("score", new Float32(), true),
|
|
new Field("name", new Utf8(), true),
|
|
]);
|
|
|
|
const data = makeArrowTable(
|
|
[
|
|
{ id: 1n, score: 3.5, name: "charlie" },
|
|
{ id: 2n, score: 1.2, name: "alice" },
|
|
{ id: 3n, score: 2.8, name: "bob" },
|
|
{ id: 4n, score: 0.5, name: "david" },
|
|
{ id: 5n, score: 4.1, name: "eve" },
|
|
],
|
|
{ schema },
|
|
);
|
|
table = await db.createTable("test", data);
|
|
});
|
|
|
|
afterEach(() => {
|
|
tmpDir.removeCallback();
|
|
});
|
|
|
|
it("should sort by single column ascending", async () => {
|
|
const results = await table
|
|
.query()
|
|
.orderBy({ columnName: "score", ascending: true, nullsFirst: false })
|
|
.toArray();
|
|
|
|
expect(results.length).toBe(5);
|
|
// Verify ascending order
|
|
expect(results[0].score).toBeCloseTo(0.5, 0.001);
|
|
expect(results[1].score).toBeCloseTo(1.2, 0.001);
|
|
expect(results[2].score).toBeCloseTo(2.8, 0.001);
|
|
expect(results[3].score).toBeCloseTo(3.5, 0.001);
|
|
expect(results[4].score).toBeCloseTo(4.1, 0.001);
|
|
});
|
|
|
|
it("should sort by single column descending", async () => {
|
|
const results = await table
|
|
.query()
|
|
.orderBy({ columnName: "score", ascending: false, nullsFirst: false })
|
|
.toArray();
|
|
|
|
expect(results.length).toBe(5);
|
|
// Verify descending order
|
|
expect(results[0].score).toBeCloseTo(4.1, 0.001);
|
|
expect(results[1].score).toBeCloseTo(3.5, 0.001);
|
|
expect(results[2].score).toBeCloseTo(2.8, 0.001);
|
|
expect(results[3].score).toBeCloseTo(1.2, 0.001);
|
|
expect(results[4].score).toBeCloseTo(0.5, 0.001);
|
|
});
|
|
|
|
it("should use ascending as default direction", async () => {
|
|
const results = await table
|
|
.query()
|
|
.orderBy({ columnName: "score" })
|
|
.toArray();
|
|
|
|
expect(results.length).toBe(5);
|
|
// Verify ascending order (default)
|
|
expect(results[0].score).toBeCloseTo(0.5, 0.001);
|
|
expect(results[1].score).toBeCloseTo(1.2, 0.001);
|
|
expect(results[2].score).toBeCloseTo(2.8, 0.001);
|
|
expect(results[3].score).toBeCloseTo(3.5, 0.001);
|
|
expect(results[4].score).toBeCloseTo(4.1, 0.001);
|
|
});
|
|
|
|
it("should sort by string column", async () => {
|
|
const results = await table
|
|
.query()
|
|
.orderBy({ columnName: "name" })
|
|
.toArray();
|
|
|
|
expect(results.length).toBe(5);
|
|
// Verify alphabetical order
|
|
expect(results[0].name).toBe("alice");
|
|
expect(results[1].name).toBe("bob");
|
|
expect(results[2].name).toBe("charlie");
|
|
expect(results[3].name).toBe("david");
|
|
expect(results[4].name).toBe("eve");
|
|
});
|
|
|
|
it("should support method chaining with where", async () => {
|
|
const results = await table
|
|
.query()
|
|
.where("score > 2.0")
|
|
.orderBy({ columnName: "score" })
|
|
.toArray();
|
|
expect(results.length).toBe(3);
|
|
// Verify filtered and sorted
|
|
expect(results[0].score).toBeCloseTo(2.8, 0.001);
|
|
expect(results[1].score).toBeCloseTo(3.5, 0.001);
|
|
expect(results[2].score).toBeCloseTo(4.1, 0.001);
|
|
});
|
|
|
|
it("should combine repeated where clauses with AND", async () => {
|
|
const results = await table
|
|
.query()
|
|
.where("score > 1.0")
|
|
.where("score < 3.0")
|
|
.orderBy({ columnName: "score" })
|
|
.toArray();
|
|
// Only rows matching both predicates should be returned, rather than the
|
|
// second where() silently replacing the first.
|
|
expect(results.length).toBe(2);
|
|
expect(results[0].score).toBeCloseTo(1.2, 0.001);
|
|
expect(results[1].score).toBeCloseTo(2.8, 0.001);
|
|
});
|
|
|
|
it("should support method chaining with limit", async () => {
|
|
const results = await table
|
|
.query()
|
|
.orderBy({ columnName: "score", ascending: false })
|
|
.limit(3)
|
|
.toArray();
|
|
|
|
expect(results.length).toBe(3);
|
|
// Verify top 3 in descending order
|
|
expect(results[0].score).toBeCloseTo(4.1, 0.001);
|
|
expect(results[1].score).toBeCloseTo(3.5, 0.001);
|
|
expect(results[2].score).toBeCloseTo(2.8, 0.001);
|
|
});
|
|
|
|
it("should support method chaining with offset", async () => {
|
|
const results = await table
|
|
.query()
|
|
.orderBy({ columnName: "score" })
|
|
.offset(2)
|
|
.limit(2)
|
|
.toArray();
|
|
|
|
expect(results.length).toBe(2);
|
|
// Verify results skip first 2 and take next 2
|
|
expect(results[0].score).toBeCloseTo(2.8, 0.001);
|
|
expect(results[1].score).toBeCloseTo(3.5, 0.001);
|
|
});
|
|
|
|
it("should support method chaining with select", async () => {
|
|
const results = await table
|
|
.query()
|
|
.orderBy({ columnName: "name" })
|
|
.select(["name", "score"])
|
|
.toArray();
|
|
|
|
expect(results.length).toBe(5);
|
|
// Verify only selected columns are present
|
|
expect(Object.keys(results[0])).toEqual(["name", "score"]);
|
|
expect(Object.keys(results[4])).toEqual(["name", "score"]);
|
|
// Verify sorted by name
|
|
expect(results[0].name).toBe("alice");
|
|
expect(results[4].name).toBe("eve");
|
|
});
|
|
|
|
it("should support complex method chaining", async () => {
|
|
const results = await table
|
|
.query()
|
|
.where("score > 1.0")
|
|
.orderBy({ columnName: "score", ascending: false })
|
|
.limit(3)
|
|
.select(["id", "score", "name"])
|
|
.toArray();
|
|
|
|
expect(results.length).toBe(3);
|
|
// Verify filtered, sorted, limited, and projected
|
|
expect(results[0].score).toBeCloseTo(4.1, 0.001);
|
|
expect(results[1].score).toBeCloseTo(3.5, 0.001);
|
|
expect(results[2].score).toBeCloseTo(2.8, 0.001);
|
|
expect(Object.keys(results[0])).toEqual(["id", "score", "name"]);
|
|
});
|
|
|
|
it("should support multi-column ordering and null placement", async () => {
|
|
const schema = new Schema([
|
|
new Field("group", new Int64(), true),
|
|
new Field("score", new Float32(), true),
|
|
new Field("name", new Utf8(), true),
|
|
]);
|
|
|
|
const data = makeArrowTable(
|
|
[
|
|
{ group: 1n, score: null, name: "z" },
|
|
{ group: 1n, score: 1.0, name: "b" },
|
|
{ group: 1n, score: 1.0, name: "a" },
|
|
{ group: 2n, score: 0.5, name: "c" },
|
|
],
|
|
{ schema },
|
|
);
|
|
const nullTable = await (await connect(tmpDir.name)).createTable(
|
|
"test_multi_order",
|
|
data,
|
|
{ mode: "overwrite" },
|
|
);
|
|
|
|
const results = await nullTable
|
|
.query()
|
|
.orderBy([
|
|
{ columnName: "group", ascending: true, nullsFirst: false },
|
|
{ columnName: "score", ascending: true, nullsFirst: true },
|
|
{ columnName: "name", ascending: true, nullsFirst: false },
|
|
])
|
|
.toArray();
|
|
|
|
expect(results.map((r) => [r.group, r.score, r.name])).toEqual([
|
|
[1n, null, "z"],
|
|
[1n, 1.0, "a"],
|
|
[1n, 1.0, "b"],
|
|
[2n, 0.5, "c"],
|
|
]);
|
|
});
|
|
});
|