From 1c91a14176dad425b2800390a5d5d368d67f735d Mon Sep 17 00:00:00 2001 From: G Allajmi Date: Fri, 5 Dec 2025 15:43:27 +0000 Subject: [PATCH] Fix adding property before mapper configuration is complete #12858 --- lib/sqlalchemy/orm/mapper.py | 6 ++- test/orm/test_mapper.py | 73 ++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index fb40c6f10a..ef58ccde3d 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -2100,6 +2100,10 @@ class Mapper( # to be addressable in subqueries col.key = col._tq_key_label = key + # In the rare case of adding a ColumnProperty before the mapper is fully configured (e.g. deferring a reflected column) + if not hasattr(self, "columns") or not hasattr(self, "_props"): + return prop + self.columns.add(col, key) for col in prop.columns: @@ -2299,7 +2303,7 @@ class Mapper( descriptor_props = util.preloaded.orm_descriptor_props - prop = self._props.get(key) + prop = self._props.get(key) if hasattr(self, "_props") else None if isinstance(prop, properties.ColumnProperty): return self._reconcile_prop_with_incoming_columns( diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 8bb8bb32c2..d49141ab73 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -1030,6 +1030,79 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): User() assert hasattr(User, "addresses") assert "addresses" in [p.key for p in m1._polymorphic_properties] + + @testing.variation("property_type", ["Column", "ColumnProperty"]) + def test_add_property_before_mapping_is_complete(self, property_type, connection): + m = MetaData() + users = Table( + "deferred_users", + m, + Column("id", Integer, primary_key=True), + Column("name", String), + Column("new_column", String), + ) + User = self.classes.User + class UserWithDeferred(User): + pass + + from sqlalchemy import event + # Listen for the event to be able to retrieve the mapper before completing the mapping process + @event.listens_for(UserWithDeferred, "instrument_class", propagate=True) + def before_mapper_configured(mapper, class_): + assert mapper.configured is False + + col = mapper.local_table.c.new_column + if property_type.ColumnProperty: + mapper.add_property(col.key, deferred(col, group="deferred_group")) + else: + mapper.add_property(col.key, col) + + if property_type.ColumnProperty: + # Make a deferred group + self.mapper( + UserWithDeferred, + users, + properties={ + "name": deferred(users.c.name, group="deferred_group") + } + ) + else: + self.mapper(UserWithDeferred, users) + + configure_mappers() + + assert hasattr(UserWithDeferred, "new_column") + + m.create_all(connection) + sess = Session(connection) + u = UserWithDeferred(id=1, name="testuser", new_column="test_value") + sess.add(u) + sess.commit() + sess.close() + + sess = Session(connection) + user = sess.query(UserWithDeferred).filter_by(id=1).first() + if property_type.ColumnProperty: + assert "id" in user.__dict__ + assert "new_column" not in user.__dict__ + assert "name" not in user.__dict__ + + eq_(user.new_column, "test_value") + + assert "new_column" in user.__dict__ + assert "name" in user.__dict__ + eq_(user.name, "testuser") + + + else: + assert "id" in user.__dict__ + assert "new_column" in user.__dict__ + assert "name" in user.__dict__ + eq_(user.new_column, "test_value") + eq_(user.name, "testuser") + + sess.close() + event.remove(UserWithDeferred, "instrument_class", before_mapper_configured) def test_replace_col_prop_w_syn(self): users, User = self.tables.users, self.classes.User -- 2.47.3