fix(completion): stop aliasing DELETE target tables

This commit is contained in:
onenewcode authored and GitHub committed 2026-09-16 11:28:31 +08:00
1 parent e916e9e927
commit 9bf52dab69
3 files changed
+156 -1

No files matched your search

@@ -258,6 +258,66 @@ function semanticMutationTarget(model: SqlSemanticModel): SqlSemanticRowSource |
return targetId ? model.rowSources.find((source) => source.id === targetId) : model.rowSources.find((source) => source.kind === "mutation_target");
}
/**
* A generated alias is usually a syntax error on a mutation target: an INSERT
* target carries no alias at all, and a DELETE or UPDATE target only accepts
* one in the multi-table dialects that re-list it elsewhere (issue #9186:
* SQL Server rejects `DELETE FROM t AS a`). The typed case matches the
* replacement range against the parsed mutation-target span; the empty-prefix
* case cannot produce that source yet, so the mutation slot is recognized from
* the statement verb and the introducer right before the qualified name being
* completed. JOIN/USING/APPLY sources and SELECT FROM sources stay aliasable.
*/
function tableCompletionTargetIsAliasUnsafe(model: SqlSemanticModel): boolean {
if (model.cursorIntent.kind !== "table" && model.cursorIntent.kind !== "delete_target") return false;
const replacement = model.cursorIntent.replacementRange;
if (model.rowSources.some((source) => source.kind === "mutation_target" && !source.alias && replacement.start <= source.sourceSpan.end && replacement.end >= source.sourceSpan.start)) {
return true;
}
const introducer = mutationTargetIntroducer(model);
if (model.statement.kind === "delete") {
if (introducer.normalized === "delete") return true;
if (introducer.normalized !== "from") return false;
// Only the simple `DELETE FROM t` form targets the source being completed;
// once a target list sits between the verb and FROM (`DELETE t FROM ...`),
// the FROM clause introduces aliasable row sources.
const verbSide = introducer.index > 0 ? introducer.tokens[introducer.index - 1] : undefined;
return verbSide?.kind === "word" && verbSide.normalized === "delete";
}
if (model.statement.kind === "update") return introducer.normalized === "update";
if (model.statement.kind === "insert") return introducer.normalized === "into" || introducer.normalized === "insert";
return false;
}
/**
* The last syntax word before the table name being completed, skipping that
* name's qualifier parts and typed prefix, so `DELETE FROM dh.|` still reports
* `from` instead of the qualifier itself.
*/
function mutationTargetIntroducer(model: SqlSemanticModel): { normalized: string; index: number; tokens: readonly SqlSemanticToken[] } {
const before = model.tokens.filter((token) => token.span.end <= model.cursorIntent.replacementRange.start && token.kind !== "comment");
let index = before.length - 1;
let identifiersToSkip = model.cursorIntent.qualifierParts.length + (model.cursorIntent.prefix.length > 0 ? 1 : 0);
while (index >= 0 && identifiersToSkip > 0) {
if (before[index]?.text === ".") {
index -= 1;
continue;
}
if (!isSemanticIdentifier(before[index])) break;
identifiersToSkip -= 1;
index -= 1;
}
for (; index >= 0; index -= 1) {
const token = before[index];
if (!token) continue;
if (token.kind === "word") return { normalized: token.normalized, index, tokens: before };
if (token.kind === "quoted_identifier") return { normalized: "", index, tokens: before };
if (token.text === "." || token.kind === "comment") continue;
break;
}
return { normalized: "", index: -1, tokens: before };
}
/**
* When the semantic scanner misses the trailing identifier that the legacy
* scanner still reports (semantic prefix empty with a replacement range
@@ -322,6 +382,7 @@ export function sqlCompletionContextFromSemantic(model: SqlSemanticModel, base:
insertSchema: model.cursorIntent.kind === "insert_column" ? mutationSchema : base.insertSchema,
updateTarget: model.cursorIntent.kind === "update_column" && mutationTarget ? { table: mutationTarget.name, schema: mutationSchema } : base.updateTarget,
deleteTarget: model.cursorIntent.kind === "delete_target" && mutationTarget ? { table: mutationTarget.name, schema: mutationSchema } : base.deleteTarget,
tableCompletionTargetAliasUnsafe: tableCompletionTargetIsAliasUnsafe(model),
onStar: model.cursorIntent.kind === "star" || base.onStar,
contextKind,
};