Skip to content

Commit 958e045

Browse files
committed
fix(doris): guard _fetch_native_df disconnect retry and reject CTE-prefixed write retries
Signed-off-by: Bruce Arctor <5032356+brucearctor@users.noreply.github.com>
1 parent 50563e8 commit 958e045

4 files changed

Lines changed: 51 additions & 17 deletions

File tree

‎sqlmesh/core/engine_adapter/doris.py‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# SPDX-License-Identifier: Apache-2.0
12
from __future__ import annotations
23

34
from collections import defaultdict
@@ -595,7 +596,16 @@ def _fetch_native_df(
595596
isinstance(getattr(e, "__context__", None), TimeoutError)
596597
or "timed out" in str(e).lower()
597598
)
598-
if is_disconnect and not is_timeout and not self._connection_pool.is_transaction_active:
599+
if (
600+
is_disconnect
601+
and not is_timeout
602+
and not self._connection_pool.is_transaction_active
603+
and self._is_safe_to_retry_sql(
604+
self._to_sql(query, quote=quote_identifiers)
605+
if isinstance(query, exp.Expr)
606+
else query
607+
)
608+
):
599609
logger.warning(
600610
"Doris connection lost (%s: %s) during fetchdf, reconnecting and retrying once...",
601611
err_name,
@@ -616,14 +626,23 @@ def _fetch_native_df(
616626
raise
617627

618628
_SAFE_RETRY_SQL_PREFIX_RE = re.compile(
619-
r"\A(?:\s|/\*.*?\*/|--[^\r\n]*(?:[\r\n]|\Z))*(?:SELECT|WITH|SHOW|DESCRIBE|DESC|EXPLAIN|USE|SWITCH)\b",
629+
r"\A(?:\s|/\*.*?\*/|--[^\r\n]*(?:[\r\n]|\Z))*(SELECT|WITH|SHOW|DESCRIBE|DESC|EXPLAIN|USE|SWITCH)\b",
620630
re.IGNORECASE | re.DOTALL,
621631
)
622632

623633
@classmethod
624634
def _is_safe_to_retry_sql(cls, sql: str) -> bool:
625635
"""Returns True only for read-only or idempotent catalog/database switch statements that cannot duplicate writes."""
626-
return bool(cls._SAFE_RETRY_SQL_PREFIX_RE.match(sql))
636+
match = cls._SAFE_RETRY_SQL_PREFIX_RE.match(sql)
637+
if not match:
638+
return False
639+
if match.group(1).upper() == "WITH":
640+
try:
641+
parsed = parse_one(sql, read=cls.DIALECT)
642+
except Exception:
643+
return False
644+
return isinstance(parsed, (exp.Select, exp.SetOperation))
645+
return True
627646

628647
def ping(self) -> None:
629648
self._connection_pool.get().ping(reconnect=False)

‎tests/core/engine_adapter/integration/docker/compose.doris.yaml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# SPDX-License-Identifier: Apache-2.0
12
services:
23
doris-fe:
34
image: apache/doris:fe-4.1.5

‎tests/core/engine_adapter/integration/test_integration_doris.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# SPDX-License-Identifier: Apache-2.0
12
from __future__ import annotations
23

34
import typing as t

‎tests/core/engine_adapter/test_doris.py‎

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# SPDX-License-Identifier: Apache-2.0
12
import typing as t
23

34
import pandas as pd # noqa: TID253
@@ -951,23 +952,27 @@ def timeout_side_effect(*args, **kwargs):
951952
conn_mock.ping.assert_not_called()
952953
close_spy.assert_called_once()
953954

954-
# Write statements (e.g. INSERT) must NOT be retried after OperationalError(2013) to avoid duplicating rows
955-
conn_mock.reset_mock()
956-
close_spy.reset_mock()
957-
write_calls = 0
955+
# Write statements (e.g. INSERT or CTE-prefixed INSERT) must NOT be retried after OperationalError(2013) to avoid duplicating rows
956+
for unsafe_write_sql in (
957+
"INSERT INTO `tbl` VALUES (1)",
958+
"WITH `cte` AS (SELECT 1) INSERT INTO `tbl` SELECT * FROM `cte`",
959+
):
960+
conn_mock.reset_mock()
961+
close_spy.reset_mock()
962+
write_calls = 0
958963

959-
def write_side_effect(*args, **kwargs):
960-
nonlocal write_calls
961-
write_calls += 1
962-
raise OperationalError(2013, "Lost connection to MySQL server during query")
964+
def write_side_effect(*args, **kwargs):
965+
nonlocal write_calls
966+
write_calls += 1
967+
raise OperationalError(2013, "Lost connection to MySQL server during query")
963968

964-
adapter.cursor.execute.side_effect = write_side_effect
965-
with pytest.raises(OperationalError):
966-
adapter._execute("INSERT INTO `tbl` VALUES (1)")
969+
adapter.cursor.execute.side_effect = write_side_effect
970+
with pytest.raises(OperationalError):
971+
adapter._execute(unsafe_write_sql)
967972

968-
assert write_calls == 1
969-
conn_mock.ping.assert_not_called()
970-
close_spy.assert_called_once()
973+
assert write_calls == 1
974+
conn_mock.ping.assert_not_called()
975+
close_spy.assert_called_once()
971976

972977

973978
def test_external_catalog_view_error_and_drop_noop(
@@ -2203,6 +2208,14 @@ def fake_read_sql_query(sql, con, *args, **kwargs):
22032208
assert fetch_calls == 2
22042209
assert close_spy.call_count == 1
22052210

2211+
# 4. _fetch_native_df does NOT retry non-read-only queries on disconnect
2212+
fetch_calls = 0
2213+
close_spy.reset_mock()
2214+
with pytest.raises(OperationalError):
2215+
adapter.fetchdf("WITH `cte` AS (SELECT 1) INSERT INTO `tbl` SELECT * FROM `cte`")
2216+
assert fetch_calls == 1
2217+
assert close_spy.call_count == 1
2218+
22062219

22072220
def test_strip_for_update_and_multi_col_index_and_partition_by_alias_and_deferred_mv_audit_refresh(
22082221
make_mocked_engine_adapter: t.Callable[..., DorisEngineAdapter],

0 commit comments

Comments
 (0)