diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts index 9e678dd72..dc84dc267 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts @@ -204,6 +204,19 @@ describe("semantic SQL completion candidates", () => { expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "name", "email"]); }); + it.each([ + ["table name", "SELECT orders.| FROM orders"], + ["schema-qualified table name", "SELECT public.orders.| FROM public.orders"], + ] as const)("completes PostgreSQL columns through a %s qualifier", (_label, markedSql) => { + const columnsByTable = new Map([["orders", ["id", "customer_name", "total_amount"].map((name) => ({ name, table: "orders", schema: "public" }))]]); + + const { context, items } = semanticCompletion(markedSql, { columnsByTable }, { databaseType: "postgres", dialect: "postgres" }); + + expect(context.contextKind).toBe("alias_column"); + expect(context.exclusiveColumnSuggestions).toBe(true); + expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id", "customer_name", "total_amount"]); + }); + it.each([ ["PostgreSQL", "postgres", "postgres"], ["SQL Server", "sqlserver", "sqlserver"], diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts index 4f96e42bc..8d025f24d 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/model.spec.ts @@ -80,6 +80,17 @@ describe("sqlSemanticModel baseline fixtures", () => { expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts: ["b"] })); }); + it.each([ + ["table name", "SELECT orders.| FROM orders", ["orders"]], + ["schema-qualified table name", "SELECT public.orders.| FROM public.orders", ["public", "orders"]], + ] as const)("resolves columns through a PostgreSQL %s qualifier before FROM", (_label, markedSql, qualifierParts) => { + const { sql, cursor } = sqlFixtureCursor(markedSql); + const model = buildSqlSemanticModel(sql, cursor, { databaseType: "postgres", dialect: "postgres" }); + + expect(model.rowSources).toEqual(expect.arrayContaining([expect.objectContaining({ name: "orders", alias: undefined })])); + expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "alias_column", qualifierParts, targetSourceId: model.rowSources[0]?.id })); + }); + it("keeps nested EXISTS sources and outer correlated sources visible", () => { const { sql, cursor } = sqlFixtureCursor("SELECT * FROM aa.tb t WHERE EXISTS (SELECT 1 FROM aa.tb1 t1, aa.tb2 t2 WHERE t1.|)"); const model = buildSqlSemanticModel(sql, cursor, { databaseType: "mysql", dialect: "mysql" }); diff --git a/apps/desktop/src/lib/sql/semantic/model.ts b/apps/desktop/src/lib/sql/semantic/model.ts index 49aea166e..6f7eca779 100644 --- a/apps/desktop/src/lib/sql/semantic/model.ts +++ b/apps/desktop/src/lib/sql/semantic/model.ts @@ -713,7 +713,7 @@ function buildCursorIntent(tokens: readonly SqlSemanticToken[], cursor: number, TABLE_INTRODUCERS.has(previous) || TABLE_INTRODUCERS.has(wordBeforeReplacement) || TABLE_INTRODUCERS.has(wordBeforeTrailing) || - (!!targetSource && !targetSource.alias && trailing.replacementRange.start <= targetSource.sourceSpan.end + 1 && TABLE_INTRODUCERS.has(wordBeforePosition(tokens, targetSource.sourceSpan.start)))) + (!!targetSource && !targetSource.alias && trailing.replacementRange.start >= targetSource.sourceSpan.start && trailing.replacementRange.start <= targetSource.sourceSpan.end + 1 && TABLE_INTRODUCERS.has(wordBeforePosition(tokens, targetSource.sourceSpan.start)))) ) { const role = dialect.qualifierRole(trailing.qualifierParts, "table"); return { kind: role === "catalog" ? "catalog" : "table", prefix: trailing.prefix, replacementRange: trailing.replacementRange, qualifierParts: trailing.qualifierParts, expectedObjectKinds: ["table", "view"], confidence: "medium" };