fix(completion): isolate aliases across SQL statements
This commit is contained in:
6 files changed
+114
-104
No files matched your search
@@ -199,7 +199,7 @@ export function areSqlSemanticDiagnosticsEqual(left: readonly SqlSemanticDiagnos
|
||||
|
||||
export function shouldRunSqlSemanticDiagnostics(sql: string, cursor: number, options: { databaseType?: DatabaseType } = {}): boolean {
|
||||
if (options.databaseType === "mongodb" || options.databaseType === "elasticsearch" || options.databaseType === "qdrant" || options.databaseType === "milvus" || options.databaseType === "weaviate" || options.databaseType === "chromadb" || options.databaseType === "redis") return false;
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
const context = getSqlCompletionContext(sql, cursor, options);
|
||||
if (context.exclusiveColumnSuggestions) return false;
|
||||
if (context.qualifier) return false;
|
||||
if ((context.suggestTables || context.exclusiveTableSuggestions) && isCursorAfterTableTrigger(sql, cursor)) return false;
|
||||
@@ -208,7 +208,7 @@ export function shouldRunSqlSemanticDiagnostics(sql: string, cursor: number, opt
|
||||
|
||||
export function isSqlSemanticDiagnosticInputContext(sql: string, cursor: number, options: { databaseType?: DatabaseType } = {}): boolean {
|
||||
if (options.databaseType === "mongodb" || options.databaseType === "elasticsearch" || options.databaseType === "qdrant" || options.databaseType === "milvus" || options.databaseType === "weaviate" || options.databaseType === "chromadb" || options.databaseType === "redis") return false;
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
const context = getSqlCompletionContext(sql, cursor, options);
|
||||
return context.exclusiveColumnSuggestions || !!context.qualifier || ((context.suggestTables || context.exclusiveTableSuggestions) && isCursorAfterTableTrigger(sql, cursor));
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,12 @@ export function tokenizeSqlSemantic(input: string, dialectId = "mysql"): SqlSema
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "#" && dialectId === "postgres") {
|
||||
index += 1;
|
||||
tokens.push(token("operator", ch, start, index, depth));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "/" && next === "*") {
|
||||
index += 2;
|
||||
while (index < input.length && !(input[index] === "*" && input[index + 1] === "/")) index += 1;
|
||||
@@ -116,7 +122,7 @@ export function tokenizeSqlSemantic(input: string, dialectId = "mysql"): SqlSema
|
||||
|
||||
if (WORD_START.test(ch)) {
|
||||
index += 1;
|
||||
while (index < input.length && WORD_PART.test(input[index] ?? "")) index += 1;
|
||||
while (index < input.length && WORD_PART.test(input[index] ?? "") && !(dialectId === "postgres" && input[index] === "#")) index += 1;
|
||||
tokens.push(token("word", input.slice(start, index), start, index, depth));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ import type { DatabaseType, SqlSnippet } from "@/types/database";
|
||||
import { buildMongoCompletionItemsFromContext, type MongoCompletionItem } from "@/lib/mongo/mongoCompletion";
|
||||
import { CLOUDFLARE_D1_COMMON_FUNCTION_NAMES } from "@/lib/sql/cloudflareD1";
|
||||
import type { SqlObjectNavigationType } from "@/lib/sql/sqlNavigation";
|
||||
import { sqlSemanticDialectFor } from "@/lib/sql/semantic/dialect";
|
||||
import { findActiveSqlStatementSpan, tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
|
||||
import type { SqlSemanticBuildOptions, SqlSemanticSpan } from "@/lib/sql/semantic/types";
|
||||
import { DEFAULT_SQL_SNIPPETS, MANTICORESEARCH_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||
|
||||
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||
@@ -1303,7 +1306,7 @@ export function buildSqlCompletionItems(
|
||||
},
|
||||
): SqlCompletionItem[] {
|
||||
if (isSqlCompletionSuppressedContext(sql, cursor)) return [];
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
const context = getSqlCompletionContext(sql, cursor, input);
|
||||
return buildSqlCompletionItemsFromContext(context, input);
|
||||
}
|
||||
|
||||
@@ -1433,14 +1436,14 @@ class SqlCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldAutoOpenSqlCompletion(sql: string, cursor: number): boolean {
|
||||
export function shouldAutoOpenSqlCompletion(sql: string, cursor: number, options: SqlSemanticBuildOptions = {}): boolean {
|
||||
if (isSqlCompletionSuppressedContext(sql, cursor)) return false;
|
||||
const previousChar = sql[cursor - 1];
|
||||
if (!previousChar) return false;
|
||||
if (/\bon\s+$/i.test(sql.slice(0, cursor))) return true;
|
||||
if (isAfterJoinModifierContext(sql.slice(0, cursor))) return true;
|
||||
if (/\bcall\s+(?:[A-Za-z_][\w$]*\.)?$/i.test(sql.slice(0, cursor))) return true;
|
||||
const context = getSqlCompletionContext(sql, cursor);
|
||||
const context = getSqlCompletionContext(sql, cursor, options);
|
||||
if (previousChar === "(" && context.insertTable) return true;
|
||||
if (/[,;()[\]]/.test(previousChar)) return false;
|
||||
if (context.exclusiveTableSuggestions || context.exclusiveRoutineSuggestions || context.suggestTables) {
|
||||
@@ -1559,17 +1562,25 @@ function getSqlLexicalContext(sql: string, cursor: number): { inLineComment: boo
|
||||
};
|
||||
}
|
||||
|
||||
export function isSqlLikeCompletionStatement(sql: string, cursor: number): boolean {
|
||||
const statement = extractStatementAt(sql, cursor).trimStart();
|
||||
export function isSqlLikeCompletionStatement(sql: string, cursor: number, options: SqlSemanticBuildOptions = {}): boolean {
|
||||
const activeStatementSpan = activeSqlCompletionStatementSpan(sql, cursor, options);
|
||||
const lineBlock = currentSqlLikeLineBlockSpan(sql, cursor, activeStatementSpan);
|
||||
const statementSpan = lineBlock ?? activeStatementSpan;
|
||||
const statement = sql.slice(statementSpan.start, statementSpan.end).trimStart();
|
||||
if (/^(select|with)\b/i.test(statement)) return true;
|
||||
return currentLineBlockStartsSql(sql, cursor);
|
||||
return lineBlock != null;
|
||||
}
|
||||
|
||||
function currentLineBlockStartsSql(sql: string, cursor: number): boolean {
|
||||
return currentSqlLikeLineBlockSpan(sql, cursor) != null;
|
||||
function activeSqlCompletionStatementSpan(sql: string, cursor: number, options: SqlSemanticBuildOptions): SqlSemanticSpan {
|
||||
const safeCursor = Math.max(0, Math.min(cursor, sql.length));
|
||||
const dialectId = options.databaseType || options.dialect ? sqlSemanticDialectFor(options).id : "mysql";
|
||||
const tokens = tokenizeSqlSemantic(sql, dialectId);
|
||||
const statementSpan = findActiveSqlStatementSpan(sql, tokens, safeCursor);
|
||||
const firstStatementToken = tokens.find((token) => token.kind !== "comment" && token.span.end > statementSpan.start && token.span.start < statementSpan.end);
|
||||
return firstStatementToken ? { start: firstStatementToken.span.start, end: statementSpan.end } : statementSpan;
|
||||
}
|
||||
|
||||
function currentSqlLikeLineBlockSpan(sql: string, cursor: number): { start: number; end: number } | null {
|
||||
function currentSqlLikeLineBlockSpan(sql: string, cursor: number, activeStatementSpan: SqlSemanticSpan): SqlSemanticSpan | null {
|
||||
const safeCursor = Math.max(0, Math.min(cursor, sql.length));
|
||||
const beforeCursor = sql.slice(0, safeCursor);
|
||||
const lines = beforeCursor.split(/\r?\n/);
|
||||
@@ -1587,24 +1598,10 @@ function currentSqlLikeLineBlockSpan(sql: string, cursor: number): { start: numb
|
||||
}
|
||||
|
||||
if (start == null) return null;
|
||||
|
||||
let end = sql.length;
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
for (let index = start; index < sql.length; index += 1) {
|
||||
const ch = sql[index];
|
||||
if (ch === "'" && !inDoubleQuote) inSingleQuote = !inSingleQuote;
|
||||
else if (ch === '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote;
|
||||
else if (ch === ";" && !inSingleQuote && !inDoubleQuote && index >= safeCursor) {
|
||||
end = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (activeStatementSpan.start > start) return null;
|
||||
|
||||
const blockEnd = currentLineBlockEnd(sql, safeCursor, start);
|
||||
if (blockEnd != null) end = Math.min(end, blockEnd);
|
||||
|
||||
return { start, end };
|
||||
return { start, end: blockEnd == null ? activeStatementSpan.end : Math.min(activeStatementSpan.end, blockEnd) };
|
||||
}
|
||||
|
||||
function currentLineBlockEnd(sql: string, cursor: number, start: number): number | null {
|
||||
@@ -1650,53 +1647,9 @@ export function getSqlFunctionSignatureHelp(sql: string, cursor: number, databas
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the start position of the SQL statement containing the cursor.
|
||||
* Respects semicolons and string literals.
|
||||
*/
|
||||
function extractStatementStart(sql: string, cursor: number): number {
|
||||
const lineBlock = currentSqlLikeLineBlockSpan(sql, cursor);
|
||||
if (lineBlock) return lineBlock.start;
|
||||
|
||||
let start = 0;
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
for (let i = 0; i < sql.length; i++) {
|
||||
const ch = sql[i];
|
||||
if (ch === "'" && !inDoubleQuote) inSingleQuote = !inSingleQuote;
|
||||
else if (ch === '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote;
|
||||
else if (ch === ";" && !inSingleQuote && !inDoubleQuote) {
|
||||
if (i < cursor) {
|
||||
start = i + 1;
|
||||
while (start < sql.length && /\s/.test(sql[start])) start++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the full SQL statement that contains the cursor position.
|
||||
* Respects semicolons and string literals.
|
||||
*/
|
||||
function extractStatementAt(sql: string, cursor: number): string {
|
||||
const lineBlock = currentSqlLikeLineBlockSpan(sql, cursor);
|
||||
if (lineBlock) return sql.slice(lineBlock.start, lineBlock.end).trim();
|
||||
|
||||
const start = extractStatementStart(sql, cursor);
|
||||
let end = sql.length;
|
||||
let inSingleQuote = false;
|
||||
let inDoubleQuote = false;
|
||||
for (let i = start; i < sql.length; i++) {
|
||||
const ch = sql[i];
|
||||
if (ch === "'" && !inDoubleQuote) inSingleQuote = !inSingleQuote;
|
||||
else if (ch === '"' && !inSingleQuote) inDoubleQuote = !inDoubleQuote;
|
||||
else if (ch === ";" && !inSingleQuote && !inDoubleQuote && i >= cursor) {
|
||||
end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sql.slice(start, end).trim();
|
||||
function sqlCompletionStatementSpan(sql: string, cursor: number, options: SqlSemanticBuildOptions): SqlSemanticSpan {
|
||||
const activeStatementSpan = activeSqlCompletionStatementSpan(sql, cursor, options);
|
||||
return currentSqlLikeLineBlockSpan(sql, cursor, activeStatementSpan) ?? activeStatementSpan;
|
||||
}
|
||||
|
||||
function detectStatementKind(previousStatements: string): SqlStatementKind {
|
||||
@@ -1767,13 +1720,13 @@ function skipSqlWhitespaceAndComments(sql: string, pos: number): number {
|
||||
}
|
||||
}
|
||||
|
||||
export function getSqlCompletionContext(sql: string, cursor: number): SqlCompletionContext {
|
||||
export function getSqlCompletionContext(sql: string, cursor: number, options: SqlSemanticBuildOptions = {}): SqlCompletionContext {
|
||||
const statementSpan = sqlCompletionStatementSpan(sql, cursor, options);
|
||||
// Extract the full statement at cursor position for referenced tables
|
||||
const fullStatement = extractStatementAt(sql, cursor);
|
||||
const fullStatement = sql.slice(statementSpan.start, statementSpan.end).trim();
|
||||
|
||||
// Content before cursor within the current statement
|
||||
const stmtStart = extractStatementStart(sql, cursor);
|
||||
const beforeCursor = sql.slice(stmtStart, cursor);
|
||||
const beforeCursor = sql.slice(statementSpan.start, cursor);
|
||||
|
||||
const trailingIdentifier = parseTrailingIdentifierContext(beforeCursor);
|
||||
const prefix = trailingIdentifier?.prefix ?? "";
|
||||
|
||||
Reference in new issue
Block a user