fix(clickhouse): keep ALTER TABLE UPDATE mutations together

This commit is contained in:
t8y2
2026-07-21 16:38:24 +08:00
parent 4b95a874e3
commit eb91f08369
2 changed files with 50 additions and 0 deletions
@@ -443,6 +443,44 @@ COMMENT = '测试';`;
expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]);
});
it("keeps issue #4045 ClickHouse ALTER TABLE UPDATE mutation together", () => {
const sql = `ALTER TABLE m2_db.history_data5
UPDATE value = 14.06
WHERE uuid IN (
SELECT uuid
FROM m2_db.pt_uuid_mapping
WHERE point_id = 'TEST_Location_1_L2_METER_2_AE_DIFF_H'
)
AND ts = toDateTime('2026-07-15 12:00:00');`;
expect(statementRangeAtCursor(sql, indexOf(sql, "ALTER"), "clickhouse")?.sql.trim()).toBe(sql.slice(0, -1));
expect(statementRangeAtCursor(sql, indexOf(sql, "UPDATE"), "clickhouse")?.sql.trim()).toBe(sql.slice(0, -1));
expect(rangeSqlTexts(executableStatementRanges(sql, "clickhouse"))).toEqual([sql.slice(0, -1)]);
});
it("keeps a simple ClickHouse ALTER TABLE UPDATE mutation together", () => {
const sql = "ALTER TABLE `events` ON CLUSTER 'analytics'\nUPDATE value = 1\nWHERE id = 42;";
expect(statementRangeAtCursor(sql, indexOf(sql, "UPDATE"), "clickhouse")?.sql.trim()).toBe(sql.slice(0, -1));
expect(rangeSqlTexts(executableStatementRanges(sql, "clickhouse"))).toEqual([sql.slice(0, -1)]);
});
it("separates statements after a ClickHouse ALTER TABLE UPDATE mutation", () => {
const mutation = "ALTER TABLE events\nUPDATE value = 1\nWHERE id = 42";
const sql = `${mutation};\nSELECT * FROM events;\nUPDATE audit SET processed = 1;`;
expect(rangeSqlTexts(executableStatementRanges(sql, "clickhouse"))).toEqual([mutation, "SELECT * FROM events", "UPDATE audit SET processed = 1"]);
});
it("does not merge independent UPDATE statements into other ALTER statements", () => {
const otherDatabaseSql = "ALTER TABLE events\nUPDATE audit SET processed = 1;";
const clickHouseSql = "ALTER TABLE events ADD COLUMN source String\nUPDATE audit SET processed = 1;";
expect(rangeSqlTexts(executableStatementRanges(otherDatabaseSql, "postgres"))).toEqual(["ALTER TABLE events", "UPDATE audit SET processed = 1"]);
expect(rangeSqlTexts(executableStatementRanges(otherDatabaseSql, "mysql"))).toEqual(["ALTER TABLE events", "UPDATE audit SET processed = 1"]);
expect(rangeSqlTexts(executableStatementRanges(clickHouseSql, "clickhouse"))).toEqual(["ALTER TABLE events ADD COLUMN source String", "UPDATE audit SET processed = 1"]);
});
it("keeps insert-select with the INSERT statement", () => {
const sql = "INSERT INTO archived_users (id, name)\nSELECT id, name FROM users\nUPDATE users SET archived = 1;";
const range = statementRangeAtCursor(sql, indexOf(sql, "archived_users"));
@@ -246,6 +246,7 @@ const EXPLAIN_STATEMENT_KEYWORDS = new Set(["SELECT", "WITH", "INSERT", "UPDATE"
const CREATE_BODY_KEYWORDS = new Set(["SELECT", "WITH", "BEGIN", "DECLARE"]);
const INSERT_BODY_KEYWORDS = new Set(["SELECT", "WITH"]);
const ALTER_BODY_KEYWORDS = new Set(["ADD", "ALTER", "COMMENT", "DROP", "MODIFY", "RENAME", "SET"]);
const CLICKHOUSE_ALTER_TABLE_HEADER = /^ALTER\s+TABLE\s+(?:(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+")\s*\.\s*)?(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+")(?:\s+ON\s+CLUSTER\s+(?:[A-Za-z_][\w$]*|`(?:``|[^`])+`|"(?:""|[^"])+"|'(?:''|[^'])+'))?\s*$/i;
const SET_OPERATION_KEYWORDS = new Set(["UNION", "INTERSECT", "EXCEPT", "MINUS"]);
const SET_OPERATION_MODIFIER_KEYWORDS = new Set(["ALL", "DISTINCT"]);
const ORACLE_LIKE_PL_SQL_DATABASES: ReadonlySet<DatabaseType> = new Set(["oracle", "dameng", "gaussdb", "yashandb", "oscar", "oceanbase-oracle"]);
@@ -682,6 +683,12 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d
continue;
}
if (currentBodyKeyword === "ALTER" && isClickHouseAlterTableUpdateContinuation(sql, boundaries[boundaries.length - 1].from, lineStart.from, lineStart.keyword, databaseType)) {
// ClickHouse mutations use UPDATE as the first ALTER TABLE action, not as
// a standalone statement. Keep this dialect-specific to preserve soft boundaries elsewhere.
continue;
}
if (currentBodyKeyword === "ALTER" && ALTER_BODY_KEYWORDS.has(lineStart.keyword)) {
continue;
}
@@ -917,6 +924,11 @@ function isMysqlCreateTableOptionContinuation(sql: string, statementFrom: number
return next === "=" || next === "'" || next === '"';
}
function isClickHouseAlterTableUpdateContinuation(sql: string, statementFrom: number, lineStartFrom: number, keyword: string, databaseType?: DatabaseType): boolean {
if (databaseType !== "clickhouse" || keyword !== "UPDATE") return false;
return CLICKHOUSE_ALTER_TABLE_HEADER.test(sql.slice(statementFrom, lineStartFrom));
}
function startsWithMysqlCreateTable(sql: string, statementFrom: number): boolean {
const text = sql.slice(statementFrom, statementFrom + 256);
return /^CREATE\s+(?:TEMPORARY\s+)?TABLE\b/i.test(text);