diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index d96a48217..1abead9d5 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -2241,9 +2241,20 @@ onMounted(async () => { return rt; }); - // Check if identifier has a qualifier (e.g., c.card_name) + // Check if identifier has a qualifier (e.g., c.card_name or schema.table) const qualifierMatch = /^(.+)\.(.+)$/.exec(identifier); const qualifier = qualifierMatch ? qualifierMatch[1] : null; + + // 2b. Qualified identifier (schema.table): check against SQL-parsed referenced tables + if (qualifierMatch) { + const qQualifier = qualifierMatch[1].toLowerCase(); + const qTableName = qualifierMatch[2].toLowerCase(); + const matchedRef = referencedTables.find((rt) => rt.name.toLowerCase() === qTableName && rt.schema?.toLowerCase() === qQualifier); + if (matchedRef) { + emit("clickTable", matchedRef.schema ? `${matchedRef.schema}.${matchedRef.name}` : matchedRef.name); + return; + } + } const colName = qualifierMatch ? qualifierMatch[2] : identifier; const colLower = colName.toLowerCase(); diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 0c1ae4758..bd26335cf 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -3604,6 +3604,13 @@ function treeItemMenuItems(): ContextMenuItem[] { icon: ListFilter, }); } + if (canConfigureVisibleSchemas.value) { + items.push({ + label: t("visibleSchemas.title"), + action: openVisibleSchemasDialog, + icon: ListFilter, + }); + } items.push({ label: t("contextMenu.editConnection"), action: editConnection, icon: Pencil }); if (revealConnectionFilePath.value) { items.push({ diff --git a/apps/desktop/src/lib/__tests__/sqlNavigation.spec.ts b/apps/desktop/src/lib/__tests__/sqlNavigation.spec.ts new file mode 100644 index 000000000..0af09dccf --- /dev/null +++ b/apps/desktop/src/lib/__tests__/sqlNavigation.spec.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { matchTable } from "@/lib/sqlNavigation"; + +describe("matchTable", () => { + it("matches schema-qualified table identifiers", () => { + const table = { schema: "MAAC00", name: "Accounts" }; + + expect(matchTable("maac00.accounts", [table])).toBe(table); + }); + + it("does not treat non-schema qualifiers as table matches", () => { + expect(matchTable("u.users", [{ schema: "public", name: "users" }])).toBeNull(); + }); +}); diff --git a/apps/desktop/src/lib/sqlNavigation.ts b/apps/desktop/src/lib/sqlNavigation.ts index 66dcb55dd..6c2dc9bd9 100644 --- a/apps/desktop/src/lib/sqlNavigation.ts +++ b/apps/desktop/src/lib/sqlNavigation.ts @@ -164,8 +164,22 @@ export function isSqlKeyword(identifier: string): boolean { return SQL_KEYWORDS_SET.has(identifier.toLowerCase()); } -/** Match identifier against known table names (case-insensitive). */ +/** Match identifier against known table names (case-insensitive). Supports qualified identifiers like schema.table. */ export function matchTable(identifier: string, tables: Array<{ name: string; schema?: string }>): { name: string; schema?: string } | null { const lower = identifier.toLowerCase(); - return tables.find((t) => t.name.toLowerCase() === lower) ?? null; + + // Direct match (simple table name) + const direct = tables.find((t) => t.name.toLowerCase() === lower); + if (direct) return direct; + + // Qualified match: schema.table + const dotIndex = identifier.indexOf("."); + if (dotIndex > 0) { + const qualifier = identifier.substring(0, dotIndex).toLowerCase(); + const name = identifier.substring(dotIndex + 1).toLowerCase(); + const qualified = tables.find((t) => t.name.toLowerCase() === name && t.schema?.toLowerCase() === qualifier); + if (qualified) return qualified; + } + + return null; }