From d24cd987761e6813f773968a6869821f8ff47c6d Mon Sep 17 00:00:00 2001 From: zipg Date: Tue, 28 Jul 2026 21:08:24 +0800 Subject: [PATCH] fix(completion): isolate aliases across SQL statements --- .../src/components/editor/QueryEditor.vue | 43 ++++--- .../__tests__/sql/semantic/completion.spec.ts | 53 ++++++++- .../lib/__tests__/sql/semantic/tokens.spec.ts | 5 +- .../src/lib/sql/semantic/diagnostics.ts | 4 +- apps/desktop/src/lib/sql/semantic/tokens.ts | 8 +- apps/desktop/src/lib/sql/sqlCompletion.ts | 105 +++++------------- 6 files changed, 114 insertions(+), 104 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 840df2ee9..160462e94 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -407,6 +407,13 @@ const cachedInsertValueHintColumnsByTable = new Map(); const cachedForeignKeysByTable = new Map(); const loadedColumnsByTable = new Set(); +function sqlCompletionDialectOptions() { + return { + databaseType: props.databaseType, + dialect: props.syntaxDialect ?? props.dialect, + }; +} + function usesOracleSessionCompletionColumns(schema?: string | null): boolean { return shouldUseOracleSessionCompletionColumns({ databaseType: props.databaseType, @@ -1780,12 +1787,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number) 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.syntaxDialect ?? props.dialect, - }) - : null; + const semanticModel = SEMANTIC_SQL_COMPLETION_ENABLED ? buildSqlSemanticModel(sql, pos, sqlCompletionDialectOptions()) : 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; @@ -1814,7 +1816,7 @@ async function resolveSqlHoverTooltip(currentView: EditorViewType, pos: number) }; } - const legacyContext = getSqlCompletionContext(sql, pos); + const legacyContext = getSqlCompletionContext(sql, pos, sqlCompletionDialectOptions()); 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; @@ -2509,7 +2511,7 @@ async function provideSqlCompletions(context: CompletionContext) { return provideMongoCompletions(currentState, position, explicit); } if (props.databaseType === "elasticsearch") { - if (!isSqlLikeCompletionStatement(fullDoc, position)) { + if (!isSqlLikeCompletionStatement(fullDoc, position, sqlCompletionDialectOptions())) { return provideElasticsearchCompletions(currentState, position, explicit); } } @@ -2522,15 +2524,10 @@ async function provideSqlCompletions(context: CompletionContext) { try { if (isSqlCompletionSuppressedContext(fullDoc, position)) return null; - if (!explicit && !shouldAutoOpenSqlCompletion(fullDoc, position)) return null; + if (!explicit && !shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions())) return null; - const legacyCompletionContext = getSqlCompletionContext(fullDoc, position); - const semanticModel = SEMANTIC_SQL_COMPLETION_ENABLED - ? buildSqlSemanticModel(fullDoc, position, { - databaseType: props.databaseType, - dialect: props.syntaxDialect ?? props.dialect, - }) - : null; + const legacyCompletionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions()); + const semanticModel = SEMANTIC_SQL_COMPLETION_ENABLED ? buildSqlSemanticModel(fullDoc, position, sqlCompletionDialectOptions()) : null; const completionContext = semanticModel ? sqlCompletionContextFromSemantic(semanticModel, legacyCompletionContext) : legacyCompletionContext; if (!hasDatabase) { @@ -2640,7 +2637,7 @@ function flushImeComposition() { emit("cursorChange", currentView.state.selection.main.head); latestSelection = readEditorSelection(currentView); if (editorIsActive) emitEditorSelection(latestSelection); - if (shouldAutoOpenSqlCompletion(currentView.state.doc.toString(), currentView.state.selection.main.head)) { + if (shouldAutoOpenSqlCompletion(currentView.state.doc.toString(), currentView.state.selection.main.head, sqlCompletionDialectOptions())) { scheduleSqlCompletionStart(currentView); } } @@ -2649,20 +2646,20 @@ function shouldStartSqlCompletionAfterInput(insertedText: string, removedText: s const position = currentView.state.selection.main.head; const fullDoc = currentView.state.doc.toString(); if (!insertedText && removedText) { - const completionContext = getSqlCompletionContext(fullDoc, position); - return isTableNameCompletionContext(completionContext) && shouldAutoOpenSqlCompletion(fullDoc, position); + const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions()); + return isTableNameCompletionContext(completionContext) && shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions()); } if (insertedText.endsWith(".")) return true; if (/[,(]$/.test(insertedText)) { - const completionContext = getSqlCompletionContext(fullDoc, position); + const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions()); return !!completionContext.insertTable; } if (/\s$/.test(insertedText)) { - return shouldAutoOpenSqlCompletion(fullDoc, position); + return shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions()); } if (!/[\w$@]$/.test(insertedText)) return false; - const completionContext = getSqlCompletionContext(fullDoc, position); - return isTableNameCompletionContext(completionContext) || shouldAutoOpenSqlCompletion(fullDoc, position); + const completionContext = getSqlCompletionContext(fullDoc, position, sqlCompletionDialectOptions()); + return isTableNameCompletionContext(completionContext) || shouldAutoOpenSqlCompletion(fullDoc, position, sqlCompletionDialectOptions()); } function buildLocalSqlCompletionResult(completionContext: ReturnType, fullDoc: string, position: number) { diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts index 6e7e8f2d6..f05b88d94 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts @@ -16,7 +16,7 @@ function mergeColumns(...maps: Array | undefi function semanticCompletion(markedSql: string, input: Partial = {}, options: { databaseType?: DatabaseType; dialect?: "mysql" | "postgres" | "sqlserver" } = {}) { const { sql, cursor } = sqlFixtureCursor(markedSql); const model = buildSqlSemanticModel(sql, cursor, options); - const context = sqlCompletionContextFromSemantic(model, getSqlCompletionContext(sql, cursor)); + const context = sqlCompletionContextFromSemantic(model, getSqlCompletionContext(sql, cursor, options)); const columnsByTable = mergeColumns(sqlSemanticLocalColumnsByTable(model), input.columnsByTable); const items = buildSqlCompletionItemsFromContext(context, { tables: input.tables ?? [], @@ -35,6 +35,57 @@ function semanticCompletion(markedSql: string, input: Partial { + it("does not mix SELECT aliases into a following UPDATE statement", () => { + const columnsByTable = new Map([ + ["codex_completion_a", [{ name: "id", table: "codex_completion_a", schema: "public" }]], + ["codex_completion_b", [{ name: "id", table: "codex_completion_b", schema: "public" }]], + ]); + + const { context, items } = semanticCompletion("SELECT ph.id FROM codex_completion_a AS ph;\n\nUPDATE codex_completion_b\nSET status = 0\nWHERE id|", { columnsByTable }, { databaseType: "postgres", dialect: "postgres" }); + + expect(context.referencedTables).toEqual([expect.objectContaining({ name: "codex_completion_b" })]); + expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["id"]); + }); + + it("treats PostgreSQL hash operators as part of the preceding statement", () => { + const columnsByTable = new Map([ + ["codex_completion_a", [{ name: "legacy_id", table: "codex_completion_a", schema: "public" }]], + ["codex_completion_b", [{ name: "current_id", table: "codex_completion_b", schema: "public" }]], + ]); + + const { context, items } = semanticCompletion("SELECT ph.legacy_id # 1 FROM codex_completion_a AS ph;\nUPDATE codex_completion_b SET current_id = 0 WHERE ph.|", { columnsByTable }, { databaseType: "postgres", dialect: "postgres" }); + + expect(context.statementKind).toBe("update"); + expect(context.referencedTables).toEqual([expect.objectContaining({ name: "codex_completion_b" })]); + expect(items.filter((item) => item.type === "column").map((item) => item.label)).not.toContain("legacy_id"); + }); + + it("ignores line-comment semicolons after a real statement boundary", () => { + const columnsByTable = new Map([ + ["codex_completion_a", [{ name: "legacy_id", table: "codex_completion_a", schema: "public" }]], + ["codex_completion_b", [{ name: "current_id", table: "codex_completion_b", schema: "public" }]], + ]); + + const { context, items } = semanticCompletion("SELECT ph.legacy_id FROM codex_completion_a AS ph; -- separator ; trailing words\nUPDATE codex_completion_b SET current_id = 0 WHERE current_|", { columnsByTable }, { databaseType: "postgres", dialect: "postgres" }); + + expect(context.statementKind).toBe("update"); + expect(context.referencedTables).toEqual([expect.objectContaining({ name: "codex_completion_b" })]); + expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["current_id"]); + }); + + it("ignores block-comment semicolons after a real statement boundary", () => { + const columnsByTable = new Map([ + ["codex_completion_a", [{ name: "legacy_id", table: "codex_completion_a", schema: "public" }]], + ["codex_completion_b", [{ name: "current_id", table: "codex_completion_b", schema: "public" }]], + ]); + + const { context, items } = semanticCompletion("SELECT ph.legacy_id FROM codex_completion_a AS ph; /* separator ; trailing words */\nUPDATE codex_completion_b SET current_id = 0 WHERE current_|", { columnsByTable }, { databaseType: "postgres", dialect: "postgres" }); + + expect(context.statementKind).toBe("update"); + expect(context.referencedTables).toEqual([expect.objectContaining({ name: "codex_completion_b" })]); + expect(items.filter((item) => item.type === "column").map((item) => item.label)).toEqual(["current_id"]); + }); + it("loads nested alias columns through the database-qualified metadata key", () => { const { context, items } = semanticCompletion( "SELECT * FROM aa.tb t WHERE EXISTS (SELECT 1 FROM aa.tb1 t1, aa.tb2 t2 WHERE t1.|)", diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/tokens.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/tokens.spec.ts index e8c0c55bd..c3efd2aa3 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/tokens.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/tokens.spec.ts @@ -21,12 +21,15 @@ describe("sqlSemanticTokens", () => { expect(isSuppressedSqlSemanticContext(tokens, "select * from users".length)).toBe(false); }); - it("treats hash prefixes as identifiers for SQL Server but comments for MySQL", () => { + it("handles hash tokens according to the SQL dialect", () => { const sqlServerSql = "SELECT * FROM #temp; SELECT * FROM ##global_temp; SELECT * FROM tempdb..#temp"; const sqlServerTokens = tokenizeSqlSemantic(sqlServerSql, "sqlserver"); + const postgresTokens = tokenizeSqlSemantic("SELECT left_value#right_value", "postgres"); expect(sqlServerTokens.filter((token) => token.kind === "word" && token.text.startsWith("#")).map((token) => token.text)).toEqual(["#temp", "##global_temp", "#temp"]); expect(sqlServerTokens.some((token) => token.kind === "comment")).toBe(false); + expect(postgresTokens.some((token) => token.kind === "operator" && token.text === "#")).toBe(true); + expect(postgresTokens.some((token) => token.kind === "comment")).toBe(false); expect(tokenizeSqlSemantic("SELECT 1 # comment", "mysql").some((token) => token.kind === "comment" && token.text === "# comment")).toBe(true); }); diff --git a/apps/desktop/src/lib/sql/semantic/diagnostics.ts b/apps/desktop/src/lib/sql/semantic/diagnostics.ts index 638ec3359..ddbf175a4 100644 --- a/apps/desktop/src/lib/sql/semantic/diagnostics.ts +++ b/apps/desktop/src/lib/sql/semantic/diagnostics.ts @@ -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)); } diff --git a/apps/desktop/src/lib/sql/semantic/tokens.ts b/apps/desktop/src/lib/sql/semantic/tokens.ts index b333e7148..7fce81f11 100644 --- a/apps/desktop/src/lib/sql/semantic/tokens.ts +++ b/apps/desktop/src/lib/sql/semantic/tokens.ts @@ -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; } diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 96f9e4728..af56d0ea2 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -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 ?? "";