From 1700341fb3b1e5a931d98442ac150bff237552b4 Mon Sep 17 00:00:00 2001 From: eilrovisoft Date: Wed, 27 May 2026 17:38:28 +0000 Subject: [PATCH 1/2] Added escaping for column name when backup is created --- ch_backup/clickhouse/control.py | 43 ++++++++++++++++++++++++++++++++- tests/unit/test_control.py | 23 +++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/ch_backup/clickhouse/control.py b/ch_backup/clickhouse/control.py index 89be3cf2..e69cef59 100644 --- a/ch_backup/clickhouse/control.py +++ b/ch_backup/clickhouse/control.py @@ -525,6 +525,46 @@ ) +def _fix_create_statement(create_statement: str) -> str: + """ + Fix DDL that was stored by ClickHouse without properly escaping reserved + keywords used as identifiers (column names, aliases). + + This is a known ClickHouse bug where keywords like TOP are not backtick-escaped + during DDL serialization to .sql files, making stored DDL unparseable on ATTACH. + + Example of broken DDL: + SELECT TOP AS c FROM default.test_3 + -- TOP is treated as TOP N syntax + + Fixed DDL: + SELECT `TOP` AS c FROM default.test_3 + """ + UNESCAPED_KEYWORDS = [ + "TOP", + ] + + fixed = create_statement + for keyword in UNESCAPED_KEYWORDS: + fixed = re.sub( + rf'(? None: """ Restore table. """ - self._ch_client.query(table.create_statement) + fixed_statement = _fix_create_statement(table.create_statement) + self._ch_client.query(fixed_statement) def restore_replica(self, table: Table) -> None: """ diff --git a/tests/unit/test_control.py b/tests/unit/test_control.py index 89d3b972..eb65e766 100644 --- a/tests/unit/test_control.py +++ b/tests/unit/test_control.py @@ -1,6 +1,6 @@ import pytest -from ch_backup.clickhouse.control import _format_string_array, _parse_version +from ch_backup.clickhouse.control import _format_string_array, _parse_version, _fix_create_statement from tests.unit.utils import parametrize @@ -49,3 +49,24 @@ def test_format_string_array(value, result): ) def test_parse_version(version: str, expected: list[int]) -> None: assert _parse_version(version) == expected + +def test_fix_create_statement_top_keyword(): + broken = ( + "ATTACH VIEW test_4 UUID '1c465905-164d-400b-8a39-ff382c92fb81' " + "(c String) AS WITH A AS (SELECT TOP AS c FROM default.test_3) " + "SELECT c FROM A" + ) + fixed = _fix_create_statement(broken) + + assert "`TOP`" in fixed + assert "SELECT `TOP` AS c" in fixed + + +def test_fix_create_statement_no_change_when_valid(): + valid = ( + "ATTACH VIEW test_4 UUID '1c465905-164d-400b-8a39-ff382c92fb81' " + "(c String) AS SELECT c FROM default.test_3" + ) + fixed = _fix_create_statement(valid) + + assert fixed == valid From 60b66f70290e0d530e9a7b1923fcdd9ca70cbf6a Mon Sep 17 00:00:00 2001 From: eilrovisoft Date: Wed, 27 May 2026 17:45:30 +0000 Subject: [PATCH 2/2] Fixed linter --- ch_backup/clickhouse/control.py | 13 +++++++------ tests/unit/test_control.py | 7 ++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/ch_backup/clickhouse/control.py b/ch_backup/clickhouse/control.py index e69cef59..b3d73c69 100644 --- a/ch_backup/clickhouse/control.py +++ b/ch_backup/clickhouse/control.py @@ -524,6 +524,10 @@ """ ) +UNESCAPED_KEYWORDS = [ + "TOP", +] + def _fix_create_statement(create_statement: str) -> str: """ @@ -531,7 +535,7 @@ def _fix_create_statement(create_statement: str) -> str: keywords used as identifiers (column names, aliases). This is a known ClickHouse bug where keywords like TOP are not backtick-escaped - during DDL serialization to .sql files, making stored DDL unparseable on ATTACH. + during DDL serialization to .sql files, making stored DDL unparsable on ATTACH. Example of broken DDL: SELECT TOP AS c FROM default.test_3 @@ -540,15 +544,12 @@ def _fix_create_statement(create_statement: str) -> str: Fixed DDL: SELECT `TOP` AS c FROM default.test_3 """ - UNESCAPED_KEYWORDS = [ - "TOP", - ] fixed = create_statement for keyword in UNESCAPED_KEYWORDS: fixed = re.sub( - rf'(? None: assert _parse_version(version) == expected + def test_fix_create_statement_top_keyword(): broken = ( "ATTACH VIEW test_4 UUID '1c465905-164d-400b-8a39-ff382c92fb81' "