fix(editor): scope alias column completion by parent table
This commit is contained in:
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([]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user