fix(editor): scope alias column completion by parent table

This commit is contained in:
zipg authored and GitHub committed 2026-07-30 00:07:45 +08:00
1 parent 0edba2f023
commit 682b9d53c5
3 files changed
+82 -5

No files matched your search

@@ -230,6 +230,27 @@ describe("semantic SQL completion candidates", () => {
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "status"]);
});
it("keeps a SELECT-list alias scoped when comma-separated sources follow the cursor", () => {
const columnsByTable = new Map<string, SqlCompletionColumn[]>([
["tb_kpi_set_score", ["id", "score_name"].map((name) => ({ name, table: "tb_kpi_set_score" }))],
["tb_kpi_set_score_detail", ["id", "fk_kpi_set_score_id", "detail_score"].map((name) => ({ name, table: "tb_kpi_set_score_detail" }))],
["tb_kpi_set_score_relationship", ["priority", "exclude_users_account"].map((name) => ({ name, table: "tb_kpi_set_score_relationship" }))],
]);
const { context, items } = semanticCompletion(
`SELECT
b.|
FROM tb_kpi_set_score a,
tb_kpi_set_score_detail b
WHERE a.id = b.fk_kpi_set_score_id`,
{ columnsByTable },
{ databaseType: "mysql", dialect: "mysql" },
);
expect(context.referencedTables).toEqual(expect.arrayContaining([expect.objectContaining({ name: "tb_kpi_set_score_detail", alias: "b" })]));
expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "fk_kpi_set_score_id", "detail_score"]);
});
it("completes correlation columns for generic PostgreSQL table functions", () => {
const { context, items } = semanticCompletion("SELECT * FROM generate_series(1, 3) g(value) WHERE g.|", {}, { databaseType: "postgres", dialect: "postgres" });
@@ -25,6 +25,17 @@ function postgresConnection(): ConnectionConfig {
} as ConnectionConfig;
}
function mysqlConnection(): ConnectionConfig {
return {
...postgresConnection(),
id: "mysql-1",
name: "MySQL",
db_type: "mysql",
port: 3306,
username: "root",
} as ConnectionConfig;
}
function oracleConnection(): ConnectionConfig {
return {
...postgresConnection(),
@@ -284,6 +295,39 @@ describe("connectionStore completion assistant", () => {
expect(store.lookupLocalCompletionColumns("oracle-1", "ORCL", "ORDERS")).toEqual([]);
});
it("rejects assistant columns returned for a different MySQL parent table", async () => {
const completionAssistantSearch = vi.fn().mockResolvedValue({
candidates: [
{ name: "status", kind: "column", schema: "app", parent_schema: "app", parent_name: "TB_KPI_SET_SCORE_DETAIL", data_type: "tinyint" },
{ name: "priority", kind: "column", schema: "app", parent_schema: "app", parent_name: "tb_kpi_set_score_relationship", data_type: "smallint" },
{ name: "archived_status", kind: "column", schema: "archive", parent_schema: "archive", parent_name: "tb_kpi_set_score_detail", data_type: "tinyint" },
{ name: "legacy_flag", kind: "column", schema: "app", data_type: "tinyint" },
],
incomplete: false,
fallback_used: false,
});
vi.doMock("@/lib/backend/tauriRuntime", () => ({ isTauriRuntime: () => false }));
vi.doMock("@/lib/backend/api", () => ({
checkConnectionHealth: vi.fn().mockResolvedValue(undefined),
completionAssistantSearch,
getColumns: vi.fn(),
}));
const { useConnectionStore } = await import("@/stores/connectionStore");
const store = useConnectionStore();
store.connections = [mysqlConnection()];
store.connectedIds.add("mysql-1");
const columns = await store.listCompletionColumns("mysql-1", "app", "tb_kpi_set_score_detail", "app");
expect(completionAssistantSearch).toHaveBeenCalledWith(expect.objectContaining({ parent_name: "tb_kpi_set_score_detail", parent_schema: "app" }));
expect(columns.map((column) => [column.name, column.table])).toEqual([
["status", "tb_kpi_set_score_detail"],
["legacy_flag", "tb_kpi_set_score_detail"],
]);
});
it("normalizes unquoted Oracle aliases while preserving quoted case before catalog lookup", async () => {
const completionAssistantSearch = vi.fn();
const getColumns = vi.fn().mockResolvedValue([]);
+17 -5
View File
@@ -5304,9 +5304,21 @@ export const useConnectionStore = defineStore("connection", () => {
return 0;
}
function completionAssistantColumns(candidates: CompletionAssistantCandidate[], table: string, schema?: string): SqlCompletionColumn[] {
function completionAssistantIdentifierMatches(candidate: string, requested: string, quoted?: boolean): boolean {
return quoted ? candidate === requested : candidate.toLowerCase() === requested.toLowerCase();
}
function completionAssistantColumns(candidates: CompletionAssistantCandidate[], table: string, schema?: string, context?: { tableQuoted?: boolean; schemaQuoted?: boolean }): SqlCompletionColumn[] {
const requestedSchema = schema?.trim();
return candidates
.filter((candidate) => candidate.kind === "column")
.filter((candidate) => {
if (candidate.kind !== "column") return false;
const parentName = candidate.parent_name?.trim();
if (parentName && !completionAssistantIdentifierMatches(parentName, table, context?.tableQuoted)) return false;
const parentSchema = candidate.parent_schema?.trim() || candidate.schema?.trim();
if (requestedSchema && parentSchema && !completionAssistantIdentifierMatches(parentSchema, requestedSchema, context?.schemaQuoted)) return false;
return true;
})
.map((candidate) => ({
name: candidate.name,
table: candidate.parent_name ?? table,
@@ -5371,7 +5383,7 @@ export const useConnectionStore = defineStore("connection", () => {
return objects;
}
async function listCompletionAssistantColumns(connectionId: string, database: string, table: string, schema?: string): Promise<SqlCompletionColumn[]> {
async function listCompletionAssistantColumns(connectionId: string, database: string, table: string, schema?: string, context?: { tableQuoted?: boolean; schemaQuoted?: boolean }): Promise<SqlCompletionColumn[]> {
const response = await completionAssistantSearch({
connection_id: connectionId,
database,
@@ -5383,7 +5395,7 @@ export const useConnectionStore = defineStore("connection", () => {
parent_name: table,
match_mode: "prefix",
});
const columns = completionAssistantColumns(response.candidates, table, schema);
const columns = completionAssistantColumns(response.candidates, table, schema, context);
if (columns.length > 0) indexCompletionColumns(connectionId, database, table, schema, columns);
return columns;
}
@@ -5924,7 +5936,7 @@ export const useConnectionStore = defineStore("connection", () => {
await ensureConnected(connectionId);
if (!usesOracleCurrentSchema && !catalog) {
try {
const assistantColumns = await listCompletionAssistantColumns(connectionId, database, completionTable, completionSchema);
const assistantColumns = await listCompletionAssistantColumns(connectionId, database, completionTable, completionSchema, context);
if (assistantColumns.length > 0) {
completionColumnsCache.value[cacheKey] = assistantColumns.map((column) => ({
name: column.name,