Skip to content

Commit e9b704a

Browse files
authored
Merge branch 'main' into jahnvi/fix-fetch-diagnostics
2 parents 1758590 + 111e0cf commit e9b704a

11 files changed

Lines changed: 2430 additions & 183 deletions

‎mssql_python/async_query/async_connection.py‎

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ class AsyncConnection:
4646
ProgrammingError = ProgrammingError
4747
NotSupportedError = NotSupportedError
4848

49-
def __init__(self, native_connection: Any) -> None:
50-
self._native_connection = native_connection
49+
def __init__(self, py_core_async_connection: Any) -> None:
50+
self._py_core_async_connection = py_core_async_connection
5151

5252
@classmethod
5353
async def connect(
@@ -58,54 +58,61 @@ async def connect(
5858
python_logger: Optional[Any] = None,
5959
) -> "AsyncConnection":
6060
"""Establish an asynchronous connection from an ODBC connection string."""
61+
logger_bridge = python_logger
62+
if logger_bridge is None and logger.is_debug_enabled:
63+
logger_bridge = logger
6164
logger.debug(
62-
"AsyncConnection.connect: starting; autocommit=%s; custom_logger=%s",
65+
"AsyncConnection.connect: starting; autocommit=%s; logger_source=%s",
6366
autocommit,
64-
python_logger is not None,
67+
(
68+
"custom"
69+
if python_logger is not None
70+
else "mssql_python" if logger_bridge is not None else "disabled"
71+
),
6572
)
6673
with translate_py_core_exceptions():
6774
client_context_dict = build_async_connection_context(connection_str, timeout)
6875
py_core = load_py_core()
69-
native_connection = await py_core.PyAsyncConnection.connect(
76+
py_core_async_connection = await py_core.PyAsyncConnection.connect(
7077
client_context_dict,
71-
python_logger=python_logger,
78+
python_logger=logger_bridge,
7279
autocommit=autocommit,
7380
)
7481
logger.debug("AsyncConnection.connect: connected")
75-
return cls(native_connection)
82+
return cls(py_core_async_connection)
7683

7784
def cursor(self) -> AsyncCursor:
7885
"""Create a public asynchronous cursor sharing this connection."""
7986
with translate_py_core_exceptions():
80-
native_cursor = self._native_connection.cursor()
87+
py_core_async_cursor = self._py_core_async_connection.cursor()
8188
logger.debug("AsyncConnection.cursor: cursor created")
82-
return AsyncCursor(native_cursor)
89+
return AsyncCursor(py_core_async_cursor, self)
8390

8491
async def commit(self) -> None:
8592
"""Commit the active transaction, if any."""
8693
logger.debug("AsyncConnection.commit: starting")
8794
with translate_py_core_exceptions():
88-
await self._native_connection.commit()
95+
await self._py_core_async_connection.commit()
8996
logger.debug("AsyncConnection.commit: completed")
9097

9198
async def rollback(self) -> None:
9299
"""Roll back the active transaction, if any."""
93100
logger.debug("AsyncConnection.rollback: starting")
94101
with translate_py_core_exceptions():
95-
await self._native_connection.rollback()
102+
await self._py_core_async_connection.rollback()
96103
logger.debug("AsyncConnection.rollback: completed")
97104

98105
async def close(self) -> None:
99-
"""Close the native connection."""
106+
"""Close the py-core async connection."""
100107
logger.debug("AsyncConnection.close: starting")
101108
with translate_py_core_exceptions():
102-
await self._native_connection.close()
109+
await self._py_core_async_connection.close()
103110
logger.debug("AsyncConnection.close: completed")
104111

105112
async def __aenter__(self) -> "AsyncConnection":
106113
logger.debug("AsyncConnection.__aenter__: entering context")
107114
with translate_py_core_exceptions():
108-
await self._native_connection.__aenter__()
115+
await self._py_core_async_connection.__aenter__()
109116
logger.debug("AsyncConnection.__aenter__: context entered")
110117
return self
111118

@@ -115,38 +122,38 @@ async def __aexit__(self, exc_type, exc_value, traceback) -> Any:
115122
exc_type is not None,
116123
)
117124
with translate_py_core_exceptions():
118-
result = await self._native_connection.__aexit__(exc_type, exc_value, traceback)
125+
result = await self._py_core_async_connection.__aexit__(exc_type, exc_value, traceback)
119126
logger.debug("AsyncConnection.__aexit__: context exited")
120127
return result
121128

122129
@property
123130
def timeout(self) -> int:
124131
"""Default query timeout inherited by subsequently created cursors."""
125132
with translate_py_core_exceptions():
126-
return self._native_connection.timeout
133+
return self._py_core_async_connection.timeout
127134

128135
@timeout.setter
129136
def timeout(self, value: int) -> None:
130137
with translate_py_core_exceptions():
131-
self._native_connection.timeout = value
138+
self._py_core_async_connection.timeout = value
132139
logger.debug("AsyncConnection.timeout: updated")
133140

134141
@property
135142
def autocommit(self) -> bool:
136143
"""Whether the connection was opened in autocommit mode."""
137144
with translate_py_core_exceptions():
138-
return self._native_connection.autocommit
145+
return self._py_core_async_connection.autocommit
139146

140147
@property
141148
def closed(self) -> bool:
142-
"""Whether close has been initiated on the native connection."""
149+
"""Whether close has been initiated on the py-core async connection."""
143150
with translate_py_core_exceptions():
144-
return self._native_connection.closed
151+
return self._py_core_async_connection.closed
145152

146153
def is_connected(self) -> bool:
147-
"""Return whether the native connection remains open."""
154+
"""Return whether the py-core async connection remains open."""
148155
with translate_py_core_exceptions():
149-
return self._native_connection.is_connected()
156+
return self._py_core_async_connection.is_connected()
150157

151158
def __repr__(self) -> str:
152159
state = "closed" if self.closed else "connected"

‎mssql_python/async_query/async_cursor.py‎

Lines changed: 154 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@
66
may change without notice.
77
"""
88

9+
import asyncio
10+
from collections.abc import Mapping, Sequence
11+
from contextlib import asynccontextmanager
912
from typing import Any, Optional
13+
import uuid
1014

15+
from ..exceptions import OperationalError
16+
from ..helpers import get_settings
1117
from ..logging import logger
18+
from ..row import Row
19+
from . import async_execute, async_fetch
1220
from .exception_translator import translate_py_core_exceptions
1321

1422

@@ -20,8 +28,100 @@ class AsyncCursor:
2028
Its signatures, behavior, error handling, and compatibility may change without notice.
2129
"""
2230

23-
def __init__(self, native_cursor: Any) -> None:
24-
self._native_cursor = native_cursor
31+
def __init__(self, py_core_async_cursor: Any, connection: Any = None) -> None:
32+
self._py_core_async_cursor = py_core_async_cursor
33+
self._connection = connection
34+
self._closed = False
35+
self._result_transition_lock = asyncio.Lock()
36+
self._result_ready = asyncio.Event()
37+
self._result_ready.set()
38+
self._result_generation = 0
39+
self._fetched_row_count = 0
40+
self._fetch_rowcount: int | None = None
41+
self._description: list[tuple[Any, ...]] | None = None
42+
self._column_map: dict[str, int] = {}
43+
self._column_map_lower: dict[str, int] | None = None
44+
self._column_names: tuple[str, ...] | None = None
45+
self._uuid_str_indices: tuple[int, ...] | None = None
46+
47+
def _clear_result_metadata(self) -> None:
48+
self._result_generation += 1
49+
self._description = None
50+
self._column_map = {}
51+
self._column_map_lower = None
52+
self._column_names = None
53+
self._uuid_str_indices = None
54+
55+
def _initialize_result_metadata(self) -> None:
56+
with translate_py_core_exceptions():
57+
description = self._py_core_async_cursor.description
58+
if description is None:
59+
self._clear_result_metadata()
60+
return
61+
62+
settings = get_settings()
63+
self._description = [
64+
((column[0].lower() if settings.lowercase else column[0]), *column[1:])
65+
for column in description
66+
]
67+
self._column_names = tuple(column[0] for column in self._description)
68+
self._column_map = {column[0]: index for index, column in enumerate(self._description)}
69+
self._column_map_lower = (
70+
{name.lower(): index for name, index in self._column_map.items()}
71+
if settings.lowercase
72+
else None
73+
)
74+
self._uuid_str_indices = (
75+
tuple(index for index, column in enumerate(self._description) if column[1] is uuid.UUID)
76+
if not settings.native_uuid
77+
else None
78+
)
79+
80+
def _reset_fetch_tracking(self) -> None:
81+
self._fetched_row_count = 0
82+
self._fetch_rowcount = None
83+
84+
@asynccontextmanager
85+
async def _result_transition(self):
86+
async with self._result_transition_lock:
87+
self._result_ready.clear()
88+
try:
89+
yield
90+
finally:
91+
self._result_ready.set()
92+
93+
async def _wait_for_result_publication(self) -> None:
94+
while True:
95+
await self._result_ready.wait()
96+
if self._result_ready.is_set():
97+
return
98+
99+
def _reconcile_failed_result_operation(self, operation: str, error: BaseException) -> None:
100+
if isinstance(error, OperationalError) and str(error.__cause__).startswith(
101+
"Connection is busy"
102+
):
103+
return
104+
self._reset_fetch_tracking()
105+
self._clear_result_metadata()
106+
try:
107+
self._initialize_result_metadata()
108+
except Exception as error:
109+
logger.debug("AsyncCursor.%s: metadata recovery failed: %s", operation, error)
110+
111+
def _check_closed(self) -> None:
112+
if self._closed or (self._connection is not None and self._connection.closed):
113+
message = "Cursor is closed" if self._closed else "Connection is closed"
114+
with translate_py_core_exceptions():
115+
raise RuntimeError(message)
116+
117+
def _record_fetch(self, generation: int, count: int, exhausted: bool) -> None:
118+
if generation != self._result_generation:
119+
return
120+
if count:
121+
self._fetched_row_count += count
122+
self._fetch_rowcount = self._fetched_row_count
123+
elif exhausted and self._fetched_row_count == 0:
124+
self._fetch_rowcount = 0
25125

26126
async def execute(
27127
self,
@@ -30,86 +130,101 @@ async def execute(
30130
use_prepare: bool = True,
31131
reset_cursor: bool = True,
32132
) -> "AsyncCursor":
33-
if len(parameters) == 1 and isinstance(parameters[0], (tuple, list)):
34-
parameters = tuple(parameters[0])
35-
36-
logger.debug("AsyncCursor.execute: starting")
37-
with translate_py_core_exceptions():
38-
await self._native_cursor.execute(
133+
async with self._result_transition():
134+
return await async_execute.execute(
135+
self,
39136
operation,
40137
*parameters,
41138
use_prepare=use_prepare,
42139
reset_cursor=reset_cursor,
43140
)
44-
logger.debug("AsyncCursor.execute: completed")
45-
return self
46141

47142
async def executemany(
48143
self,
49144
operation: str,
50-
seq_of_parameters: Any,
145+
seq_of_parameters: Sequence[Sequence[Any]] | Sequence[Mapping[str, Any]],
51146
*,
52147
use_prepare: bool = True,
53-
) -> "AsyncCursor":
54-
logger.debug("AsyncCursor.executemany: starting")
55-
with translate_py_core_exceptions():
56-
await self._native_cursor.executemany(
148+
) -> None:
149+
async with self._result_transition():
150+
await async_execute.executemany(
151+
self,
57152
operation,
58153
seq_of_parameters,
59154
use_prepare=use_prepare,
60155
)
61-
logger.debug("AsyncCursor.executemany: completed")
62-
return self
63156

64-
async def fetchone(self) -> Any:
65-
with translate_py_core_exceptions():
66-
return await self._native_cursor.fetchone()
157+
async def fetchone(self) -> Row | None:
158+
return await async_fetch.fetchone(self)
67159

68-
async def fetchmany(self, size: Optional[int] = None) -> Any:
69-
with translate_py_core_exceptions():
70-
if size is None:
71-
return await self._native_cursor.fetchmany()
72-
return await self._native_cursor.fetchmany(size)
160+
async def fetchmany(self, size: Optional[int] = None) -> list[Row]:
161+
return await async_fetch.fetchmany(self, size)
73162

74-
async def fetchall(self) -> Any:
75-
with translate_py_core_exceptions():
76-
return await self._native_cursor.fetchall()
163+
async def fetchall(self) -> list[Row]:
164+
return await async_fetch.fetchall(self)
77165

78166
async def nextset(self) -> bool:
79-
with translate_py_core_exceptions():
80-
return await self._native_cursor.nextset()
167+
async with self._result_transition():
168+
try:
169+
with translate_py_core_exceptions():
170+
has_next = await self._py_core_async_cursor.nextset()
171+
except (Exception, asyncio.CancelledError) as error:
172+
self._reconcile_failed_result_operation("nextset", error)
173+
raise
174+
self._reset_fetch_tracking()
175+
self._clear_result_metadata()
176+
if has_next:
177+
self._initialize_result_metadata()
178+
return has_next
81179

82180
async def close(self) -> None:
83181
logger.debug("AsyncCursor.close: starting")
84-
with translate_py_core_exceptions():
85-
await self._native_cursor.close()
182+
async with self._result_transition():
183+
with translate_py_core_exceptions():
184+
close_awaitable = self._py_core_async_cursor.close()
185+
close_accepted = True
186+
try:
187+
with translate_py_core_exceptions():
188+
await close_awaitable
189+
except (Exception, asyncio.CancelledError) as error:
190+
if isinstance(error, OperationalError) and str(error.__cause__).startswith(
191+
"Connection is busy"
192+
):
193+
close_accepted = False
194+
raise
195+
finally:
196+
if close_accepted:
197+
self._closed = True
198+
self._reset_fetch_tracking()
199+
self._clear_result_metadata()
86200
logger.debug("AsyncCursor.close: completed")
87201

88202
def setinputsizes(self, sizes: Any) -> None:
89203
with translate_py_core_exceptions():
90-
self._native_cursor.setinputsizes(sizes)
204+
self._py_core_async_cursor.setinputsizes(sizes)
91205

92206
@property
93207
def timeout(self) -> int:
94208
with translate_py_core_exceptions():
95-
return self._native_cursor.timeout
209+
return self._py_core_async_cursor.timeout
96210

97211
@property
98212
def description(self) -> Any:
99-
with translate_py_core_exceptions():
100-
return self._native_cursor.description
213+
return self._description
101214

102215
@property
103216
def rowcount(self) -> int:
217+
if self._fetch_rowcount is not None:
218+
return self._fetch_rowcount
104219
with translate_py_core_exceptions():
105-
return self._native_cursor.rowcount
220+
return self._py_core_async_cursor.rowcount
106221

107222
@property
108223
def arraysize(self) -> int:
109224
with translate_py_core_exceptions():
110-
return self._native_cursor.arraysize
225+
return self._py_core_async_cursor.arraysize
111226

112227
@arraysize.setter
113228
def arraysize(self, value: int) -> None:
114229
with translate_py_core_exceptions():
115-
self._native_cursor.arraysize = value
230+
self._py_core_async_cursor.arraysize = value

0 commit comments

Comments
 (0)