mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-19 16:16:45 +00:00
fix(sql): treat HAVING as alias-visible context
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user