fix(sql): gate having alias visibility by dialect reject list

This commit is contained in:
dienaso authored and GitHub committed 2026-09-14 01:44:58 +08:00
1 parent ac7fbaeb43
commit 1b6ff2ba54
5 files changed
+158 -13

No files matched your search

@@ -286,7 +286,7 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
// would otherwise show a false "Unknown column" diagnostic. Reuse the
// completion context, which already extracts aliases and knows when they
// are visible, to keep diagnostics aligned with executable SQL semantics.
if (schema.sql && isVisibleProjectionAlias(schema.sql, column.span, column.name)) continue;
if (schema.sql && isVisibleProjectionAlias(schema.sql, column.span, column.name, schema.databaseType)) continue;
const displayName = column.qualifier ? `${column.qualifier}.${column.name}` : column.name;
diagnostics.push({
@@ -327,10 +327,10 @@ export function isSqlVirtualTableReference(table: { name: string; schema?: strin
return databaseType === "mysql" && !table.schema && normalizeName(table.name) === "dual";
}
function isVisibleProjectionAlias(sql: string, span: SqlTextSpan, name: string): boolean {
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);
const context = getSqlCompletionContext(sql, range.to, { databaseType });
if (!context.prioritizeSelectAliases) return false;
return context.selectAliases.some((alias) => normalizeName(alias) === normalizeName(name));
}
+32 -10
View File
@@ -19,6 +19,7 @@ import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/c
import { quoteTableIdentifier } from "@/lib/table/tableSelectSql";
import { driverProfileCompletionObjects, driverProfileCompletionTableMetadata, driverProfileCompletionTables, driverProfileRoutineSignatures } from "@/lib/database/driverProfileExtensions";
import { DORIS_FUNCTION_DOCS, DORIS_FUNCTION_SIGNATURES } from "@/lib/sql/doris/functions";
import { rejectsAliasReferenceInHaving } from "@/lib/database/databaseFeatureSupport";
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
@@ -2303,9 +2304,9 @@ export function getSqlCompletionContext(sql: string, cursor: number, options: Sq
// Check if we're in a context where columns are expected
const selectListColumnContext = isInSelectListContext(beforeCursor);
const inColumnContext = selectListColumnContext || isInColumnContext(beforeCursor) || !!insertInfo;
const inColumnContext = selectListColumnContext || isInColumnContext(beforeCursor, options.databaseType) || !!insertInfo;
const inJoinConditionContext = isInJoinConditionContext(beforeCursor);
const prioritizeSelectAliases = isInOrderOrGroupByContext(beforeCursor);
const prioritizeSelectAliases = isInOrderOrGroupByContext(beforeCursor, options.databaseType);
const inCallRoutineContext = isCallRoutineContext(beforeCursor);
const inPotentialPackageMemberContext = !!qualifier && !exclusiveTableSuggestions && !insertInfo && !updateInfo?.inSetClause && !oracleTableFunctionContext;
const suggestColumns = !!qualifier || !!updateInfo?.inSetClause || !!insertInfo || (inColumnContext && referencedTables.length > 0);
@@ -2531,11 +2532,11 @@ function parseTrailingIdentifierPart(input: string, endExclusive: number): { sta
/**
* Check if the content before cursor is in a column-expected context.
*/
function isInColumnContext(beforeCursor: string): boolean {
function isInColumnContext(beforeCursor: string, databaseType?: DatabaseType): boolean {
if (!beforeCursor) return false;
if (isInSelectListContext(beforeCursor)) return true;
if (isInOrderOrGroupByContext(beforeCursor)) return true;
if (isInOrderOrGroupByContext(beforeCursor, databaseType)) return true;
// Strip string literals
const cleaned = beforeCursor.replace(/'[^']*'/g, "''").replace(/"[^"]*"/g, "''");
@@ -2645,21 +2646,42 @@ function isInJoinConditionContext(beforeCursor: string): boolean {
return /\b(?:on|and)\s+[a-z0-9_$]*$/i.test(currentJoinSegment);
}
function isInOrderOrGroupByContext(beforeCursor: string): boolean {
function lastStandaloneKeywordIndex(cleaned: string, keyword: string): number {
// `\bhaving\b` — a bare `lastIndexOf("having")` also anchors on identifiers
// like `having_count` (t8y2/dbx#8953 review).
const pattern = new RegExp(`\\b${keyword}\\b`, "g");
let last = -1;
for (let match = pattern.exec(cleaned); match; match = pattern.exec(cleaned)) {
last = match.index;
}
return last;
}
function isInOrderOrGroupByContext(beforeCursor: string, databaseType?: DatabaseType): boolean {
const cleaned = beforeCursor
.replace(/'[^']*'/g, "''")
.replace(/"[^"]*"/g, '""')
.toLowerCase();
const lastOrderBy = cleaned.lastIndexOf("order by");
const lastGroupBy = cleaned.lastIndexOf("group by");
// 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");
// SELECT aliases may be referenced inside HAVING for permissive engines
// (MySQL family, SQLite family, DuckDB, BigQuery, Spark, Snowflake, ...),
// so aliases stay visible there just like in ORDER BY/GROUP BY — but
// confirmed rejecters (PostgreSQL family, SQL Server, DB2, Oracle family,
// Trino, ...) hide them, and their "Unknown column" diagnostic keeps
// flagging those (t8y2/dbx#8953 review).
const lastHaving = lastStandaloneKeywordIndex(cleaned, "having");
const lastContext = Math.max(lastOrderBy, lastGroupBy, lastHaving);
if (lastContext < 0) return false;
const segment = cleaned.slice(lastContext);
return !/\b(?:where|limit|offset|union|intersect|except|join|from)\b/.test(segment);
if (/\b(?:where|limit|offset|union|intersect|except|join|from)\b/.test(segment)) return false;
// An unknown database type keeps the permissive legacy behavior; only
// dialects known to reject HAVING aliases lose the alias visibility
// (everything else — Spark, Snowflake, Hive family, unlisted types —
// stays permissive).
if (lastContext === lastHaving && rejectsAliasReferenceInHaving(databaseType)) return false;
return true;
}
function isInGroupByContext(beforeCursor: string): boolean {
@@ -4365,7 +4387,7 @@ function isFollowedByJoin(beforeToken: string): boolean {
}
function isInTableListContext(beforeToken: string, databaseType: DatabaseType | undefined): boolean {
if (isInOrderOrGroupByContext(beforeToken)) return false;
if (isInOrderOrGroupByContext(beforeToken, databaseType)) return false;
const cleaned = activeQueryBlockSql(maskSqlLiteralsAndComments(beforeToken, databaseType).trimEnd());
if (!/,\s*$/.test(cleaned)) return false;