Skip to content

Commit 15e750b

Browse files
committed
fix(doris): guard _fetch_native_df disconnect retry and reject CTE-prefixed write retries
1 parent fc8c743 commit 15e750b

2 files changed

Lines changed: 47 additions & 17 deletions

File tree

‎sqlmesh/core/engine_adapter/doris.py‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -595,7 +595,16 @@ def _fetch_native_df(
595595
isinstance(getattr(e, "__context__", None), TimeoutError)
596596
or "timed out" in str(e).lower()
597597
)
598-
if is_disconnect and not is_timeout and not self._connection_pool.is_transaction_active:
598+
if (
599+
is_disconnect
600+
and not is_timeout
601+
and not self._connection_pool.is_transaction_active
602+
and self._is_safe_to_retry_sql(
603+
self._to_sql(query, quote=quote_identifiers)
604+
if isinstance(query, exp.Expr)
605+
else query
606+
)
607+
):
599608
logger.warning(
600609
"Doris connection lost (%s: %s) during fetchdf, reconnecting and retrying once...",
601610
err_name,
@@ -616,14 +625,23 @@ def _fetch_native_df(
616625
raise
617626

618627
_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",
628+
r"\A(?:\s|/\*.*?\*/|--[^\r\n]*(?:[\r\n]|\Z))*(SELECT|WITH|SHOW|DESCRIBE|DESC|EXPLAIN|USE|SWITCH)\b",
620629
re.IGNORECASE | re.DOTALL,
621630
)
622631

623632
@classmethod
624633
def _is_safe_to_retry_sql(cls, sql: str) -> bool:
625634
"""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))
635+
match = cls._SAFE_RETRY_SQL_PREFIX_RE.match(sql)
636+
if not match:
637+
return False
638+
if match.group(1).upper() == "WITH":
639+
try:
640+
parsed = parse_one(sql, read=cls.DIALECT)
641+
except Exception:
642+
return False
643+
return isinstance(parsed, (exp.Select, exp.SetOperation))
644+
return True
627645

628646
def ping(self) -> None:
629647
self._connection_pool.get().ping(reconnect=False)

‎tests/core/engine_adapter/test_doris.py‎

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -951,23 +951,27 @@ def timeout_side_effect(*args, **kwargs):
951951
conn_mock.ping.assert_not_called()
952952
close_spy.assert_called_once()
953953

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

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")
963+
def write_side_effect(*args, **kwargs):
964+
nonlocal write_calls
965+
write_calls += 1
966+
raise OperationalError(2013, "Lost connection to MySQL server during query")
963967

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

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

972976

973977
def test_external_catalog_view_error_and_drop_noop(
@@ -2203,6 +2207,14 @@ def fake_read_sql_query(sql, con, *args, **kwargs):
22032207
assert fetch_calls == 2
22042208
assert close_spy.call_count == 1
22052209

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

22072219
def test_strip_for_update_and_multi_col_index_and_partition_by_alias_and_deferred_mv_audit_refresh(
22082220
make_mocked_engine_adapter: t.Callable[..., DorisEngineAdapter],

0 commit comments

Comments
 (0)