66 may change without notice.
77"""
88
9+ import asyncio
10+ from collections .abc import Mapping , Sequence
11+ from contextlib import asynccontextmanager
912from typing import Any , Optional
13+ import uuid
1014
15+ from ..exceptions import OperationalError
16+ from ..helpers import get_settings
1117from ..logging import logger
18+ from ..row import Row
19+ from . import async_execute , async_fetch
1220from .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