summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--doc/build/changelog/unreleased_13/5262.rst8
-rw-r--r--doc/build/orm/internals.rst11
-rw-r--r--doc/build/orm/nonstandard_mappings.rst13
-rw-r--r--lib/sqlalchemy/orm/properties.py30
-rw-r--r--test/orm/inheritance/test_basic.py44
5 files changed, 105 insertions, 1 deletions
diff --git a/doc/build/changelog/unreleased_13/5262.rst b/doc/build/changelog/unreleased_13/5262.rst
new file mode 100644
index 000000000..32d3405b4
--- /dev/null
+++ b/doc/build/changelog/unreleased_13/5262.rst
@@ -0,0 +1,8 @@
+.. change::
+ :tags: usecase, orm
+ :tickets: 5262
+
+ Added an accessor :attr:`.ColumnProperty.Comparator.expressions` which
+ provides access to the group of columns mapped under a multi-column
+ :class:`.ColumnProperty` attribute.
+
diff --git a/doc/build/orm/internals.rst b/doc/build/orm/internals.rst
index d0e7c1ce4..e9f1b431e 100644
--- a/doc/build/orm/internals.rst
+++ b/doc/build/orm/internals.rst
@@ -21,6 +21,17 @@ sections, are listed here.
.. autoclass:: sqlalchemy.orm.properties.ColumnProperty
:members:
+ .. attribute:: Comparator.expressions
+
+ The full sequence of columns referenced by this
+ attribute, adjusted for any aliasing in progress.
+
+ .. versionadded:: 1.3.17
+
+ .. seealso::
+
+ :ref:`maptojoin` - usage example
+
.. autoclass:: sqlalchemy.orm.descriptor_props.CompositeProperty
:members:
diff --git a/doc/build/orm/nonstandard_mappings.rst b/doc/build/orm/nonstandard_mappings.rst
index 01a615ae2..81679dd01 100644
--- a/doc/build/orm/nonstandard_mappings.rst
+++ b/doc/build/orm/nonstandard_mappings.rst
@@ -68,6 +68,19 @@ The natural primary key of the above mapping is the composite of
is represented from an ``AddressUser`` object as
``(AddressUser.id, AddressUser.address_id)``.
+When referring to the ``AddressUser.id`` column, most SQL expressions will
+make use of only the first column in the list of columns mapped, as the
+two columns are synonymous. However, for the special use case such as
+a GROUP BY expression where both columns must be referenced at the same
+time while making use of the proper context, that is, accommodating for
+aliases and similar, the accessor :attr:`.ColumnProperty.Comparator.expressions`
+may be used::
+
+ q = session.query(AddressUser).group_by(*AddressUser.id.expressions)
+
+.. versionadded:: 1.3.17 Added the
+ :attr:`.ColumnProperty.Comparator.expressions` accessor.
+
.. note::
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index 1022f4ef6..4cf316ac7 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -337,7 +337,7 @@ class ColumnProperty(StrategizedProperty):
"""
- __slots__ = "__clause_element__", "info"
+ __slots__ = "__clause_element__", "info", "expressions"
def _memoized_method___clause_element__(self):
if self.adapter:
@@ -354,12 +354,40 @@ class ColumnProperty(StrategizedProperty):
)
def _memoized_attr_info(self):
+ """The .info dictionary for this attribute."""
+
ce = self.__clause_element__()
try:
return ce.info
except AttributeError:
return self.prop.info
+ def _memoized_attr_expressions(self):
+ """The full sequence of columns referenced by this
+ attribute, adjusted for any aliasing in progress.
+
+ .. versionadded:: 1.3.17
+
+ """
+ if self.adapter:
+ return [
+ self.adapter(col, self.prop.key)
+ for col in self.prop.columns
+ ]
+ else:
+ # no adapter, so we aren't aliased
+ # assert self._parententity is self._parentmapper
+ return [
+ col._annotate(
+ {
+ "parententity": self._parententity,
+ "parentmapper": self._parententity,
+ "orm_key": self.prop.key,
+ }
+ )
+ for col in self.prop.columns
+ ]
+
def _fallback_getattr(self, key):
"""proxy attribute access down to the mapped column.
diff --git a/test/orm/inheritance/test_basic.py b/test/orm/inheritance/test_basic.py
index cb4e70ecb..02addc6f5 100644
--- a/test/orm/inheritance/test_basic.py
+++ b/test/orm/inheritance/test_basic.py
@@ -131,6 +131,50 @@ class O2MTest(fixtures.MappedTest):
eq_(result[1].parent_foo.data, "foo #1")
+class ColExpressionsTest(fixtures.DeclarativeMappedTest):
+ __backend__ = True
+
+ @classmethod
+ def setup_classes(cls):
+ Base = cls.DeclarativeBasic
+
+ class A(Base):
+ __tablename__ = "a"
+ id = Column(
+ Integer, primary_key=True, test_needs_autoincrement=True
+ )
+ type = Column(String(10))
+ __mapper_args__ = {
+ "polymorphic_on": type,
+ "polymorphic_identity": "a",
+ }
+
+ class B(A):
+ __tablename__ = "b"
+ id = Column(ForeignKey("a.id"), primary_key=True)
+ data = Column(Integer)
+ __mapper_args__ = {"polymorphic_identity": "b"}
+
+ @classmethod
+ def insert_data(cls, connection):
+ A, B = cls.classes("A", "B")
+ s = Session(connection)
+
+ s.add_all([B(data=5), B(data=7)])
+ s.commit()
+
+ def test_group_by(self):
+ B = self.classes.B
+ s = Session()
+
+ rows = (
+ s.query(B.id.expressions[0], B.id.expressions[1], func.sum(B.data))
+ .group_by(*B.id.expressions)
+ .all()
+ )
+ eq_(rows, [(1, 1, 5), (2, 2, 7)])
+
+
class PolyExpressionEagerLoad(fixtures.DeclarativeMappedTest):
run_setup_mappers = "once"
__dialect__ = "default"