fix(sql): treat HAVING as alias-visible context

This commit is contained in:
dienaso
2026-09-13 01:40:05 +08:00
committed by GitHub
parent 8ff56b4837
commit 4fe04c8bb9
3 changed files with 71 additions and 2 deletions
@@ -58,6 +58,34 @@ describe("sqlSemanticReferences shared consumers", () => {
expect(diagnostics).toEqual([]);
});
it("does not flag a SELECT alias used by MySQL HAVING", () => {
const sql = `SELECT COUNT(*) AS cnt, g FROM amounts GROUP BY g HAVING cnt > 1`;
const aliasStart = sql.indexOf("cnt", sql.indexOf("HAVING"));
const analysis: SqlReferenceAnalysis = {
tables: [
{
name: "amounts",
span: span(sql.indexOf("amounts") + 1, sql.indexOf("amounts") + "amounts".length),
},
],
columns: [
{
name: "cnt",
span: span(aliasStart + 1, aliasStart + "cnt".length),
},
],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [],
columnsByTable: new Map([["amounts", [{ name: "price" }, { name: "g" }]]]),
sql,
databaseType: "mysql",
});
expect(diagnostics).toEqual([]);
});
it("resolves navigation targets from subquery aliases and projected columns", () => {
const { sql, cursor } = sqlFixtureCursor("SELECT sq.user_| FROM (SELECT id, name AS user_name FROM users) sq");
const model = buildSqlSemanticModel(sql, cursor);
@@ -1344,3 +1344,41 @@ describe("originForSqlCompletionProvider", () => {
expect(originForSqlCompletionProvider("explicit", false)).toBe("explicit");
});
});
describe("select alias visibility", () => {
it("prioritizes SELECT aliases inside HAVING for MySQL", () => {
const sql = "SELECT COUNT(*) AS cnt, g FROM orders GROUP BY g HAVING cnt > ";
const context = getSqlCompletionContext(sql, sql.length, { databaseType: "mysql", dialect: "mysql" });
expect(context.prioritizeSelectAliases).toBe(true);
expect(context.selectAliases).toContain("cnt");
});
it("prioritizes SELECT aliases inside ORDER BY even with later clauses", () => {
const sql = "SELECT SUM(price) AS total FROM orders ORDER BY total LIMIT 10";
const cursor = sql.indexOf("total", sql.indexOf("ORDER BY")) + "total".length;
const context = getSqlCompletionContext(sql, cursor, { databaseType: "mysql", dialect: "mysql" });
expect(context.prioritizeSelectAliases).toBe(true);
expect(context.selectAliases).toContain("total");
});
it("does not prioritize SELECT aliases inside WHERE", () => {
const sql = "SELECT id AS uid FROM orders WHERE uid > ";
const context = getSqlCompletionContext(sql, sql.length, { databaseType: "mysql", dialect: "mysql" });
expect(context.prioritizeSelectAliases).toBe(false);
expect(context.selectAliases).toEqual([]);
});
it("offers HAVING alias completion items", () => {
const sql = "SELECT COUNT(*) AS cnt, g FROM orders GROUP BY g HAVING cn";
const items = buildSqlCompletionItems(sql, sql.length, {
dialect: "mysql",
tables: [{ name: "orders", type: "table" }],
columnsByTable: new Map([["orders", [{ name: "g" } as never]]]),
});
expect(items.some((item) => item.label === "cnt")).toBe(true);
});
});
+5 -2
View File
@@ -2652,11 +2652,14 @@ function isInOrderOrGroupByContext(beforeCursor: string): boolean {
.toLowerCase();
const lastOrderBy = cleaned.lastIndexOf("order by");
const lastGroupBy = cleaned.lastIndexOf("group by");
const lastContext = Math.max(lastOrderBy, lastGroupBy);
// MySQL (and DuckDB-like engines) also resolve SELECT aliases inside HAVING,
// so aliases stay visible there just like in ORDER BY/GROUP BY.
const lastHaving = cleaned.lastIndexOf("having");
const lastContext = Math.max(lastOrderBy, lastGroupBy, lastHaving);
if (lastContext < 0) return false;
const segment = cleaned.slice(lastContext);
return !/\b(?:where|having|limit|offset|union|intersect|except|join|from)\b/.test(segment);
return !/\b(?:where|limit|offset|union|intersect|except|join|from)\b/.test(segment);
}
function isInGroupByContext(beforeCursor: string): boolean {