fix(sql): resolve GROUP BY/ORDER BY aliases in semicolon-terminated SQL

This commit is contained in:
lxk955
2026-10-11 19:37:25 +08:00
committed by GitHub
parent ed3ed28d2b
commit f5806ba961
2 changed files with 89 additions and 3 deletions
@@ -98,3 +98,80 @@ describe("buildSqlSemanticDiagnostics Oracle pseudo-columns", () => {
expect(diagnostics.map((diagnostic) => diagnostic.message)).toEqual(["Unknown column ROWNUM"]);
});
});
describe("buildSqlSemanticDiagnostics projection aliases in GROUP BY / ORDER BY (#11627)", () => {
it("accepts SELECT projection aliases in GROUP BY even when terminated with a semicolon", () => {
const sql = "SELECT CASE WHEN submitter_id IS NOT NULL THEN 'A' ELSE 'B' END AS repair_type, COUNT(*) AS row_cnt FROM ipu_quotation GROUP BY repair_type;";
const aliasStart = sql.indexOf("GROUP BY repair_type") + "GROUP BY ".length;
const aliasEnd = aliasStart + "repair_type".length;
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "ipu_quotation", span: span(sql.indexOf("ipu_quotation") + 1, sql.indexOf("ipu_quotation") + "ipu_quotation".length) }],
columns: [
{ name: "submitter_id", qualifier: null, span: span(sql.indexOf("submitter_id") + 1, sql.indexOf("submitter_id") + "submitter_id".length) },
// span from reference parser points to token including 1-based bounds before ';'
{ name: "repair_type", qualifier: null, span: span(aliasStart + 1, aliasEnd + 1) },
],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "ipu_quotation" }],
columnsByTable: new Map([["ipu_quotation", [{ name: "submitter_id" }]]]),
loadedColumnTables: new Set(["ipu_quotation"]),
sql,
databaseType: "mysql",
});
expect(diagnostics).toEqual([]);
});
it("accepts projection aliases in ORDER BY terminated with a semicolon", () => {
const sql = "SELECT id, name AS user_name FROM users ORDER BY user_name;";
const aliasStart = sql.indexOf("ORDER BY user_name") + "ORDER BY ".length;
const aliasEnd = aliasStart + "user_name".length;
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "users", span: span(sql.indexOf("users") + 1, sql.indexOf("users") + "users".length) }],
columns: [
{ name: "id", qualifier: null, span: span(sql.indexOf("id") + 1, sql.indexOf("id") + 2) },
{ name: "name", qualifier: null, span: span(sql.indexOf("name") + 1, sql.indexOf("name") + 5) },
{ name: "user_name", qualifier: null, span: span(aliasStart + 1, aliasEnd + 1) },
],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "users" }],
columnsByTable: new Map([["users", [{ name: "id" }, { name: "name" }]]]),
loadedColumnTables: new Set(["users"]),
sql,
databaseType: "mysql",
});
expect(diagnostics).toEqual([]);
});
it("still reports genuinely unknown columns in GROUP BY", () => {
const sql = "SELECT id, name AS user_name FROM users GROUP BY missing_col;";
const colStart = sql.indexOf("missing_col");
const colEnd = colStart + "missing_col".length;
const analysis: SqlReferenceAnalysis = {
tables: [{ name: "users", span: span(sql.indexOf("users") + 1, sql.indexOf("users") + "users".length) }],
columns: [
{ name: "id", qualifier: null, span: span(sql.indexOf("id") + 1, sql.indexOf("id") + 2) },
{ name: "name", qualifier: null, span: span(sql.indexOf("name") + 1, sql.indexOf("name") + 5) },
{ name: "missing_col", qualifier: null, span: span(colStart + 1, colEnd + 1) },
],
};
const diagnostics = buildSqlSemanticDiagnostics(analysis, {
tables: [{ name: "users" }],
columnsByTable: new Map([["users", [{ name: "id" }, { name: "name" }]]]),
loadedColumnTables: new Set(["users"]),
sql,
databaseType: "mysql",
});
expect(diagnostics.map((d) => d.message)).toEqual(["Unknown column missing_col"]);
});
});
@@ -369,9 +369,18 @@ export function isSqlVirtualTableReference(table: { name: string; schema?: strin
function isVisibleProjectionAlias(sql: string, span: SqlTextSpan, name: string, databaseType?: DatabaseType): boolean {
const range = sqlTextSpanToOffsetRange(sql, span);
if (!range) return false;
const context = getSqlCompletionContext(sql, range.to, { databaseType });
if (!context.prioritizeSelectAliases) return false;
return context.selectAliases.some((alias) => normalizeName(alias) === normalizeName(name));
// If the identifier is at the end of a statement terminated by ';' or punctuation,
// evaluating completion strictly at range.to can place the cursor on or past the
// semicolon, causing getSqlCompletionContext to treat it as an empty subsequent
// statement. Testing at range.from (and range.to as fallback) ensures we evaluate
// inside the clause (GROUP BY / ORDER BY / HAVING) containing this alias reference.
for (const cursor of [range.from, range.to]) {
const context = getSqlCompletionContext(sql, cursor, { databaseType });
if (context.prioritizeSelectAliases && context.selectAliases.some((alias) => normalizeName(alias) === normalizeName(name))) {
return true;
}
}
return false;
}
function trimSqlTextSpanWhitespace(sql: string | undefined, span: SqlTextSpan): SqlTextSpan {