From cf36425b0aa150b425e11a6677618f7c9a3b0e5a Mon Sep 17 00:00:00 2001 From: t8y2 Date: Tue, 22 Sep 2026 15:57:23 +0800 Subject: [PATCH] fix(completion): bound statement block scans to active SQL Refs: #5997 --- .../sql/sqlCompletion.performance.spec.ts | 43 +++++++++++++++++++ apps/desktop/src/lib/sql/sqlCompletion.ts | 12 +++--- 2 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/lib/__tests__/sql/sqlCompletion.performance.spec.ts diff --git a/apps/desktop/src/lib/__tests__/sql/sqlCompletion.performance.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.performance.spec.ts new file mode 100644 index 0000000000..d206ce499f --- /dev/null +++ b/apps/desktop/src/lib/__tests__/sql/sqlCompletion.performance.spec.ts @@ -0,0 +1,43 @@ +import { PostgreSQL, sql } from "@codemirror/lang-sql"; +import { ensureSyntaxTree } from "@codemirror/language"; +import { EditorState } from "@codemirror/state"; +import { describe, expect, it, vi } from "vitest"; +import { getSqlCompletionContext } from "@/lib/sql/sqlCompletion"; + +describe("SQL completion statement-local scanning", () => { + it.each(["\n", " "])("does not split unrelated statements separated by %j before the active syntax-tree window", (separator) => { + const prefix = `SELECT id FROM previous_table;${separator}`.repeat(2_000); + const activeStatement = "SELECT customer_ FROM current_table"; + const suffix = ";\n" + "SELECT id FROM later_table;\n".repeat(2_000); + const document = prefix + activeStatement + suffix; + const cursor = prefix.length + activeStatement.indexOf(" FROM"); + const state = EditorState.create({ doc: document, extensions: [sql({ dialect: PostgreSQL })] }); + ensureSyntaxTree(state, document.length, 5_000); + const scannedLengths: number[] = []; + const originalSplit = String.prototype.split; + const splitSpy = vi.spyOn(String.prototype, "split").mockImplementation(function (this: string, separator, limit) { + if (separator instanceof RegExp && separator.source === "\\r?\\n") scannedLengths.push(this.length); + return originalSplit.call(this, separator, limit); + }); + + try { + const context = getSqlCompletionContext(document, cursor, { databaseType: "postgres", editorState: state }); + expect(context.referencedTables.map((table) => table.name)).toEqual(["current_table"]); + expect(scannedLengths.length).toBeGreaterThan(0); + expect(Math.max(...scannedLengths)).toBeLessThanOrEqual(activeStatement.length); + } finally { + splitSpy.mockRestore(); + } + }); + + it("preserves CRLF offsets when selecting a semicolon-free statement block", () => { + const document = "SELECT id FROM previous_table;\r\nSELECT id FROM first_table\r\nSELECT customer_ FROM current_table\r\nSELECT id FROM later_table"; + const cursor = document.indexOf("customer_") + "customer_".length; + const state = EditorState.create({ doc: document, extensions: [sql({ dialect: PostgreSQL })] }); + ensureSyntaxTree(state, document.length, 5_000); + + const context = getSqlCompletionContext(document, cursor, { databaseType: "postgres", editorState: state }); + expect(context.prefix).toBe("customer_"); + expect(context.referencedTables.map((table) => table.name)).toEqual(["current_table"]); + }); +}); diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index 3ded3d5232..f70cc9fa41 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -2211,10 +2211,10 @@ function activeSqlCompletionStatementSpan(sql: string, cursor: number, options: 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 beforeCursor = sql.slice(activeStatementSpan.start, safeCursor); const lines = beforeCursor.split(/\r?\n/); let start: number | null = null; - let offset = 0; + let offset = activeStatementSpan.start; for (const line of lines) { const trimmed = line.trimStart(); @@ -2223,14 +2223,16 @@ function currentSqlLikeLineBlockSpan(sql: string, cursor: number, activeStatemen if (/^(select|with)\b/i.test(trimmed)) start = offset + indentation; if (/^(get|post|put|delete|patch|head)\s+\//i.test(trimmed)) start = null; } - offset += line.length + 1; + offset += line.length; + offset += sql[offset] === "\r" && sql[offset + 1] === "\n" ? 2 : 1; } if (start == null) return null; if (activeStatementSpan.start > start) return null; - const blockEnd = currentLineBlockEnd(sql, safeCursor, start); - return { start, end: blockEnd == null ? activeStatementSpan.end : Math.min(activeStatementSpan.end, blockEnd) }; + const statementSql = sql.slice(activeStatementSpan.start, activeStatementSpan.end); + const blockEnd = currentLineBlockEnd(statementSql, safeCursor - activeStatementSpan.start, start - activeStatementSpan.start); + return { start, end: blockEnd == null ? activeStatementSpan.end : Math.min(activeStatementSpan.end, activeStatementSpan.start + blockEnd) }; } // Equal-length masking of string/comment characters, so parenthesis depth and