From a593a2b20094973794cf4627dfa4f66d86e4d391 Mon Sep 17 00:00:00 2001 From: dienaso <89768641@qq.com> Date: Thu, 24 Sep 2026 17:18:06 +0800 Subject: [PATCH] fix(sql): keep SQL Server table hints out of soft statement starts --- .../__tests__/sql/sqlStatementRanges.spec.ts | 39 +++++++++++++++++++ .../desktop/src/lib/sql/sqlStatementRanges.ts | 5 +++ 2 files changed, 44 insertions(+) diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index e9cfdea9f6..692858a53b 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -1254,6 +1254,45 @@ COMMENT = '测试';`; expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual([sql.slice(0, -1)]); }); + it("keeps SQL Server table hints broken onto their own line with the host statement (#10098)", () => { + const sql = "SELECT u.id\nFROM users u\nWITH (NOLOCK)\nWHERE u.id = 1\nSELECT 2;"; + const expected = "SELECT u.id\nFROM users u\nWITH (NOLOCK)\nWHERE u.id = 1"; + + expect(rangeSqlTexts(executableStatementRanges(sql, "sqlserver"))).toEqual([expected, "SELECT 2"]); + expect(statementRangeAtCursor(sql, indexOf(sql, "NOLOCK"), "sqlserver")?.sql.trim()).toBe(expected); + expect(statementRangeAtCursor(sql, indexOf(sql, "WHERE"), "sqlserver")?.sql.trim()).toBe(expected); + }); + + it("keeps JOIN table hints broken onto their own line with the host statement", () => { + const sql = "SELECT *\nFROM a\nWITH (NOLOCK)\nJOIN b\nWITH (NOLOCK) ON a.id = b.id\nSELECT 2;"; + const expected = "SELECT *\nFROM a\nWITH (NOLOCK)\nJOIN b\nWITH (NOLOCK) ON a.id = b.id"; + + expect(rangeSqlTexts(executableStatementRanges(sql, "sqlserver"))).toEqual([expected, "SELECT 2"]); + }); + + it("keeps INSERT table hints broken onto their own line with the INSERT", () => { + const sql = "INSERT INTO t\nWITH (TABLOCK) (id)\nVALUES (1)\nSELECT 2;"; + // INSERT ... SELECT keeps the source SELECT attached; the table-hint WITH + // must not open a soft statement of its own inside the INSERT. + const expected = "INSERT INTO t\nWITH (TABLOCK) (id)\nVALUES (1)\nSELECT 2"; + + expect(rangeSqlTexts(executableStatementRanges(sql, "sqlserver"))).toEqual([expected]); + expect(statementRangeAtCursor(sql, indexOf(sql, "TABLOCK"), "sqlserver")?.sql.trim()).toBe(expected); + }); + + it("still treats a line-start CTE WITH as a soft statement start", () => { + const sql = "SELECT 1\nWITH x AS (SELECT 2)\nSELECT * FROM x\nSELECT 3;"; + + expect(rangeSqlTexts(executableStatementRanges(sql, "sqlserver"))).toEqual(["SELECT 1", "WITH x AS (SELECT 2)\nSELECT * FROM x", "SELECT 3"]); + expect(rangeSqlTexts(executableStatementRanges(sql))).toEqual(["SELECT 1", "WITH x AS (SELECT 2)\nSELECT * FROM x", "SELECT 3"]); + }); + + it("still treats WITH RECURSIVE as a soft statement start", () => { + const sql = "SELECT 1\nWITH RECURSIVE t AS (SELECT 2)\nSELECT * FROM t;"; + + expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual(["SELECT 1", "WITH RECURSIVE t AS (SELECT 2)\nSELECT * FROM t"]); + }); + it("keeps MySQL ALTER TABLE drop column clauses with the statement", () => { const sql = "ALTER TABLE t\n DROP COLUMN a,\n DROP COLUMN b;"; diff --git a/apps/desktop/src/lib/sql/sqlStatementRanges.ts b/apps/desktop/src/lib/sql/sqlStatementRanges.ts index 59ef2c28e3..38ceb78f94 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -1229,6 +1229,11 @@ function softStatementKeywordAt(sql: string, pos: number, databaseType?: Databas // COMMENT is also a common column name. Only COMMENT ON starts a standalone // SQL command; otherwise a line-start projection column must stay in SELECT. if (keyword === "COMMENT" && nextSqlWord(sql, pos + match[0].length, databaseType, parameterOptions) !== "ON") return null; + // `WITH (` is a SQL Server table hint (`FROM t WITH (NOLOCK)`), not a CTE + // opener — CTEs are always `WITH name AS (` / `WITH RECURSIVE name AS (`. + // Formatters break table hints onto their own line; without this, the + // following statement loses its run target (#10098). + if (keyword === "WITH" && nextNonWhitespaceChar(sql, pos + match[0].length) === "(") return null; return softStatementStartKeywords(databaseType).has(keyword) ? keyword : null; }