From: Mike Bayer Date: Thu, 10 Jul 2025 19:29:29 +0000 (-0400) Subject: Unconditionally flush session on all statement executions X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=45a5c31e5c49e9b6e288465bcce30b87136871c0;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Unconditionally flush session on all statement executions This change simplifies session execution logic to always run autoflush for all statement executions, regardless of whether they are ORM or Core statements. Previously, autoflush was conditionally applied only when ORM-related statements were detected, which had become difficult to define clearly with the unified v2 syntax that blurs the lines between Core and ORM execution patterns. The implementation adds a call to self._autoflush() in the Core execution path (when compile_state_cls is None) in Session._execute_internal(), ensuring consistent autoflush behavior across all types of SQL execution. This provides more predictable session behavior and eliminates the confusion around which statements trigger autoflush. Also included is some improvement to the migration_21 document Fixes: #9809 Change-Id: I0b1974f1981c8a747a0feaaca74ed45106b2b160 --- diff --git a/doc/build/changelog/migration_21.rst b/doc/build/changelog/migration_21.rst index 5634cdda64..7d25cf09df 100644 --- a/doc/build/changelog/migration_21.rst +++ b/doc/build/changelog/migration_21.rst @@ -10,75 +10,18 @@ What's New in SQLAlchemy 2.1? version 2.1. -.. _change_10635: - -``Row`` now represents individual column types directly without ``Tuple`` --------------------------------------------------------------------------- - -SQLAlchemy 2.0 implemented a broad array of :pep:`484` typing throughout -all components, including a new ability for row-returning statements such -as :func:`_sql.select` to maintain track of individual column types, which -were then passed through the execution phase onto the :class:`_engine.Result` -object and then to the individual :class:`_engine.Row` objects. Described -at :ref:`change_result_typing_20`, this approach solved several issues -with statement / row typing, but some remained unsolvable. In 2.1, one -of those issues, that the individual column types needed to be packaged -into a ``typing.Tuple``, is now resolved using new :pep:`646` integration, -which allows for tuple-like types that are not actually typed as ``Tuple``. +Introduction +============ -In SQLAlchemy 2.0, a statement such as:: +This guide introduces what's new in SQLAlchemy version 2.1 +and also documents changes which affect users migrating +their applications from the 2.0 series of SQLAlchemy to 2.1. - stmt = select(column("x", Integer), column("y", String)) - -Would be typed as:: - - Select[Tuple[int, str]] - -In 2.1, it's now typed as:: - - Select[int, str] - -When executing ``stmt``, the :class:`_engine.Result` and :class:`_engine.Row` -objects will be typed as ``Result[int, str]`` and ``Row[int, str]``, respectively. -The prior workaround using :attr:`_engine.Row._t` to type as a real ``Tuple`` -is no longer needed and projects can migrate off this pattern. - -Mypy users will need to make use of **Mypy 1.7 or greater** for pep-646 -integration to be available. - -Limitations -^^^^^^^^^^^ - -Not yet solved by pep-646 or any other pep is the ability for an arbitrary -number of expressions within :class:`_sql.Select` and others to be mapped to -row objects, without stating each argument position explicitly within typing -annotations. To work around this issue, SQLAlchemy makes use of automated -"stub generation" tools to generate hardcoded mappings of different numbers of -positional arguments to constructs like :func:`_sql.select` to resolve to -individual ``Unpack[]`` expressions (in SQLAlchemy 2.0, this generation -produced ``Tuple[]`` annotations instead). This means that there are arbitrary -limits on how many specific column expressions will be typed within the -:class:`_engine.Row` object, without restoring to ``Any`` for remaining -expressions; for :func:`_sql.select`, it's currently ten expressions, and -for DML expressions like :func:`_dml.insert` that use :meth:`_dml.Insert.returning`, -it's eight. If and when a new pep that provides a ``Map`` operator -to pep-646 is proposed, this limitation can be lifted. [1]_ Originally, it was -mistakenly assumed that this limitation prevented pep-646 from being usable at all, -however, the ``Unpack`` construct does in fact replace everything that -was done using ``Tuple`` in 2.0. - -An additional limitation for which there is no proposed solution is that -there's no way for the name-based attributes on :class:`_engine.Row` to be -automatically typed, so these continue to be typed as ``Any`` (e.g. ``row.x`` -and ``row.y`` for the above example). With current language features, -this could only be fixed by having an explicit class-based construct that -allows one to compose an explicit :class:`_engine.Row` with explicit fields -up front, which would be verbose and not automatic. - -.. [1] https://github.com/python/typing/discussions/1001#discussioncomment-1897813 - -:ticket:`10635` +Please carefully review the sections on behavioral changes for +potentially backwards-incompatible changes in behavior. +General +======= .. _change_10197: @@ -104,6 +47,47 @@ need to be aware of this extra installation dependency. :ticket:`10197` +New Features and Improvements - ORM +==================================== + + + +.. _change_9809: + +Session autoflush behavior simplified to be unconditional +--------------------------------------------------------- + +Session autoflush behavior has been simplified to unconditionally flush the +session each time an execution takes place, regardless of whether an ORM +statement or Core statement is being executed. This change eliminates the +previous conditional logic that only flushed when ORM-related statements +were detected. + +Previously, the session would only autoflush when executing ORM queries:: + + # 2.0 behavior - autoflush only occurred for ORM statements + session.add(User(name="new user")) + + # This would trigger autoflush + users = session.execute(select(User)).scalars().all() + + # This would NOT trigger autoflush + result = session.execute(text("SELECT * FROM users")) + +In 2.1, autoflush occurs for all statement executions:: + + # 2.1 behavior - autoflush occurs for all executions + session.add(User(name="new user")) + + # Both of these now trigger autoflush + users = session.execute(select(User)).scalars().all() + result = session.execute(text("SELECT * FROM users")) + +This change provides more consistent and predictable session behavior across +all types of SQL execution. + +:ticket:`9809` + .. _change_10050: @@ -344,6 +328,79 @@ This change includes the following API changes: :ticket:`12168` +New Features and Improvements - Core +===================================== + + +.. _change_10635: + +``Row`` now represents individual column types directly without ``Tuple`` +-------------------------------------------------------------------------- + +SQLAlchemy 2.0 implemented a broad array of :pep:`484` typing throughout +all components, including a new ability for row-returning statements such +as :func:`_sql.select` to maintain track of individual column types, which +were then passed through the execution phase onto the :class:`_engine.Result` +object and then to the individual :class:`_engine.Row` objects. Described +at :ref:`change_result_typing_20`, this approach solved several issues +with statement / row typing, but some remained unsolvable. In 2.1, one +of those issues, that the individual column types needed to be packaged +into a ``typing.Tuple``, is now resolved using new :pep:`646` integration, +which allows for tuple-like types that are not actually typed as ``Tuple``. + +In SQLAlchemy 2.0, a statement such as:: + + stmt = select(column("x", Integer), column("y", String)) + +Would be typed as:: + + Select[Tuple[int, str]] + +In 2.1, it's now typed as:: + + Select[int, str] + +When executing ``stmt``, the :class:`_engine.Result` and :class:`_engine.Row` +objects will be typed as ``Result[int, str]`` and ``Row[int, str]``, respectively. +The prior workaround using :attr:`_engine.Row._t` to type as a real ``Tuple`` +is no longer needed and projects can migrate off this pattern. + +Mypy users will need to make use of **Mypy 1.7 or greater** for pep-646 +integration to be available. + +Limitations +^^^^^^^^^^^ + +Not yet solved by pep-646 or any other pep is the ability for an arbitrary +number of expressions within :class:`_sql.Select` and others to be mapped to +row objects, without stating each argument position explicitly within typing +annotations. To work around this issue, SQLAlchemy makes use of automated +"stub generation" tools to generate hardcoded mappings of different numbers of +positional arguments to constructs like :func:`_sql.select` to resolve to +individual ``Unpack[]`` expressions (in SQLAlchemy 2.0, this generation +produced ``Tuple[]`` annotations instead). This means that there are arbitrary +limits on how many specific column expressions will be typed within the +:class:`_engine.Row` object, without restoring to ``Any`` for remaining +expressions; for :func:`_sql.select`, it's currently ten expressions, and +for DML expressions like :func:`_dml.insert` that use :meth:`_dml.Insert.returning`, +it's eight. If and when a new pep that provides a ``Map`` operator +to pep-646 is proposed, this limitation can be lifted. [1]_ Originally, it was +mistakenly assumed that this limitation prevented pep-646 from being usable at all, +however, the ``Unpack`` construct does in fact replace everything that +was done using ``Tuple`` in 2.0. + +An additional limitation for which there is no proposed solution is that +there's no way for the name-based attributes on :class:`_engine.Row` to be +automatically typed, so these continue to be typed as ``Any`` (e.g. ``row.x`` +and ``row.y`` for the above example). With current language features, +this could only be fixed by having an explicit class-based construct that +allows one to compose an explicit :class:`_engine.Row` with explicit fields +up front, which would be verbose and not automatic. + +.. [1] https://github.com/python/typing/discussions/1001#discussioncomment-1897813 + +:ticket:`10635` + .. _change_11234: @@ -441,3 +498,5 @@ For implementations using the ``asyncpg`` driver, the new type is incompatible w the existing ``asyncpg.BitString`` type. :ticket:`10556` + + diff --git a/doc/build/changelog/unreleased_21/9809.rst b/doc/build/changelog/unreleased_21/9809.rst new file mode 100644 index 0000000000..b264529a8e --- /dev/null +++ b/doc/build/changelog/unreleased_21/9809.rst @@ -0,0 +1,16 @@ +.. change:: + :tags: feature, orm + :tickets: 9809 + + Session autoflush behavior has been simplified to unconditionally flush the + session each time an execution takes place, regardless of whether an ORM + statement or Core statement is being executed. This change eliminates the + previous conditional logic that only flushed when ORM-related statements + were detected, which had become difficult to define clearly with the unified + v2 syntax that allows both Core and ORM execution patterns. The change + provides more consistent and predictable session behavior across all types + of SQL execution. + + .. seealso:: + + :ref:`change_9809` diff --git a/doc/build/index.rst b/doc/build/index.rst index add84a51e2..b5e70727dc 100644 --- a/doc/build/index.rst +++ b/doc/build/index.rst @@ -55,7 +55,7 @@ SQLAlchemy Documentation .. container:: - Users upgrading to SQLAlchemy version 2.0 will want to read: + Users upgrading to SQLAlchemy version 2.1 will want to read: * :doc:`What's New in SQLAlchemy 2.1? ` - New features and behaviors in version 2.1 diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index d590d5de73..69d0f8aca9 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -2228,6 +2228,9 @@ class Session(_SessionClassMethods, EventTarget): bind_arguments, False, ) + else: + # Issue #9809: unconditionally autoflush for Core statements + self._autoflush() bind = self.get_bind(**bind_arguments) diff --git a/test/orm/test_core_compilation.py b/test/orm/test_core_compilation.py index 10b831f837..b49d4286bf 100644 --- a/test/orm/test_core_compilation.py +++ b/test/orm/test_core_compilation.py @@ -501,10 +501,8 @@ class PropagateAttrsTest(QueryTest): r = s.execute(stmt) r.close() - if expected: - eq_(before_flush.mock_calls, [mock.call()]) - else: - eq_(before_flush.mock_calls, []) + # After issue #9809: unconditionally autoflush on all executions + eq_(before_flush.mock_calls, [mock.call()]) class DMLTest(QueryTest, AssertsCompiledSQL):