arrowsql 0.1.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arrowsql/__init__.py +653 -0
- arrowsql/_rust_auth.py +170 -0
- arrowsql/_server_sqlstate.py +11 -0
- arrowsql/_utf8_strings.py +78 -0
- arrowsql/agent.py +620 -0
- arrowsql/aio.py +533 -0
- arrowsql/arrow_parallel.py +464 -0
- arrowsql/async_query/__init__.py +40 -0
- arrowsql/async_query/_connection_context.py +39 -0
- arrowsql/async_query/_native.py +24 -0
- arrowsql/async_query/async_connection.py +160 -0
- arrowsql/async_query/async_cursor.py +225 -0
- arrowsql/async_query/async_execute.py +113 -0
- arrowsql/async_query/async_fetch.py +153 -0
- arrowsql/async_query/exception_translator.py +132 -0
- arrowsql/auth.py +913 -0
- arrowsql/bulk_parallel.py +385 -0
- arrowsql/connection.py +2377 -0
- arrowsql/connection_string_builder.py +114 -0
- arrowsql/connection_string_parser.py +417 -0
- arrowsql/constants.py +701 -0
- arrowsql/cursor.py +4516 -0
- arrowsql/db_connection.py +98 -0
- arrowsql/ddbc_bindings.cp313-win_amd64.pyd +0 -0
- arrowsql/ddbc_bindings.pyi +283 -0
- arrowsql/decimal_config.py +98 -0
- arrowsql/exceptions.py +378 -0
- arrowsql/helpers.py +436 -0
- arrowsql/logging.py +700 -0
- arrowsql/mssql_python.pyi +579 -0
- arrowsql/odbc_provider.py +269 -0
- arrowsql/parameter_helper.py +442 -0
- arrowsql/perf_timer.py +286 -0
- arrowsql/pooling.py +147 -0
- arrowsql/py.typed +1 -0
- arrowsql/row.py +462 -0
- arrowsql/type.py +160 -0
- arrowsql-0.1.0.dist-info/METADATA +146 -0
- arrowsql-0.1.0.dist-info/RECORD +45 -0
- arrowsql-0.1.0.dist-info/WHEEL +4 -0
- arrowsql-0.1.0.dist-info/entry_points.txt +2 -0
- arrowsql-0.1.0.dist-info/licenses/LICENSE +21 -0
- arrowsql-0.1.0.dist-info/licenses/LICENSE.upstream-mssql-python +47 -0
- arrowsql-0.1.0.dist-info/sboms/ddbc_bindings.cyclonedx.json +5195 -0
- mssql_python/__init__.py +39 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Public asynchronous connection backed directly by mssql-py-core.
|
|
2
|
+
|
|
3
|
+
Warning:
|
|
4
|
+
Async query execution APIs are under active development and are not intended
|
|
5
|
+
for production use. Their signatures, behavior, error handling, and compatibility
|
|
6
|
+
may change without notice.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from ..logging import logger
|
|
12
|
+
from ._native import load_py_core
|
|
13
|
+
from ._connection_context import build_async_connection_context
|
|
14
|
+
from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]
|
|
15
|
+
from .exception_translator import (
|
|
16
|
+
DataError,
|
|
17
|
+
DatabaseError,
|
|
18
|
+
Error,
|
|
19
|
+
IntegrityError,
|
|
20
|
+
InterfaceError,
|
|
21
|
+
InternalError,
|
|
22
|
+
NotSupportedError,
|
|
23
|
+
OperationalError,
|
|
24
|
+
ProgrammingError,
|
|
25
|
+
Warning,
|
|
26
|
+
translate_py_core_exceptions,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _AsyncConnection:
|
|
31
|
+
"""Thin Python wrapper over ``mssql_py_core.PyAsyncConnection``.
|
|
32
|
+
|
|
33
|
+
Warning:
|
|
34
|
+
This preview API is under active development and is not intended for production use.
|
|
35
|
+
Its signatures, behavior, error handling, and compatibility may change without notice.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
Warning = Warning
|
|
39
|
+
Error = Error
|
|
40
|
+
InterfaceError = InterfaceError
|
|
41
|
+
DatabaseError = DatabaseError
|
|
42
|
+
DataError = DataError
|
|
43
|
+
OperationalError = OperationalError
|
|
44
|
+
IntegrityError = IntegrityError
|
|
45
|
+
InternalError = InternalError
|
|
46
|
+
ProgrammingError = ProgrammingError
|
|
47
|
+
NotSupportedError = NotSupportedError
|
|
48
|
+
|
|
49
|
+
def __init__(self, py_core_async_connection: Any) -> None:
|
|
50
|
+
self._py_core_async_connection = py_core_async_connection
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
async def connect(
|
|
54
|
+
cls,
|
|
55
|
+
connection_str: str = "",
|
|
56
|
+
autocommit: bool = False,
|
|
57
|
+
timeout: int = 0,
|
|
58
|
+
python_logger: Optional[Any] = None,
|
|
59
|
+
) -> "_AsyncConnection":
|
|
60
|
+
"""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
|
|
64
|
+
logger.debug(
|
|
65
|
+
"AsyncConnection.connect: starting; autocommit=%s; logger_source=%s",
|
|
66
|
+
autocommit,
|
|
67
|
+
(
|
|
68
|
+
"custom"
|
|
69
|
+
if python_logger is not None
|
|
70
|
+
else "mssql_python" if logger_bridge is not None else "disabled"
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
with translate_py_core_exceptions():
|
|
74
|
+
client_context_dict = build_async_connection_context(connection_str, timeout)
|
|
75
|
+
py_core = load_py_core()
|
|
76
|
+
py_core_async_connection = await py_core.PyAsyncConnection.connect(
|
|
77
|
+
client_context_dict,
|
|
78
|
+
python_logger=logger_bridge,
|
|
79
|
+
autocommit=autocommit,
|
|
80
|
+
)
|
|
81
|
+
logger.debug("AsyncConnection.connect: connected")
|
|
82
|
+
return cls(py_core_async_connection)
|
|
83
|
+
|
|
84
|
+
def cursor(self) -> _AsyncCursor:
|
|
85
|
+
"""Create a public asynchronous cursor sharing this connection."""
|
|
86
|
+
with translate_py_core_exceptions():
|
|
87
|
+
py_core_async_cursor = self._py_core_async_connection.cursor()
|
|
88
|
+
logger.debug("AsyncConnection.cursor: cursor created")
|
|
89
|
+
return _AsyncCursor(py_core_async_cursor, self)
|
|
90
|
+
|
|
91
|
+
async def commit(self) -> None:
|
|
92
|
+
"""Commit the active transaction, if any."""
|
|
93
|
+
logger.debug("AsyncConnection.commit: starting")
|
|
94
|
+
with translate_py_core_exceptions():
|
|
95
|
+
await self._py_core_async_connection.commit()
|
|
96
|
+
logger.debug("AsyncConnection.commit: completed")
|
|
97
|
+
|
|
98
|
+
async def rollback(self) -> None:
|
|
99
|
+
"""Roll back the active transaction, if any."""
|
|
100
|
+
logger.debug("AsyncConnection.rollback: starting")
|
|
101
|
+
with translate_py_core_exceptions():
|
|
102
|
+
await self._py_core_async_connection.rollback()
|
|
103
|
+
logger.debug("AsyncConnection.rollback: completed")
|
|
104
|
+
|
|
105
|
+
async def close(self) -> None:
|
|
106
|
+
"""Close the py-core async connection."""
|
|
107
|
+
logger.debug("AsyncConnection.close: starting")
|
|
108
|
+
with translate_py_core_exceptions():
|
|
109
|
+
await self._py_core_async_connection.close()
|
|
110
|
+
logger.debug("AsyncConnection.close: completed")
|
|
111
|
+
|
|
112
|
+
async def __aenter__(self) -> "_AsyncConnection":
|
|
113
|
+
logger.debug("AsyncConnection.__aenter__: entering context")
|
|
114
|
+
with translate_py_core_exceptions():
|
|
115
|
+
await self._py_core_async_connection.__aenter__()
|
|
116
|
+
logger.debug("AsyncConnection.__aenter__: context entered")
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> Any:
|
|
120
|
+
logger.debug(
|
|
121
|
+
"AsyncConnection.__aexit__: exiting context; block_error=%s",
|
|
122
|
+
exc_type is not None,
|
|
123
|
+
)
|
|
124
|
+
with translate_py_core_exceptions():
|
|
125
|
+
result = await self._py_core_async_connection.__aexit__(exc_type, exc_value, traceback)
|
|
126
|
+
logger.debug("AsyncConnection.__aexit__: context exited")
|
|
127
|
+
return result
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def timeout(self) -> int:
|
|
131
|
+
"""Default query timeout inherited by subsequently created cursors."""
|
|
132
|
+
with translate_py_core_exceptions():
|
|
133
|
+
return self._py_core_async_connection.timeout
|
|
134
|
+
|
|
135
|
+
@timeout.setter
|
|
136
|
+
def timeout(self, value: int) -> None:
|
|
137
|
+
with translate_py_core_exceptions():
|
|
138
|
+
self._py_core_async_connection.timeout = value
|
|
139
|
+
logger.debug("AsyncConnection.timeout: updated")
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def autocommit(self) -> bool:
|
|
143
|
+
"""Whether the connection was opened in autocommit mode."""
|
|
144
|
+
with translate_py_core_exceptions():
|
|
145
|
+
return self._py_core_async_connection.autocommit
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def closed(self) -> bool:
|
|
149
|
+
"""Whether close has been initiated on the py-core async connection."""
|
|
150
|
+
with translate_py_core_exceptions():
|
|
151
|
+
return self._py_core_async_connection.closed
|
|
152
|
+
|
|
153
|
+
def is_connected(self) -> bool:
|
|
154
|
+
"""Return whether the py-core async connection remains open."""
|
|
155
|
+
with translate_py_core_exceptions():
|
|
156
|
+
return self._py_core_async_connection.is_connected()
|
|
157
|
+
|
|
158
|
+
def __repr__(self) -> str:
|
|
159
|
+
state = "closed" if self.closed else "connected"
|
|
160
|
+
return f"_AsyncConnection({state})"
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Internal asynchronous cursor backed directly by mssql-py-core.
|
|
2
|
+
|
|
3
|
+
Warning:
|
|
4
|
+
Async query execution APIs are under active development and are not intended
|
|
5
|
+
for production use. Their signatures, behavior, error handling, and compatibility
|
|
6
|
+
may change without notice.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
from collections.abc import Mapping, Sequence
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
from ..helpers import get_settings
|
|
16
|
+
from ..logging import logger
|
|
17
|
+
from ..row import Row
|
|
18
|
+
from . import async_execute, async_fetch
|
|
19
|
+
from .exception_translator import is_connection_busy, translate_py_core_exceptions
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _AsyncCursor:
|
|
23
|
+
"""Internal Python wrapper over ``mssql_py_core.PyAsyncCursor``.
|
|
24
|
+
|
|
25
|
+
Warning:
|
|
26
|
+
This preview API is under active development and is not intended for production use.
|
|
27
|
+
Its signatures, behavior, error handling, and compatibility may change without notice.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, py_core_async_cursor: Any, connection: Any = None) -> None:
|
|
31
|
+
self._py_core_async_cursor = py_core_async_cursor
|
|
32
|
+
self._connection = connection
|
|
33
|
+
self._closed = False
|
|
34
|
+
self._result_transition_lock = asyncio.Lock()
|
|
35
|
+
self._result_ready = asyncio.Event()
|
|
36
|
+
self._result_ready.set()
|
|
37
|
+
self._result_generation = 0
|
|
38
|
+
self._fetched_row_count = 0
|
|
39
|
+
self._fetch_rowcount: int | None = None
|
|
40
|
+
self._description: list[tuple[Any, ...]] | None = None
|
|
41
|
+
self._column_map: dict[str, int] = {}
|
|
42
|
+
self._column_map_lower: dict[str, int] | None = None
|
|
43
|
+
self._column_names: tuple[str, ...] | None = None
|
|
44
|
+
self._uuid_str_indices: tuple[int, ...] | None = None
|
|
45
|
+
|
|
46
|
+
def _clear_result_metadata(self) -> None:
|
|
47
|
+
self._result_generation += 1
|
|
48
|
+
self._description = None
|
|
49
|
+
self._column_map = {}
|
|
50
|
+
self._column_map_lower = None
|
|
51
|
+
self._column_names = None
|
|
52
|
+
self._uuid_str_indices = None
|
|
53
|
+
|
|
54
|
+
def _initialize_result_metadata(self) -> None:
|
|
55
|
+
with translate_py_core_exceptions():
|
|
56
|
+
description = self._py_core_async_cursor.description
|
|
57
|
+
if description is None:
|
|
58
|
+
self._clear_result_metadata()
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
settings = get_settings()
|
|
62
|
+
self._description = [
|
|
63
|
+
((column[0].lower() if settings.lowercase else column[0]), *column[1:])
|
|
64
|
+
for column in description
|
|
65
|
+
]
|
|
66
|
+
self._column_names = tuple(column[0] for column in self._description)
|
|
67
|
+
self._column_map = {column[0]: index for index, column in enumerate(self._description)}
|
|
68
|
+
self._column_map_lower = (
|
|
69
|
+
{name.lower(): index for name, index in self._column_map.items()}
|
|
70
|
+
if settings.lowercase
|
|
71
|
+
else None
|
|
72
|
+
)
|
|
73
|
+
self._uuid_str_indices = (
|
|
74
|
+
tuple(index for index, column in enumerate(self._description) if column[1] is uuid.UUID)
|
|
75
|
+
if not settings.native_uuid
|
|
76
|
+
else None
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def _reset_fetch_tracking(self) -> None:
|
|
80
|
+
self._fetched_row_count = 0
|
|
81
|
+
self._fetch_rowcount = None
|
|
82
|
+
|
|
83
|
+
@asynccontextmanager
|
|
84
|
+
async def _result_transition(self):
|
|
85
|
+
async with self._result_transition_lock:
|
|
86
|
+
self._result_ready.clear()
|
|
87
|
+
try:
|
|
88
|
+
yield
|
|
89
|
+
finally:
|
|
90
|
+
self._result_ready.set()
|
|
91
|
+
|
|
92
|
+
async def _wait_for_result_publication(self) -> None:
|
|
93
|
+
while True:
|
|
94
|
+
await self._result_ready.wait()
|
|
95
|
+
if self._result_ready.is_set():
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
def _reconcile_failed_result_operation(self, operation: str, error: BaseException) -> None:
|
|
99
|
+
if is_connection_busy(error):
|
|
100
|
+
return
|
|
101
|
+
self._reset_fetch_tracking()
|
|
102
|
+
self._clear_result_metadata()
|
|
103
|
+
try:
|
|
104
|
+
self._initialize_result_metadata()
|
|
105
|
+
except Exception as error:
|
|
106
|
+
logger.debug("AsyncCursor.%s: metadata recovery failed: %s", operation, error)
|
|
107
|
+
|
|
108
|
+
def _check_closed(self) -> None:
|
|
109
|
+
if self._closed or (self._connection is not None and self._connection.closed):
|
|
110
|
+
message = "Cursor is closed" if self._closed else "Connection is closed"
|
|
111
|
+
with translate_py_core_exceptions():
|
|
112
|
+
raise RuntimeError(message)
|
|
113
|
+
|
|
114
|
+
def _record_fetch(self, generation: int, count: int, exhausted: bool) -> None:
|
|
115
|
+
if generation != self._result_generation:
|
|
116
|
+
return
|
|
117
|
+
if count:
|
|
118
|
+
self._fetched_row_count += count
|
|
119
|
+
self._fetch_rowcount = self._fetched_row_count
|
|
120
|
+
elif exhausted and self._fetched_row_count == 0:
|
|
121
|
+
self._fetch_rowcount = 0
|
|
122
|
+
|
|
123
|
+
async def execute(
|
|
124
|
+
self,
|
|
125
|
+
operation: str,
|
|
126
|
+
*parameters: Any,
|
|
127
|
+
use_prepare: bool = True,
|
|
128
|
+
reset_cursor: bool = True,
|
|
129
|
+
) -> "_AsyncCursor":
|
|
130
|
+
async with self._result_transition():
|
|
131
|
+
return await async_execute.execute(
|
|
132
|
+
self,
|
|
133
|
+
operation,
|
|
134
|
+
*parameters,
|
|
135
|
+
use_prepare=use_prepare,
|
|
136
|
+
reset_cursor=reset_cursor,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
async def executemany(
|
|
140
|
+
self,
|
|
141
|
+
operation: str,
|
|
142
|
+
seq_of_parameters: Sequence[Sequence[Any]] | Sequence[Mapping[str, Any]],
|
|
143
|
+
*,
|
|
144
|
+
use_prepare: bool = True,
|
|
145
|
+
) -> None:
|
|
146
|
+
async with self._result_transition():
|
|
147
|
+
await async_execute.executemany(
|
|
148
|
+
self,
|
|
149
|
+
operation,
|
|
150
|
+
seq_of_parameters,
|
|
151
|
+
use_prepare=use_prepare,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
async def fetchone(self) -> Row | None:
|
|
155
|
+
return await async_fetch.fetchone(self)
|
|
156
|
+
|
|
157
|
+
async def fetchmany(self, size: Optional[int] = None) -> list[Row]:
|
|
158
|
+
return await async_fetch.fetchmany(self, size)
|
|
159
|
+
|
|
160
|
+
async def fetchall(self) -> list[Row]:
|
|
161
|
+
return await async_fetch.fetchall(self)
|
|
162
|
+
|
|
163
|
+
async def nextset(self) -> bool:
|
|
164
|
+
async with self._result_transition():
|
|
165
|
+
try:
|
|
166
|
+
with translate_py_core_exceptions():
|
|
167
|
+
has_next = await self._py_core_async_cursor.nextset()
|
|
168
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
169
|
+
self._reconcile_failed_result_operation("nextset", error)
|
|
170
|
+
raise
|
|
171
|
+
self._reset_fetch_tracking()
|
|
172
|
+
self._clear_result_metadata()
|
|
173
|
+
if has_next:
|
|
174
|
+
self._initialize_result_metadata()
|
|
175
|
+
return has_next
|
|
176
|
+
|
|
177
|
+
async def close(self) -> None:
|
|
178
|
+
logger.debug("AsyncCursor.close: starting")
|
|
179
|
+
async with self._result_transition():
|
|
180
|
+
with translate_py_core_exceptions():
|
|
181
|
+
close_awaitable = self._py_core_async_cursor.close()
|
|
182
|
+
close_accepted = True
|
|
183
|
+
try:
|
|
184
|
+
with translate_py_core_exceptions():
|
|
185
|
+
await close_awaitable
|
|
186
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
187
|
+
if is_connection_busy(error):
|
|
188
|
+
close_accepted = False
|
|
189
|
+
raise
|
|
190
|
+
finally:
|
|
191
|
+
if close_accepted:
|
|
192
|
+
self._closed = True
|
|
193
|
+
self._reset_fetch_tracking()
|
|
194
|
+
self._clear_result_metadata()
|
|
195
|
+
logger.debug("AsyncCursor.close: completed")
|
|
196
|
+
|
|
197
|
+
def setinputsizes(self, sizes: Any) -> None:
|
|
198
|
+
with translate_py_core_exceptions():
|
|
199
|
+
self._py_core_async_cursor.setinputsizes(sizes)
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def timeout(self) -> int:
|
|
203
|
+
with translate_py_core_exceptions():
|
|
204
|
+
return self._py_core_async_cursor.timeout
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def description(self) -> Any:
|
|
208
|
+
return self._description
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def rowcount(self) -> int:
|
|
212
|
+
if self._fetch_rowcount is not None:
|
|
213
|
+
return self._fetch_rowcount
|
|
214
|
+
with translate_py_core_exceptions():
|
|
215
|
+
return self._py_core_async_cursor.rowcount
|
|
216
|
+
|
|
217
|
+
@property
|
|
218
|
+
def arraysize(self) -> int:
|
|
219
|
+
with translate_py_core_exceptions():
|
|
220
|
+
return self._py_core_async_cursor.arraysize
|
|
221
|
+
|
|
222
|
+
@arraysize.setter
|
|
223
|
+
def arraysize(self, value: int) -> None:
|
|
224
|
+
with translate_py_core_exceptions():
|
|
225
|
+
self._py_core_async_cursor.arraysize = value
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Asynchronous statement execution through mssql-py-core."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from ..exceptions import ProgrammingError
|
|
8
|
+
from ..logging import logger
|
|
9
|
+
from ..row import Row
|
|
10
|
+
from .exception_translator import is_connection_busy, translate_py_core_exceptions
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_py_core_async_cursor(cursor: "_AsyncCursor") -> Any:
|
|
17
|
+
return cursor._py_core_async_cursor # pyright: ignore[reportPrivateUsage]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _is_non_mutating_rejection(error: BaseException) -> bool:
|
|
21
|
+
"""Return whether py-core guarantees rejection before result-state mutation."""
|
|
22
|
+
if is_connection_busy(error):
|
|
23
|
+
return True
|
|
24
|
+
return isinstance(error, (TypeError, KeyError)) or (
|
|
25
|
+
isinstance(error, ProgrammingError) and isinstance(error.__cause__, TypeError)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _reconcile_failed_execution(cursor: "_AsyncCursor", error: BaseException) -> None:
|
|
30
|
+
if _is_non_mutating_rejection(error):
|
|
31
|
+
return
|
|
32
|
+
cursor._reset_fetch_tracking() # pyright: ignore[reportPrivateUsage]
|
|
33
|
+
cursor._clear_result_metadata() # pyright: ignore[reportPrivateUsage]
|
|
34
|
+
try:
|
|
35
|
+
cursor._initialize_result_metadata() # pyright: ignore[reportPrivateUsage]
|
|
36
|
+
except Exception as error:
|
|
37
|
+
logger.debug("Async execution metadata recovery failed: %s", error)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def execute(
|
|
41
|
+
cursor: "_AsyncCursor",
|
|
42
|
+
operation: str,
|
|
43
|
+
*parameters: Any,
|
|
44
|
+
use_prepare: bool = True,
|
|
45
|
+
reset_cursor: bool = True,
|
|
46
|
+
) -> "_AsyncCursor":
|
|
47
|
+
"""Execute a statement using the py-core async cursor."""
|
|
48
|
+
if len(parameters) == 1 and isinstance(parameters[0], (tuple, list, Row)):
|
|
49
|
+
parameters = tuple(parameters[0])
|
|
50
|
+
|
|
51
|
+
logger.debug(
|
|
52
|
+
"AsyncCursor.execute: starting; param_count=%d; use_prepare=%s; reset_cursor=%s",
|
|
53
|
+
len(parameters),
|
|
54
|
+
use_prepare,
|
|
55
|
+
reset_cursor,
|
|
56
|
+
)
|
|
57
|
+
with translate_py_core_exceptions():
|
|
58
|
+
execute_awaitable = _get_py_core_async_cursor(cursor).execute(
|
|
59
|
+
operation,
|
|
60
|
+
*parameters,
|
|
61
|
+
use_prepare=use_prepare,
|
|
62
|
+
reset_cursor=reset_cursor,
|
|
63
|
+
)
|
|
64
|
+
try:
|
|
65
|
+
with translate_py_core_exceptions():
|
|
66
|
+
await execute_awaitable
|
|
67
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
68
|
+
_reconcile_failed_execution(cursor, error)
|
|
69
|
+
raise
|
|
70
|
+
cursor._reset_fetch_tracking() # pyright: ignore[reportPrivateUsage]
|
|
71
|
+
cursor._clear_result_metadata() # pyright: ignore[reportPrivateUsage]
|
|
72
|
+
cursor._initialize_result_metadata() # pyright: ignore[reportPrivateUsage]
|
|
73
|
+
description = cursor.description
|
|
74
|
+
logger.debug(
|
|
75
|
+
"AsyncCursor.execute: completed; rowcount=%d; column_count=%d; has_result_set=%s",
|
|
76
|
+
cursor.rowcount,
|
|
77
|
+
len(description) if description is not None else 0,
|
|
78
|
+
description is not None,
|
|
79
|
+
)
|
|
80
|
+
return cursor
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def executemany(
|
|
84
|
+
cursor: "_AsyncCursor",
|
|
85
|
+
operation: str,
|
|
86
|
+
seq_of_parameters: Sequence[Sequence[Any]] | Sequence[Mapping[str, Any]],
|
|
87
|
+
*,
|
|
88
|
+
use_prepare: bool = True,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Execute a statement for every parameter row using the py-core async cursor."""
|
|
91
|
+
cursor._check_closed() # pyright: ignore[reportPrivateUsage]
|
|
92
|
+
batch_count = len(seq_of_parameters)
|
|
93
|
+
logger.debug(
|
|
94
|
+
"AsyncCursor.executemany: starting; batch_count=%d; use_prepare=%s",
|
|
95
|
+
batch_count,
|
|
96
|
+
use_prepare,
|
|
97
|
+
)
|
|
98
|
+
with translate_py_core_exceptions():
|
|
99
|
+
executemany_awaitable = _get_py_core_async_cursor(cursor).executemany(
|
|
100
|
+
operation,
|
|
101
|
+
seq_of_parameters,
|
|
102
|
+
use_prepare=use_prepare,
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
with translate_py_core_exceptions():
|
|
106
|
+
await executemany_awaitable
|
|
107
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
108
|
+
_reconcile_failed_execution(cursor, error)
|
|
109
|
+
raise
|
|
110
|
+
cursor._reset_fetch_tracking() # pyright: ignore[reportPrivateUsage]
|
|
111
|
+
cursor._clear_result_metadata() # pyright: ignore[reportPrivateUsage]
|
|
112
|
+
cursor._initialize_result_metadata() # pyright: ignore[reportPrivateUsage]
|
|
113
|
+
logger.debug("AsyncCursor.executemany: completed; rowcount=%d", cursor.rowcount)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Asynchronous result fetching through mssql-py-core."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from ..exceptions import ProgrammingError
|
|
9
|
+
from ..logging import logger
|
|
10
|
+
from ..row import Row
|
|
11
|
+
from .async_execute import _get_py_core_async_cursor
|
|
12
|
+
from .exception_translator import (
|
|
13
|
+
is_connection_busy,
|
|
14
|
+
translate_py_core_exception,
|
|
15
|
+
translate_py_core_exceptions,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from .async_cursor import _AsyncCursor # pyright: ignore[reportPrivateUsage]
|
|
20
|
+
|
|
21
|
+
_ResultSnapshot = tuple[
|
|
22
|
+
int,
|
|
23
|
+
dict[str, int],
|
|
24
|
+
tuple[int, ...] | None,
|
|
25
|
+
dict[str, int] | None,
|
|
26
|
+
tuple[str, ...] | None,
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _snapshot_result(cursor: "_AsyncCursor") -> _ResultSnapshot:
|
|
31
|
+
return (
|
|
32
|
+
cursor._result_generation, # pyright: ignore[reportPrivateUsage]
|
|
33
|
+
cursor._column_map, # pyright: ignore[reportPrivateUsage]
|
|
34
|
+
cursor._uuid_str_indices, # pyright: ignore[reportPrivateUsage]
|
|
35
|
+
cursor._column_map_lower, # pyright: ignore[reportPrivateUsage]
|
|
36
|
+
cursor._column_names, # pyright: ignore[reportPrivateUsage]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _reconcile_failed_fetch(
|
|
41
|
+
cursor: "_AsyncCursor", generation: int, operation: str, error: BaseException
|
|
42
|
+
) -> None:
|
|
43
|
+
if generation != cursor._result_generation: # pyright: ignore[reportPrivateUsage]
|
|
44
|
+
return
|
|
45
|
+
if is_connection_busy(error):
|
|
46
|
+
return
|
|
47
|
+
logger.debug("AsyncCursor.%s: invalidating result state after fetch failure", operation)
|
|
48
|
+
cursor._reset_fetch_tracking() # pyright: ignore[reportPrivateUsage]
|
|
49
|
+
cursor._clear_result_metadata() # pyright: ignore[reportPrivateUsage]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _wrap_row(snapshot: _ResultSnapshot, values: tuple[Any, ...]) -> Row:
|
|
53
|
+
_, column_map, uuid_str_indices, column_map_lower, column_names = snapshot
|
|
54
|
+
if not uuid_str_indices:
|
|
55
|
+
return Row._fast_create(list(values), column_map, None, column_map_lower, column_names) # pyright: ignore[reportPrivateUsage]
|
|
56
|
+
return Row(
|
|
57
|
+
list(values),
|
|
58
|
+
column_map,
|
|
59
|
+
uuid_str_indices=uuid_str_indices,
|
|
60
|
+
column_map_lower=column_map_lower,
|
|
61
|
+
column_names=column_names,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _wrap_rows(snapshot: _ResultSnapshot, rows: list[tuple[Any, ...]]) -> list[Row]:
|
|
66
|
+
_, column_map, uuid_str_indices, column_map_lower, column_names = snapshot
|
|
67
|
+
if uuid_str_indices:
|
|
68
|
+
return [_wrap_row(snapshot, row) for row in rows]
|
|
69
|
+
create = Row._fast_create # pyright: ignore[reportPrivateUsage]
|
|
70
|
+
return [create(list(row), column_map, None, column_map_lower, column_names) for row in rows]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def fetchone(cursor: "_AsyncCursor") -> Row | None:
|
|
74
|
+
"""Fetch the next row through the py-core async cursor."""
|
|
75
|
+
logger.debug("AsyncCursor.fetchone: starting")
|
|
76
|
+
if not cursor._result_ready.is_set(): # pyright: ignore[reportPrivateUsage]
|
|
77
|
+
await cursor._wait_for_result_publication() # pyright: ignore[reportPrivateUsage]
|
|
78
|
+
snapshot = _snapshot_result(cursor)
|
|
79
|
+
try:
|
|
80
|
+
try:
|
|
81
|
+
row = await _get_py_core_async_cursor(cursor).fetchone()
|
|
82
|
+
except Exception as error:
|
|
83
|
+
translated = translate_py_core_exception(error)
|
|
84
|
+
if translated is error:
|
|
85
|
+
raise
|
|
86
|
+
raise translated from error
|
|
87
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
88
|
+
_reconcile_failed_fetch(cursor, snapshot[0], "fetchone", error)
|
|
89
|
+
raise
|
|
90
|
+
cursor._record_fetch( # pyright: ignore[reportPrivateUsage]
|
|
91
|
+
snapshot[0], row is not None, row is None
|
|
92
|
+
)
|
|
93
|
+
if logger.isEnabledFor(logging.DEBUG):
|
|
94
|
+
logger.debug(
|
|
95
|
+
"AsyncCursor.fetchone: completed; row_found=%s; rowcount=%d",
|
|
96
|
+
row is not None,
|
|
97
|
+
cursor.rowcount,
|
|
98
|
+
)
|
|
99
|
+
return None if row is None else _wrap_row(snapshot, row)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def fetchmany(cursor: "_AsyncCursor", size: int | None = None) -> list[Row]:
|
|
103
|
+
"""Fetch up to size rows, using cursor arraysize when size is omitted."""
|
|
104
|
+
if not cursor._result_ready.is_set(): # pyright: ignore[reportPrivateUsage]
|
|
105
|
+
await cursor._wait_for_result_publication() # pyright: ignore[reportPrivateUsage]
|
|
106
|
+
cursor._check_closed() # pyright: ignore[reportPrivateUsage]
|
|
107
|
+
requested_size = cursor.arraysize if size is None else size
|
|
108
|
+
logger.debug("AsyncCursor.fetchmany: starting; requested_size=%s", requested_size)
|
|
109
|
+
if requested_size <= 0:
|
|
110
|
+
if cursor.description is None:
|
|
111
|
+
raise ProgrammingError("Async operation failed", "No active result set")
|
|
112
|
+
logger.debug("AsyncCursor.fetchmany: completed; row_count=0; rowcount=%d", cursor.rowcount)
|
|
113
|
+
return []
|
|
114
|
+
snapshot = _snapshot_result(cursor)
|
|
115
|
+
with translate_py_core_exceptions():
|
|
116
|
+
if size is None:
|
|
117
|
+
fetch_awaitable = _get_py_core_async_cursor(cursor).fetchmany()
|
|
118
|
+
else:
|
|
119
|
+
fetch_awaitable = _get_py_core_async_cursor(cursor).fetchmany(size)
|
|
120
|
+
try:
|
|
121
|
+
with translate_py_core_exceptions():
|
|
122
|
+
rows = await fetch_awaitable
|
|
123
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
124
|
+
_reconcile_failed_fetch(cursor, snapshot[0], "fetchmany", error)
|
|
125
|
+
raise
|
|
126
|
+
cursor._record_fetch(snapshot[0], len(rows), not rows) # pyright: ignore[reportPrivateUsage]
|
|
127
|
+
logger.debug(
|
|
128
|
+
"AsyncCursor.fetchmany: completed; row_count=%d; rowcount=%d",
|
|
129
|
+
len(rows),
|
|
130
|
+
cursor.rowcount,
|
|
131
|
+
)
|
|
132
|
+
return _wrap_rows(snapshot, rows)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
async def fetchall(cursor: "_AsyncCursor") -> list[Row]:
|
|
136
|
+
"""Fetch all remaining rows through the py-core async cursor."""
|
|
137
|
+
logger.debug("AsyncCursor.fetchall: starting")
|
|
138
|
+
if not cursor._result_ready.is_set(): # pyright: ignore[reportPrivateUsage]
|
|
139
|
+
await cursor._wait_for_result_publication() # pyright: ignore[reportPrivateUsage]
|
|
140
|
+
snapshot = _snapshot_result(cursor)
|
|
141
|
+
try:
|
|
142
|
+
with translate_py_core_exceptions():
|
|
143
|
+
rows = await _get_py_core_async_cursor(cursor).fetchall()
|
|
144
|
+
except (Exception, asyncio.CancelledError) as error:
|
|
145
|
+
_reconcile_failed_fetch(cursor, snapshot[0], "fetchall", error)
|
|
146
|
+
raise
|
|
147
|
+
cursor._record_fetch(snapshot[0], len(rows), not rows) # pyright: ignore[reportPrivateUsage]
|
|
148
|
+
logger.debug(
|
|
149
|
+
"AsyncCursor.fetchall: completed; row_count=%d; rowcount=%d",
|
|
150
|
+
len(rows),
|
|
151
|
+
cursor.rowcount,
|
|
152
|
+
)
|
|
153
|
+
return _wrap_rows(snapshot, rows)
|