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:
1 parent
73a996ebe6
commit
c38bc05417
12 files changed
+417
-31
No files matched your search
@@ -34,6 +34,7 @@ import {
|
||||
type DesktopIconTheme,
|
||||
type InterfaceLayout,
|
||||
type DisconnectTabHandlingMode,
|
||||
type SqlSemanticDiagnosticsMode,
|
||||
type UpdateDownloadSource,
|
||||
type CustomThemeColors,
|
||||
type CustomTheme,
|
||||
@@ -190,6 +191,8 @@ const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
|
||||
const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker);
|
||||
const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
|
||||
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
|
||||
const editSqlSemanticDiagnosticsMode = ref<SqlSemanticDiagnosticsMode>(settingsStore.editorSettings.sqlSemanticDiagnosticsMode);
|
||||
const editSqlSemanticDiagnosticsEnabled = ref(settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled);
|
||||
const editConfirmDangerousSqlExecution = ref(settingsStore.editorSettings.confirmDangerousSqlExecution);
|
||||
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
|
||||
const editShowTrayIcon = ref(settingsStore.desktopSettings.show_tray_icon);
|
||||
@@ -468,6 +471,8 @@ watch(
|
||||
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
|
||||
editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
|
||||
editWordWrap.value = settingsStore.editorSettings.wordWrap;
|
||||
editSqlSemanticDiagnosticsMode.value = settingsStore.editorSettings.sqlSemanticDiagnosticsMode;
|
||||
editSqlSemanticDiagnosticsEnabled.value = settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled;
|
||||
editConfirmDangerousSqlExecution.value = settingsStore.editorSettings.confirmDangerousSqlExecution;
|
||||
editAppLayout.value = settingsStore.editorSettings.appLayout;
|
||||
editShowTrayIcon.value = settingsStore.desktopSettings.show_tray_icon;
|
||||
@@ -532,6 +537,8 @@ function hasChanges(): boolean {
|
||||
editShowExecutionTargetPicker.value !== settingsStore.editorSettings.showExecutionTargetPicker ||
|
||||
editAutoAliasTables.value !== settingsStore.editorSettings.autoAliasTables ||
|
||||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
|
||||
editSqlSemanticDiagnosticsMode.value !== settingsStore.editorSettings.sqlSemanticDiagnosticsMode ||
|
||||
editSqlSemanticDiagnosticsEnabled.value !== settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled ||
|
||||
editConfirmDangerousSqlExecution.value !== settingsStore.editorSettings.confirmDangerousSqlExecution ||
|
||||
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
|
||||
editShowTrayIcon.value !== settingsStore.desktopSettings.show_tray_icon ||
|
||||
@@ -582,6 +589,7 @@ async function persistSettings() {
|
||||
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
|
||||
autoAliasTables: editAutoAliasTables.value,
|
||||
wordWrap: editWordWrap.value,
|
||||
sqlSemanticDiagnosticsMode: editSqlSemanticDiagnosticsMode.value,
|
||||
confirmDangerousSqlExecution: editConfirmDangerousSqlExecution.value,
|
||||
appLayout: editAppLayout.value,
|
||||
showColumnCommentsInHeader: editShowColumnCommentsInHeader.value,
|
||||
@@ -643,6 +651,8 @@ function resetDefaultsForTab(tab: SettingsCategory) {
|
||||
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editSqlSemanticDiagnosticsMode.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsMode;
|
||||
editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled;
|
||||
editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution;
|
||||
} else if (tab === "formatter") {
|
||||
editSqlFormatter.value = normalizeSqlFormatterSettings(DEFAULT_EDITOR_SETTINGS.sqlFormatter);
|
||||
@@ -702,6 +712,8 @@ function resetAllDefaults() {
|
||||
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
|
||||
editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
|
||||
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
|
||||
editSqlSemanticDiagnosticsMode.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsMode;
|
||||
editSqlSemanticDiagnosticsEnabled.value = DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsEnabled;
|
||||
editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution;
|
||||
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
|
||||
editShowTrayIcon.value = DEFAULT_DESKTOP_SETTINGS.show_tray_icon;
|
||||
@@ -866,6 +878,11 @@ function onExecuteModeChange(v: any) {
|
||||
if (v === "all" || v === "current") editExecuteMode.value = v;
|
||||
}
|
||||
|
||||
function onSqlSemanticDiagnosticsEnabledChange(value: boolean) {
|
||||
editSqlSemanticDiagnosticsEnabled.value = value;
|
||||
editSqlSemanticDiagnosticsMode.value = value ? "enabled" : "disabled";
|
||||
}
|
||||
|
||||
function onFontFamilyChange(v: any) {
|
||||
if (typeof v === "string") editFontFamily.value = v;
|
||||
}
|
||||
@@ -2005,14 +2022,26 @@ watch(
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="editor-confirm-dangerous-sql">{{ t("settings.confirmDangerousSqlExecution") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.confirmDangerousSqlExecutionDescription") }}
|
||||
</p>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="editor-sql-semantic-diagnostics">{{ t("settings.sqlSemanticDiagnosticsEnabled") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.sqlSemanticDiagnosticsEnabledDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="editor-sql-semantic-diagnostics" :model-value="editSqlSemanticDiagnosticsEnabled" class="mt-0.5" @update:model-value="onSqlSemanticDiagnosticsEnabledChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border bg-muted/20 px-3 py-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="editor-confirm-dangerous-sql">{{ t("settings.confirmDangerousSqlExecution") }}</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{{ t("settings.confirmDangerousSqlExecutionDescription") }}
|
||||
</p>
|
||||
</div>
|
||||
<Switch id="editor-confirm-dangerous-sql" v-model="editConfirmDangerousSqlExecution" class="mt-0.5" />
|
||||
</div>
|
||||
<Switch id="editor-confirm-dangerous-sql" v-model="editConfirmDangerousSqlExecution" class="mt-0.5" />
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
@@ -926,6 +926,17 @@ function setSemanticDiagnostics(next: SqlSemanticDiagnostic[]) {
|
||||
reconfigureDiagnostics();
|
||||
}
|
||||
|
||||
function clearScheduledSemanticDiagnostics() {
|
||||
semanticDiagnosticRunId++;
|
||||
if (semanticDiagnosticTimer) clearTimeout(semanticDiagnosticTimer);
|
||||
semanticDiagnosticTimer = null;
|
||||
pendingSemanticDiagnosticPreserveOutsideRanges = false;
|
||||
}
|
||||
|
||||
function shouldSkipSqlSemanticDiagnostics() {
|
||||
return props.databaseType !== "redis" && !settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled;
|
||||
}
|
||||
|
||||
function rangesOverlap(left: { from: number; to: number }, right: { from: number; to: number }): boolean {
|
||||
return left.from < right.to && right.from < left.to;
|
||||
}
|
||||
@@ -1046,6 +1057,10 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
|
||||
setSemanticDiagnostics(buildRedisSyntaxDiagnostics(sql));
|
||||
return;
|
||||
}
|
||||
if (shouldSkipSqlSemanticDiagnostics()) {
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
if (!shouldRunSqlSemanticDiagnostics(sql, currentView.state.selection.main.head, { databaseType: props.databaseType })) {
|
||||
scheduleSemanticDiagnostics(1200, { preserveOutsideRanges: options.preserveOutsideRanges });
|
||||
return;
|
||||
@@ -1102,6 +1117,11 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
|
||||
|
||||
function scheduleSemanticDiagnostics(delay = 500, options: { preserveOutsideRanges?: boolean } = {}) {
|
||||
if (!editorIsActive) return;
|
||||
if (shouldSkipSqlSemanticDiagnostics()) {
|
||||
clearScheduledSemanticDiagnostics();
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
pendingSemanticDiagnosticPreserveOutsideRanges = !!options.preserveOutsideRanges;
|
||||
if (semanticDiagnosticTimer) clearTimeout(semanticDiagnosticTimer);
|
||||
semanticDiagnosticTimer = setTimeout(() => {
|
||||
@@ -2512,14 +2532,25 @@ watch(
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingsStore.editorSettings.sqlSemanticDiagnosticsEnabled,
|
||||
(enabled) => {
|
||||
if (props.databaseType === "redis") return;
|
||||
if (!shouldSkipSqlSemanticDiagnostics() && enabled) {
|
||||
scheduleSemanticDiagnostics(0);
|
||||
return;
|
||||
}
|
||||
clearScheduledSemanticDiagnostics();
|
||||
setSemanticDiagnostics([]);
|
||||
},
|
||||
);
|
||||
|
||||
function pauseQueryEditorBackgroundWork() {
|
||||
flushEditorViewport();
|
||||
flushEditorSelection();
|
||||
clearTableNavigationHover();
|
||||
editorIsActive = false;
|
||||
semanticDiagnosticRunId++;
|
||||
if (semanticDiagnosticTimer) clearTimeout(semanticDiagnosticTimer);
|
||||
semanticDiagnosticTimer = null;
|
||||
clearScheduledSemanticDiagnostics();
|
||||
completionEpoch++;
|
||||
unregisterTableReferenceDropListener();
|
||||
}
|
||||
|
||||
@@ -2671,6 +2671,8 @@ export default {
|
||||
showExecutionTargetPickerDescription: "When enabled, running without a selection lets you choose between the current statement and all SQL.",
|
||||
wordWrap: "Word wrap",
|
||||
wordWrapDescription: "Wrap long SQL lines within the editor width",
|
||||
sqlSemanticDiagnosticsEnabled: "SQL semantic diagnostics",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "When enabled, the editor reports semantic issues such as unknown tables and columns. Disable it to reduce parsing and metadata checks for large SQL.",
|
||||
confirmDangerousSqlExecution: "Confirm before dangerous SQL",
|
||||
confirmDangerousSqlExecutionDescription: "When disabled, ALTER, DROP, DELETE, TRUNCATE, and other dangerous SQL run without the warning dialog.",
|
||||
autoAliasTables: "Automatically add table aliases",
|
||||
|
||||
@@ -2644,6 +2644,8 @@ export default withEnglishFallback({
|
||||
showExecutionTargetPickerDescription: "有効にすると、選択なしで実行するときに現在の文とすべてのSQLを一時的に選べます。",
|
||||
wordWrap: "折り返し",
|
||||
wordWrapDescription: "長いSQL行をエディタ幅内で折り返します",
|
||||
sqlSemanticDiagnosticsEnabled: "SQLセマンティック診断",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "有効時、エディタは不明なテーブルや列などの意味的な問題を表示します。大きなSQLの解析とメタデータ確認の負荷を減らすには無効にします。",
|
||||
confirmDangerousSqlExecution: "危険なSQLの前に確認",
|
||||
confirmDangerousSqlExecutionDescription: "無効時、ALTER、DROP、DELETE、TRUNCATEなどの危険なSQLが警告ダイアログなしで実行されます。",
|
||||
autoAliasTables: "テーブル別名を自動追加",
|
||||
|
||||
@@ -2678,6 +2678,8 @@ export default withEnglishFallback({
|
||||
showExecutionTargetPickerDescription: "开启后,无选区执行时可在当前语句和全部 SQL 之间临时选择。",
|
||||
wordWrap: "自动换行",
|
||||
wordWrapDescription: "长 SQL 在编辑器宽度内自动折行显示",
|
||||
sqlSemanticDiagnosticsEnabled: "SQL 语义诊断",
|
||||
sqlSemanticDiagnosticsEnabledDescription: "开启后,编辑器会提示未知表、字段等语义问题;关闭可减少 SQL 解析和元数据检查的性能开销。",
|
||||
confirmDangerousSqlExecution: "执行危险 SQL 前弹出确认",
|
||||
confirmDangerousSqlExecutionDescription: "关闭后,ALTER、DROP、DELETE、TRUNCATE 等危险 SQL 将直接执行。",
|
||||
autoAliasTables: "自动添加表别名",
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -10,6 +10,22 @@ describe("normalizeEditorSettings", () => {
|
||||
expect(normalizeEditorSettings({ autoAliasTables: false }).autoAliasTables).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps SQL semantic diagnostics in auto mode and disabled by default", () => {
|
||||
const settings = normalizeEditorSettings({});
|
||||
expect(settings.sqlSemanticDiagnosticsMode).toBe("auto");
|
||||
expect(settings.sqlSemanticDiagnosticsEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves explicit SQL semantic diagnostics modes", () => {
|
||||
expect(normalizeEditorSettings({ sqlSemanticDiagnosticsMode: "enabled" }).sqlSemanticDiagnosticsEnabled).toBe(true);
|
||||
expect(normalizeEditorSettings({ sqlSemanticDiagnosticsMode: "disabled" }).sqlSemanticDiagnosticsEnabled).toBe(false);
|
||||
});
|
||||
|
||||
it("migrates legacy SQL semantic diagnostics booleans to explicit modes", () => {
|
||||
expect(normalizeEditorSettings({ sqlSemanticDiagnosticsEnabled: true } as any).sqlSemanticDiagnosticsMode).toBe("enabled");
|
||||
expect(normalizeEditorSettings({ sqlSemanticDiagnosticsEnabled: false } as any).sqlSemanticDiagnosticsMode).toBe("disabled");
|
||||
});
|
||||
|
||||
it("defaults update downloads to the official source", () => {
|
||||
expect(normalizeEditorSettings({}).updateDownloadSource).toBe("official");
|
||||
});
|
||||
|
||||
@@ -59,8 +59,10 @@ export type DesktopIconTheme = "default" | "black";
|
||||
export type InterfaceLayout = "separated" | "classic";
|
||||
|
||||
export type UpdateDownloadSource = "official" | "cnb";
|
||||
export type SqlSemanticDiagnosticsMode = "auto" | "enabled" | "disabled";
|
||||
|
||||
export const DEFAULT_SIDEBAR_TABLE_PAGE_SIZE = 1000;
|
||||
const SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED = false;
|
||||
|
||||
export const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
|
||||
show_tray_icon: true,
|
||||
@@ -330,6 +332,8 @@ export interface EditorSettings {
|
||||
showExecutionTargetPicker: boolean;
|
||||
autoAliasTables: boolean;
|
||||
wordWrap: boolean;
|
||||
sqlSemanticDiagnosticsMode: SqlSemanticDiagnosticsMode;
|
||||
sqlSemanticDiagnosticsEnabled: boolean;
|
||||
confirmDangerousSqlExecution: boolean;
|
||||
compactTabTitle: boolean;
|
||||
appLayout: "separated" | "classic";
|
||||
@@ -436,6 +440,8 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
|
||||
showExecutionTargetPicker: false,
|
||||
autoAliasTables: true,
|
||||
wordWrap: false,
|
||||
sqlSemanticDiagnosticsMode: "auto",
|
||||
sqlSemanticDiagnosticsEnabled: SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED,
|
||||
confirmDangerousSqlExecution: true,
|
||||
compactTabTitle: false,
|
||||
appLayout: "classic",
|
||||
@@ -514,6 +520,18 @@ function normalizeUpdateDownloadSource(value: unknown): UpdateDownloadSource {
|
||||
return value === "cnb" ? "cnb" : DEFAULT_EDITOR_SETTINGS.updateDownloadSource;
|
||||
}
|
||||
|
||||
function normalizeSqlSemanticDiagnosticsMode(value: unknown, legacyEnabled?: unknown): SqlSemanticDiagnosticsMode {
|
||||
if (value === "auto" || value === "enabled" || value === "disabled") return value;
|
||||
if (typeof legacyEnabled === "boolean") return legacyEnabled ? "enabled" : "disabled";
|
||||
return DEFAULT_EDITOR_SETTINGS.sqlSemanticDiagnosticsMode;
|
||||
}
|
||||
|
||||
function sqlSemanticDiagnosticsEnabledForMode(mode: SqlSemanticDiagnosticsMode): boolean {
|
||||
if (mode === "enabled") return true;
|
||||
if (mode === "disabled") return false;
|
||||
return SQL_SEMANTIC_DIAGNOSTICS_AUTO_ENABLED;
|
||||
}
|
||||
|
||||
function normalizeDisconnectTabHandlingMode(value: unknown, legacyCloseTabsOnDisconnect?: unknown): DisconnectTabHandlingMode {
|
||||
if (DISCONNECT_TAB_HANDLING_MODES.includes(value as DisconnectTabHandlingMode)) {
|
||||
return value as DisconnectTabHandlingMode;
|
||||
@@ -582,6 +600,7 @@ function normalizeToolbarItems(items: Partial<ToolbarItems> | undefined): Toolba
|
||||
}
|
||||
|
||||
export function normalizeEditorSettings(settings: Partial<EditorSettings>, existing?: EditorSettings): EditorSettings {
|
||||
const sqlSemanticDiagnosticsMode = normalizeSqlSemanticDiagnosticsMode(settings.sqlSemanticDiagnosticsMode, settings.sqlSemanticDiagnosticsEnabled);
|
||||
return {
|
||||
fontFamily: settings.fontFamily ?? DEFAULT_EDITOR_SETTINGS.fontFamily,
|
||||
fontSize: settings.fontSize ?? DEFAULT_EDITOR_SETTINGS.fontSize,
|
||||
@@ -618,6 +637,8 @@ export function normalizeEditorSettings(settings: Partial<EditorSettings>, exist
|
||||
showExecutionTargetPicker: settings.showExecutionTargetPicker ?? DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker,
|
||||
autoAliasTables: settings.autoAliasTables ?? DEFAULT_EDITOR_SETTINGS.autoAliasTables,
|
||||
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
|
||||
sqlSemanticDiagnosticsMode,
|
||||
sqlSemanticDiagnosticsEnabled: sqlSemanticDiagnosticsEnabledForMode(sqlSemanticDiagnosticsMode),
|
||||
confirmDangerousSqlExecution: settings.confirmDangerousSqlExecution ?? DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution,
|
||||
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
|
||||
appLayout: settings.appLayout ?? DEFAULT_EDITOR_SETTINGS.appLayout,
|
||||
@@ -791,6 +812,11 @@ export const useSettingsStore = defineStore("settings", () => {
|
||||
if (partial.showExecutionTargetPicker !== undefined) editorSettings.value.showExecutionTargetPicker = partial.showExecutionTargetPicker;
|
||||
if (partial.autoAliasTables !== undefined) editorSettings.value.autoAliasTables = partial.autoAliasTables;
|
||||
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
|
||||
if (partial.sqlSemanticDiagnosticsMode !== undefined || partial.sqlSemanticDiagnosticsEnabled !== undefined) {
|
||||
const nextMode = normalizeSqlSemanticDiagnosticsMode(partial.sqlSemanticDiagnosticsMode, partial.sqlSemanticDiagnosticsEnabled);
|
||||
editorSettings.value.sqlSemanticDiagnosticsMode = nextMode;
|
||||
editorSettings.value.sqlSemanticDiagnosticsEnabled = sqlSemanticDiagnosticsEnabledForMode(nextMode);
|
||||
}
|
||||
if (partial.confirmDangerousSqlExecution !== undefined) editorSettings.value.confirmDangerousSqlExecution = partial.confirmDangerousSqlExecution;
|
||||
if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle;
|
||||
if (partial.appLayout !== undefined) editorSettings.value.appLayout = partial.appLayout;
|
||||
|
||||
@@ -448,17 +448,25 @@ export interface SqlTableReference {
|
||||
schema?: string | null;
|
||||
alias?: string | null;
|
||||
span: SqlTextSpan;
|
||||
scope_id?: number;
|
||||
}
|
||||
|
||||
export interface SqlColumnReference {
|
||||
name: string;
|
||||
qualifier?: string | null;
|
||||
span: SqlTextSpan;
|
||||
scope_id?: number;
|
||||
}
|
||||
|
||||
export interface SqlReferenceScope {
|
||||
id: number;
|
||||
parent_id?: number | null;
|
||||
}
|
||||
|
||||
export interface SqlReferenceAnalysis {
|
||||
tables: SqlTableReference[];
|
||||
columns: SqlColumnReference[];
|
||||
scopes?: SqlReferenceScope[];
|
||||
}
|
||||
|
||||
export type TreeNodeType =
|
||||
|
||||
Reference in new issue
Block a user