feat(sql): add semantic SQL completion engine
This commit is contained in:
1 parent
32a9496d2a
commit
90f0594e93
16 files changed
+2038
-30
No files matched your search
@@ -29,6 +29,9 @@ import {
|
||||
shouldAutoOpenSqlCompletion,
|
||||
extractCteDefinitions,
|
||||
} from "@/lib/sqlCompletion";
|
||||
import { sqlCompletionContextFromSemantic } from "@/lib/sqlSemanticCompletion";
|
||||
import { buildSqlSemanticModel } from "@/lib/sqlSemanticModel";
|
||||
import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget } from "@/lib/sqlSemanticReferences";
|
||||
import { buildElasticsearchCompletionItemsFromContext, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionItem } from "@/lib/elasticsearchCompletion";
|
||||
import { buildMongoCompletionItemsFromContext, getMongoCompletionContext, getMongoCompletionResultValidFor, shouldAutoOpenMongoCompletion, type MongoCompletionItem } from "@/lib/mongoCompletion";
|
||||
import { extractIdentifierAt, isSqlKeyword, matchTable, splitQualifiedIdentifier } from "@/lib/sqlNavigation";
|
||||
@@ -78,6 +81,8 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const COMPLETION_REMOTE_LATENCY_BUDGET_MS = 120;
|
||||
// Internal rollback switch: flip to false to route completion, diagnostics, and navigation through the legacy SQL context path.
|
||||
const SEMANTIC_SQL_COMPLETION_ENABLED = true;
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: string];
|
||||
@@ -992,24 +997,29 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
|
||||
if (!range) return null;
|
||||
|
||||
const identifier = range.text;
|
||||
const parts = identifier.split(".");
|
||||
const parts = splitQualifiedIdentifier(identifier);
|
||||
const name = parts[parts.length - 1] ?? identifier;
|
||||
const qualifier = parts.length > 1 ? parts[parts.length - 2] : undefined;
|
||||
const semanticModel = SEMANTIC_SQL_COMPLETION_ENABLED ? buildSqlSemanticModel(sql, pos, { databaseType: props.databaseType, dialect: props.dialect }) : null;
|
||||
const semanticTarget = semanticModel ? resolveSqlSemanticNavigationTarget(semanticModel, parts) : null;
|
||||
const semanticQualifierIsRowSource = !!qualifier && !!semanticTarget && (semanticTarget.alias?.toLowerCase() === qualifier.toLowerCase() || semanticTarget.source.name.toLowerCase() === qualifier.toLowerCase());
|
||||
const tableLookupName = semanticTarget && !semanticQualifierIsRowSource ? semanticTarget.name : name;
|
||||
const qualifiedTableLookup = semanticTarget?.schema ? `${semanticTarget.schema}.${semanticTarget.name}` : identifier;
|
||||
|
||||
try {
|
||||
if (cachedTables.length === 0) {
|
||||
cachedTables = usesLocalOnlyCompletionMetadata()
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId, props.database, name, MAX_COMPLETION_TABLES, props.schema)
|
||||
: await connectionStore.listCompletionTables(props.connectionId, props.database, name, MAX_COMPLETION_TABLES, props.schema);
|
||||
? connectionStore.lookupLocalCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, props.schema)
|
||||
: await connectionStore.listCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, props.schema);
|
||||
}
|
||||
|
||||
let table = matchTable(identifier, cachedTables) ?? matchTable(name, cachedTables);
|
||||
let table = matchTable(qualifiedTableLookup, cachedTables) ?? matchTable(tableLookupName, cachedTables) ?? matchTable(identifier, cachedTables) ?? matchTable(name, cachedTables);
|
||||
if (!table && !usesLocalOnlyCompletionMetadata()) {
|
||||
const hoverTables = await connectionStore.listCompletionTables(props.connectionId, props.database, name, MAX_COMPLETION_TABLES, props.schema);
|
||||
const hoverTables = await connectionStore.listCompletionTables(props.connectionId, props.database, tableLookupName, MAX_COMPLETION_TABLES, semanticTarget?.schema ?? props.schema);
|
||||
cachedTables = [...cachedTables, ...hoverTables];
|
||||
table = matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables);
|
||||
table = matchTable(qualifiedTableLookup, hoverTables) ?? matchTable(tableLookupName, hoverTables) ?? matchTable(identifier, hoverTables) ?? matchTable(name, hoverTables);
|
||||
}
|
||||
if (table && (!qualifier || table.schema?.toLowerCase() === qualifier.toLowerCase() || table.name === name)) {
|
||||
if (table && !semanticQualifierIsRowSource && (!qualifier || table.schema?.toLowerCase() === qualifier.toLowerCase() || table.name === name)) {
|
||||
return {
|
||||
pos: range.from,
|
||||
end: range.to,
|
||||
@@ -1019,12 +1029,21 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number)
|
||||
};
|
||||
}
|
||||
|
||||
const context = getSqlCompletionContext(sql, pos);
|
||||
const legacyContext = getSqlCompletionContext(sql, pos);
|
||||
const context = semanticModel ? sqlCompletionContextFromSemantic(semanticModel, legacyContext) : legacyContext;
|
||||
const candidates = qualifier ? context.referencedTables.filter((rt) => rt.alias?.toLowerCase() === qualifier.toLowerCase() || rt.name.toLowerCase() === qualifier.toLowerCase()) : context.referencedTables;
|
||||
|
||||
for (const refTable of candidates) {
|
||||
await ensureColumnsForTable(refTable);
|
||||
const columns = cachedColumnsByTable.get(completionCacheKey(refTable)) ?? [];
|
||||
const columns: SqlCompletionColumn[] =
|
||||
refTable.columns?.map((columnName) => ({
|
||||
name: columnName,
|
||||
table: refTable.name,
|
||||
...(refTable.schema ? { schema: refTable.schema } : {}),
|
||||
})) ?? [];
|
||||
if (columns.length === 0) {
|
||||
await ensureColumnsForTable(refTable);
|
||||
columns.push(...(cachedColumnsByTable.get(completionCacheKey(refTable)) ?? []));
|
||||
}
|
||||
const column = columns.find((col) => col.name.toLowerCase() === name.toLowerCase());
|
||||
if (!column) continue;
|
||||
return {
|
||||
@@ -1164,6 +1183,10 @@ async function enrichSemanticDiagnosticTables(tables: SqlTableReference[]): Prom
|
||||
const enriched: SqlTableReference[] = [];
|
||||
const missingTables = new Set<string>();
|
||||
for (const table of tables) {
|
||||
if (isStatementLocalSemanticTable(table)) {
|
||||
enriched.push(table);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const match = await findExactSemanticDiagnosticTable(table);
|
||||
if (!match) missingTables.add(tableReferenceKey(table));
|
||||
@@ -1180,6 +1203,7 @@ async function ensureColumnsForSemanticDiagnostics(tables: SqlTableReference[]):
|
||||
const seen = new Set<string>();
|
||||
const targets: SqlTableReference[] = [];
|
||||
for (const table of tables) {
|
||||
if (isStatementLocalSemanticTable(table)) continue;
|
||||
const tableWithInlineColumns = table as SqlTableReference & { columns?: string[] };
|
||||
if (tableWithInlineColumns.columns && tableWithInlineColumns.columns.length > 0) continue;
|
||||
const cacheKey = completionCacheKey(table);
|
||||
@@ -1204,6 +1228,11 @@ async function ensureColumnsForSemanticDiagnostics(tables: SqlTableReference[]):
|
||||
return missingTables;
|
||||
}
|
||||
|
||||
function isStatementLocalSemanticTable(table: SqlTableReference): boolean {
|
||||
const kind = (table as SqlTableReference & { semanticSourceKind?: string }).semanticSourceKind;
|
||||
return kind === "cte" || kind === "subquery" || kind === "table_function";
|
||||
}
|
||||
|
||||
async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boolean } = {}) {
|
||||
const currentView = view.value;
|
||||
const runId = ++semanticDiagnosticRunId;
|
||||
@@ -1256,12 +1285,15 @@ async function refreshSemanticDiagnostics(options: { preserveOutsideRanges?: boo
|
||||
const analysis = await api.analyzeSqlReferences(range.sql, props.formatDialect ?? props.dialect ?? "generic");
|
||||
if (runId !== semanticDiagnosticRunId) return;
|
||||
|
||||
const { tables, missingTables } = await enrichSemanticDiagnosticTables(analysis.tables);
|
||||
const semanticCursor = Math.max(0, Math.min(currentView.state.selection.main.head - range.from, range.sql.length));
|
||||
const semanticModel = SEMANTIC_SQL_COMPLETION_ENABLED ? buildSqlSemanticModel(range.sql, semanticCursor, { databaseType: props.databaseType, dialect: props.dialect }) : null;
|
||||
const semanticAnalysis = semanticModel ? mergeSqlSemanticReferenceAnalysis(analysis, semanticModel) : analysis;
|
||||
const { tables, missingTables } = await enrichSemanticDiagnosticTables(semanticAnalysis.tables);
|
||||
const columnMetadataMissingTables = await ensureColumnsForSemanticDiagnostics(tables);
|
||||
for (const tableKey of columnMetadataMissingTables) missingTables.add(tableKey);
|
||||
if (runId !== semanticDiagnosticRunId) return;
|
||||
|
||||
const enrichedAnalysis: SqlReferenceAnalysis = { ...analysis, tables };
|
||||
const enrichedAnalysis: SqlReferenceAnalysis = { ...semanticAnalysis, tables };
|
||||
nextDiagnostics.push(
|
||||
...offsetSqlSemanticDiagnostics(
|
||||
buildSqlSemanticDiagnostics(enrichedAnalysis, {
|
||||
@@ -1562,7 +1594,10 @@ async function provideMongoCompletions(currentState: import("@codemirror/state")
|
||||
};
|
||||
}
|
||||
|
||||
async function provideSqlCompletions(currentState: import("@codemirror/state").EditorState, position: number, explicit: boolean) {
|
||||
async function provideSqlCompletions(context: CompletionContext) {
|
||||
const currentState = context.state;
|
||||
const position = context.pos;
|
||||
const explicit = context.explicit;
|
||||
if (imeCompositionActive || view.value?.compositionStarted || view.value?.composing) return null;
|
||||
if (!props.connectionId) return null;
|
||||
const fullDoc = currentState.doc.toString();
|
||||
@@ -1585,7 +1620,8 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
|
||||
if (isSqlCompletionSuppressedContext(fullDoc, position)) return null;
|
||||
if (!explicit && !shouldAutoOpenSqlCompletion(fullDoc, position)) return null;
|
||||
|
||||
const completionContext = getSqlCompletionContext(fullDoc, position);
|
||||
const legacyCompletionContext = getSqlCompletionContext(fullDoc, position);
|
||||
const completionContext = SEMANTIC_SQL_COMPLETION_ENABLED ? sqlCompletionContextFromSemantic(buildSqlSemanticModel(fullDoc, position, { databaseType: props.databaseType, dialect: props.dialect }), legacyCompletionContext) : legacyCompletionContext;
|
||||
|
||||
if (!hasDatabase) {
|
||||
const items = buildSqlCompletionItemsFromContext(completionContext, {
|
||||
@@ -1643,6 +1679,9 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
|
||||
// This prevents wasted backend calls during rapid typing while still
|
||||
// showing table/column names in the first popup.
|
||||
return new Promise<ReturnType<typeof buildCompletionResult>>((resolve) => {
|
||||
context.addEventListener("abort", () => {
|
||||
if (epoch === completionEpoch) completionEpoch++;
|
||||
});
|
||||
completionDebounceTimer = setTimeout(async () => {
|
||||
completionDebounceTimer = null;
|
||||
if (epoch !== completionEpoch) {
|
||||
@@ -2250,7 +2289,7 @@ onMounted(async () => {
|
||||
buildSqlCompletionExtension = () =>
|
||||
autocompletion({
|
||||
activateOnTyping: true,
|
||||
override: [async (context: CompletionContext) => provideSqlCompletions(context.state, context.pos, context.explicit)],
|
||||
override: [async (context: CompletionContext) => provideSqlCompletions(context)],
|
||||
});
|
||||
|
||||
const dialect = createDbxCodeMirrorSqlDialect(langSql, props.dialect);
|
||||
|
||||
Reference in new issue
Block a user