Skip to content

Commit 87b5fde

Browse files
Extract _build_and_connect shared by sync and async connections
The construct-then-connect-then-wrap sequence lived twice — in Connection._get_async_connection and AsyncConnection._ensure_connection with the same kwargs and the same "Failed to connect: ..." wrapping. A future governor addition would have to be threaded through both sites or drift silently. Factor the body into a module-local _build_and_connect coroutine in dqlitedbapi.connection and call it from both flavours. Preserve the OperationalError message phrasing verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e10d3c commit 87b5fde

File tree

5 files changed

+38
-17
lines changed

5 files changed

+38
-17
lines changed

src/dqlitedbapi/aio/connection.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from dqliteclient import DqliteConnection
1212
from dqliteclient.protocol import _validate_positive_int_or_none
1313
from dqlitedbapi.aio.cursor import AsyncCursor
14-
from dqlitedbapi.connection import _is_no_transaction_error
14+
from dqlitedbapi.connection import _build_and_connect, _is_no_transaction_error
1515
from dqlitedbapi.exceptions import InterfaceError, OperationalError, ProgrammingError
1616

1717
__all__ = ["AsyncConnection"]
@@ -114,20 +114,14 @@ async def _ensure_connection(self) -> DqliteConnection:
114114
if self._async_conn is not None:
115115
return self._async_conn
116116

117-
conn = DqliteConnection(
117+
self._async_conn = await _build_and_connect(
118118
self._address,
119119
database=self._database,
120120
timeout=self._timeout,
121121
max_total_rows=self._max_total_rows,
122122
max_continuation_frames=self._max_continuation_frames,
123123
trust_server_heartbeat=self._trust_server_heartbeat,
124124
)
125-
try:
126-
await conn.connect()
127-
except Exception as e:
128-
raise OperationalError(f"Failed to connect: {e}") from e
129-
130-
self._async_conn = conn
131125

132126
return self._async_conn
133127

src/dqlitedbapi/connection.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,38 @@
2929
_NO_TX_SUBSTRING = "no transaction is active"
3030

3131

32+
async def _build_and_connect(
33+
address: str,
34+
*,
35+
database: str,
36+
timeout: float,
37+
max_total_rows: int | None,
38+
max_continuation_frames: int | None,
39+
trust_server_heartbeat: bool,
40+
) -> DqliteConnection:
41+
"""Build a DqliteConnection with the given governors and connect it.
42+
43+
Wraps the construct-then-connect sequence that both the sync and
44+
async Connection flavours execute under their respective locks. The
45+
``OperationalError`` message phrasing ("Failed to connect: ...") is
46+
intentionally verbatim so test assertions that match on the prefix
47+
continue to pass.
48+
"""
49+
conn = DqliteConnection(
50+
address,
51+
database=database,
52+
timeout=timeout,
53+
max_total_rows=max_total_rows,
54+
max_continuation_frames=max_continuation_frames,
55+
trust_server_heartbeat=trust_server_heartbeat,
56+
)
57+
try:
58+
await conn.connect()
59+
except Exception as e:
60+
raise OperationalError(f"Failed to connect: {e}") from e
61+
return conn
62+
63+
3264
def _is_no_transaction_error(exc: Exception) -> bool:
3365
"""True if ``exc`` is a genuine "no active transaction" server reply.
3466
@@ -254,19 +286,14 @@ async def _get_async_connection(self) -> DqliteConnection:
254286
if self._async_conn is not None:
255287
return self._async_conn
256288

257-
conn = DqliteConnection(
289+
self._async_conn = await _build_and_connect(
258290
self._address,
259291
database=self._database,
260292
timeout=self._timeout,
261293
max_total_rows=self._max_total_rows,
262294
max_continuation_frames=self._max_continuation_frames,
263295
trust_server_heartbeat=self._trust_server_heartbeat,
264296
)
265-
try:
266-
await conn.connect()
267-
except Exception as e:
268-
raise OperationalError(f"Failed to connect: {e}") from e
269-
self._async_conn = conn
270297

271298
return self._async_conn
272299

tests/test_async_close_race.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ async def slow_execute(_sql: str, _params: object) -> tuple[int, int]:
3131
async def fake_query_raw_typed(_sql: str, _params: object) -> tuple[list, list, list]:
3232
return ([], [], [])
3333

34-
with patch("dqlitedbapi.aio.connection.DqliteConnection") as MockDqliteConn:
34+
with patch("dqlitedbapi.connection.DqliteConnection") as MockDqliteConn:
3535
mock_instance = AsyncMock()
3636
mock_instance.connect = AsyncMock()
3737
mock_instance.execute = slow_execute

tests/test_async_connection_race.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ async def slow_connect() -> None:
2222
await asyncio.sleep(0.1) # Simulate slow TCP handshake
2323
connect_finished.set()
2424

25-
with patch("dqlitedbapi.aio.connection.DqliteConnection") as MockDqliteConn:
25+
with patch("dqlitedbapi.connection.DqliteConnection") as MockDqliteConn:
2626
mock_instance = AsyncMock()
2727
mock_instance.connect = slow_connect
2828
mock_instance._protocol = AsyncMock()

tests/test_protocol_serialization.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ async def mock_execute(sql: str, params: object) -> tuple:
4242
call_log.append((sql, "end"))
4343
return (0, 1)
4444

45-
with patch("dqlitedbapi.aio.connection.DqliteConnection") as MockDqliteConn:
45+
with patch("dqlitedbapi.connection.DqliteConnection") as MockDqliteConn:
4646
mock_instance = AsyncMock()
4747
mock_instance.connect = AsyncMock()
4848
mock_instance.query_raw_typed = mock_query_raw_typed

0 commit comments

Comments
 (0)