From a73adfdcf07e43b5c75a27494aae4f0544a02b60 Mon Sep 17 00:00:00 2001 From: zipg Date: Wed, 22 Jul 2026 18:42:42 +0800 Subject: [PATCH] fix(sql): recognize MySQL truncate partition statements --- .../__tests__/sql/sqlStatementRanges.spec.ts | 29 ++++++++++ .../desktop/src/lib/sql/sqlStatementRanges.ts | 54 ++++++++++++++++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index 227a3b02de..c6e5911fbe 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -443,6 +443,35 @@ COMMENT = '测试';`; expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); }); + it("keeps MySQL ALTER TABLE truncate partition clauses with the statement", () => { + const sql = "ALTER TABLE ems.r_r_curve_e_hour\nTRUNCATE PARTITION p202602, p202603, p202604, p202605, p202606;"; + + expect(statementRangeAtCursor(sql, indexOf(sql, "ALTER"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(statementRangeAtCursor(sql, indexOf(sql, "TRUNCATE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); + }); + + it("keeps MySQL truncate partition clauses when comments separate ALTER TABLE", () => { + const sql = "ALTER /* online ddl */ TABLE t\nTRUNCATE PARTITION p0;"; + + expect(statementRangeAtCursor(sql, indexOf(sql, "TRUNCATE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); + }); + + it("keeps MySQL truncate partition clauses when comments precede PARTITION", () => { + const sql = "ALTER TABLE t\nTRUNCATE /* keep */ PARTITION p0;"; + + expect(statementRangeAtCursor(sql, indexOf(sql, "TRUNCATE"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); + }); + + it("keeps standalone truncate table statements separate from preceding alter statements", () => { + const sql = "ALTER TABLE events ADD COLUMN source varchar(100)\nTRUNCATE TABLE events;"; + + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual(["ALTER TABLE events ADD COLUMN source varchar(100)", "TRUNCATE TABLE events"]); + expect(rangeSqlTexts(executableStatementRanges(sql, "postgres"))).toEqual(["ALTER TABLE events ADD COLUMN source varchar(100)", "TRUNCATE TABLE events"]); + }); + it("keeps issue #4045 ClickHouse ALTER TABLE UPDATE mutation together", () => { const sql = `ALTER TABLE m2_db.history_data5 UPDATE value = 14.06 diff --git a/apps/desktop/src/lib/sql/sqlStatementRanges.ts b/apps/desktop/src/lib/sql/sqlStatementRanges.ts index a8bc59b6ce..649274eaa1 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -689,6 +689,10 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d continue; } + if (currentBodyKeyword === "ALTER" && isMysqlAlterTableTruncatePartitionContinuation(sql, boundaries[boundaries.length - 1].from, lineStart.from, lineStart.keyword, databaseType)) { + continue; + } + if (currentBodyKeyword === "ALTER" && ALTER_BODY_KEYWORDS.has(lineStart.keyword)) { continue; } @@ -929,6 +933,12 @@ function isClickHouseAlterTableUpdateContinuation(sql: string, statementFrom: nu return CLICKHOUSE_ALTER_TABLE_HEADER.test(sql.slice(statementFrom, lineStartFrom)); } +function isMysqlAlterTableTruncatePartitionContinuation(sql: string, statementFrom: number, lineStartFrom: number, keyword: string, databaseType?: DatabaseType): boolean { + if (databaseType !== "mysql" || keyword !== "TRUNCATE") return false; + if (!startsWithSqlWords(sql, statementFrom, ["ALTER", "TABLE"])) return false; + return nextSqlWord(sql, lineStartFrom + keyword.length) === "PARTITION"; +} + function startsWithMysqlCreateTable(sql: string, statementFrom: number): boolean { const text = sql.slice(statementFrom, statementFrom + 256); return /^CREATE\s+(?:TEMPORARY\s+)?TABLE\b/i.test(text); @@ -1097,9 +1107,49 @@ function nextNonWhitespaceChar(sql: string, pos: number): string | null { } function nextSqlWord(sql: string, pos: number): string | null { + return nextSqlWordToken(sql, pos)?.word ?? null; +} + +function startsWithSqlWords(sql: string, pos: number, expectedWords: readonly string[]): boolean { let i = pos; - while (i < sql.length && isSqlWhitespace(sql[i])) i += 1; - return /^[A-Za-z_][\w$]*/.exec(sql.slice(i))?.[0]?.toUpperCase() ?? null; + for (const expectedWord of expectedWords) { + const token = nextSqlWordToken(sql, i); + if (token?.word !== expectedWord) return false; + i = token.end; + } + return true; +} + +function nextSqlWordToken(sql: string, pos: number): { word: string; end: number } | null { + let i = pos; + // SQL comments may legally appear anywhere whitespace can separate keywords. + while (i < sql.length) { + if (isSqlWhitespace(sql[i])) { + i += 1; + continue; + } + if (sql[i] === "-" && sql[i + 1] === "-") { + i += 2; + while (i < sql.length && sql[i] !== "\n" && sql[i] !== "\r") i += 1; + continue; + } + if (sql[i] === "#") { + i += 1; + while (i < sql.length && sql[i] !== "\n" && sql[i] !== "\r") i += 1; + continue; + } + if (sql[i] === "/" && sql[i + 1] === "*") { + const commentEnd = sql.indexOf("*/", i + 2); + if (commentEnd < 0) return null; + i = commentEnd + 2; + continue; + } + break; + } + + const match = /^[A-Za-z_][\w$]*/.exec(sql.slice(i)); + if (!match) return null; + return { word: match[0].toUpperCase(), end: i + match[0].length }; } function isExplainLikeKeyword(keyword: string | null): boolean {