]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
populate existing can be in exec option in session.get
authorFederico Caselli <cfederico87@gmail.com>
Wed, 29 Apr 2026 21:09:57 +0000 (23:09 +0200)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 10 May 2026 15:39:45 +0000 (11:39 -0400)
The ``populate_existing`` execution option is now honored when passed
in the :paramref:`.Session.get.execution_options` dict by the method
:meth:`.Session.get` and analogous in other session kinds. The current
:paramref:`.Session.get.populate_existing` parameter will takes precedence
if specified, overriding the value of the execution options.

Fixes: #10610
Change-Id: I4ddc9a7c6dda8f31f4dd413b49a9196efb3edaa6

doc/build/changelog/unreleased_21/10610.rst [new file with mode: 0644]
lib/sqlalchemy/ext/asyncio/scoping.py
lib/sqlalchemy/ext/asyncio/session.py
lib/sqlalchemy/orm/scoping.py
lib/sqlalchemy/orm/session.py
test/orm/test_scoping.py
test/orm/test_session.py

diff --git a/doc/build/changelog/unreleased_21/10610.rst b/doc/build/changelog/unreleased_21/10610.rst
new file mode 100644 (file)
index 0000000..03c1466
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+  :tags: usecase, orm
+  :tickets: 10610
+
+  The ``populate_existing`` execution option is now honored when passed in the
+  :paramref:`.Session.get.execution_options` dict by the method
+  :meth:`.Session.get` and analogous in other session kinds. The current
+  :paramref:`.Session.get.populate_existing` parameter will takes precedence if
+  specified, overriding the value of the execution options.
index 80d7a15a0f8f9e60c6307b211ef5c4275b6578a0..2571e5ed549892bfbdd3fa1f08a6add54d705595 100644 (file)
@@ -1171,7 +1171,7 @@ class async_scoped_session(Generic[_AS]):
         ident: _PKIdentityArgument,
         *,
         options: Optional[Sequence[ORMOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
@@ -1209,7 +1209,7 @@ class async_scoped_session(Generic[_AS]):
         ident: _PKIdentityArgument,
         *,
         options: Optional[Sequence[ORMOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
index 69c98c57bbe13cc46e5be44a11361525d0f01b2b..ee7b6df9602084cbe6561ac7ba464e67c1c49910 100644 (file)
@@ -593,7 +593,7 @@ class AsyncSession(ReversibleProxy[Session]):
         ident: _PKIdentityArgument,
         *,
         options: Optional[Sequence[ORMOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
@@ -625,7 +625,7 @@ class AsyncSession(ReversibleProxy[Session]):
         ident: _PKIdentityArgument,
         *,
         options: Optional[Sequence[ORMOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
index 0634086ea24eeffb5931b59a3a8f7d1c4e495c66..4d0a034872cfbccd036b7574637e78f33106c13a 100644 (file)
@@ -969,7 +969,7 @@ class scoped_session(Generic[_S]):
         ident: _PKIdentityArgument,
         *,
         options: Optional[Sequence[ORMOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
@@ -1048,6 +1048,8 @@ class scoped_session(Generic[_S]):
         :param populate_existing: causes the method to unconditionally emit
          a SQL query and refresh the object with the newly loaded data,
          regardless of whether or not the object is already present.
+         Setting this flag takes precedence over passing it as an
+         execution option.
 
         :param with_for_update: optional boolean ``True`` indicating FOR UPDATE
           should be used, or may be a dictionary containing flags to
@@ -1098,7 +1100,7 @@ class scoped_session(Generic[_S]):
         ident: _PKIdentityArgument,
         *,
         options: Optional[Sequence[ORMOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
index 0aa6458f97dff8f41d3fca87c4e30f367ac1dbd7..a73bd2a698ee50aa658c01b0e0b48fb583327f90 100644 (file)
@@ -3661,7 +3661,7 @@ class Session(_SessionClassMethods, EventTarget):
         ident: _PKIdentityArgument,
         *,
         options: Optional[Sequence[ORMOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
@@ -3735,6 +3735,8 @@ class Session(_SessionClassMethods, EventTarget):
         :param populate_existing: causes the method to unconditionally emit
          a SQL query and refresh the object with the newly loaded data,
          regardless of whether or not the object is already present.
+         Setting this flag takes precedence over passing it as an
+         execution option.
 
         :param with_for_update: optional boolean ``True`` indicating FOR UPDATE
           should be used, or may be a dictionary containing flags to
@@ -3784,7 +3786,7 @@ class Session(_SessionClassMethods, EventTarget):
         ident: _PKIdentityArgument,
         *,
         options: Optional[Sequence[ORMOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
@@ -3835,12 +3837,24 @@ class Session(_SessionClassMethods, EventTarget):
         db_load_fn: Callable[..., _O],
         *,
         options: Optional[Sequence[ExecutableOption]] = None,
-        populate_existing: bool = False,
+        populate_existing: bool | None = None,
         with_for_update: ForUpdateParameter = None,
         identity_token: Optional[Any] = None,
         execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT,
         bind_arguments: Optional[_BindArguments] = None,
     ) -> Optional[_O]:
+        # set populate_existing value; direct parameter
+        # takes precedence over execution_options
+        if populate_existing is not None:
+            execution_options = {
+                **execution_options,  # type: ignore[typeddict-item]
+                "populate_existing": populate_existing,
+            }
+        else:
+            populate_existing = execution_options.get(
+                "populate_existing", False
+            )
+
         # convert composite types to individual args
         if (
             is_composite_class(primary_key_identity)
index 509137afe379d1983cc90b6945fd660045a36d70..88bc62ecf36175913a7bbbfa1a4598c1215c9671 100644 (file)
@@ -157,7 +157,7 @@ class ScopedSessionTest(fixtures.MappedTest):
                     "Cls",
                     5,
                     options=None,
-                    populate_existing=False,
+                    populate_existing=None,
                     with_for_update=None,
                     identity_token=None,
                     execution_options=util.EMPTY_DICT,
index fd14f5acdabf4c7f568b0d07ab848c6ac07939e4..065ca8145ea594fdc4af91dc5bfc7c9ae5306b18 100644 (file)
@@ -863,6 +863,80 @@ class SessionStateTest(_fixtures.FixtureTest):
         for key, value in expected_opts.items():
             eq_(gather_options[key], value)
 
+    @testing.combinations(
+        ("default", None, {}, None),
+        ("arg_true", True, {}, True),
+        ("arg_false", False, {}, False),
+        ("arg_true_exe_false", True, {"populate_existing": False}, True),
+        ("arg_false_exe_true", False, {"populate_existing": True}, False),
+        (
+            "exe_true",
+            None,
+            {"populate_existing": True},
+            True,
+        ),
+        (
+            "exe_false",
+            None,
+            {"populate_existing": False},
+            False,
+        ),
+        argnames="session_parameter,execution_opt,expected_pe",
+        id_="iaaa",
+    )
+    @testing.variation("object_in_session", [True, False])
+    def test_get_populate_existing(
+        self,
+        session_parameter,
+        execution_opt,
+        expected_pe,
+        object_in_session,
+    ):
+        users, User = self.tables.users, self.classes.User
+        self.mapper_registry.map_imperatively(User, users)
+
+        s = fixture_session()
+
+        s.add(User(id=1, name="name"))
+        s.commit()
+        s.close()
+        if object_in_session:
+            # prevent GC of the object
+            _ = s.get(User, 1)
+        s.connection().execute(
+            update(User).where(User.id == 1).values(name="newname")
+        )
+
+        gather_options = []
+
+        @event.listens_for(s, "do_orm_execute")
+        def check(ctx: ORMExecuteState) -> None:
+            gather_options.append(ctx.execution_options)
+
+        res = s.get(
+            User,
+            1,
+            populate_existing=session_parameter,
+            execution_options=execution_opt,
+        )
+
+        if not object_in_session or expected_pe:
+            # object not in session (so we load) or we expected
+            # populate_existing to be set (so we load), ensure newer value and
+            # gather_options is present
+            eq_(res.name, "newname")
+
+            if expected_pe is None:
+                assert "populate_existing" not in gather_options[0]
+            else:
+                eq_(gather_options[0]["populate_existing"], expected_pe)
+
+        else:
+            # object was in the session and no populate_existing, so
+            # do_orm_execute never called
+            eq_(gather_options, [])
+            eq_(res.name, "name")
+
     def test_autocommit_kw_accepted_but_must_be_false(self):
         Session(autocommit=False)