Fix:修复 SQL 子查询语义诊断误报 (#2184)

* Fix:修复 SQL 子查询语义诊断误报

* Improve:添加 SQL 语义诊断开关

* Improve:优化 SQL 语义诊断开关说明

* Fix:收紧 SQL 语义诊断飘红范围

* Improve:调整 SQL 语义诊断默认策略

---------

Co-authored-by: staff <staff@qimaos-MacBook-Pro.local>
This commit is contained in:
zipgandstaff authored and GitHub committed 2026-06-30 20:15:59 +08:00
1 parent 73a996ebe6
commit c38bc05417
12 files changed
+417 -31

No files matched your search

+105 -6
View File
@@ -1,7 +1,7 @@
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sqlCompletion";
import { getSqlCompletionContext } from "@/lib/sqlCompletion";
import { executableStatementRanges, type SqlTextRange } from "@/lib/sqlStatementRanges";
import type { DatabaseType, SqlColumnReference, SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
import type { DatabaseType, SqlColumnReference, SqlReferenceAnalysis, SqlReferenceScope, SqlTableReference, SqlTextSpan } from "@/types/database";
export interface SqlSemanticDiagnostic {
span: SqlTextSpan;
@@ -42,6 +42,7 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
const diagnostics: SqlSemanticDiagnostic[] = [];
const tables = analysis.tables.filter((table) => table.name.trim());
const knownTables = new Map<string, SqlTableReference>();
const scopesById = scopesByIdMap(analysis.scopes);
for (const table of tables) {
knownTables.set(normalizeName(table.name), table);
@@ -52,14 +53,14 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
for (const table of tables) {
if (!schema.missingTables?.has(tableReferenceKey(table))) continue;
diagnostics.push({
span: table.span,
span: trimSqlTextSpanWhitespace(schema.sql, table.span),
message: `Unknown table ${displayTableName(table)}`,
severity: "error",
});
}
for (const column of analysis.columns) {
const table = resolveColumnTable(column, tables, knownTables, schema.sql);
const table = resolveColumnTable(column, tables, knownTables, schema.sql, scopesById);
if (!table) continue;
if (schema.missingTables?.has(tableReferenceKey(table))) continue;
@@ -71,7 +72,7 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
const displayName = column.qualifier ? `${column.qualifier}.${column.name}` : column.name;
diagnostics.push({
span: column.span,
span: trimSqlTextSpanWhitespace(schema.sql, column.span),
message: `Unknown column ${displayName}`,
severity: "error",
});
@@ -80,6 +81,70 @@ export function buildSqlSemanticDiagnostics(analysis: SqlReferenceAnalysis, sche
return diagnostics;
}
function trimSqlTextSpanWhitespace(sql: string | undefined, span: SqlTextSpan): SqlTextSpan {
if (!sql) return span;
const range = sqlTextSpanToOffsetRange(sql, span);
if (!range) return span;
let from = range.from;
let to = range.to;
while (from < to && /\s/.test(sql[from] ?? "")) from += 1;
while (to > from && /\s/.test(sql[to - 1] ?? "")) to -= 1;
if (from === range.from && to === range.to) return span;
const start = offsetToSqlTextStartPosition(sql, from);
const end = offsetToSqlTextEndPosition(sql, to);
if (!start || !end) return span;
return {
start_line: start.line,
start_column: start.column,
end_line: end.line,
end_column: Math.max(end.column, start.column),
};
}
function sqlTextSpanToOffsetRange(sql: string, span: SqlTextSpan): { from: number; to: number } | null {
if (!span.start_line || !span.start_column) return null;
const from = sqlTextPositionToOffset(sql, span.start_line, span.start_column - 1);
const to = sqlTextPositionToOffset(sql, Math.max(span.end_line, span.start_line), Math.max(span.end_column, span.start_column));
if (from == null || to == null || to <= from) return null;
return { from, to };
}
function sqlTextPositionToOffset(sql: string, line: number, column: number): number | null {
const lines = sql.split(/\r?\n/);
if (line < 1 || line > lines.length) return null;
let offset = 0;
for (let index = 0; index < line - 1; index += 1) {
offset += lines[index].length + 1;
}
return Math.min(offset + Math.max(column, 0), offset + lines[line - 1].length);
}
function offsetToSqlTextStartPosition(sql: string, offset: number): { line: number; column: number } | null {
const position = offsetToLineColumn(sql, offset);
return position ? { line: position.line, column: position.column + 1 } : null;
}
function offsetToSqlTextEndPosition(sql: string, offset: number): { line: number; column: number } | null {
return offsetToLineColumn(sql, offset);
}
function offsetToLineColumn(sql: string, offset: number): { line: number; column: number } | null {
if (offset < 0 || offset > sql.length) return null;
const lines = sql.split(/\r?\n/);
let remaining = offset;
for (let index = 0; index < lines.length; index += 1) {
const lineLength = lines[index].length;
if (remaining <= lineLength) {
return { line: index + 1, column: remaining };
}
remaining -= lineLength + 1;
}
const lastLine = lines[lines.length - 1] ?? "";
return { line: lines.length, column: lastLine.length };
}
export function buildSqlParserErrorDiagnostic(error: unknown, sql: string): SqlSemanticDiagnostic | null {
const message = errorMessage(error);
const location = /\bat Line:\s*(\d+),\s*Column:\s*(\d+)\b/i.exec(message);
@@ -129,8 +194,8 @@ export function isSqlSemanticDiagnosticInputContext(sql: string, cursor: number,
return context.exclusiveColumnSuggestions || !!context.qualifier || ((context.suggestTables || context.exclusiveTableSuggestions) && isCursorAfterTableTrigger(sql, cursor));
}
function resolveColumnTable(column: SqlColumnReference, tables: SqlTableReference[], knownTables: Map<string, SqlTableReference>, sql?: string): SqlTableReference | null {
const candidateTables = sql ? tablesInSameStatement(tables, column, sql) : tables;
function resolveColumnTable(column: SqlColumnReference, tables: SqlTableReference[], knownTables: Map<string, SqlTableReference>, sql?: string, scopesById?: Map<number, SqlReferenceScope>): SqlTableReference | null {
const candidateTables = candidateTablesForColumn(tables, column, sql, scopesById);
if (column.qualifier) {
return tableLookupFor(candidateTables).get(normalizeName(column.qualifier)) ?? (sql ? null : (knownTables.get(normalizeName(column.qualifier)) ?? null));
}
@@ -138,6 +203,34 @@ function resolveColumnTable(column: SqlColumnReference, tables: SqlTableReferenc
return candidateTables[0];
}
function candidateTablesForColumn(tables: SqlTableReference[], column: SqlColumnReference, sql?: string, scopesById?: Map<number, SqlReferenceScope>): SqlTableReference[] {
const scoped = tablesInVisibleScopes(tables, column, scopesById);
if (scoped) return scoped;
return sql ? tablesInSameStatement(tables, column, sql) : tables;
}
function tablesInVisibleScopes(tables: SqlTableReference[], column: SqlColumnReference, scopesById?: Map<number, SqlReferenceScope>): SqlTableReference[] | null {
if (column.scope_id == null || !scopesById || scopesById.size === 0) return null;
if (!column.qualifier) {
const currentScopeTables = tables.filter((table) => table.scope_id === column.scope_id);
if (currentScopeTables.length > 0) return currentScopeTables;
}
const visibleScopeIds = scopeAndParents(column.scope_id, scopesById);
if (visibleScopeIds.size === 0) return null;
return tables.filter((table) => table.scope_id != null && visibleScopeIds.has(table.scope_id));
}
function scopeAndParents(scopeId: number, scopesById: Map<number, SqlReferenceScope>): Set<number> {
const ids = new Set<number>();
let current: number | undefined = scopeId;
while (current != null && !ids.has(current)) {
ids.add(current);
const scope = scopesById.get(current);
current = scope?.parent_id ?? undefined;
}
return ids;
}
function tableLookupFor(tables: SqlTableReference[]): Map<string, SqlTableReference> {
const lookup = new Map<string, SqlTableReference>();
for (const table of tables) {
@@ -157,6 +250,12 @@ function tablesInSameStatement(tables: SqlTableReference[], column: SqlColumnRef
});
}
function scopesByIdMap(scopes: readonly SqlReferenceScope[] | undefined): Map<number, SqlReferenceScope> {
const map = new Map<number, SqlReferenceScope>();
for (const scope of scopes ?? []) map.set(scope.id, scope);
return map;
}
function spanStartOffset(sql: string, span: SqlTextSpan): number | null {
if (!span.start_line || !span.start_column) return null;
const lines = sql.split(/\r?\n/);