Skip to content

Commit 9e0a2a5

Browse files
committed
fix(clickhouse): re-raise non-NOT_IMPLEMENTED errors from EXCHANGE TABLES
_exchange_tables caught every DatabaseError and acted only on NOT_IMPLEMENTED, so any other failure was dropped and a FULL model reported success over stale data. Fixes #6087 Signed-off-by: wolfgang-aura <169568318+wolfgang-aura@users.noreply.github.com>
1 parent af1dc08 commit 9e0a2a5

2 files changed

Lines changed: 94 additions & 11 deletions

File tree

‎sqlmesh/core/engine_adapter/clickhouse.py‎

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -668,17 +668,19 @@ def _exchange_tables(
668668
f"EXCHANGE TABLES {old_table_sql} AND {new_table_sql}{self._on_cluster_sql()}"
669669
)
670670
except DatabaseError as e:
671-
if "NOT_IMPLEMENTED" in str(e):
672-
# If someone is using an old Clickhouse version, an OS that doesn't support atomic exchanges,
673-
# or a database engine that doesn't support atomic exchanges, we do a non-atomic rename instead.
674-
#
675-
# Executing multiple renames in one call like `RENAME TABLE a to b, c to a` is supported
676-
# but not an atomic operation. Because it is not atomic, doing it in two calls is equivalent
677-
# and does not require defining an additional method.
678-
throwaway_table_name = self._get_temp_table(old_table_name)
679-
self._rename_table(old_table_name, throwaway_table_name)
680-
self._rename_table(new_table_name, old_table_name)
681-
self.drop_table(throwaway_table_name)
671+
if "NOT_IMPLEMENTED" not in str(e):
672+
raise
673+
674+
# If someone is using an old Clickhouse version, an OS that doesn't support atomic exchanges,
675+
# or a database engine that doesn't support atomic exchanges, we do a non-atomic rename instead.
676+
#
677+
# Executing multiple renames in one call like `RENAME TABLE a to b, c to a` is supported
678+
# but not an atomic operation. Because it is not atomic, doing it in two calls is equivalent
679+
# and does not require defining an additional method.
680+
throwaway_table_name = self._get_temp_table(old_table_name)
681+
self._rename_table(old_table_name, throwaway_table_name)
682+
self._rename_table(new_table_name, old_table_name)
683+
self.drop_table(throwaway_table_name)
682684

683685
def _rename_table(
684686
self,

‎tests/core/engine_adapter/test_clickhouse.py‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1428,6 +1428,87 @@ def test_exchange_tables(
14281428
]
14291429

14301430

1431+
def _executed_sql(execute_mock: t.Any) -> t.List[str]:
1432+
return [
1433+
quote_identifiers(call.args[0]).sql("clickhouse")
1434+
if isinstance(call.args[0], exp.Expr)
1435+
else call.args[0]
1436+
for call in execute_mock.call_args_list
1437+
]
1438+
1439+
1440+
def test_exchange_tables_reraises_other_errors(
1441+
make_mocked_engine_adapter: t.Callable, mocker: MockerFixture, make_temp_table_name: t.Callable
1442+
):
1443+
from clickhouse_connect.driver.exceptions import DatabaseError # type: ignore
1444+
1445+
adapter = make_mocked_engine_adapter(ClickhouseEngineAdapter)
1446+
1447+
temp_table_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter._get_temp_table")
1448+
temp_table_mock.return_value = make_temp_table_name("table1", "abcd")
1449+
1450+
execute_mock = mocker.patch("sqlmesh.core.engine_adapter.ClickhouseEngineAdapter.execute")
1451+
execute_mock.side_effect = [
1452+
DatabaseError("DB::Exception: Not enough privileges. (ACCESS_DENIED)"),
1453+
None,
1454+
None,
1455+
None,
1456+
]
1457+
1458+
with pytest.raises(DatabaseError, match="ACCESS_DENIED"):
1459+
adapter._exchange_tables("table1", "table2")
1460+
1461+
# No RENAME fallback and no throwaway table drop
1462+
assert _executed_sql(execute_mock) == ['EXCHANGE TABLES "table1" AND "table2"']
1463+
1464+
1465+
def test_insert_overwrite_by_condition_replace_exchange_error_propagates(
1466+
make_mocked_engine_adapter: t.Callable, mocker: MockerFixture, make_temp_table_name: t.Callable
1467+
):
1468+
from clickhouse_connect.driver.exceptions import DatabaseError # type: ignore
1469+
1470+
adapter = make_mocked_engine_adapter(ClickhouseEngineAdapter)
1471+
1472+
temp_table_mock = mocker.patch("sqlmesh.core.engine_adapter.EngineAdapter._get_temp_table")
1473+
temp_table_mock.return_value = make_temp_table_name("target", "abcd")
1474+
1475+
fetchone_mock = mocker.patch("sqlmesh.core.engine_adapter.ClickhouseEngineAdapter.fetchone")
1476+
fetchone_mock.return_value = None
1477+
1478+
insert_table_name = make_temp_table_name("new_records", "abcd")
1479+
existing_table_name = make_temp_table_name("existing_records", "abcd")
1480+
1481+
source_queries, columns_to_types = adapter._get_source_queries_and_columns_to_types(
1482+
parse_one(f"SELECT * FROM {insert_table_name}"),
1483+
{
1484+
"id": exp.DataType.build("Int8", dialect="clickhouse"),
1485+
"ds": exp.DataType.build("Date", dialect="clickhouse"),
1486+
},
1487+
existing_table_name,
1488+
)
1489+
1490+
def execute_side_effect(sql: t.Any, *args: t.Any, **kwargs: t.Any) -> None:
1491+
if str(sql).startswith("EXCHANGE TABLES"):
1492+
raise DatabaseError("DB::Exception: Not enough privileges. (ACCESS_DENIED)")
1493+
1494+
execute_mock = mocker.patch("sqlmesh.core.engine_adapter.ClickhouseEngineAdapter.execute")
1495+
execute_mock.side_effect = execute_side_effect
1496+
1497+
with pytest.raises(DatabaseError, match="ACCESS_DENIED"):
1498+
adapter._insert_overwrite_by_condition(
1499+
existing_table_name.sql(),
1500+
source_queries,
1501+
columns_to_types,
1502+
)
1503+
1504+
executed = _executed_sql(execute_mock)
1505+
assert executed[-2:] == [
1506+
'EXCHANGE TABLES "__temp_existing_records_abcd" AND "__temp_target_abcd"',
1507+
'DROP TABLE IF EXISTS "__temp_target_abcd"',
1508+
]
1509+
assert not any(sql.startswith("RENAME") for sql in executed)
1510+
1511+
14311512
def test_virtual_catalog_ddl_stripping(make_mocked_engine_adapter: t.Callable):
14321513
"""After inject_virtual_catalog(), create_schema() with the virtual catalog prefix must strip
14331514
the catalog and execute without raising, and with a wrong catalog must raise SQLMeshError."""

0 commit comments

Comments
 (0)