diff options
Diffstat (limited to 'lib/sqlalchemy/engine')
| -rw-r--r-- | lib/sqlalchemy/engine/__init__.py | 5 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/base.py | 3 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/cursor.py | 136 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/default.py | 109 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/interfaces.py | 22 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/result.py | 1 |
6 files changed, 148 insertions, 128 deletions
diff --git a/lib/sqlalchemy/engine/__init__.py b/lib/sqlalchemy/engine/__init__.py index 32f3f2ecc..29dd6aff9 100644 --- a/lib/sqlalchemy/engine/__init__.py +++ b/lib/sqlalchemy/engine/__init__.py @@ -25,12 +25,7 @@ from .base import Transaction as Transaction from .base import TwoPhaseTransaction as TwoPhaseTransaction from .create import create_engine as create_engine from .create import engine_from_config as engine_from_config -from .cursor import BaseCursorResult as BaseCursorResult -from .cursor import BufferedColumnResultProxy as BufferedColumnResultProxy -from .cursor import BufferedColumnRow as BufferedColumnRow -from .cursor import BufferedRowResultProxy as BufferedRowResultProxy from .cursor import CursorResult as CursorResult -from .cursor import FullyBufferedResultProxy as FullyBufferedResultProxy from .cursor import ResultProxy as ResultProxy from .interfaces import AdaptedConnection as AdaptedConnection from .interfaces import BindTyping as BindTyping diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 37faa880e..d8009e26c 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -23,6 +23,7 @@ from typing import Tuple from typing import Type from typing import Union +from .interfaces import _IsolationLevel from .interfaces import BindTyping from .interfaces import ConnectionEventsTarget from .interfaces import DBAPICursor @@ -510,7 +511,7 @@ class Connection(ConnectionEventsTarget, inspection.Inspectable["Inspector"]): self._handle_dbapi_exception(e, None, None, None, None) @property - def default_isolation_level(self) -> str: + def default_isolation_level(self) -> Optional[_IsolationLevel]: """The default isolation level assigned to this :class:`_engine.Connection`. diff --git a/lib/sqlalchemy/engine/cursor.py b/lib/sqlalchemy/engine/cursor.py index 78805bac1..821c0cb8e 100644 --- a/lib/sqlalchemy/engine/cursor.py +++ b/lib/sqlalchemy/engine/cursor.py @@ -23,8 +23,9 @@ from typing import List from typing import Optional from typing import Sequence from typing import Tuple -from typing import Type +from typing import Union +from .result import MergedResult from .result import Result from .result import ResultMetaData from .result import SimpleResultMetaData @@ -36,10 +37,12 @@ from ..sql import elements from ..sql import sqltypes from ..sql import util as sql_util from ..sql.base import _generative +from ..sql.compiler import ResultColumnsEntry from ..sql.compiler import RM_NAME from ..sql.compiler import RM_OBJECTS from ..sql.compiler import RM_RENDERED_NAME from ..sql.compiler import RM_TYPE +from ..sql.type_api import TypeEngine from ..util import compat from ..util.typing import Literal @@ -101,6 +104,7 @@ class CursorResultMetaData(ResultMetaData): _keymap_by_result_column_idx: Optional[Dict[int, _KeyMapRecType]] _unpickled: bool _safe_for_cache: bool + _translated_indexes: Optional[List[int]] returns_rows: ClassVar[bool] = True @@ -123,7 +127,6 @@ class CursorResultMetaData(ResultMetaData): if self._translated_indexes: indexes = [self._translated_indexes[idx] for idx in indexes] - tup = tuplegetter(*indexes) new_metadata = self.__class__.__new__(self.__class__) @@ -526,7 +529,7 @@ class CursorResultMetaData(ResultMetaData): def _merge_textual_cols_by_position( self, context, cursor_description, result_columns ): - num_ctx_cols = len(result_columns) if result_columns else None + num_ctx_cols = len(result_columns) if num_ctx_cols > len(cursor_description): util.warn( @@ -568,6 +571,8 @@ class CursorResultMetaData(ResultMetaData): match_map = self._create_description_match_map( result_columns, loose_column_name_matching ) + mapped_type: TypeEngine[Any] + for ( idx, colname, @@ -597,15 +602,17 @@ class CursorResultMetaData(ResultMetaData): @classmethod def _create_description_match_map( cls, - result_columns, - loose_column_name_matching=False, - ): + result_columns: List[ResultColumnsEntry], + loose_column_name_matching: bool = False, + ) -> Dict[Union[str, object], Tuple[str, List[Any], TypeEngine[Any], int]]: """when matching cursor.description to a set of names that are present in a Compiled object, as is the case with TextualSelect, get all the names we expect might match those in cursor.description. """ - d = {} + d: Dict[ + Union[str, object], Tuple[str, List[Any], TypeEngine[Any], int] + ] = {} for ridx, elem in enumerate(result_columns): key = elem[RM_RENDERED_NAME] @@ -630,7 +637,6 @@ class CursorResultMetaData(ResultMetaData): r_key, (elem[RM_NAME], elem[RM_OBJECTS], elem[RM_TYPE], ridx), ) - return d def _merge_cols_by_none(self, context, cursor_description): @@ -739,7 +745,9 @@ class CursorResultMetaData(ResultMetaData): self._keys = state["_keys"] self._unpickled = True if state["_translated_indexes"]: - self._translated_indexes = state["_translated_indexes"] + self._translated_indexes = cast( + "List[int]", state["_translated_indexes"] + ) self._tuplefilter = tuplegetter(*self._translated_indexes) else: self._translated_indexes = self._tuplefilter = None @@ -1144,12 +1152,32 @@ class _NoResultMetaData(ResultMetaData): _NO_RESULT_METADATA = _NoResultMetaData() -class BaseCursorResult: - """Base class for database result objects.""" +class CursorResult(Result): + """A Result that is representing state from a DBAPI cursor. + + .. versionchanged:: 1.4 The :class:`.CursorResult`` + class replaces the previous :class:`.ResultProxy` interface. + This classes are based on the :class:`.Result` calling API + which provides an updated usage model and calling facade for + SQLAlchemy Core and SQLAlchemy ORM. + + Returns database rows via the :class:`.Row` class, which provides + additional API features and behaviors on top of the raw data returned by + the DBAPI. Through the use of filters such as the :meth:`.Result.scalars` + method, other kinds of objects may also be returned. + + .. seealso:: + + :ref:`coretutorial_selecting` - introductory material for accessing + :class:`_engine.CursorResult` and :class:`.Row` objects. - _metadata: ResultMetaData + """ + + _metadata: Union[CursorResultMetaData, _NoResultMetaData] + _no_result_metadata = _NO_RESULT_METADATA _soft_closed: bool = False closed: bool = False + _is_cursor = True def __init__(self, context, cursor_strategy, cursor_description): self.context = context @@ -1169,11 +1197,11 @@ class BaseCursorResult: if echo: log = self.context.connection._log_debug - def log_row(row): + def _log_row(row): log("Row %r", sql_util._repr_row(row)) return row - self._row_logging_fn = log_row + self._row_logging_fn = log_row = _log_row else: log_row = None @@ -1188,13 +1216,16 @@ class BaseCursorResult: ) if log_row: - def make_row(row): + def _make_row_2(row): made_row = _make_row(row) + assert log_row is not None log_row(made_row) return made_row + make_row = _make_row_2 else: make_row = _make_row + self._set_memoized_attribute("_row_getter", make_row) else: @@ -1208,7 +1239,7 @@ class BaseCursorResult: if compiled._cached_metadata: metadata = compiled._cached_metadata else: - metadata = self._cursor_metadata(self, cursor_description) + metadata = CursorResultMetaData(self, cursor_description) if metadata._safe_for_cache: compiled._cached_metadata = metadata @@ -1239,7 +1270,7 @@ class BaseCursorResult: self._metadata = metadata else: - self._metadata = metadata = self._cursor_metadata( + self._metadata = metadata = CursorResultMetaData( self, cursor_description ) if self._echo: @@ -1669,33 +1700,6 @@ class BaseCursorResult: """ return self.context.isinsert - -class CursorResult(BaseCursorResult, Result): - """A Result that is representing state from a DBAPI cursor. - - .. versionchanged:: 1.4 The :class:`.CursorResult`` - class replaces the previous :class:`.ResultProxy` interface. - This classes are based on the :class:`.Result` calling API - which provides an updated usage model and calling facade for - SQLAlchemy Core and SQLAlchemy ORM. - - Returns database rows via the :class:`.Row` class, which provides - additional API features and behaviors on top of the raw data returned by - the DBAPI. Through the use of filters such as the :meth:`.Result.scalars` - method, other kinds of objects may also be returned. - - .. seealso:: - - :ref:`coretutorial_selecting` - introductory material for accessing - :class:`_engine.CursorResult` and :class:`.Row` objects. - - """ - - _cursor_metadata: Type[ResultMetaData] = CursorResultMetaData - _cursor_strategy_cls = CursorFetchStrategy - _no_result_metadata = _NO_RESULT_METADATA - _is_cursor = True - def _fetchiter_impl(self): fetchone = self.cursor_strategy.fetchone @@ -1717,12 +1721,13 @@ class CursorResult(BaseCursorResult, Result): def _raw_row_iterator(self): return self._fetchiter_impl() - def merge(self, *others): - merged_result = super(CursorResult, self).merge(*others) + def merge(self, *others: Result) -> MergedResult: + merged_result = super().merge(*others) setup_rowcounts = not self._metadata.returns_rows if setup_rowcounts: merged_result.rowcount = sum( - result.rowcount for result in (self,) + others + cast(CursorResult, result).rowcount + for result in (self,) + others ) return merged_result @@ -1756,40 +1761,3 @@ class CursorResult(BaseCursorResult, Result): ResultProxy = CursorResult - - -class BufferedRowResultProxy(ResultProxy): - """A ResultProxy with row buffering behavior. - - .. deprecated:: 1.4 this class is now supplied using a strategy object. - See :class:`.BufferedRowCursorFetchStrategy`. - - """ - - _cursor_strategy_cls: Type[ - CursorFetchStrategy - ] = BufferedRowCursorFetchStrategy - - -class FullyBufferedResultProxy(ResultProxy): - """A result proxy that buffers rows fully upon creation. - - .. deprecated:: 1.4 this class is now supplied using a strategy object. - See :class:`.FullyBufferedCursorFetchStrategy`. - - """ - - _cursor_strategy_cls = FullyBufferedCursorFetchStrategy - - -class BufferedColumnRow(Row): - """Row is now BufferedColumn in all cases""" - - -class BufferedColumnResultProxy(ResultProxy): - """A ResultProxy with column buffering behavior. - - .. versionchanged:: 1.4 This is now the default behavior of the Row - and this class does not change behavior in any way. - - """ diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 0e0c76389..2579f573c 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -55,6 +55,9 @@ from ..sql.compiler import SQLCompiler from ..sql.elements import quoted_name if typing.TYPE_CHECKING: + from .base import Connection + from .base import Engine + from .characteristics import ConnectionCharacteristic from .interfaces import _AnyMultiExecuteParams from .interfaces import _CoreMultiExecuteParams from .interfaces import _CoreSingleExecuteParams @@ -62,6 +65,7 @@ if typing.TYPE_CHECKING: from .interfaces import _DBAPIMultiExecuteParams from .interfaces import _DBAPISingleExecuteParams from .interfaces import _ExecuteOptions + from .interfaces import _MutableCoreSingleExecuteParams from .result import _ProcessorType from .row import Row from .url import URL @@ -71,6 +75,7 @@ if typing.TYPE_CHECKING: from ..sql import Executable from ..sql.compiler import Compiled from ..sql.compiler import ResultColumnsEntry + from ..sql.compiler import TypeCompiler from ..sql.schema import Column from ..sql.type_api import TypeEngine @@ -92,7 +97,11 @@ class DefaultDialect(Dialect): statement_compiler = compiler.SQLCompiler ddl_compiler = compiler.DDLCompiler - type_compiler = compiler.GenericTypeCompiler # type: ignore + if typing.TYPE_CHECKING: + type_compiler: TypeCompiler + else: + type_compiler = compiler.GenericTypeCompiler + preparer = compiler.IdentifierPreparer supports_alter = True supports_comments = False @@ -202,7 +211,7 @@ class DefaultDialect(Dialect): server_version_info = None - default_schema_name = None + default_schema_name: Optional[str] = None # indicates symbol names are # UPPERCASEd if they are case insensitive @@ -290,7 +299,12 @@ class DefaultDialect(Dialect): self.positional = self.paramstyle in ("qmark", "format", "numeric") self.identifier_preparer = self.preparer(self) self._on_connect_isolation_level = isolation_level - self.type_compiler = self.type_compiler(self) + + tt_callable = cast( + Type[compiler.GenericTypeCompiler], + self.type_compiler, + ) + self.type_compiler = tt_callable(self) if supports_native_boolean is not None: self.supports_native_boolean = supports_native_boolean @@ -490,12 +504,14 @@ class DefaultDialect(Dialect): opts.update(url.query) return [[], opts] - def set_engine_execution_options(self, engine, opts): + def set_engine_execution_options( + self, engine: Engine, opts: Mapping[str, str] + ) -> None: supported_names = set(self.connection_characteristics).intersection( opts ) if supported_names: - characteristics = util.immutabledict( + characteristics: Mapping[str, str] = util.immutabledict( (name, opts[name]) for name in supported_names ) @@ -505,12 +521,14 @@ class DefaultDialect(Dialect): connection, characteristics ) - def set_connection_execution_options(self, connection, opts): + def set_connection_execution_options( + self, connection: Connection, opts: Mapping[str, str] + ) -> None: supported_names = set(self.connection_characteristics).intersection( opts ) if supported_names: - characteristics = util.immutabledict( + characteristics: Mapping[str, str] = util.immutabledict( (name, opts[name]) for name in supported_names ) self._set_connection_characteristics(connection, characteristics) @@ -800,7 +818,7 @@ class DefaultExecutionContext(ExecutionContext): dialect: Dialect unicode_statement: str cursor: DBAPICursor - compiled_parameters: _CoreMultiExecuteParams + compiled_parameters: List[_MutableCoreSingleExecuteParams] parameters: _DBAPIMultiExecuteParams extracted_parameters: _CoreSingleExecuteParams @@ -1157,7 +1175,11 @@ class DefaultExecutionContext(ExecutionContext): parameters = {} conn._cursor_execute(self.cursor, stmt, parameters, context=self) - r = self.cursor.fetchone()[0] + row = self.cursor.fetchone() + if row is not None: + r = row[0] + else: + r = None if type_ is not None: # apply type post processors to the result proc = type_._cached_result_processor( @@ -1299,10 +1321,11 @@ class DefaultExecutionContext(ExecutionContext): result = _cursor.CursorResult(self, strategy, cursor_description) + compiled = self.compiled if ( - self.compiled + compiled and not self.isddl - and self.compiled.has_out_parameters + and cast(SQLCompiler, compiled).has_out_parameters ): self._setup_out_parameters(result) @@ -1311,10 +1334,11 @@ class DefaultExecutionContext(ExecutionContext): return result def _setup_out_parameters(self, result): + compiled = cast(SQLCompiler, self.compiled) out_bindparams = [ (param, name) - for param, name in self.compiled.bind_names.items() + for param, name in compiled.bind_names.items() if param.isoutparam ] out_parameters = {} @@ -1339,9 +1363,10 @@ class DefaultExecutionContext(ExecutionContext): result.out_parameters = out_parameters def _setup_dml_or_text_result(self): + compiled = cast(SQLCompiler, self.compiled) if self.isinsert: - if self.compiled.postfetch_lastrowid: + if compiled.postfetch_lastrowid: self.inserted_primary_key_rows = ( self._setup_ins_pk_from_lastrowid() ) @@ -1397,7 +1422,8 @@ class DefaultExecutionContext(ExecutionContext): result.rowcount row = result.fetchone() - self.returned_default_rows = [row] + if row is not None: + self.returned_default_rows = [row] result._soft_close() @@ -1420,13 +1446,17 @@ class DefaultExecutionContext(ExecutionContext): return self._setup_ins_pk_from_empty() def _setup_ins_pk_from_lastrowid(self): - getter = self.compiled._inserted_primary_key_from_lastrowid_getter + getter = cast( + SQLCompiler, self.compiled + )._inserted_primary_key_from_lastrowid_getter lastrowid = self.get_lastrowid() return [getter(lastrowid, self.compiled_parameters[0])] def _setup_ins_pk_from_empty(self): - getter = self.compiled._inserted_primary_key_from_lastrowid_getter + getter = cast( + SQLCompiler, self.compiled + )._inserted_primary_key_from_lastrowid_getter return [getter(None, param) for param in self.compiled_parameters] def _setup_ins_pk_from_implicit_returning(self, result, rows): @@ -1434,7 +1464,9 @@ class DefaultExecutionContext(ExecutionContext): if not rows: return [] - getter = self.compiled._inserted_primary_key_from_returning_getter + getter = cast( + SQLCompiler, self.compiled + )._inserted_primary_key_from_returning_getter compiled_params = self.compiled_parameters return [ @@ -1443,7 +1475,7 @@ class DefaultExecutionContext(ExecutionContext): def lastrow_has_defaults(self): return (self.isinsert or self.isupdate) and bool( - self.compiled.postfetch + cast(SQLCompiler, self.compiled).postfetch ) def _set_input_sizes(self): @@ -1464,7 +1496,7 @@ class DefaultExecutionContext(ExecutionContext): if self.isddl or self.is_text: return - compiled = self.compiled + compiled = cast(SQLCompiler, self.compiled) inputsizes = compiled._get_set_input_sizes_lookup() @@ -1487,7 +1519,8 @@ class DefaultExecutionContext(ExecutionContext): if dialect.positional: items = [ - (key, compiled.binds[key]) for key in compiled.positiontup + (key, compiled.binds[key]) + for key in compiled.positiontup or () ] else: items = [ @@ -1495,7 +1528,7 @@ class DefaultExecutionContext(ExecutionContext): for bindparam, key in compiled.bind_names.items() ] - generic_inputsizes = [] + generic_inputsizes: List[Tuple[str, Any, TypeEngine[Any]]] = [] for key, bindparam in items: if bindparam in compiled.literal_execute_params: continue @@ -1578,20 +1611,19 @@ class DefaultExecutionContext(ExecutionContext): compiled_params = compiled.construct_params() processors = compiled._bind_processors if compiled.positional: - positiontup = compiled.positiontup parameters = self.dialect.execute_sequence_format( [ - processors[key](compiled_params[key]) + processors[key](compiled_params[key]) # type: ignore if key in processors else compiled_params[key] - for key in positiontup + for key in compiled.positiontup or () ] ) else: parameters = dict( ( key, - processors[key](compiled_params[key]) + processors[key](compiled_params[key]) # type: ignore if key in processors else compiled_params[key], ) @@ -1667,15 +1699,18 @@ class DefaultExecutionContext(ExecutionContext): "get_current_parameters() can only be invoked in the " "context of a Python side column default function" ) - - compile_state = self.compiled.compile_state + else: + assert column is not None + assert parameters is not None + compile_state = cast(SQLCompiler, self.compiled).compile_state + assert compile_state is not None if ( isolate_multiinsert_groups and self.isinsert and compile_state._has_multi_parameters ): if column._is_multiparam_column: - index = column.index + 1 + index = column.index + 1 # type: ignore d = {column.original.key: parameters[column.key]} else: d = {column.key: parameters[column.key]} @@ -1701,12 +1736,14 @@ class DefaultExecutionContext(ExecutionContext): return self._exec_default(column, column.onupdate, column.type) def _process_executemany_defaults(self): - key_getter = self.compiled._within_exec_param_key_getter + compiled = cast(SQLCompiler, self.compiled) - scalar_defaults = {} + key_getter = compiled._within_exec_param_key_getter - insert_prefetch = self.compiled.insert_prefetch - update_prefetch = self.compiled.update_prefetch + scalar_defaults: Dict[Column[Any], Any] = {} + + insert_prefetch = compiled.insert_prefetch + update_prefetch = compiled.update_prefetch # pre-determine scalar Python-side defaults # to avoid many calls of get_insert_default()/ @@ -1739,12 +1776,14 @@ class DefaultExecutionContext(ExecutionContext): del self.current_parameters def _process_executesingle_defaults(self): - key_getter = self.compiled._within_exec_param_key_getter + compiled = cast(SQLCompiler, self.compiled) + + key_getter = compiled._within_exec_param_key_getter self.current_parameters = ( compiled_parameters ) = self.compiled_parameters[0] - for c in self.compiled.insert_prefetch: + for c in compiled.insert_prefetch: if c.default and not c.default.is_sequence and c.default.is_scalar: val = c.default.arg else: @@ -1753,7 +1792,7 @@ class DefaultExecutionContext(ExecutionContext): if val is not None: compiled_parameters[key_getter(c)] = val - for c in self.compiled.update_prefetch: + for c in compiled.update_prefetch: val = self.get_update_default(c) if val is not None: diff --git a/lib/sqlalchemy/engine/interfaces.py b/lib/sqlalchemy/engine/interfaces.py index 5aefcf5b5..e65546eb7 100644 --- a/lib/sqlalchemy/engine/interfaces.py +++ b/lib/sqlalchemy/engine/interfaces.py @@ -36,7 +36,6 @@ from ..sql.compiler import TypeCompiler as TypeCompiler from ..sql.compiler import TypeCompiler # noqa from ..util import immutabledict from ..util.concurrency import await_only -from ..util.typing import _TypeToInstance from ..util.typing import Literal from ..util.typing import NotRequired from ..util.typing import Protocol @@ -58,6 +57,8 @@ if TYPE_CHECKING: from ..sql.elements import ClauseElement from ..sql.schema import Column from ..sql.schema import ColumnDefault + from ..sql.schema import Sequence as Sequence_SchemaItem + from ..sql.sqltypes import Integer from ..sql.type_api import TypeEngine ConnectArgsType = Tuple[Tuple[str], MutableMapping[str, Any]] @@ -156,6 +157,8 @@ class DBAPICursor(Protocol): arraysize: int + lastrowid: int + def close(self) -> None: ... @@ -196,6 +199,7 @@ class DBAPICursor(Protocol): _CoreSingleExecuteParams = Mapping[str, Any] +_MutableCoreSingleExecuteParams = MutableMapping[str, Any] _CoreMultiExecuteParams = Sequence[_CoreSingleExecuteParams] _CoreAnyExecuteParams = Union[ _CoreMultiExecuteParams, _CoreSingleExecuteParams @@ -605,7 +609,7 @@ class Dialect(EventTarget): ddl_compiler: Type[DDLCompiler] """a :class:`.Compiled` class used to compile DDL statements""" - type_compiler: _TypeToInstance[TypeCompiler] + type_compiler: Union[Type[TypeCompiler], TypeCompiler] """a :class:`.Compiled` class used to compile SQL type objects""" preparer: Type[IdentifierPreparer] @@ -633,7 +637,7 @@ class Dialect(EventTarget): """ - default_isolation_level: _IsolationLevel + default_isolation_level: Optional[_IsolationLevel] """the isolation that is implicitly present on new connections""" execution_ctx_cls: Type["ExecutionContext"] @@ -653,6 +657,13 @@ class Dialect(EventTarget): max_identifier_length: int """The maximum length of identifier names.""" + supports_server_side_cursors: bool + """indicates if the dialect supports server side cursors""" + + server_side_cursors: bool + """deprecated; indicates if the dialect should attempt to use server + side cursors by default""" + supports_sane_rowcount: bool """Indicate whether the dialect properly implements rowcount for ``UPDATE`` and ``DELETE`` statements. @@ -2302,6 +2313,11 @@ class ExecutionContext: def _setup_result_proxy(self) -> Result: raise NotImplementedError() + def fire_sequence(self, seq: Sequence_SchemaItem, type_: Integer) -> int: + """given a :class:`.Sequence`, invoke it and return the next int + value""" + raise NotImplementedError() + def create_cursor(self) -> DBAPICursor: """Return a new cursor generated from this ExecutionContext's connection. diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 0951d5770..87d3cac1c 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -1880,6 +1880,7 @@ class MergedResult(IteratorResult): """ closed = False + rowcount: Optional[int] def __init__( self, cursor_metadata: ResultMetaData, results: Sequence[Result] |
