summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/orm/util.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-06-20 19:28:29 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2012-06-20 19:28:29 -0400
commit3dd536ac06808adcf9c10707dbf2ebb6e3842be7 (patch)
treed102291da86021aa4584ef836dbb0471596c3eeb /lib/sqlalchemy/orm/util.py
parent40098941007ff3aa1593e834915c4042c1668dc2 (diff)
downloadsqlalchemy-3dd536ac06808adcf9c10707dbf2ebb6e3842be7.tar.gz
- [feature] The of_type() construct on attributes
now accepts aliased() class constructs as well as with_polymorphic constructs, and works with query.join(), any(), has(), and also eager loaders subqueryload(), joinedload(), contains_eager() [ticket:2438] [ticket:1106] - a rewrite of the query path system to use an object based approach for more succinct usage. the system has been designed carefully to not add an excessive method overhead. - [feature] select() features a correlate_except() method, auto correlates all selectables except those passed. Is needed here for the updated any()/has() functionality. - remove some old cruft from LoaderStrategy, init(),debug_callable() - use a namedtuple for _extended_entity_info. This method should become standard within the orm internals - some tweaks to the memory profile tests, number of runs can be customized to work around pysqlite's very annoying behavior - try to simplify PropertyOption._get_paths(), rename to _process_paths(), returns a single list now. overall works more completely as was needed for of_type() functionality
Diffstat (limited to 'lib/sqlalchemy/orm/util.py')
-rw-r--r--lib/sqlalchemy/orm/util.py204
1 files changed, 182 insertions, 22 deletions
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index 51aaa3152..0978ab693 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -9,6 +9,7 @@ from sqlalchemy import sql, util, event, exc as sa_exc, inspection
from sqlalchemy.sql import expression, util as sql_util, operators
from sqlalchemy.orm.interfaces import MapperExtension, EXT_CONTINUE,\
PropComparator, MapperProperty
+from itertools import chain
from sqlalchemy.orm import attributes, exc
import operator
import re
@@ -233,6 +234,144 @@ class ORMAdapter(sql_util.ColumnAdapter):
else:
return None
+class PathRegistry(object):
+ """Represent query load paths and registry functions.
+
+ Basically represents structures like:
+
+ (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>)
+
+ These structures are generated by things like
+ query options (joinedload(), subqueryload(), etc.) and are
+ used to compose keys stored in the query._attributes dictionary
+ for various options.
+
+ They are then re-composed at query compile/result row time as
+ the query is formed and as rows are fetched, where they again
+ serve to compose keys to look up options in the context.attributes
+ dictionary, which is copied from query._attributes.
+
+ The path structure has a limited amount of caching, where each
+ "root" ultimately pulls from a fixed registry associated with
+ the first mapper, that also contains elements for each of its
+ property keys. However paths longer than two elements, which
+ are the exception rather than the rule, are generated on an
+ as-needed basis.
+
+ """
+
+ def __eq__(self, other):
+ return other is not None and \
+ self.path == other.path
+
+ def set(self, reg, key, value):
+ reg._attributes[(key, self.reduced_path)] = value
+
+ def setdefault(self, reg, key, value):
+ reg._attributes.setdefault((key, self.reduced_path), value)
+
+ def get(self, reg, key, value=None):
+ key = (key, self.reduced_path)
+ if key in reg._attributes:
+ return reg._attributes[key]
+ else:
+ return value
+
+ @property
+ def length(self):
+ return len(self.path)
+
+ def contains_mapper(self, mapper):
+ return mapper.base_mapper in self.reduced_path
+
+ def contains(self, reg, key):
+ return (key, self.reduced_path) in reg._attributes
+
+ def serialize(self):
+ path = self.path
+ return zip(
+ [m.class_ for m in [path[i] for i in range(0, len(path), 2)]],
+ [path[i] for i in range(1, len(path), 2)] + [None]
+ )
+
+ @classmethod
+ def deserialize(cls, path):
+ if path is None:
+ return None
+
+ p = tuple(chain(*[(class_mapper(mcls), key) for mcls, key in path]))
+ if p and p[-1] is None:
+ p = p[0:-1]
+ return cls.coerce(p)
+
+ @classmethod
+ def per_mapper(cls, mapper):
+ return EntityRegistry(
+ cls.root, mapper
+ )
+
+ @classmethod
+ def coerce(cls, raw):
+ return util.reduce(lambda prev, next:prev[next], raw, cls.root)
+
+ @classmethod
+ def token(cls, token):
+ return KeyRegistry(cls.root, token)
+
+ def __add__(self, other):
+ return util.reduce(
+ lambda prev, next:prev[next],
+ other.path, self)
+
+ def __repr__(self):
+ return "%s(%r)" % (self.__class__.__name__, self.path, )
+
+class RootRegistry(PathRegistry):
+ """Root registry, defers to mappers so that
+ paths are maintained per-root-mapper.
+
+ """
+ path = ()
+ reduced_path = ()
+
+ def __getitem__(self, mapper):
+ return mapper._sa_path_registry
+PathRegistry.root = RootRegistry()
+
+class KeyRegistry(PathRegistry):
+ def __init__(self, parent, key):
+ self.key = key
+ self.parent = parent
+ self.path = parent.path + (key,)
+ self.reduced_path = parent.reduced_path + (key,)
+
+ def __getitem__(self, entity):
+ return EntityRegistry(
+ self, entity
+ )
+
+class EntityRegistry(PathRegistry, dict):
+ is_aliased_class = False
+
+ def __init__(self, parent, entity):
+ self.key = reduced_key = entity
+ self.parent = parent
+ if hasattr(entity, 'base_mapper'):
+ reduced_key = entity.base_mapper
+ else:
+ self.is_aliased_class = True
+
+ self.path = parent.path + (entity,)
+ self.reduced_path = parent.reduced_path + (reduced_key,)
+
+ def __nonzero__(self):
+ return True
+
+ def __missing__(self, key):
+ self[key] = item = KeyRegistry(self, key)
+ return item
+
+
class AliasedClass(object):
"""Represents an "aliased" form of a mapped class for usage with Query.
@@ -321,6 +460,10 @@ class AliasedClass(object):
self._sa_label_name = name
self.__name__ = 'AliasedClass_' + str(self.__target)
+ @util.memoized_property
+ def _sa_path_registry(self):
+ return PathRegistry.per_mapper(self)
+
def __getstate__(self):
return {
'mapper':self.__mapper,
@@ -408,7 +551,8 @@ def aliased(element, alias=None, name=None, adapt_on_names=False):
name=name, adapt_on_names=adapt_on_names)
def with_polymorphic(base, classes, selectable=False,
- polymorphic_on=None, aliased=False):
+ polymorphic_on=None, aliased=False,
+ innerjoin=False):
"""Produce an :class:`.AliasedClass` construct which specifies
columns for descendant mappers of the given base.
@@ -422,23 +566,23 @@ def with_polymorphic(base, classes, selectable=False,
criterion to be used against those tables. The resulting
instances will also have those columns already loaded so that
no "post fetch" of those columns will be required.
-
+
See the examples at :ref:`with_polymorphic`.
:param base: Base class to be aliased.
-
+
:param cls_or_mappers: a single class or mapper, or list of
class/mappers, which inherit from the base class.
Alternatively, it may also be the string ``'*'``, in which case
all descending mapped classes will be added to the FROM clause.
-
+
:param aliased: when True, the selectable will be wrapped in an
alias, that is ``(SELECT * FROM <fromclauses>) AS anon_1``.
This can be important when using the with_polymorphic()
to create the target of a JOIN on a backend that does not
support parenthesized joins, such as SQLite and older
versions of MySQL.
-
+
:param selectable: a table or select() statement that will
be used in place of the generated FROM clause. This argument is
required if any of the desired classes use concrete table
@@ -455,10 +599,12 @@ def with_polymorphic(base, classes, selectable=False,
is useful for mappings that don't have polymorphic loading
behavior by default.
+ :param innerjoin: if True, an INNER JOIN will be used. This should
+ only be specified if querying for one specific subtype only
"""
- primary_mapper = class_mapper(base)
+ primary_mapper = _class_to_mapper(base)
mappers, selectable = primary_mapper.\
- _with_polymorphic_args(classes, selectable)
+ _with_polymorphic_args(classes, selectable, innerjoin=innerjoin)
if aliased:
selectable = selectable.alias()
return AliasedClass(base,
@@ -478,11 +624,11 @@ def _orm_annotate(element, exclude=None):
def _orm_deannotate(element):
"""Remove annotations that link a column to a particular mapping.
-
+
Note this doesn't affect "remote" and "foreign" annotations
passed by the :func:`.orm.foreign` and :func:`.orm.remote`
annotators.
-
+
"""
return sql_util._deep_deannotate(element,
@@ -644,13 +790,24 @@ def with_parent(instance, prop):
value_is_parent=True)
+_extended_entity_info_tuple = util.namedtuple("extended_entity_info", [
+ "entity",
+ "mapper",
+ "selectable",
+ "is_aliased_class",
+ "with_polymorphic_mappers",
+ "with_polymorphic_discriminator"
+])
def _extended_entity_info(entity, compile=True):
if isinstance(entity, AliasedClass):
- return entity._AliasedClass__mapper, \
- entity._AliasedClass__alias, \
- True, \
- entity._AliasedClass__with_polymorphic_mappers, \
- entity._AliasedClass__with_polymorphic_discriminator
+ return _extended_entity_info_tuple(
+ entity,
+ entity._AliasedClass__mapper, \
+ entity._AliasedClass__alias, \
+ True, \
+ entity._AliasedClass__with_polymorphic_mappers, \
+ entity._AliasedClass__with_polymorphic_discriminator
+ )
if isinstance(entity, mapperlib.Mapper):
mapper = entity
@@ -659,19 +816,22 @@ def _extended_entity_info(entity, compile=True):
class_manager = attributes.manager_of_class(entity)
if class_manager is None:
- return None, entity, False, [], None
+ return _extended_entity_info_tuple(entity, None, entity, False, [], None)
mapper = class_manager.mapper
else:
- return None, entity, False, [], None
+ return _extended_entity_info_tuple(entity, None, entity, False, [], None)
if compile and mapperlib.module._new_mappers:
mapperlib.configure_mappers()
- return mapper, \
+ return _extended_entity_info_tuple(
+ entity,
+ mapper, \
mapper._with_polymorphic_selectable, \
False, \
mapper._with_polymorphic_mappers, \
mapper.polymorphic_on
+ )
def _entity_info(entity, compile=True):
"""Return mapping information given a class, mapper, or AliasedClass.
@@ -684,7 +844,7 @@ def _entity_info(entity, compile=True):
unmapped selectables through.
"""
- return _extended_entity_info(entity, compile)[0:3]
+ return _extended_entity_info(entity, compile)[1:4]
def _entity_descriptor(entity, key):
"""Return a class attribute given an entity and string name.
@@ -738,7 +898,7 @@ def object_mapper(instance):
Raises UnmappedInstanceError if no mapping is configured.
This function is available via the inspection system as::
-
+
inspect(instance).mapper
"""
@@ -752,7 +912,7 @@ def object_state(instance):
Raises UnmappedInstanceError if no mapping is configured.
This function is available via the inspection system as::
-
+
inspect(instance)
"""
@@ -776,9 +936,9 @@ def class_mapper(class_, compile=True):
object is passed.
This function is available via the inspection system as::
-
+
inspect(some_mapped_class)
-
+
"""
try: