180 lines
7.6 KiB
TypeScript
Vendored
180 lines
7.6 KiB
TypeScript
Vendored
import { describe, expect, it } from "vitest";
|
|
import { containsHan, matchesPinyinInitials, orderedSubsequenceSpan, pinyinAwareMatchScore, pinyinFirstLetters } from "@/lib/common/pinyin";
|
|
import { completionMatchRanges } from "@/lib/common/completionMatch";
|
|
import { buildSqlCompletionItems } from "@/lib/sql/sqlCompletion";
|
|
import { identifierMatchScore, matchesIdentifierSearch } from "@/lib/sql/identifierSearch";
|
|
|
|
describe("identifier search", () => {
|
|
it("matches camel-case initials and ranks them above loose fuzzy matches", () => {
|
|
expect(matchesIdentifierSearch("userProfile", "up")).toBe(true);
|
|
expect(identifierMatchScore("userProfile", "up")).toBeGreaterThan(identifierMatchScore("userPreference", "up"));
|
|
});
|
|
|
|
it("matches an identifier from any position", () => {
|
|
expect(matchesIdentifierSearch("order_id", "id")).toBe(true);
|
|
expect(matchesIdentifierSearch("created_at", "id")).toBe(false);
|
|
expect(matchesIdentifierSearch("customer_order_total", "cot")).toBe(true);
|
|
});
|
|
|
|
it("keeps unrelated identifiers out of the result", () => {
|
|
expect(matchesIdentifierSearch("userProfile", "xyz")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("pinyinAwareMatchScore", () => {
|
|
it("ranks prefix above pinyin prefix above substring above pinyin subsequence", () => {
|
|
const prefix = pinyinAwareMatchScore("总租金", "总");
|
|
const pinyinPrefix = pinyinAwareMatchScore("总租金", "zz");
|
|
const substring = pinyinAwareMatchScore("总租金", "金");
|
|
const subsequence = pinyinAwareMatchScore("总租金", "zj");
|
|
expect(prefix).toBeGreaterThan(pinyinPrefix);
|
|
expect(pinyinPrefix).toBeGreaterThan(substring);
|
|
expect(substring).toBeGreaterThan(subsequence);
|
|
expect(subsequence).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("returns -1 when nothing matches", () => {
|
|
expect(pinyinAwareMatchScore("总租金", "jz")).toBe(-1);
|
|
expect(pinyinAwareMatchScore("amount", "zz")).toBe(-1);
|
|
expect(pinyinAwareMatchScore("amount", "")).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("ordered subsequence span", () => {
|
|
it("matches initials subsequences", () => {
|
|
expect(orderedSubsequenceSpan("zzj", "zj")).toEqual({ first: 0, span: 3 });
|
|
expect(orderedSubsequenceSpan("zzj", "j")).toEqual({ first: 2, span: 1 });
|
|
expect(orderedSubsequenceSpan("zzj", "jz")).toBeNull();
|
|
expect(orderedSubsequenceSpan("zzj", "")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("completion match highlight ranges", () => {
|
|
it("highlights a Han substring match", () => {
|
|
expect(completionMatchRanges("总租金", "金")).toEqual([2, 3]);
|
|
});
|
|
|
|
it("highlights every Han character matched by pinyin initials", () => {
|
|
expect(completionMatchRanges("总租金", "zzj")).toEqual([0, 1, 1, 2, 2, 3]);
|
|
expect(completionMatchRanges("总租金", "zz")).toEqual([0, 1, 1, 2]);
|
|
});
|
|
|
|
it("highlights only the Han characters hit by an initials subsequence", () => {
|
|
expect(completionMatchRanges("总租金", "zj")).toEqual([0, 1, 2, 3]);
|
|
expect(completionMatchRanges("总租金", "j")).toEqual([2, 3]);
|
|
});
|
|
|
|
it("highlights ASCII substrings and falls back to in-order fuzzy", () => {
|
|
expect(completionMatchRanges("amount", "am")).toEqual([0, 2]);
|
|
expect(completionMatchRanges("order_detail", "od")).toEqual([0, 1, 2, 3]);
|
|
expect(completionMatchRanges("amount", "zz")).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("pinyin first letters", () => {
|
|
it("maps Han characters to their pinyin initials", () => {
|
|
expect(pinyinFirstLetters("总租金")).toBe("zzj");
|
|
expect(pinyinFirstLetters("租赁日期")).toBe("zlrq");
|
|
});
|
|
|
|
it("keeps ASCII alphanumerics and drops other characters", () => {
|
|
expect(pinyinFirstLetters("order明细")).toBe("ordermx");
|
|
expect(pinyinFirstLetters("Amount")).toBe("amount");
|
|
});
|
|
|
|
it("matches only ASCII queries against Han candidates", () => {
|
|
expect(matchesPinyinInitials("总租金", "zzj")).toBe(true);
|
|
expect(matchesPinyinInitials("总租金", "zz")).toBe(true);
|
|
expect(matchesPinyinInitials("总租金", "zj")).toBe(false);
|
|
expect(matchesPinyinInitials("总租金", "租金")).toBe(false);
|
|
expect(matchesPinyinInitials("amount", "am")).toBe(false);
|
|
expect(containsHan("amount")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("sqlCompletion Chinese column matching", () => {
|
|
function completionItems(typedPrefix: string) {
|
|
const sql = `SELECT * FROM orders WHERE ${typedPrefix}`;
|
|
return buildSqlCompletionItems(sql, sql.length, {
|
|
databaseType: "mysql",
|
|
tables: [{ name: "orders", type: "table" }],
|
|
columnsByTable: new Map([
|
|
[
|
|
"orders",
|
|
[
|
|
{ name: "总租金", table: "orders" },
|
|
{ name: "租赁日期", table: "orders" },
|
|
{ name: "amount", table: "orders" },
|
|
],
|
|
],
|
|
]),
|
|
});
|
|
}
|
|
|
|
it("matches a single Han character anywhere in the column name", () => {
|
|
const items = completionItems("金");
|
|
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "总租金", type: "column" })]));
|
|
});
|
|
|
|
it("matches pinyin initials like DataGrip", () => {
|
|
const items = completionItems("zzj");
|
|
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "总租金", type: "column" })]));
|
|
expect(items.some((item) => item.label === "租赁日期")).toBe(false);
|
|
expect(items.some((item) => item.label === "amount")).toBe(false);
|
|
});
|
|
|
|
it("matches a single ASCII letter via pinyin initials", () => {
|
|
const items = completionItems("z");
|
|
const labels = items.filter((item) => item.type === "column").map((item) => item.label);
|
|
expect(labels).toEqual(expect.arrayContaining(["总租金", "租赁日期"]));
|
|
});
|
|
|
|
it("matches initials subsequences like DataGrip", () => {
|
|
const subsequenceItems = completionItems("zj");
|
|
expect(subsequenceItems).toEqual(expect.arrayContaining([expect.objectContaining({ label: "总租金", type: "column" })]));
|
|
expect(subsequenceItems.some((item) => item.label === "租赁日期")).toBe(false);
|
|
|
|
const lastInitialItems = completionItems("j");
|
|
expect(lastInitialItems).toEqual(expect.arrayContaining([expect.objectContaining({ label: "总租金", type: "column" })]));
|
|
expect(lastInitialItems.some((item) => item.label === "租赁日期")).toBe(false);
|
|
});
|
|
|
|
it("keeps plain English matching unchanged", () => {
|
|
const items = completionItems("am");
|
|
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "amount", type: "column" })]));
|
|
expect(items.some((item) => item.label === "总租金")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("sqlCompletion identifier column matching", () => {
|
|
function completionItems(typedPrefix: string) {
|
|
const sql = `SELECT * FROM orders WHERE ${typedPrefix}`;
|
|
return buildSqlCompletionItems(sql, sql.length, {
|
|
databaseType: "mysql",
|
|
tables: [{ name: "orders", type: "table" }],
|
|
columnsByTable: new Map([
|
|
[
|
|
"orders",
|
|
[
|
|
{ name: "userProfile", table: "orders" },
|
|
{ name: "order_id", table: "orders" },
|
|
{ name: "created_at", table: "orders" },
|
|
],
|
|
],
|
|
]),
|
|
});
|
|
}
|
|
|
|
it("matches camel-case initials in WHERE column completion", () => {
|
|
const items = completionItems("up");
|
|
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "userProfile", type: "column" })]));
|
|
expect(items.some((item) => item.label === "order_id")).toBe(false);
|
|
});
|
|
|
|
it("matches WHERE columns from any position", () => {
|
|
const items = completionItems("id");
|
|
expect(items).toEqual(expect.arrayContaining([expect.objectContaining({ label: "order_id", type: "column" })]));
|
|
expect(items.some((item) => item.label === "created_at")).toBe(false);
|
|
});
|
|
});
|