codekingpro/portable-devtools
114k
1"""2psycopg server-side cursor objects.3"""4 5# Copyright (C) 2020 The Psycopg Team6 7from typing import Any, AsyncIterator, List, Iterable, Iterator8from typing import Optional, TypeVar, TYPE_CHECKING, overload9from warnings import warn10 11from . import pq12from . import sql13from . import errors as e14from .abc import ConnectionType, Query, Params, PQGen15from .rows import Row, RowFactory, AsyncRowFactory16from .cursor import BaseCursor, Cursor17from .generators import execute18from .cursor_async import AsyncCursor19 20if TYPE_CHECKING:21 from .connection import Connection22 from .connection_async import AsyncConnection23 24DEFAULT_ITERSIZE = 10025 26TEXT = pq.Format.TEXT27BINARY = pq.Format.BINARY28 29COMMAND_OK = pq.ExecStatus.COMMAND_OK30TUPLES_OK = pq.ExecStatus.TUPLES_OK31 32IDLE = pq.TransactionStatus.IDLE33INTRANS = pq.TransactionStatus.INTRANS34 35 36class ServerCursorMixin(BaseCursor[ConnectionType, Row]):37 """Mixin to add ServerCursor behaviour and implementation a BaseCursor."""38 39 __slots__ = "_name _scrollable _withhold _described itersize _format".split()40 41 def __init__(42 self,43 name: str,44 scrollable: Optional[bool],45 withhold: bool,46 ):47 self._name = name48 self._scrollable = scrollable49 self._withhold = withhold50 self._described = False51 self.itersize: int = DEFAULT_ITERSIZE52 self._format = TEXT53 54 def __repr__(self) -> str:55 # Insert the name as the second word56 parts = super().__repr__().split(None, 1)57 parts.insert(1, f"{self._name!r}")58 return " ".join(parts)59 60 @property61 def name(self) -> str:62 """The name of the cursor."""63 return self._name64 65 @property66 def scrollable(self) -> Optional[bool]:67 """68 Whether the cursor is scrollable or not.69 70 If `!None` leave the choice to the server. Use `!True` if you want to71 use `scroll()` on the cursor.72 """73 return self._scrollable74 75 @property76 def withhold(self) -> bool:77 """78 If the cursor can be used after the creating transaction has committed.79 """80 return self._withhold81 82 @property83 def rownumber(self) -> Optional[int]:84 """Index of the next row to fetch in the current result.85 86 `!None` if there is no result to fetch.87 """88 res = self.pgresult89 # command_status is empty if the result comes from90 # describe_portal, which means that we have just executed the DECLARE,91 # so we can assume we are at the first row.92 tuples = res and (res.status == TUPLES_OK or res.command_status == b"")93 return self._pos if tuples else None94 95 def _declare_gen(96 self,97 query: Query,98 params: Optional[Params] = None,99 binary: Optional[bool] = None,100 ) -> PQGen[None]:101 """Generator implementing `ServerCursor.execute()`."""102 103 query = self._make_declare_statement(query)104 105 # If the cursor is being reused, the previous one must be closed.106 if self._described:107 yield from self._close_gen()108 self._described = False109 110 yield from self._start_query(query)111 pgq = self._convert_query(query, params)112 self._execute_send(pgq, force_extended=True)113 results = yield from execute(self._conn.pgconn)114 if results[-1].status != COMMAND_OK:115 self._raise_for_result(results[-1])116 117 # Set the format, which will be used by describe and fetch operations118 if binary is None:119 self._format = self.format120 else:121 self._format = BINARY if binary else TEXT122 123 # The above result only returned COMMAND_OK. Get the cursor shape124 yield from self._describe_gen()125 126 def _describe_gen(self) -> PQGen[None]:127 self._pgconn.send_describe_portal(self._name.encode(self._encoding))128 results = yield from execute(self._pgconn)129 self._check_results(results)130 self._results = results131 self._select_current_result(0, format=self._format)132 self._described = True133 134 def _close_gen(self) -> PQGen[None]:135 ts = self._conn.pgconn.transaction_status136 137 # if the connection is not in a sane state, don't even try138 if ts != IDLE and ts != INTRANS:139 return140 141 # If we are IDLE, a WITHOUT HOLD cursor will surely have gone already.142 if not self._withhold and ts == IDLE:143 return144 145 # if we didn't declare the cursor ourselves we still have to close it146 # but we must make sure it exists.147 if not self._described:148 query = sql.SQL(149 "SELECT 1 FROM pg_catalog.pg_cursors WHERE name = {}"150 ).format(sql.Literal(self._name))151 res = yield from self._conn._exec_command(query)152 # pipeline mode otherwise, unsupported here.153 assert res is not None154 if res.ntuples == 0:155 return156 157 query = sql.SQL("CLOSE {}").format(sql.Identifier(self._name))158 yield from self._conn._exec_command(query)159 160 def _fetch_gen(self, num: Optional[int]) -> PQGen[List[Row]]:161 if self.closed:162 raise e.InterfaceError("the cursor is closed")163 # If we are stealing the cursor, make sure we know its shape164 if not self._described:165 yield from self._start_query()166 yield from self._describe_gen()167 168 query = sql.SQL("FETCH FORWARD {} FROM {}").format(169 sql.SQL("ALL") if num is None else sql.Literal(num),170 sql.Identifier(self._name),171 )172 res = yield from self._conn._exec_command(query, result_format=self._format)173 # pipeline mode otherwise, unsupported here.174 assert res is not None175 176 self.pgresult = res177 self._tx.set_pgresult(res, set_loaders=False)178 return self._tx.load_rows(0, res.ntuples, self._make_row)179 180 def _scroll_gen(self, value: int, mode: str) -> PQGen[None]:181 if mode not in ("relative", "absolute"):182 raise ValueError(f"bad mode: {mode}. It should be 'relative' or 'absolute'")183 query = sql.SQL("MOVE{} {} FROM {}").format(184 sql.SQL(" ABSOLUTE" if mode == "absolute" else ""),185 sql.Literal(value),186 sql.Identifier(self._name),187 )188 yield from self._conn._exec_command(query)189 190 def _make_declare_statement(self, query: Query) -> sql.Composed:191 if isinstance(query, bytes):192 query = query.decode(self._encoding)193 if not isinstance(query, sql.Composable):194 query = sql.SQL(query)195 196 parts = [197 sql.SQL("DECLARE"),198 sql.Identifier(self._name),199 ]200 if self._scrollable is not None:201 parts.append(sql.SQL("SCROLL" if self._scrollable else "NO SCROLL"))202 parts.append(sql.SQL("CURSOR"))203 if self._withhold:204 parts.append(sql.SQL("WITH HOLD"))205 parts.append(sql.SQL("FOR"))206 parts.append(query)207 208 return sql.SQL(" ").join(parts)209 210 211class ServerCursor(ServerCursorMixin["Connection[Any]", Row], Cursor[Row]):212 __module__ = "psycopg"213 __slots__ = ()214 _Self = TypeVar("_Self", bound="ServerCursor[Any]")215 216 @overload217 def __init__(218 self: "ServerCursor[Row]",219 connection: "Connection[Row]",220 name: str,221 *,222 scrollable: Optional[bool] = None,223 withhold: bool = False,224 ):225 ...226 227 @overload228 def __init__(229 self: "ServerCursor[Row]",230 connection: "Connection[Any]",231 name: str,232 *,233 row_factory: RowFactory[Row],234 scrollable: Optional[bool] = None,235 withhold: bool = False,236 ):237 ...238 239 def __init__(240 self,241 connection: "Connection[Any]",242 name: str,243 *,244 row_factory: Optional[RowFactory[Row]] = None,245 scrollable: Optional[bool] = None,246 withhold: bool = False,247 ):248 Cursor.__init__(249 self, connection, row_factory=row_factory or connection.row_factory250 )251 ServerCursorMixin.__init__(self, name, scrollable, withhold)252 253 def __del__(self) -> None:254 if not self.closed:255 warn(256 f"the server-side cursor {self} was deleted while still open."257 " Please use 'with' or '.close()' to close the cursor properly",258 ResourceWarning,259 )260 261 def close(self) -> None:262 """263 Close the current cursor and free associated resources.264 """265 with self._conn.lock:266 if self.closed:267 return268 if not self._conn.closed:269 self._conn.wait(self._close_gen())270 super().close()271 272 def execute(273 self: _Self,274 query: Query,275 params: Optional[Params] = None,276 *,277 binary: Optional[bool] = None,278 **kwargs: Any,279 ) -> _Self:280 """281 Open a cursor to execute a query to the database.282 """283 if kwargs:284 raise TypeError(f"keyword not supported: {list(kwargs)[0]}")285 if self._pgconn.pipeline_status:286 raise e.NotSupportedError(287 "server-side cursors not supported in pipeline mode"288 )289 290 try:291 with self._conn.lock:292 self._conn.wait(self._declare_gen(query, params, binary))293 except e._NO_TRACEBACK as ex:294 raise ex.with_traceback(None)295 296 return self297 298 def executemany(299 self,300 query: Query,301 params_seq: Iterable[Params],302 *,303 returning: bool = True,304 ) -> None:305 """Method not implemented for server-side cursors."""306 raise e.NotSupportedError("executemany not supported on server-side cursors")307 308 def fetchone(self) -> Optional[Row]:309 with self._conn.lock:310 recs = self._conn.wait(self._fetch_gen(1))311 if recs:312 self._pos += 1313 return recs[0]314 else:315 return None316 317 def fetchmany(self, size: int = 0) -> List[Row]:318 if not size:319 size = self.arraysize320 with self._conn.lock:321 recs = self._conn.wait(self._fetch_gen(size))322 self._pos += len(recs)323 return recs324 325 def fetchall(self) -> List[Row]:326 with self._conn.lock:327 recs = self._conn.wait(self._fetch_gen(None))328 self._pos += len(recs)329 return recs330 331 def __iter__(self) -> Iterator[Row]:332 while True:333 with self._conn.lock:334 recs = self._conn.wait(self._fetch_gen(self.itersize))335 for rec in recs:336 self._pos += 1337 yield rec338 if len(recs) < self.itersize:339 break340 341 def scroll(self, value: int, mode: str = "relative") -> None:342 with self._conn.lock:343 self._conn.wait(self._scroll_gen(value, mode))344 # Postgres doesn't have a reliable way to report a cursor out of bound345 if mode == "relative":346 self._pos += value347 else:348 self._pos = value349 350 351class AsyncServerCursor(352 ServerCursorMixin["AsyncConnection[Any]", Row], AsyncCursor[Row]353):354 __module__ = "psycopg"355 __slots__ = ()356 _Self = TypeVar("_Self", bound="AsyncServerCursor[Any]")357 358 @overload359 def __init__(360 self: "AsyncServerCursor[Row]",361 connection: "AsyncConnection[Row]",362 name: str,363 *,364 scrollable: Optional[bool] = None,365 withhold: bool = False,366 ):367 ...368 369 @overload370 def __init__(371 self: "AsyncServerCursor[Row]",372 connection: "AsyncConnection[Any]",373 name: str,374 *,375 row_factory: AsyncRowFactory[Row],376 scrollable: Optional[bool] = None,377 withhold: bool = False,378 ):379 ...380 381 def __init__(382 self,383 connection: "AsyncConnection[Any]",384 name: str,385 *,386 row_factory: Optional[AsyncRowFactory[Row]] = None,387 scrollable: Optional[bool] = None,388 withhold: bool = False,389 ):390 AsyncCursor.__init__(391 self, connection, row_factory=row_factory or connection.row_factory392 )393 ServerCursorMixin.__init__(self, name, scrollable, withhold)394 395 def __del__(self) -> None:396 if not self.closed:397 warn(398 f"the server-side cursor {self} was deleted while still open."399 " Please use 'with' or '.close()' to close the cursor properly",400 ResourceWarning,401 )402 403 async def close(self) -> None:404 async with self._conn.lock:405 if self.closed:406 return407 if not self._conn.closed:408 await self._conn.wait(self._close_gen())409 await super().close()410 411 async def execute(412 self: _Self,413 query: Query,414 params: Optional[Params] = None,415 *,416 binary: Optional[bool] = None,417 **kwargs: Any,418 ) -> _Self:419 if kwargs:420 raise TypeError(f"keyword not supported: {list(kwargs)[0]}")421 if self._pgconn.pipeline_status:422 raise e.NotSupportedError(423 "server-side cursors not supported in pipeline mode"424 )425 426 try:427 async with self._conn.lock:428 await self._conn.wait(self._declare_gen(query, params, binary))429 except e._NO_TRACEBACK as ex:430 raise ex.with_traceback(None)431 432 return self433 434 async def executemany(435 self,436 query: Query,437 params_seq: Iterable[Params],438 *,439 returning: bool = True,440 ) -> None:441 raise e.NotSupportedError("executemany not supported on server-side cursors")442 443 async def fetchone(self) -> Optional[Row]:444 async with self._conn.lock:445 recs = await self._conn.wait(self._fetch_gen(1))446 if recs:447 self._pos += 1448 return recs[0]449 else:450 return None451 452 async def fetchmany(self, size: int = 0) -> List[Row]:453 if not size:454 size = self.arraysize455 async with self._conn.lock:456 recs = await self._conn.wait(self._fetch_gen(size))457 self._pos += len(recs)458 return recs459 460 async def fetchall(self) -> List[Row]:461 async with self._conn.lock:462 recs = await self._conn.wait(self._fetch_gen(None))463 self._pos += len(recs)464 return recs465 466 async def __aiter__(self) -> AsyncIterator[Row]:467 while True:468 async with self._conn.lock:469 recs = await self._conn.wait(self._fetch_gen(self.itersize))470 for rec in recs:471 self._pos += 1472 yield rec473 if len(recs) < self.itersize:474 break475 476 async def scroll(self, value: int, mode: str = "relative") -> None:477 async with self._conn.lock:478 await self._conn.wait(self._scroll_gen(value, mode))479 