This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# engine/cursor.py
|
||||
# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
|
||||
# Copyright (C) 2005-2023 the SQLAlchemy authors and contributors
|
||||
# <see AUTHORS file>
|
||||
#
|
||||
# This module is part of SQLAlchemy and is released under
|
||||
@@ -20,7 +20,6 @@ from typing import Any
|
||||
from typing import cast
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
@@ -121,7 +120,7 @@ _CursorKeyMapRecType = Tuple[
|
||||
List[Any], # MD_OBJECTS
|
||||
str, # MD_LOOKUP_KEY
|
||||
str, # MD_RENDERED_NAME
|
||||
Optional["_ResultProcessorType[Any]"], # MD_PROCESSOR
|
||||
Optional["_ResultProcessorType"], # MD_PROCESSOR
|
||||
Optional[str], # MD_UNTRANSLATED
|
||||
]
|
||||
|
||||
@@ -135,7 +134,7 @@ _NonAmbigCursorKeyMapRecType = Tuple[
|
||||
List[Any],
|
||||
str,
|
||||
str,
|
||||
Optional["_ResultProcessorType[Any]"],
|
||||
Optional["_ResultProcessorType"],
|
||||
str,
|
||||
]
|
||||
|
||||
@@ -152,7 +151,7 @@ class CursorResultMetaData(ResultMetaData):
|
||||
"_translated_indexes",
|
||||
"_safe_for_cache",
|
||||
"_unpickled",
|
||||
"_key_to_index",
|
||||
"_key_to_index"
|
||||
# don't need _unique_filters support here for now. Can be added
|
||||
# if a need arises.
|
||||
)
|
||||
@@ -226,11 +225,9 @@ class CursorResultMetaData(ResultMetaData):
|
||||
{
|
||||
key: (
|
||||
# int index should be None for ambiguous key
|
||||
(
|
||||
value[0] + offset
|
||||
if value[0] is not None and key not in keymap
|
||||
else None
|
||||
),
|
||||
value[0] + offset
|
||||
if value[0] is not None and key not in keymap
|
||||
else None,
|
||||
value[1] + offset,
|
||||
*value[2:],
|
||||
)
|
||||
@@ -365,11 +362,13 @@ class CursorResultMetaData(ResultMetaData):
|
||||
) = context.result_column_struct
|
||||
num_ctx_cols = len(result_columns)
|
||||
else:
|
||||
result_columns = cols_are_ordered = ( # type: ignore
|
||||
result_columns = ( # type: ignore
|
||||
cols_are_ordered
|
||||
) = (
|
||||
num_ctx_cols
|
||||
) = ad_hoc_textual = loose_column_name_matching = (
|
||||
textual_ordered
|
||||
) = False
|
||||
) = (
|
||||
ad_hoc_textual
|
||||
) = loose_column_name_matching = textual_ordered = False
|
||||
|
||||
# merge cursor.description with the column info
|
||||
# present in the compiled structure, if any
|
||||
@@ -689,7 +688,6 @@ class CursorResultMetaData(ResultMetaData):
|
||||
% (num_ctx_cols, len(cursor_description))
|
||||
)
|
||||
seen = set()
|
||||
|
||||
for (
|
||||
idx,
|
||||
colname,
|
||||
@@ -1163,7 +1161,7 @@ class BufferedRowCursorFetchStrategy(CursorFetchStrategy):
|
||||
|
||||
result = conn.execution_options(
|
||||
stream_results=True, max_row_buffer=50
|
||||
).execute(text("select * from table"))
|
||||
).execute(text("select * from table"))
|
||||
|
||||
.. versionadded:: 1.4 ``max_row_buffer`` may now exceed 1000 rows.
|
||||
|
||||
@@ -1248,9 +1246,8 @@ class BufferedRowCursorFetchStrategy(CursorFetchStrategy):
|
||||
if size is None:
|
||||
return self.fetchall(result, dbapi_cursor)
|
||||
|
||||
rb = self._rowbuffer
|
||||
lb = len(rb)
|
||||
close = False
|
||||
buf = list(self._rowbuffer)
|
||||
lb = len(buf)
|
||||
if size > lb:
|
||||
try:
|
||||
new = dbapi_cursor.fetchmany(size - lb)
|
||||
@@ -1258,15 +1255,13 @@ class BufferedRowCursorFetchStrategy(CursorFetchStrategy):
|
||||
self.handle_exception(result, dbapi_cursor, e)
|
||||
else:
|
||||
if not new:
|
||||
# defer closing since it may clear the row buffer
|
||||
close = True
|
||||
result._soft_close()
|
||||
else:
|
||||
rb.extend(new)
|
||||
buf.extend(new)
|
||||
|
||||
res = [rb.popleft() for _ in range(min(size, len(rb)))]
|
||||
if close:
|
||||
result._soft_close()
|
||||
return res
|
||||
result = buf[0:size]
|
||||
self._rowbuffer = collections.deque(buf[size:])
|
||||
return result
|
||||
|
||||
def fetchall(self, result, dbapi_cursor):
|
||||
try:
|
||||
@@ -1290,16 +1285,12 @@ class FullyBufferedCursorFetchStrategy(CursorFetchStrategy):
|
||||
__slots__ = ("_rowbuffer", "alternate_cursor_description")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dbapi_cursor: Optional[DBAPICursor],
|
||||
alternate_description: Optional[_DBAPICursorDescription] = None,
|
||||
initial_buffer: Optional[Iterable[Any]] = None,
|
||||
self, dbapi_cursor, alternate_description=None, initial_buffer=None
|
||||
):
|
||||
self.alternate_cursor_description = alternate_description
|
||||
if initial_buffer is not None:
|
||||
self._rowbuffer = collections.deque(initial_buffer)
|
||||
else:
|
||||
assert dbapi_cursor is not None
|
||||
self._rowbuffer = collections.deque(dbapi_cursor.fetchall())
|
||||
|
||||
def yield_per(self, result, dbapi_cursor, num):
|
||||
@@ -1324,8 +1315,9 @@ class FullyBufferedCursorFetchStrategy(CursorFetchStrategy):
|
||||
if size is None:
|
||||
return self.fetchall(result, dbapi_cursor)
|
||||
|
||||
rb = self._rowbuffer
|
||||
rows = [rb.popleft() for _ in range(min(size, len(rb)))]
|
||||
buf = list(self._rowbuffer)
|
||||
rows = buf[0:size]
|
||||
self._rowbuffer = collections.deque(buf[size:])
|
||||
if not rows:
|
||||
result._soft_close()
|
||||
return rows
|
||||
@@ -1358,15 +1350,15 @@ class _NoResultMetaData(ResultMetaData):
|
||||
self._we_dont_return_rows()
|
||||
|
||||
@property
|
||||
def _keymap(self): # type: ignore[override]
|
||||
def _keymap(self):
|
||||
self._we_dont_return_rows()
|
||||
|
||||
@property
|
||||
def _key_to_index(self): # type: ignore[override]
|
||||
def _key_to_index(self):
|
||||
self._we_dont_return_rows()
|
||||
|
||||
@property
|
||||
def _processors(self): # type: ignore[override]
|
||||
def _processors(self):
|
||||
self._we_dont_return_rows()
|
||||
|
||||
@property
|
||||
@@ -1446,7 +1438,6 @@ class CursorResult(Result[_T]):
|
||||
|
||||
metadata = self._init_metadata(context, cursor_description)
|
||||
|
||||
_make_row: Any
|
||||
_make_row = functools.partial(
|
||||
Row,
|
||||
metadata,
|
||||
@@ -1619,11 +1610,11 @@ class CursorResult(Result[_T]):
|
||||
"""
|
||||
if not self.context.compiled:
|
||||
raise exc.InvalidRequestError(
|
||||
"Statement is not a compiled expression construct."
|
||||
"Statement is not a compiled " "expression construct."
|
||||
)
|
||||
elif not self.context.isinsert:
|
||||
raise exc.InvalidRequestError(
|
||||
"Statement is not an insert() expression construct."
|
||||
"Statement is not an insert() " "expression construct."
|
||||
)
|
||||
elif self.context._is_explicit_returning:
|
||||
raise exc.InvalidRequestError(
|
||||
@@ -1690,11 +1681,11 @@ class CursorResult(Result[_T]):
|
||||
"""
|
||||
if not self.context.compiled:
|
||||
raise exc.InvalidRequestError(
|
||||
"Statement is not a compiled expression construct."
|
||||
"Statement is not a compiled " "expression construct."
|
||||
)
|
||||
elif not self.context.isupdate:
|
||||
raise exc.InvalidRequestError(
|
||||
"Statement is not an update() expression construct."
|
||||
"Statement is not an update() " "expression construct."
|
||||
)
|
||||
elif self.context.executemany:
|
||||
return self.context.compiled_parameters
|
||||
@@ -1712,11 +1703,11 @@ class CursorResult(Result[_T]):
|
||||
"""
|
||||
if not self.context.compiled:
|
||||
raise exc.InvalidRequestError(
|
||||
"Statement is not a compiled expression construct."
|
||||
"Statement is not a compiled " "expression construct."
|
||||
)
|
||||
elif not self.context.isinsert:
|
||||
raise exc.InvalidRequestError(
|
||||
"Statement is not an insert() expression construct."
|
||||
"Statement is not an insert() " "expression construct."
|
||||
)
|
||||
elif self.context.executemany:
|
||||
return self.context.compiled_parameters
|
||||
@@ -1761,9 +1752,11 @@ class CursorResult(Result[_T]):
|
||||
|
||||
r1 = connection.execute(
|
||||
users.insert().returning(
|
||||
users.c.user_name, users.c.user_id, sort_by_parameter_order=True
|
||||
users.c.user_name,
|
||||
users.c.user_id,
|
||||
sort_by_parameter_order=True
|
||||
),
|
||||
user_values,
|
||||
user_values
|
||||
)
|
||||
|
||||
r2 = connection.execute(
|
||||
@@ -1771,16 +1764,19 @@ class CursorResult(Result[_T]):
|
||||
addresses.c.address_id,
|
||||
addresses.c.address,
|
||||
addresses.c.user_id,
|
||||
sort_by_parameter_order=True,
|
||||
sort_by_parameter_order=True
|
||||
),
|
||||
address_values,
|
||||
address_values
|
||||
)
|
||||
|
||||
rows = r1.splice_horizontally(r2).all()
|
||||
assert rows == [
|
||||
("john", 1, 1, "foo@bar.com", 1),
|
||||
("jack", 2, 2, "bar@bat.com", 2),
|
||||
]
|
||||
assert (
|
||||
rows ==
|
||||
[
|
||||
("john", 1, 1, "foo@bar.com", 1),
|
||||
("jack", 2, 2, "bar@bat.com", 2),
|
||||
]
|
||||
)
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
@@ -1789,7 +1785,7 @@ class CursorResult(Result[_T]):
|
||||
:meth:`.CursorResult.splice_vertically`
|
||||
|
||||
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
clone = self._generate()
|
||||
total_rows = [
|
||||
@@ -1924,7 +1920,7 @@ class CursorResult(Result[_T]):
|
||||
|
||||
if not self.context.compiled:
|
||||
raise exc.InvalidRequestError(
|
||||
"Statement is not a compiled expression construct."
|
||||
"Statement is not a compiled " "expression construct."
|
||||
)
|
||||
elif not self.context.isinsert and not self.context.isupdate:
|
||||
raise exc.InvalidRequestError(
|
||||
@@ -1947,7 +1943,7 @@ class CursorResult(Result[_T]):
|
||||
|
||||
if not self.context.compiled:
|
||||
raise exc.InvalidRequestError(
|
||||
"Statement is not a compiled expression construct."
|
||||
"Statement is not a compiled " "expression construct."
|
||||
)
|
||||
elif not self.context.isinsert and not self.context.isupdate:
|
||||
raise exc.InvalidRequestError(
|
||||
@@ -1978,28 +1974,8 @@ class CursorResult(Result[_T]):
|
||||
def rowcount(self) -> int:
|
||||
"""Return the 'rowcount' for this result.
|
||||
|
||||
The primary purpose of 'rowcount' is to report the number of rows
|
||||
matched by the WHERE criterion of an UPDATE or DELETE statement
|
||||
executed once (i.e. for a single parameter set), which may then be
|
||||
compared to the number of rows expected to be updated or deleted as a
|
||||
means of asserting data integrity.
|
||||
|
||||
This attribute is transferred from the ``cursor.rowcount`` attribute
|
||||
of the DBAPI before the cursor is closed, to support DBAPIs that
|
||||
don't make this value available after cursor close. Some DBAPIs may
|
||||
offer meaningful values for other kinds of statements, such as INSERT
|
||||
and SELECT statements as well. In order to retrieve ``cursor.rowcount``
|
||||
for these statements, set the
|
||||
:paramref:`.Connection.execution_options.preserve_rowcount`
|
||||
execution option to True, which will cause the ``cursor.rowcount``
|
||||
value to be unconditionally memoized before any results are returned
|
||||
or the cursor is closed, regardless of statement type.
|
||||
|
||||
For cases where the DBAPI does not support rowcount for a particular
|
||||
kind of statement and/or execution, the returned value will be ``-1``,
|
||||
which is delivered directly from the DBAPI and is part of :pep:`249`.
|
||||
All DBAPIs should support rowcount for single-parameter-set
|
||||
UPDATE and DELETE statements, however.
|
||||
The 'rowcount' reports the number of rows *matched*
|
||||
by the WHERE criterion of an UPDATE or DELETE statement.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -2008,47 +1984,38 @@ class CursorResult(Result[_T]):
|
||||
|
||||
* This attribute returns the number of rows *matched*,
|
||||
which is not necessarily the same as the number of rows
|
||||
that were actually *modified*. For example, an UPDATE statement
|
||||
that were actually *modified* - an UPDATE statement, for example,
|
||||
may have no net change on a given row if the SET values
|
||||
given are the same as those present in the row already.
|
||||
Such a row would be matched but not modified.
|
||||
On backends that feature both styles, such as MySQL,
|
||||
rowcount is configured to return the match
|
||||
rowcount is configured by default to return the match
|
||||
count in all cases.
|
||||
|
||||
* :attr:`_engine.CursorResult.rowcount` in the default case is
|
||||
*only* useful in conjunction with an UPDATE or DELETE statement,
|
||||
and only with a single set of parameters. For other kinds of
|
||||
statements, SQLAlchemy will not attempt to pre-memoize the value
|
||||
unless the
|
||||
:paramref:`.Connection.execution_options.preserve_rowcount`
|
||||
execution option is used. Note that contrary to :pep:`249`, many
|
||||
DBAPIs do not support rowcount values for statements that are not
|
||||
UPDATE or DELETE, particularly when rows are being returned which
|
||||
are not fully pre-buffered. DBAPIs that dont support rowcount
|
||||
for a particular kind of statement should return the value ``-1``
|
||||
for such statements.
|
||||
* :attr:`_engine.CursorResult.rowcount`
|
||||
is *only* useful in conjunction
|
||||
with an UPDATE or DELETE statement. Contrary to what the Python
|
||||
DBAPI says, it does *not* reliably return the
|
||||
number of rows available from the results of a SELECT statement
|
||||
as DBAPIs cannot support this functionality when rows are
|
||||
unbuffered.
|
||||
|
||||
* :attr:`_engine.CursorResult.rowcount` may not be meaningful
|
||||
when executing a single statement with multiple parameter sets
|
||||
(i.e. an :term:`executemany`). Most DBAPIs do not sum "rowcount"
|
||||
values across multiple parameter sets and will return ``-1``
|
||||
when accessed.
|
||||
* :attr:`_engine.CursorResult.rowcount`
|
||||
may not be fully implemented by
|
||||
all dialects. In particular, most DBAPIs do not support an
|
||||
aggregate rowcount result from an executemany call.
|
||||
The :meth:`_engine.CursorResult.supports_sane_rowcount` and
|
||||
:meth:`_engine.CursorResult.supports_sane_multi_rowcount` methods
|
||||
will report from the dialect if each usage is known to be
|
||||
supported.
|
||||
|
||||
* SQLAlchemy's :ref:`engine_insertmanyvalues` feature does support
|
||||
a correct population of :attr:`_engine.CursorResult.rowcount`
|
||||
when the :paramref:`.Connection.execution_options.preserve_rowcount`
|
||||
execution option is set to True.
|
||||
|
||||
* Statements that use RETURNING may not support rowcount, returning
|
||||
a ``-1`` value instead.
|
||||
* Statements that use RETURNING may not return a correct
|
||||
rowcount.
|
||||
|
||||
.. seealso::
|
||||
|
||||
:ref:`tutorial_update_delete_rowcount` - in the :ref:`unified_tutorial`
|
||||
|
||||
:paramref:`.Connection.execution_options.preserve_rowcount`
|
||||
|
||||
""" # noqa: E501
|
||||
try:
|
||||
return self.context.rowcount
|
||||
@@ -2142,7 +2109,8 @@ class CursorResult(Result[_T]):
|
||||
|
||||
def merge(self, *others: Result[Any]) -> MergedResult[Any]:
|
||||
merged_result = super().merge(*others)
|
||||
if self.context._has_rowcount:
|
||||
setup_rowcounts = self.context._has_rowcount
|
||||
if setup_rowcounts:
|
||||
merged_result.rowcount = sum(
|
||||
cast("CursorResult[Any]", result).rowcount
|
||||
for result in (self,) + others
|
||||
|
||||
Reference in New Issue
Block a user