summaryrefslogtreecommitdiff
path: root/test/orm
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2023-03-09 12:41:03 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2023-03-09 16:25:01 -0500
commitb09e9198bc8722a59ad37958ee5944085fe2dee5 (patch)
treeeb73b6952850fa91653f6be0d01385d2db39cb4c /test/orm
parent9c0715181de6f03543c7ac9038c481f57f773d49 (diff)
downloadsqlalchemy-b09e9198bc8722a59ad37958ee5944085fe2dee5.tar.gz
implement active_history for composites
Fixed bug where the "active history" feature was not fully implemented for composite attributes, making it impossible to receive events that included the "old" value. This seems to have been the case with older SQLAlchemy versions as well, where "active_history" would be propagated to the underlying column-based attributes, but an event handler listening to the composite attribute itself would not be given the "old" value being replaced, even if the composite() were set up with active_history=True. Additionally, fixed a regression that's local to 2.0 which disallowed active_history on composite from being assigned to the impl with ``attr.impl.active_history=True``. Fixes: #9460 Change-Id: I6d7752a01c8d3fd78de7a90de10e8c52f9b3cd4e
Diffstat (limited to 'test/orm')
-rw-r--r--test/orm/test_composites.py188
1 files changed, 188 insertions, 0 deletions
diff --git a/test/orm/test_composites.py b/test/orm/test_composites.py
index ddd9a45b8..ded2c25db 100644
--- a/test/orm/test_composites.py
+++ b/test/orm/test_composites.py
@@ -3,8 +3,10 @@ import operator
import random
import sqlalchemy as sa
+from sqlalchemy import event
from sqlalchemy import ForeignKey
from sqlalchemy import insert
+from sqlalchemy import inspect
from sqlalchemy import Integer
from sqlalchemy import select
from sqlalchemy import String
@@ -14,13 +16,16 @@ from sqlalchemy.orm import aliased
from sqlalchemy.orm import Composite
from sqlalchemy.orm import composite
from sqlalchemy.orm import configure_mappers
+from sqlalchemy.orm import mapped_column
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
+from sqlalchemy.orm.attributes import LoaderCallableStatus
from sqlalchemy.testing import assert_raises_message
from sqlalchemy.testing import eq_
from sqlalchemy.testing import expect_raises_message
from sqlalchemy.testing import fixtures
from sqlalchemy.testing import is_
+from sqlalchemy.testing import mock
from sqlalchemy.testing.fixtures import fixture_session
from sqlalchemy.testing.schema import Column
from sqlalchemy.testing.schema import Table
@@ -851,6 +856,189 @@ class NestedTest(fixtures.MappedTest, testing.AssertsCompiledSQL):
eq_(t1.ab, AB("a", "b", CD("c", "d")))
+class EventsEtcTest(fixtures.MappedTest):
+ @testing.fixture
+ def point_fixture(self, decl_base):
+ def go(active_history):
+ @dataclasses.dataclass
+ class Point:
+ x: int
+ y: int
+
+ class Edge(decl_base):
+ __tablename__ = "edge"
+ id = mapped_column(Integer, primary_key=True)
+
+ start = composite(
+ Point,
+ mapped_column("x1", Integer),
+ mapped_column("y1", Integer),
+ active_history=active_history,
+ )
+ end = composite(
+ Point,
+ mapped_column("x2", Integer, nullable=True),
+ mapped_column("y2", Integer, nullable=True),
+ active_history=active_history,
+ )
+
+ decl_base.metadata.create_all(testing.db)
+
+ return Point, Edge
+
+ return go
+
+ @testing.variation("active_history", [True, False])
+ @testing.variation("hist_on_mapping", [True, False])
+ def test_event_listener_no_value_to_set(
+ self, point_fixture, active_history, hist_on_mapping
+ ):
+ if hist_on_mapping:
+ config_active_history = bool(active_history)
+ else:
+ config_active_history = False
+
+ Point, Edge = point_fixture(config_active_history)
+
+ if not hist_on_mapping and active_history:
+ Edge.start.impl.active_history = True
+
+ m1 = mock.Mock()
+
+ event.listen(Edge.start, "set", m1)
+
+ e1 = Edge()
+ e1.start = Point(5, 6)
+
+ eq_(
+ m1.mock_calls,
+ [
+ mock.call(
+ e1,
+ Point(5, 6),
+ LoaderCallableStatus.NO_VALUE
+ if not active_history
+ else None,
+ Edge.start.impl,
+ )
+ ],
+ )
+
+ eq_(
+ inspect(e1).attrs.start.history,
+ ([Point(5, 6)], (), [Point(None, None)]),
+ )
+
+ @testing.variation("active_history", [True, False])
+ @testing.variation("hist_on_mapping", [True, False])
+ def test_event_listener_set_to_new(
+ self, point_fixture, active_history, hist_on_mapping
+ ):
+ if hist_on_mapping:
+ config_active_history = bool(active_history)
+ else:
+ config_active_history = False
+
+ Point, Edge = point_fixture(config_active_history)
+
+ if not hist_on_mapping and active_history:
+ Edge.start.impl.active_history = True
+
+ e1 = Edge()
+ e1.start = Point(5, 6)
+
+ sess = fixture_session()
+
+ sess.add(e1)
+ sess.commit()
+ assert "start" not in e1.__dict__
+
+ m1 = mock.Mock()
+
+ event.listen(Edge.start, "set", m1)
+
+ e1.start = Point(7, 8)
+
+ eq_(
+ m1.mock_calls,
+ [
+ mock.call(
+ e1,
+ Point(7, 8),
+ LoaderCallableStatus.NO_VALUE
+ if not active_history
+ else Point(5, 6),
+ Edge.start.impl,
+ )
+ ],
+ )
+
+ if active_history:
+ eq_(
+ inspect(e1).attrs.start.history,
+ ([Point(7, 8)], (), [Point(5, 6)]),
+ )
+ else:
+ eq_(
+ inspect(e1).attrs.start.history,
+ ([Point(7, 8)], (), [Point(None, None)]),
+ )
+
+ @testing.variation("active_history", [True, False])
+ @testing.variation("hist_on_mapping", [True, False])
+ def test_event_listener_set_to_deleted(
+ self, point_fixture, active_history, hist_on_mapping
+ ):
+ if hist_on_mapping:
+ config_active_history = bool(active_history)
+ else:
+ config_active_history = False
+
+ Point, Edge = point_fixture(config_active_history)
+
+ if not hist_on_mapping and active_history:
+ Edge.start.impl.active_history = True
+
+ e1 = Edge()
+ e1.start = Point(5, 6)
+
+ sess = fixture_session()
+
+ sess.add(e1)
+ sess.commit()
+ assert "start" not in e1.__dict__
+
+ m1 = mock.Mock()
+
+ event.listen(Edge.start, "remove", m1)
+
+ del e1.start
+
+ eq_(
+ m1.mock_calls,
+ [
+ mock.call(
+ e1,
+ LoaderCallableStatus.NO_VALUE
+ if not active_history
+ else Point(5, 6),
+ Edge.start.impl,
+ )
+ ],
+ )
+
+ if active_history:
+ eq_(
+ inspect(e1).attrs.start.history,
+ ([Point(None, None)], (), [Point(5, 6)]),
+ )
+ else:
+ eq_(
+ inspect(e1).attrs.start.history,
+ ([Point(None, None)], (), [Point(None, None)]),
+ )
+
+
class PrimaryKeyTest(fixtures.MappedTest):
@classmethod
def define_tables(cls, metadata):