mirror of
https://github.com/t8y2/dbx.git
synced 2026-11-02 00:29:30 +00:00
feat(editor): add SQL semantic diagnostics
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
buildSqlCompletionItemsFromContext,
|
||||
getSqlFunctionSignatureHelp,
|
||||
getSqlCompletionContext,
|
||||
getSqlCompletionResultValidFor,
|
||||
shouldAutoOpenSqlCompletion,
|
||||
} from "@/lib/sqlCompletion";
|
||||
import { extractIdentifierAt, isSqlKeyword, matchTable } from "@/lib/sqlNavigation";
|
||||
@@ -39,7 +40,16 @@ import {
|
||||
fontSizeFromWheelDelta,
|
||||
} from "@/lib/editorZoom";
|
||||
import { shortcutToCodeMirrorKey } from "@/lib/shortcutRegistry";
|
||||
import * as api from "@/lib/api";
|
||||
import {
|
||||
areSqlSemanticDiagnosticsEqual,
|
||||
buildSqlParserErrorDiagnostic,
|
||||
buildSqlSemanticDiagnostics,
|
||||
shouldRunSqlSemanticDiagnostics,
|
||||
type SqlSemanticDiagnostic,
|
||||
} from "@/lib/sqlSemanticDiagnostics";
|
||||
import type { SqlCompletionColumn } from "@/lib/sqlCompletion";
|
||||
import type { SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
@@ -99,6 +109,11 @@ let diagnosticComp: import("@codemirror/state").Compartment | null = null;
|
||||
let buildSqlDiagnosticExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let buildSqlSignatureExtension: (() => import("@codemirror/state").Extension) | null = null;
|
||||
let codeMirrorSnippetCompletion: typeof import("@codemirror/autocomplete").snippetCompletion;
|
||||
let codeMirrorCompletionStatus: typeof import("@codemirror/autocomplete").completionStatus | null = null;
|
||||
let setSqlDiagnosticsEffect: import("@codemirror/state").StateEffectType<SqlSemanticDiagnostic[]> | null = null;
|
||||
let semanticDiagnostics: SqlSemanticDiagnostic[] = [];
|
||||
let semanticDiagnosticTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let semanticDiagnosticRunId = 0;
|
||||
|
||||
function editorThemeAppearance() {
|
||||
return isDark.value ? "dark" : "light";
|
||||
@@ -245,18 +260,18 @@ function identifierRangeAt(sql: string, pos: number): { from: number; to: number
|
||||
return { from, to, text };
|
||||
}
|
||||
|
||||
function completionCacheKey(table: { name: string; schema?: string }) {
|
||||
function completionCacheKey(table: { name: string; schema?: string | null }) {
|
||||
return table.schema ? `${table.schema}.${table.name}` : table.name;
|
||||
}
|
||||
|
||||
async function ensureColumnsForTable(table: { name: string; schema?: string }) {
|
||||
async function ensureColumnsForTable(table: { name: string; schema?: string | null }) {
|
||||
const cacheKey = completionCacheKey(table);
|
||||
if (cachedColumnsByTable.has(cacheKey) || !props.connectionId || !props.database) return;
|
||||
const columns = await connectionStore.listCompletionColumns(
|
||||
props.connectionId,
|
||||
props.database,
|
||||
table.name,
|
||||
table.schema,
|
||||
table.schema ?? undefined,
|
||||
);
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
}
|
||||
@@ -409,6 +424,131 @@ function sqlErrorDecorationRange(currentState: import("@codemirror/state").Edito
|
||||
];
|
||||
}
|
||||
|
||||
function sqlTextSpanToRange(sql: string, span: SqlTextSpan): { from: number; to: number } | null {
|
||||
if (!span.start_line || !span.start_column) return null;
|
||||
const from = lineColumnToOffset(sql, { line: span.start_line - 1, column: span.start_column - 1 });
|
||||
const to = lineColumnToOffset(sql, {
|
||||
line: Math.max(span.end_line - 1, span.start_line - 1),
|
||||
column: Math.max(span.end_column, span.start_column),
|
||||
});
|
||||
if (from == null || to == null || to <= from) return null;
|
||||
return { from, to };
|
||||
}
|
||||
|
||||
function sqlSemanticDecorationRanges(currentState: import("@codemirror/state").EditorState) {
|
||||
const sql = currentState.doc.toString();
|
||||
return semanticDiagnostics
|
||||
.map((diagnostic) => {
|
||||
const range = sqlTextSpanToRange(sql, diagnostic.span);
|
||||
return range ? { ...range, message: diagnostic.message, severity: diagnostic.severity } : null;
|
||||
})
|
||||
.filter((range): range is { from: number; to: number; message: string; severity: "error" | "warning" } => !!range);
|
||||
}
|
||||
|
||||
function reconfigureDiagnostics() {
|
||||
if (!view.value) return;
|
||||
if (setSqlDiagnosticsEffect) {
|
||||
view.value.dispatch({
|
||||
effects: setSqlDiagnosticsEffect.of(semanticDiagnostics),
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!diagnosticComp || !buildSqlDiagnosticExtension) return;
|
||||
view.value.dispatch({
|
||||
effects: diagnosticComp.reconfigure(buildSqlDiagnosticExtension()),
|
||||
});
|
||||
}
|
||||
|
||||
function setSemanticDiagnostics(next: SqlSemanticDiagnostic[]) {
|
||||
if (areSqlSemanticDiagnosticsEqual(semanticDiagnostics, next)) return;
|
||||
semanticDiagnostics = next;
|
||||
reconfigureDiagnostics();
|
||||
}
|
||||
|
||||
async function enrichSemanticDiagnosticTables(tables: SqlTableReference[]) {
|
||||
if (!props.connectionId || !props.database) return tables;
|
||||
|
||||
const enriched: SqlTableReference[] = [];
|
||||
for (const table of tables) {
|
||||
if (table.schema) {
|
||||
enriched.push(table);
|
||||
continue;
|
||||
}
|
||||
const cached = cachedTables.find((item) => item.name.toLowerCase() === table.name.toLowerCase());
|
||||
if (cached?.schema) {
|
||||
enriched.push({ ...table, schema: cached.schema });
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const matches = await connectionStore.listCompletionTables(
|
||||
props.connectionId,
|
||||
props.database,
|
||||
table.name,
|
||||
MAX_COMPLETION_TABLES,
|
||||
);
|
||||
cachedTables = [...cachedTables, ...matches];
|
||||
const match = matches.find((item) => item.name.toLowerCase() === table.name.toLowerCase());
|
||||
enriched.push(match?.schema ? { ...table, schema: match.schema } : table);
|
||||
} catch {
|
||||
enriched.push(table);
|
||||
}
|
||||
}
|
||||
return enriched;
|
||||
}
|
||||
|
||||
async function refreshSemanticDiagnostics() {
|
||||
const currentView = view.value;
|
||||
const runId = ++semanticDiagnosticRunId;
|
||||
if (!currentView || !props.connectionId || !props.database) {
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const sql = currentView.state.doc.toString();
|
||||
if (!sql.trim()) {
|
||||
setSemanticDiagnostics([]);
|
||||
return;
|
||||
}
|
||||
if (!shouldRunSqlSemanticDiagnostics(sql, currentView.state.selection.main.head)) {
|
||||
scheduleSemanticDiagnostics(1200);
|
||||
return;
|
||||
}
|
||||
if (codeMirrorCompletionStatus?.(currentView.state)) {
|
||||
scheduleSemanticDiagnostics(900);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const analysis = await api.analyzeSqlReferences(sql, props.formatDialect ?? props.dialect ?? "generic");
|
||||
if (runId !== semanticDiagnosticRunId) return;
|
||||
|
||||
const tables = await enrichSemanticDiagnosticTables(analysis.tables);
|
||||
await Promise.all(tables.map((table) => ensureColumnsForTable(table)));
|
||||
if (runId !== semanticDiagnosticRunId) return;
|
||||
|
||||
const enrichedAnalysis: SqlReferenceAnalysis = { ...analysis, tables };
|
||||
setSemanticDiagnostics(
|
||||
buildSqlSemanticDiagnostics(enrichedAnalysis, {
|
||||
tables: cachedTables,
|
||||
columnsByTable: cachedColumnsByTable,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
if (runId === semanticDiagnosticRunId) {
|
||||
const diagnostic = buildSqlParserErrorDiagnostic(error, sql);
|
||||
setSemanticDiagnostics(diagnostic ? [diagnostic] : []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleSemanticDiagnostics(delay = 500) {
|
||||
if (semanticDiagnosticTimer) clearTimeout(semanticDiagnosticTimer);
|
||||
semanticDiagnosticTimer = setTimeout(() => {
|
||||
semanticDiagnosticTimer = null;
|
||||
void refreshSemanticDiagnostics();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
async function formatCurrentSql() {
|
||||
const currentView = view.value;
|
||||
if (!currentView) return;
|
||||
@@ -563,7 +703,7 @@ async function provideSqlCompletions(
|
||||
boost: item.boost,
|
||||
},
|
||||
),
|
||||
validFor: /^[\w$]*$/,
|
||||
validFor: getSqlCompletionResultValidFor(fullDoc, position),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -580,10 +720,10 @@ onMounted(async () => {
|
||||
|
||||
const [
|
||||
{ EditorView, keymap, rectangularSelection, hoverTooltip, showTooltip, Decoration },
|
||||
{ EditorState, Compartment, Prec, StateField },
|
||||
{ EditorState, Compartment, Prec, StateEffect, StateField },
|
||||
{ sql, MSSQL, MySQL, PostgreSQL, SQLDialect },
|
||||
{ basicSetup },
|
||||
{ autocompletion, startCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion },
|
||||
{ autocompletion, startCompletion, closeBrackets, closeBracketsKeymap, snippetCompletion, completionStatus },
|
||||
{ indentWithTab },
|
||||
{ bracketMatching },
|
||||
] = await Promise.all([
|
||||
@@ -603,29 +743,44 @@ onMounted(async () => {
|
||||
readOnlyComp = new Compartment();
|
||||
runKeymapComp = new Compartment();
|
||||
diagnosticComp = new Compartment();
|
||||
setSqlDiagnosticsEffect = StateEffect.define<SqlSemanticDiagnostic[]>();
|
||||
codeMirrorCompletionStatus = completionStatus;
|
||||
|
||||
const diagnosticTheme = EditorView.baseTheme({
|
||||
".cm-sql-error": {
|
||||
textDecoration: "underline wavy var(--destructive)",
|
||||
textUnderlineOffset: "3px",
|
||||
},
|
||||
".cm-sql-semantic-warning": {
|
||||
textDecoration: "underline wavy hsl(var(--warning, 38 92% 50%))",
|
||||
textUnderlineOffset: "3px",
|
||||
},
|
||||
});
|
||||
|
||||
buildSqlDiagnosticExtension = () => {
|
||||
const buildDecorations = (state: import("@codemirror/state").EditorState) =>
|
||||
Decoration.set(
|
||||
sqlErrorDecorationRange(state).map((range) =>
|
||||
Decoration.mark({
|
||||
class: "cm-sql-error",
|
||||
attributes: { title: range.message },
|
||||
}).range(range.from, range.to),
|
||||
),
|
||||
const diagnosticEffect = setSqlDiagnosticsEffect;
|
||||
const buildDecorations = (state: import("@codemirror/state").EditorState) => {
|
||||
const errorDecorations = sqlErrorDecorationRange(state).map((range) =>
|
||||
Decoration.mark({
|
||||
class: "cm-sql-error",
|
||||
attributes: { title: range.message },
|
||||
}).range(range.from, range.to),
|
||||
);
|
||||
const semanticDecorations = sqlSemanticDecorationRanges(state).map((range) =>
|
||||
Decoration.mark({
|
||||
class: range.severity === "error" ? "cm-sql-error" : "cm-sql-semantic-warning",
|
||||
attributes: { title: range.message },
|
||||
}).range(range.from, range.to),
|
||||
);
|
||||
return Decoration.set([...errorDecorations, ...semanticDecorations], true);
|
||||
};
|
||||
|
||||
const field = StateField.define({
|
||||
create: buildDecorations,
|
||||
update(value, transaction) {
|
||||
return transaction.docChanged ? buildDecorations(transaction.state) : value;
|
||||
const diagnosticsChanged =
|
||||
!!diagnosticEffect && transaction.effects.some((effect) => effect.is(diagnosticEffect));
|
||||
return transaction.docChanged || diagnosticsChanged ? buildDecorations(transaction.state) : value;
|
||||
},
|
||||
provide: (field) => EditorView.decorations.from(field),
|
||||
});
|
||||
@@ -691,6 +846,7 @@ onMounted(async () => {
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.docChanged) {
|
||||
emit("update:modelValue", update.state.doc.toString());
|
||||
scheduleSemanticDiagnostics();
|
||||
let insertedText = "";
|
||||
update.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => {
|
||||
insertedText += inserted.toString();
|
||||
@@ -858,6 +1014,7 @@ onMounted(async () => {
|
||||
syncEditorFontCssVars(liveFontSize.value, ss.fontFamily);
|
||||
|
||||
cachedTables = [];
|
||||
scheduleSemanticDiagnostics();
|
||||
});
|
||||
|
||||
watch(
|
||||
@@ -881,10 +1038,7 @@ watch(
|
||||
watch(
|
||||
() => props.executionError,
|
||||
() => {
|
||||
if (!view.value || !diagnosticComp || !buildSqlDiagnosticExtension) return;
|
||||
view.value.dispatch({
|
||||
effects: diagnosticComp.reconfigure(buildSqlDiagnosticExtension()),
|
||||
});
|
||||
reconfigureDiagnostics();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -892,6 +1046,8 @@ watch(
|
||||
() => props.connectionId,
|
||||
() => {
|
||||
refreshCompletionCache();
|
||||
setSemanticDiagnostics([]);
|
||||
scheduleSemanticDiagnostics();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -899,6 +1055,8 @@ watch(
|
||||
() => props.database,
|
||||
() => {
|
||||
refreshCompletionCache();
|
||||
setSemanticDiagnostics([]);
|
||||
scheduleSemanticDiagnostics();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -937,6 +1095,7 @@ watch(
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
zoomCommitScheduler.dispose();
|
||||
if (semanticDiagnosticTimer) clearTimeout(semanticDiagnosticTimer);
|
||||
view.value?.destroy();
|
||||
});
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ export const executeScript = forward("executeScript");
|
||||
export const executeInTransaction = forward("executeInTransaction");
|
||||
export const cancelQuery = forward("cancelQuery");
|
||||
export const closeQuerySession = forward("closeQuerySession");
|
||||
export const analyzeSqlReferences = forward("analyzeSqlReferences");
|
||||
|
||||
// AI
|
||||
export const aiComplete = forward("aiComplete");
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
ForeignKeyInfo,
|
||||
TriggerInfo,
|
||||
QueryResult,
|
||||
SqlReferenceAnalysis,
|
||||
InstalledPlugin,
|
||||
JdbcDriverInfo,
|
||||
JdbcPluginStatus,
|
||||
@@ -398,6 +399,10 @@ export async function cancelQuery(executionId: string): Promise<boolean> {
|
||||
return post("/api/query/cancel", { executionId });
|
||||
}
|
||||
|
||||
export async function analyzeSqlReferences(sql: string, dialect?: string): Promise<SqlReferenceAnalysis> {
|
||||
return post("/api/query/analyze-sql-references", { sql, dialect });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AI
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -418,9 +418,17 @@ export function shouldAutoOpenSqlCompletion(sql: string, cursor: number): boolea
|
||||
const previousChar = sql[cursor - 1];
|
||||
if (!previousChar) return false;
|
||||
if (/\bon\s+$/i.test(sql.slice(0, cursor))) return true;
|
||||
if (/[,;()[\]]/.test(previousChar)) return false;
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
if (context.exclusiveTableSuggestions || context.exclusiveColumnSuggestions || context.suggestTables) return true;
|
||||
return /[\w$.]/.test(previousChar);
|
||||
}
|
||||
|
||||
export function getSqlCompletionResultValidFor(sql: string, cursor: number): RegExp | undefined {
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
return context.suggestTables ? undefined : /^[\w$]*$/;
|
||||
}
|
||||
|
||||
export function getSqlFunctionSignatureHelp(sql: string, cursor: number): SqlFunctionSignatureHelp | null {
|
||||
const beforeCursor = sql.slice(0, cursor);
|
||||
const openParenIndex = findActiveFunctionOpenParen(beforeCursor);
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { SqlCompletionColumn, SqlCompletionTable } from "@/lib/sqlCompletion";
|
||||
import { getSqlCompletionContext } from "@/lib/sqlCompletion";
|
||||
import type { SqlColumnReference, SqlReferenceAnalysis, SqlTableReference, SqlTextSpan } from "@/types/database";
|
||||
|
||||
export interface SqlSemanticDiagnostic {
|
||||
span: SqlTextSpan;
|
||||
message: string;
|
||||
severity: "error" | "warning";
|
||||
}
|
||||
|
||||
export interface SqlSemanticDiagnosticSchema {
|
||||
tables: SqlCompletionTable[];
|
||||
columnsByTable: Map<string, SqlCompletionColumn[]>;
|
||||
}
|
||||
|
||||
export function buildSqlSemanticDiagnostics(
|
||||
analysis: SqlReferenceAnalysis,
|
||||
schema: SqlSemanticDiagnosticSchema,
|
||||
): SqlSemanticDiagnostic[] {
|
||||
const diagnostics: SqlSemanticDiagnostic[] = [];
|
||||
const tables = analysis.tables.filter((table) => table.name.trim());
|
||||
const knownTables = new Map<string, SqlTableReference>();
|
||||
|
||||
for (const table of tables) {
|
||||
knownTables.set(normalizeName(table.name), table);
|
||||
if (table.alias) knownTables.set(normalizeName(table.alias), table);
|
||||
if (table.schema) knownTables.set(normalizeName(`${table.schema}.${table.name}`), table);
|
||||
}
|
||||
|
||||
for (const column of analysis.columns) {
|
||||
const table = resolveColumnTable(column, tables, knownTables);
|
||||
if (!table) continue;
|
||||
|
||||
const columns = columnsForTable(table, schema.columnsByTable);
|
||||
if (!columns) continue;
|
||||
|
||||
const columnNames = new Set(columns.map((item) => normalizeName(item.name)));
|
||||
if (columnNames.has(normalizeName(column.name))) continue;
|
||||
|
||||
const displayName = column.qualifier ? `${column.qualifier}.${column.name}` : column.name;
|
||||
diagnostics.push({
|
||||
span: column.span,
|
||||
message: `Unknown column ${displayName}`,
|
||||
severity: "warning",
|
||||
});
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
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);
|
||||
if (!location) return null;
|
||||
|
||||
const startLine = Number.parseInt(location[1], 10);
|
||||
const startColumn = Number.parseInt(location[2], 10);
|
||||
if (!Number.isFinite(startLine) || !Number.isFinite(startColumn) || startLine < 1 || startColumn < 1) return null;
|
||||
|
||||
const lineText = sql.split(/\r?\n/)[startLine - 1] ?? "";
|
||||
const startIndex = Math.max(startColumn - 1, 0);
|
||||
const token = /^[\w$]+/.exec(lineText.slice(startIndex))?.[0];
|
||||
const tokenLength = Math.max(token?.length ?? 1, 1);
|
||||
|
||||
return {
|
||||
span: {
|
||||
start_line: startLine,
|
||||
start_column: startColumn,
|
||||
end_line: startLine,
|
||||
end_column: startColumn + tokenLength - 1,
|
||||
},
|
||||
message,
|
||||
severity: "error",
|
||||
};
|
||||
}
|
||||
|
||||
export function areSqlSemanticDiagnosticsEqual(
|
||||
left: readonly SqlSemanticDiagnostic[],
|
||||
right: readonly SqlSemanticDiagnostic[],
|
||||
): boolean {
|
||||
if (left.length !== right.length) return false;
|
||||
return left.every((item, index) => {
|
||||
const other = right[index];
|
||||
return (
|
||||
!!other &&
|
||||
item.message === other.message &&
|
||||
item.severity === other.severity &&
|
||||
item.span.start_line === other.span.start_line &&
|
||||
item.span.start_column === other.span.start_column &&
|
||||
item.span.end_line === other.span.end_line &&
|
||||
item.span.end_column === other.span.end_column
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldRunSqlSemanticDiagnostics(sql: string, cursor: number): boolean {
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
if (context.suggestTables || context.exclusiveTableSuggestions || context.exclusiveColumnSuggestions) return false;
|
||||
if (context.qualifier) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function resolveColumnTable(
|
||||
column: SqlColumnReference,
|
||||
tables: SqlTableReference[],
|
||||
knownTables: Map<string, SqlTableReference>,
|
||||
): SqlTableReference | null {
|
||||
if (column.qualifier) {
|
||||
return knownTables.get(normalizeName(column.qualifier)) ?? null;
|
||||
}
|
||||
if (tables.length !== 1) return null;
|
||||
return tables[0];
|
||||
}
|
||||
|
||||
function columnsForTable(
|
||||
table: SqlTableReference,
|
||||
columnsByTable: Map<string, SqlCompletionColumn[]>,
|
||||
): SqlCompletionColumn[] | null {
|
||||
const keys = table.schema ? [`${table.schema}.${table.name}`, table.name] : [table.name];
|
||||
for (const key of keys) {
|
||||
const columns = columnsByTable.get(key) ?? columnsByTable.get(normalizeName(key));
|
||||
if (columns) return columns;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeName(value: string): string {
|
||||
let normalized = value;
|
||||
while (normalized && `"'\`[]`.includes(normalized[0])) normalized = normalized.slice(1);
|
||||
while (normalized && `"'\`[]`.includes(normalized[normalized.length - 1])) normalized = normalized.slice(0, -1);
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown): string {
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
ForeignKeyInfo,
|
||||
TriggerInfo,
|
||||
QueryResult,
|
||||
SqlReferenceAnalysis,
|
||||
InstalledPlugin,
|
||||
JdbcDriverInfo,
|
||||
JdbcPluginStatus,
|
||||
@@ -297,6 +298,10 @@ export async function executeInTransaction(
|
||||
return invoke("execute_in_transaction", { connectionId, database, statements, schema });
|
||||
}
|
||||
|
||||
export async function analyzeSqlReferences(sql: string, dialect?: string): Promise<SqlReferenceAnalysis> {
|
||||
return invoke("analyze_sql_references", { sql, dialect });
|
||||
}
|
||||
|
||||
export async function listIndexes(
|
||||
connectionId: string,
|
||||
database: string,
|
||||
|
||||
@@ -203,6 +203,31 @@ export interface QueryResult {
|
||||
has_more?: boolean;
|
||||
}
|
||||
|
||||
export interface SqlTextSpan {
|
||||
start_line: number;
|
||||
start_column: number;
|
||||
end_line: number;
|
||||
end_column: number;
|
||||
}
|
||||
|
||||
export interface SqlTableReference {
|
||||
name: string;
|
||||
schema?: string | null;
|
||||
alias?: string | null;
|
||||
span: SqlTextSpan;
|
||||
}
|
||||
|
||||
export interface SqlColumnReference {
|
||||
name: string;
|
||||
qualifier?: string | null;
|
||||
span: SqlTextSpan;
|
||||
}
|
||||
|
||||
export interface SqlReferenceAnalysis {
|
||||
tables: SqlTableReference[];
|
||||
columns: SqlColumnReference[];
|
||||
}
|
||||
|
||||
export type TreeNodeType =
|
||||
| "connection"
|
||||
| "connection-group"
|
||||
|
||||
Reference in New Issue
Block a user