diff options
| -rw-r--r-- | CHANGES | 14 | ||||
| -rw-r--r-- | lib/sqlalchemy/exc.py | 7 | ||||
| -rwxr-xr-x | lib/sqlalchemy/ext/declarative.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 703 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 856 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 68 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/util.py | 16 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/expression.py | 35 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/operators.py | 5 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/util.py | 164 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/visitors.py | 12 | ||||
| -rw-r--r-- | test/aaa_profiling/test_orm.py | 1 | ||||
| -rw-r--r-- | test/ext/test_declarative.py | 22 | ||||
| -rw-r--r-- | test/ext/test_serializer.py | 19 | ||||
| -rw-r--r-- | test/orm/inheritance/test_abc_inheritance.py | 6 | ||||
| -rw-r--r-- | test/orm/test_joins.py | 12 | ||||
| -rw-r--r-- | test/orm/test_mapper.py | 2 | ||||
| -rw-r--r-- | test/orm/test_query.py | 8 | ||||
| -rw-r--r-- | test/orm/test_rel_fn.py | 927 | ||||
| -rw-r--r-- | test/orm/test_relationships.py | 739 | ||||
| -rw-r--r-- | test/sql/test_generative.py | 98 | ||||
| -rw-r--r-- | test/sql/test_selectable.py | 98 |
23 files changed, 2919 insertions, 907 deletions
@@ -15,6 +15,20 @@ those which apply to an 0.7 release are noted. 0.8.0b1 ======= - orm + - [feature] Major rewrite of relationship() + internals now allow join conditions which + include columns pointing to themselves + within composite foreign keys. A new + API for very specialized primaryjoin conditions + is added, allowing conditions based on + SQL functions, CAST, etc. to be handled + by placing the annotation functions + remote() and foreign() inline within the + expression when necessary. Previous recipes + using the semi-private _local_remote_pairs + approach can be upgraded to this new + approach. [ticket:1401] + - [feature] Added prefix_with() method to Query, calls upon select().prefix_with() to allow placement of MySQL SELECT diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py index 91ffc2811..f28bd8a07 100644 --- a/lib/sqlalchemy/exc.py +++ b/lib/sqlalchemy/exc.py @@ -25,6 +25,13 @@ class ArgumentError(SQLAlchemyError): """ +class NoForeignKeysError(ArgumentError): + """Raised when no foreign keys can be located between two selectables + during a join.""" + +class AmbiguousForeignKeysError(ArgumentError): + """Raised when more than one foreign key matching can be located + between two selectables during a join.""" class CircularDependencyError(SQLAlchemyError): """Raised by topological sorts when a circular dependency is detected. diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py index faf575da1..07a2a3b95 100755 --- a/lib/sqlalchemy/ext/declarative.py +++ b/lib/sqlalchemy/ext/declarative.py @@ -1398,6 +1398,10 @@ class _GetTable(object): def _deferred_relationship(cls, prop): def resolve_arg(arg): import sqlalchemy + from sqlalchemy.orm import foreign, remote + + fallback = sqlalchemy.__dict__.copy() + fallback.update({'foreign':foreign, 'remote':remote}) def access_cls(key): if key in cls._decl_class_registry: @@ -1407,7 +1411,7 @@ def _deferred_relationship(cls, prop): elif key in cls.metadata._schemas: return _GetTable(key, cls.metadata) else: - return sqlalchemy.__dict__[key] + return fallback[key] d = util.PopulateDict(access_cls) def return_cls(): diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index ca4099d68..1813f57d8 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -44,6 +44,11 @@ from sqlalchemy.orm.properties import ( PropertyLoader, SynonymProperty, ) +from sqlalchemy.orm.relationships import ( + foreign, + remote, + remote_foreign +) from sqlalchemy.orm import mapper as mapperlib from sqlalchemy.orm.mapper import reconstructor, validates from sqlalchemy.orm import strategies @@ -81,6 +86,7 @@ __all__ = ( 'dynamic_loader', 'eagerload', 'eagerload_all', + 'foreign', 'immediateload', 'join', 'joinedload', @@ -96,6 +102,8 @@ __all__ = ( 'reconstructor', 'relationship', 'relation', + 'remote', + 'remote_foreign', 'scoped_session', 'sessionmaker', 'subqueryload', diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 74ccf0157..424795ee4 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -14,11 +14,11 @@ mapped attributes. from sqlalchemy import sql, util, log, exc as sa_exc from sqlalchemy.sql.util import ClauseAdapter, criterion_as_pairs, \ join_condition, _shallow_annotate -from sqlalchemy.sql import operators, expression +from sqlalchemy.sql import operators, expression, visitors from sqlalchemy.orm import attributes, dependency, mapper, \ - object_mapper, strategies, configure_mappers + object_mapper, strategies, configure_mappers, relationships from sqlalchemy.orm.util import CascadeOptions, _class_to_mapper, \ - _orm_annotate, _orm_deannotate + _orm_annotate, _orm_deannotate, _orm_full_deannotate from sqlalchemy.orm.interfaces import MANYTOMANY, MANYTOONE, \ MapperProperty, ONETOMANY, PropComparator, StrategizedProperty @@ -33,9 +33,9 @@ from descriptor_props import CompositeProperty, SynonymProperty, \ class ColumnProperty(StrategizedProperty): """Describes an object attribute that corresponds to a table column. - + Public constructor is the :func:`.orm.column_property` function. - + """ def __init__(self, *columns, **kwargs): @@ -62,7 +62,7 @@ class ColumnProperty(StrategizedProperty): """ self._orig_columns = [expression._labeled(c) for c in columns] - self.columns = [expression._labeled(_orm_deannotate(c)) + self.columns = [expression._labeled(_orm_full_deannotate(c)) for c in columns] self.group = kwargs.pop('group', None) self.deferred = kwargs.pop('deferred', False) @@ -177,13 +177,13 @@ log.class_logger(ColumnProperty) class RelationshipProperty(StrategizedProperty): """Describes an object property that holds a single item or list of items that correspond to a related database table. - + Public constructor is the :func:`.orm.relationship` function. - + Of note here is the :class:`.RelationshipProperty.Comparator` class, which implements comparison operations for scalar- and collection-referencing mapped attributes. - + """ strategy_wildcard_key = 'relationship:*' @@ -293,7 +293,7 @@ class RelationshipProperty(StrategizedProperty): def __init__(self, prop, mapper, of_type=None, adapter=None): """Construction of :class:`.RelationshipProperty.Comparator` is internal to the ORM's attribute mechanics. - + """ self.prop = prop self.mapper = mapper @@ -332,10 +332,10 @@ class RelationshipProperty(StrategizedProperty): def of_type(self, cls): """Produce a construct that represents a particular 'subtype' of attribute for the parent class. - + Currently this is usable in conjunction with :meth:`.Query.join` and :meth:`.Query.outerjoin`. - + """ return RelationshipProperty.Comparator( self.property, @@ -345,7 +345,7 @@ class RelationshipProperty(StrategizedProperty): def in_(self, other): """Produce an IN clause - this is not implemented for :func:`~.orm.relationship`-based attributes at this time. - + """ raise NotImplementedError('in_() not yet supported for ' 'relationships. For a simple many-to-one, use ' @@ -362,15 +362,15 @@ class RelationshipProperty(StrategizedProperty): this will typically produce a clause such as:: - + mytable.related_id == <some id> - + Where ``<some id>`` is the primary key of the given object. - + The ``==`` operator provides partial functionality for non- many-to-one comparisons: - + * Comparisons against collections are not supported. Use :meth:`~.RelationshipProperty.Comparator.contains`. * Compared to a scalar one-to-many, will produce a @@ -445,6 +445,7 @@ class RelationshipProperty(StrategizedProperty): else: j = _orm_annotate(pj, exclude=self.property.remote_side) + # MARKMARK if criterion is not None and target_adapter: # limit this adapter to annotated only? criterion = target_adapter.traverse(criterion) @@ -465,42 +466,42 @@ class RelationshipProperty(StrategizedProperty): def any(self, criterion=None, **kwargs): """Produce an expression that tests a collection against particular criterion, using EXISTS. - + An expression like:: - + session.query(MyClass).filter( MyClass.somereference.any(SomeRelated.x==2) ) - - + + Will produce a query like:: - + SELECT * FROM my_table WHERE EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id AND related.x=2) - + Because :meth:`~.RelationshipProperty.Comparator.any` uses a correlated subquery, its performance is not nearly as good when compared against large target tables as that of using a join. - + :meth:`~.RelationshipProperty.Comparator.any` is particularly useful for testing for empty collections:: - + session.query(MyClass).filter( ~MyClass.somereference.any() ) - + will produce:: - + SELECT * FROM my_table WHERE NOT EXISTS (SELECT 1 FROM related WHERE related.my_id=my_table.id) - + :meth:`~.RelationshipProperty.Comparator.any` is only valid for collections, i.e. a :func:`.relationship` that has ``uselist=True``. For scalar references, use :meth:`~.RelationshipProperty.Comparator.has`. - + """ if not self.property.uselist: raise sa_exc.InvalidRequestError( @@ -515,14 +516,14 @@ class RelationshipProperty(StrategizedProperty): particular criterion, using EXISTS. An expression like:: - + session.query(MyClass).filter( MyClass.somereference.has(SomeRelated.x==2) ) - - + + Will produce a query like:: - + SELECT * FROM my_table WHERE EXISTS (SELECT 1 FROM related WHERE related.id==my_table.related_id AND related.x=2) @@ -531,12 +532,12 @@ class RelationshipProperty(StrategizedProperty): a correlated subquery, its performance is not nearly as good when compared against large target tables as that of using a join. - + :meth:`~.RelationshipProperty.Comparator.has` is only valid for scalar references, i.e. a :func:`.relationship` that has ``uselist=False``. For collection references, use :meth:`~.RelationshipProperty.Comparator.any`. - + """ if self.property.uselist: raise sa_exc.InvalidRequestError( @@ -547,44 +548,44 @@ class RelationshipProperty(StrategizedProperty): def contains(self, other, **kwargs): """Return a simple expression that tests a collection for containment of a particular item. - + :meth:`~.RelationshipProperty.Comparator.contains` is only valid for a collection, i.e. a :func:`~.orm.relationship` that implements one-to-many or many-to-many with ``uselist=True``. - + When used in a simple one-to-many context, an expression like:: - + MyClass.contains(other) - + Produces a clause like:: - + mytable.id == <some id> - + Where ``<some id>`` is the value of the foreign key attribute on ``other`` which refers to the primary key of its parent object. From this it follows that :meth:`~.RelationshipProperty.Comparator.contains` is very useful when used with simple one-to-many operations. - + For many-to-many operations, the behavior of :meth:`~.RelationshipProperty.Comparator.contains` has more caveats. The association table will be rendered in the statement, producing an "implicit" join, that is, includes multiple tables in the FROM clause which are equated in the WHERE clause:: - + query(MyClass).filter(MyClass.contains(other)) - + Produces a query like:: - + SELECT * FROM my_table, my_association_table AS my_association_table_1 WHERE my_table.id = my_association_table_1.parent_id AND my_association_table_1.child_id = <some id> - + Where ``<some id>`` would be the primary key of ``other``. From the above, it is clear that :meth:`~.RelationshipProperty.Comparator.contains` @@ -598,7 +599,7 @@ class RelationshipProperty(StrategizedProperty): a less-performant alternative using EXISTS, or refer to :meth:`.Query.outerjoin` as well as :ref:`ormtutorial_joins` for more details on constructing outer joins. - + """ if not self.property.uselist: raise sa_exc.InvalidRequestError( @@ -649,19 +650,19 @@ class RelationshipProperty(StrategizedProperty): """Implement the ``!=`` operator. In a many-to-one context, such as:: - + MyClass.some_prop != <some object> - + This will typically produce a clause such as:: - + mytable.related_id != <some id> - + Where ``<some id>`` is the primary key of the given object. - + The ``!=`` operator provides partial functionality for non- many-to-one comparisons: - + * Comparisons against collections are not supported. Use :meth:`~.RelationshipProperty.Comparator.contains` @@ -682,7 +683,7 @@ class RelationshipProperty(StrategizedProperty): membership tests. * Comparisons against ``None`` given in a one-to-many or many-to-many context produce an EXISTS clause. - + """ if isinstance(other, (NoneType, expression._Null)): if self.property.direction == MANYTOONE: @@ -880,9 +881,9 @@ class RelationshipProperty(StrategizedProperty): def mapper(self): """Return the targeted :class:`.Mapper` for this :class:`.RelationshipProperty`. - + This is a lazy-initializing static attribute. - + """ if isinstance(self.argument, type): mapper_ = mapper.class_mapper(self.argument, @@ -914,58 +915,25 @@ class RelationshipProperty(StrategizedProperty): def do_init(self): self._check_conflicts() self._process_dependent_arguments() - self._determine_joins() - self._determine_synchronize_pairs() - self._determine_direction() - self._determine_local_remote_pairs() + self._setup_join_conditions() + self._check_cascade_settings() self._post_init() self._generate_backref() super(RelationshipProperty, self).do_init() - def _check_conflicts(self): - """Test that this relationship is legal, warn about - inheritance conflicts.""" - - if not self.is_primary() \ - and not mapper.class_mapper( - self.parent.class_, - compile=False).has_property(self.key): - raise sa_exc.ArgumentError("Attempting to assign a new " - "relationship '%s' to a non-primary mapper on " - "class '%s'. New relationships can only be added " - "to the primary mapper, i.e. the very first mapper " - "created for class '%s' " % (self.key, - self.parent.class_.__name__, - self.parent.class_.__name__)) - - # check for conflicting relationship() on superclass - if not self.parent.concrete: - for inheriting in self.parent.iterate_to_root(): - if inheriting is not self.parent \ - and inheriting.has_property(self.key): - util.warn("Warning: relationship '%s' on mapper " - "'%s' supersedes the same relationship " - "on inherited mapper '%s'; this can " - "cause dependency issues during flush" - % (self.key, self.parent, inheriting)) - def _process_dependent_arguments(self): """Convert incoming configuration arguments to their proper form. - + Callables are resolved, ORM annotations removed. - + """ # accept callables for other attributes which may require # deferred initialization. This technique is used # by declarative "string configs" and some recipes. for attr in ( - 'order_by', - 'primaryjoin', - 'secondaryjoin', - 'secondary', - '_user_defined_foreign_keys', - 'remote_side', + 'order_by', 'primaryjoin', 'secondaryjoin', + 'secondary', '_user_defined_foreign_keys', 'remote_side', ): attr_value = getattr(self, attr) if util.callable(attr_value): @@ -1008,284 +976,64 @@ class RelationshipProperty(StrategizedProperty): (self.key, self.parent.class_) ) - def _determine_joins(self): - """Determine the 'primaryjoin' and 'secondaryjoin' attributes, - if not passed to the constructor already. - - This is based on analysis of the foreign key relationships - between the parent and target mapped selectables. - - """ - if self.secondaryjoin is not None and self.secondary is None: - raise sa_exc.ArgumentError("Property '" + self.key - + "' specified with secondary join condition but " - "no secondary argument") - - # if join conditions were not specified, figure them out based - # on foreign keys - - def _search_for_join(mapper, table): - # find a join between the given mapper's mapped table and - # the given table. will try the mapper's local table first - # for more specificity, then if not found will try the more - # general mapped table, which in the case of inheritance is - # a join. - return join_condition(mapper.mapped_table, table, - a_subset=mapper.local_table) - - try: - if self.secondary is not None: - if self.secondaryjoin is None: - self.secondaryjoin = _search_for_join(self.mapper, - self.secondary) - if self.primaryjoin is None: - self.primaryjoin = _search_for_join(self.parent, - self.secondary) - else: - if self.primaryjoin is None: - self.primaryjoin = _search_for_join(self.parent, - self.target) - except sa_exc.ArgumentError, e: - raise sa_exc.ArgumentError("Could not determine join " - "condition between parent/child tables on " - "relationship %s. Specify a 'primaryjoin' " - "expression. If 'secondary' is present, " - "'secondaryjoin' is needed as well." - % self) - - def _columns_are_mapped(self, *cols): - """Return True if all columns in the given collection are - mapped by the tables referenced by this :class:`.Relationship`. - - """ - for c in cols: - if self.secondary is not None \ - and self.secondary.c.contains_column(c): - continue - if not self.parent.mapped_table.c.contains_column(c) and \ - not self.target.c.contains_column(c): - return False - return True - - def _sync_pairs_from_join(self, join_condition, primary): - """Determine a list of "source"/"destination" column pairs - based on the given join condition, as well as the - foreign keys argument. - - "source" would be a column referenced by a foreign key, - and "destination" would be the column who has a foreign key - reference to "source". - - """ - - fks = self._user_defined_foreign_keys - # locate pairs - eq_pairs = criterion_as_pairs(join_condition, - consider_as_foreign_keys=fks, - any_operator=self.viewonly) - - # couldn't find any fks, but we have - # "secondary" - assume the "secondary" columns - # are the fks - if not eq_pairs and \ - self.secondary is not None and \ - not fks: - fks = set(self.secondary.c) - eq_pairs = criterion_as_pairs(join_condition, - consider_as_foreign_keys=fks, - any_operator=self.viewonly) - - if eq_pairs: - util.warn("No ForeignKey objects were present " - "in secondary table '%s'. Assumed referenced " - "foreign key columns %s for join condition '%s' " - "on relationship %s" % ( - self.secondary.description, - ", ".join(sorted(["'%s'" % col for col in fks])), - join_condition, - self - )) - - # Filter out just to columns that are mapped. - # If viewonly, allow pairs where the FK col - # was part of "foreign keys" - the column it references - # may be in an un-mapped table - see - # test.orm.test_relationships.ViewOnlyComplexJoin.test_basic - # for an example of this. - eq_pairs = [(l, r) for (l, r) in eq_pairs - if self._columns_are_mapped(l, r) - or self.viewonly and - r in fks] - - if eq_pairs: - return eq_pairs - - # from here below is just determining the best error message - # to report. Check for a join condition using any operator - # (not just ==), perhaps they need to turn on "viewonly=True". - if not self.viewonly and criterion_as_pairs(join_condition, - consider_as_foreign_keys=self._user_defined_foreign_keys, - any_operator=True): - - err = "Could not locate any "\ - "foreign-key-equated, locally mapped column "\ - "pairs for %s "\ - "condition '%s' on relationship %s." % ( - primary and 'primaryjoin' or 'secondaryjoin', - join_condition, - self - ) - - if not self._user_defined_foreign_keys: - err += " Ensure that the "\ - "referencing Column objects have a "\ - "ForeignKey present, or are otherwise part "\ - "of a ForeignKeyConstraint on their parent "\ - "Table, or specify the foreign_keys parameter "\ - "to this relationship." - - err += " For more "\ - "relaxed rules on join conditions, the "\ - "relationship may be marked as viewonly=True." - - raise sa_exc.ArgumentError(err) - else: - if self._user_defined_foreign_keys: - raise sa_exc.ArgumentError("Could not determine " - "relationship direction for %s condition " - "'%s', on relationship %s, using manual " - "'foreign_keys' setting. Do the columns " - "in 'foreign_keys' represent all, and " - "only, the 'foreign' columns in this join " - "condition? Does the %s Table already " - "have adequate ForeignKey and/or " - "ForeignKeyConstraint objects established " - "(in which case 'foreign_keys' is usually " - "unnecessary)?" - % ( - primary and 'primaryjoin' or 'secondaryjoin', - join_condition, - self, - primary and 'mapped' or 'secondary' - )) - else: - raise sa_exc.ArgumentError("Could not determine " - "relationship direction for %s condition " - "'%s', on relationship %s. Ensure that the " - "referencing Column objects have a " - "ForeignKey present, or are otherwise part " - "of a ForeignKeyConstraint on their parent " - "Table, or specify the foreign_keys parameter " - "to this relationship." - % ( - primary and 'primaryjoin' or 'secondaryjoin', - join_condition, - self - )) - - def _determine_synchronize_pairs(self): - """Resolve 'primary'/foreign' column pairs from the primaryjoin - and secondaryjoin arguments. - - """ - if self.local_remote_pairs: - if not self._user_defined_foreign_keys: - raise sa_exc.ArgumentError( - "foreign_keys argument is " - "required with _local_remote_pairs argument") - self.synchronize_pairs = [] - for l, r in self.local_remote_pairs: - if r in self._user_defined_foreign_keys: - self.synchronize_pairs.append((l, r)) - elif l in self._user_defined_foreign_keys: - self.synchronize_pairs.append((r, l)) - else: - self.synchronize_pairs = self._sync_pairs_from_join( - self.primaryjoin, - True) - - self._calculated_foreign_keys = util.column_set( - r for (l, r) in - self.synchronize_pairs) - - if self.secondaryjoin is not None: - self.secondary_synchronize_pairs = self._sync_pairs_from_join( - self.secondaryjoin, - False) - self._calculated_foreign_keys.update( - r for (l, r) in - self.secondary_synchronize_pairs) - else: - self.secondary_synchronize_pairs = None - - def _determine_direction(self): - """Determine if this relationship is one to many, many to one, - many to many. - - This is derived from the primaryjoin, presence of "secondary", - and in the case of self-referential the "remote side". - - """ - if self.secondaryjoin is not None: - self.direction = MANYTOMANY - elif self._refers_to_parent_table(): - - # self referential defaults to ONETOMANY unless the "remote" - # side is present and does not reference any foreign key - # columns - - if self.local_remote_pairs: - remote = [r for (l, r) in self.local_remote_pairs] - elif self.remote_side: - remote = self.remote_side - else: - remote = None - if not remote or self._calculated_foreign_keys.difference(l for (l, - r) in self.synchronize_pairs).intersection(remote): - self.direction = ONETOMANY - else: - self.direction = MANYTOONE - else: - parentcols = util.column_set(self.parent.mapped_table.c) - targetcols = util.column_set(self.mapper.mapped_table.c) - - # fk collection which suggests ONETOMANY. - onetomany_fk = targetcols.intersection( - self._calculated_foreign_keys) + def _setup_join_conditions(self): + self._join_condition = jc = relationships.JoinCondition( + parent_selectable=self.parent.mapped_table, + child_selectable=self.mapper.mapped_table, + parent_local_selectable=self.parent.local_table, + child_local_selectable=self.mapper.local_table, + primaryjoin=self.primaryjoin, + secondary=self.secondary, + secondaryjoin=self.secondaryjoin, + parent_equivalents=self.parent._equivalent_columns, + child_equivalents=self.mapper._equivalent_columns, + consider_as_foreign_keys=self._user_defined_foreign_keys, + local_remote_pairs=self.local_remote_pairs, + remote_side=self.remote_side, + self_referential=self._is_self_referential, + prop=self, + support_sync=not self.viewonly, + can_be_synced_fn=self._columns_are_mapped + ) + self.primaryjoin = jc.deannotated_primaryjoin + self.secondaryjoin = jc.deannotated_secondaryjoin + self.direction = jc.direction + self.local_remote_pairs = jc.local_remote_pairs + self.remote_side = jc.remote_columns + self.local_columns = jc.local_columns + self.synchronize_pairs = jc.synchronize_pairs + self._calculated_foreign_keys = jc.foreign_key_columns + self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs - # fk collection which suggests MANYTOONE. + def _check_conflicts(self): + """Test that this relationship is legal, warn about + inheritance conflicts.""" - manytoone_fk = parentcols.intersection( - self._calculated_foreign_keys) + if not self.is_primary() \ + and not mapper.class_mapper( + self.parent.class_, + compile=False).has_property(self.key): + raise sa_exc.ArgumentError("Attempting to assign a new " + "relationship '%s' to a non-primary mapper on " + "class '%s'. New relationships can only be added " + "to the primary mapper, i.e. the very first mapper " + "created for class '%s' " % (self.key, + self.parent.class_.__name__, + self.parent.class_.__name__)) - if onetomany_fk and manytoone_fk: - # fks on both sides. do the same test only based on the - # local side. - referents = [c for (c, f) in self.synchronize_pairs] - onetomany_local = parentcols.intersection(referents) - manytoone_local = targetcols.intersection(referents) + # check for conflicting relationship() on superclass + if not self.parent.concrete: + for inheriting in self.parent.iterate_to_root(): + if inheriting is not self.parent \ + and inheriting.has_property(self.key): + util.warn("Warning: relationship '%s' on mapper " + "'%s' supersedes the same relationship " + "on inherited mapper '%s'; this can " + "cause dependency issues during flush" + % (self.key, self.parent, inheriting)) - if onetomany_local and not manytoone_local: - self.direction = ONETOMANY - elif manytoone_local and not onetomany_local: - self.direction = MANYTOONE - else: - raise sa_exc.ArgumentError( - "Can't determine relationship" - " direction for relationship '%s' - foreign " - "key columns are present in both the parent " - "and the child's mapped tables. Specify " - "'foreign_keys' argument." % self) - elif onetomany_fk: - self.direction = ONETOMANY - elif manytoone_fk: - self.direction = MANYTOONE - else: - raise sa_exc.ArgumentError("Can't determine relationship " - "direction for relationship '%s' - foreign " - "key columns are present in neither the parent " - "nor the child's mapped tables" % self) + def _check_cascade_settings(self): if self.cascade.delete_orphan and not self.single_parent \ and (self.direction is MANYTOMANY or self.direction is MANYTOONE): @@ -1300,84 +1048,24 @@ class RelationshipProperty(StrategizedProperty): "relationships only." % self) - def _determine_local_remote_pairs(self): - """Determine pairs of columns representing "local" to - "remote", where "local" columns are on the parent mapper, - "remote" are on the target mapper. - - These pairs are used on the load side only to generate - lazy loading clauses. + def _columns_are_mapped(self, *cols): + """Return True if all columns in the given collection are + mapped by the tables referenced by this :class:`.Relationship`. """ - if not self.local_remote_pairs and not self.remote_side: - # the most common, trivial case. Derive - # local/remote pairs from the synchronize pairs. - eq_pairs = util.unique_list( - self.synchronize_pairs + - (self.secondary_synchronize_pairs or [])) - if self.direction is MANYTOONE: - self.local_remote_pairs = [(r, l) for l, r in eq_pairs] - else: - self.local_remote_pairs = eq_pairs - - # "remote_side" specified, derive from the primaryjoin - # plus remote_side, similarly to how synchronize_pairs - # were determined. - elif self.remote_side: - if self.local_remote_pairs: - raise sa_exc.ArgumentError('remote_side argument is ' - 'redundant against more detailed ' - '_local_remote_side argument.') - if self.direction is MANYTOONE: - self.local_remote_pairs = [(r, l) for (l, r) in - criterion_as_pairs(self.primaryjoin, - consider_as_referenced_keys=self.remote_side, - any_operator=True)] - - else: - self.local_remote_pairs = \ - criterion_as_pairs(self.primaryjoin, - consider_as_foreign_keys=self.remote_side, - any_operator=True) - if not self.local_remote_pairs: - raise sa_exc.ArgumentError('Relationship %s could ' - 'not determine any local/remote column ' - 'pairs from remote side argument %r' - % (self, self.remote_side)) - # else local_remote_pairs were sent explcitly via - # ._local_remote_pairs. - - # create local_side/remote_side accessors - self.local_side = util.ordered_column_set( - l for l, r in self.local_remote_pairs) - self.remote_side = util.ordered_column_set( - r for l, r in self.local_remote_pairs) - - # check that the non-foreign key column in the local/remote - # collection is mapped. The foreign key - # which the individual mapped column references directly may - # itself be in a non-mapped table; see - # test.orm.test_relationships.ViewOnlyComplexJoin.test_basic - # for an example of this. - if self.direction is ONETOMANY: - for col in self.local_side: - if not self._columns_are_mapped(col): - raise sa_exc.ArgumentError( - "Local column '%s' is not " - "part of mapping %s. Specify remote_side " - "argument to indicate which column lazy join " - "condition should compare against." % (col, - self.parent)) - elif self.direction is MANYTOONE: - for col in self.remote_side: - if not self._columns_are_mapped(col): - raise sa_exc.ArgumentError( - "Remote column '%s' is not " - "part of mapping %s. Specify remote_side " - "argument to indicate which column lazy join " - "condition should bind." % (col, self.mapper)) + for c in cols: + if self.secondary is not None \ + and self.secondary.c.contains_column(c): + continue + if not self.parent.mapped_table.c.contains_column(c) and \ + not self.target.c.contains_column(c): + return False + return True def _generate_backref(self): + """Interpret the 'backref' instruction to create a + :func:`.relationship` complementary to this one.""" + if not self.is_primary(): return if self.backref is not None and not self.back_populates: @@ -1391,17 +1079,27 @@ class RelationshipProperty(StrategizedProperty): "'%s' on relationship '%s': property of that " "name exists on mapper '%s'" % (backref_key, self, mapper)) + + # determine primaryjoin/secondaryjoin for the + # backref. Use the one we had, so that + # a custom join doesn't have to be specified in + # both directions. if self.secondary is not None: - pj = kwargs.pop('primaryjoin', self.secondaryjoin) - sj = kwargs.pop('secondaryjoin', self.primaryjoin) + # for many to many, just switch primaryjoin/ + # secondaryjoin. use the annotated + # pj/sj on the _join_condition. + pj = kwargs.pop('primaryjoin', self._join_condition.secondaryjoin) + sj = kwargs.pop('secondaryjoin', self._join_condition.primaryjoin) else: - pj = kwargs.pop('primaryjoin', self.primaryjoin) + pj = kwargs.pop('primaryjoin', + self._join_condition.primaryjoin_reverse_remote) sj = kwargs.pop('secondaryjoin', None) if sj: raise sa_exc.InvalidRequestError( - "Can't assign 'secondaryjoin' on a backref against " - "a non-secondary relationship." - ) + "Can't assign 'secondaryjoin' on a backref " + "against a non-secondary relationship." + ) + foreign_keys = kwargs.pop('foreign_keys', self._user_defined_foreign_keys) parent = self.parent.primary_mapper() @@ -1410,35 +1108,17 @@ class RelationshipProperty(StrategizedProperty): kwargs.setdefault('passive_updates', self.passive_updates) self.back_populates = backref_key relationship = RelationshipProperty( - parent, - self.secondary, - pj, - sj, + parent, self.secondary, + pj, sj, foreign_keys=foreign_keys, back_populates=self.key, - **kwargs - ) + **kwargs) mapper._configure_property(backref_key, relationship) if self.back_populates: self._add_reverse_property(self.back_populates) def _post_init(self): - self.logger.info('%s setup primary join %s', self, - self.primaryjoin) - self.logger.info('%s setup secondary join %s', self, - self.secondaryjoin) - self.logger.info('%s synchronize pairs [%s]', self, - ','.join('(%s => %s)' % (l, r) for (l, r) in - self.synchronize_pairs)) - self.logger.info('%s secondary synchronize pairs [%s]', self, - ','.join('(%s => %s)' % (l, r) for (l, r) in - self.secondary_synchronize_pairs or [])) - self.logger.info('%s local/remote pairs [%s]', self, - ','.join('(%s / %s)' % (l, r) for (l, r) in - self.local_remote_pairs)) - self.logger.info('%s relationship direction %s', self, - self.direction) if self.uselist is None: self.uselist = self.direction is not MANYTOONE if not self.viewonly: @@ -1453,20 +1133,6 @@ class RelationshipProperty(StrategizedProperty): strategy = self._get_strategy(strategies.LazyLoader) return strategy.use_get - def _refers_to_parent_table(self): - pt = self.parent.mapped_table - mt = self.mapper.mapped_table - for c, f in self.synchronize_pairs: - if ( - pt.is_derived_from(c.table) and \ - pt.is_derived_from(f.table) and \ - mt.is_derived_from(c.table) and \ - mt.is_derived_from(f.table) - ): - return True - else: - return False - @util.memoized_property def _is_self_referential(self): return self.mapper.common_parent(self.parent) @@ -1496,75 +1162,22 @@ class RelationshipProperty(StrategizedProperty): else: aliased = True - # place a barrier on the destination such that - # replacement traversals won't ever dig into it. - # its internal structure remains fixed - # regardless of context. - dest_selectable = _shallow_annotate( - dest_selectable, - {'no_replacement_traverse':True}) - - aliased = aliased or (source_selectable is not None) - - primaryjoin, secondaryjoin, secondary = self.primaryjoin, \ - self.secondaryjoin, self.secondary - - # adjust the join condition for single table inheritance, - # in the case that the join is to a subclass - # this is analogous to the "_adjust_for_single_table_inheritance()" - # method in Query. - dest_mapper = of_type or self.mapper single_crit = dest_mapper._single_table_criterion - if single_crit is not None: - if secondaryjoin is not None: - secondaryjoin = secondaryjoin & single_crit - else: - primaryjoin = primaryjoin & single_crit - - if aliased: - if secondary is not None: - secondary = secondary.alias() - primary_aliasizer = ClauseAdapter(secondary) - secondary_aliasizer = \ - ClauseAdapter(dest_selectable, - equivalents=self.mapper._equivalent_columns).\ - chain(primary_aliasizer) - if source_selectable is not None: - primary_aliasizer = \ - ClauseAdapter(secondary).\ - chain(ClauseAdapter(source_selectable, - equivalents=self.parent._equivalent_columns)) - secondaryjoin = \ - secondary_aliasizer.traverse(secondaryjoin) - else: - primary_aliasizer = ClauseAdapter(dest_selectable, - exclude=self.local_side, - equivalents=self.mapper._equivalent_columns) - if source_selectable is not None: - primary_aliasizer.chain( - ClauseAdapter(source_selectable, - exclude=self.remote_side, - equivalents=self.parent._equivalent_columns)) - secondary_aliasizer = None - primaryjoin = primary_aliasizer.traverse(primaryjoin) - target_adapter = secondary_aliasizer or primary_aliasizer - target_adapter.include = target_adapter.exclude = None - else: - target_adapter = None + aliased = aliased or (source_selectable is not None) + + primaryjoin, secondaryjoin, secondary, target_adapter, dest_selectable = \ + self._join_condition.join_targets( + source_selectable, dest_selectable, aliased, single_crit + ) if source_selectable is None: source_selectable = self.parent.local_table if dest_selectable is None: dest_selectable = self.mapper.local_table - return ( - primaryjoin, - secondaryjoin, - source_selectable, - dest_selectable, - secondary, - target_adapter, - ) + return (primaryjoin, secondaryjoin, source_selectable, + dest_selectable, secondary, target_adapter) + PropertyLoader = RelationProperty = RelationshipProperty log.class_logger(RelationshipProperty) diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py new file mode 100644 index 000000000..4c64e855f --- /dev/null +++ b/lib/sqlalchemy/orm/relationships.py @@ -0,0 +1,856 @@ +# orm/relationships.py +# Copyright (C) 2005-2012 the SQLAlchemy authors and contributors <see AUTHORS file> +# +# This module is part of SQLAlchemy and is released under +# the MIT License: http://www.opensource.org/licenses/mit-license.php + +"""Heuristics related to join conditions as used in +:func:`.relationship`. + +Provides the :class:`.JoinCondition` object, which encapsulates +SQL annotation and aliasing behavior focused on the `primaryjoin` +and `secondaryjoin` aspects of :func:`.relationship`. + +""" + +from sqlalchemy import sql, util, log, exc as sa_exc, schema +from sqlalchemy.sql.util import ClauseAdapter, criterion_as_pairs, \ + join_condition, _shallow_annotate, visit_binary_product,\ + _deep_deannotate +from sqlalchemy.sql import operators, expression, visitors +from sqlalchemy.orm.interfaces import MANYTOMANY, MANYTOONE, ONETOMANY + +def remote(expr): + return _annotate_columns(expr, {"remote":True}) + +def foreign(expr): + return _annotate_columns(expr, {"foreign":True}) + +def remote_foreign(expr): + return _annotate_columns(expr, {"foreign":True, + "remote":True}) + +def _annotate_columns(element, annotations): + def clone(elem): + if isinstance(elem, expression.ColumnClause): + elem = elem._annotate(annotations.copy()) + elem._copy_internals(clone=clone) + return elem + + if element is not None: + element = clone(element) + return element + +class JoinCondition(object): + def __init__(self, + parent_selectable, + child_selectable, + parent_local_selectable, + child_local_selectable, + primaryjoin=None, + secondary=None, + secondaryjoin=None, + parent_equivalents=None, + child_equivalents=None, + consider_as_foreign_keys=None, + local_remote_pairs=None, + remote_side=None, + self_referential=False, + prop=None, + support_sync=True, + can_be_synced_fn=lambda *c: True + ): + self.parent_selectable = parent_selectable + self.parent_local_selectable = parent_local_selectable + self.child_selectable = child_selectable + self.child_local_selectable = child_local_selectable + self.parent_equivalents = parent_equivalents + self.child_equivalents = child_equivalents + self.primaryjoin = primaryjoin + self.secondaryjoin = secondaryjoin + self.secondary = secondary + self.consider_as_foreign_keys = consider_as_foreign_keys + self._local_remote_pairs = local_remote_pairs + self._remote_side = remote_side + self.prop = prop + self.self_referential = self_referential + self.support_sync = support_sync + self.can_be_synced_fn = can_be_synced_fn + self._determine_joins() + self._annotate_fks() + self._annotate_remote() + self._annotate_local() + self._setup_pairs() + self._check_foreign_cols(self.primaryjoin, True) + if self.secondaryjoin is not None: + self._check_foreign_cols(self.secondaryjoin, False) + self._determine_direction() + self._check_remote_side() + self._log_joins() + + def _log_joins(self): + if self.prop is None: + return + log = self.prop.logger + log.info('%s setup primary join %s', self, + self.primaryjoin) + log.info('%s setup secondary join %s', self, + self.secondaryjoin) + log.info('%s synchronize pairs [%s]', self, + ','.join('(%s => %s)' % (l, r) for (l, r) in + self.synchronize_pairs)) + log.info('%s secondary synchronize pairs [%s]', self, + ','.join('(%s => %s)' % (l, r) for (l, r) in + self.secondary_synchronize_pairs or [])) + log.info('%s local/remote pairs [%s]', self, + ','.join('(%s / %s)' % (l, r) for (l, r) in + self.local_remote_pairs)) + log.info('%s relationship direction %s', self, + self.direction) + + def _determine_joins(self): + """Determine the 'primaryjoin' and 'secondaryjoin' attributes, + if not passed to the constructor already. + + This is based on analysis of the foreign key relationships + between the parent and target mapped selectables. + + """ + if self.secondaryjoin is not None and self.secondary is None: + raise sa_exc.ArgumentError( + "Property %s specified with secondary " + "join condition but " + "no secondary argument" % self.prop) + + # find a join between the given mapper's mapped table and + # the given table. will try the mapper's local table first + # for more specificity, then if not found will try the more + # general mapped table, which in the case of inheritance is + # a join. + try: + if self.secondary is not None: + if self.secondaryjoin is None: + self.secondaryjoin = \ + join_condition( + self.child_selectable, + self.secondary, + a_subset=self.child_local_selectable, + consider_as_foreign_keys=\ + self.consider_as_foreign_keys or None + ) + if self.primaryjoin is None: + self.primaryjoin = \ + join_condition( + self.parent_selectable, + self.secondary, + a_subset=self.parent_local_selectable, + consider_as_foreign_keys=\ + self.consider_as_foreign_keys or None + ) + else: + if self.primaryjoin is None: + self.primaryjoin = \ + join_condition( + self.parent_selectable, + self.child_selectable, + a_subset=self.parent_local_selectable, + consider_as_foreign_keys=\ + self.consider_as_foreign_keys or None + ) + except sa_exc.NoForeignKeysError, nfke: + if self.secondary is not None: + raise sa_exc.NoForeignKeysError("Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are no foreign keys " + "linking these tables via secondary table '%s'. " + "Ensure that referencing columns are associated " + "with a ForeignKey or ForeignKeyConstraint, or " + "specify 'primaryjoin' and 'secondaryjoin' " + "expressions." + % (self.prop, self.secondary)) + else: + raise sa_exc.NoForeignKeysError("Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are no foreign keys " + "linking these tables. " + "Ensure that referencing columns are associated " + "with a ForeignKey or ForeignKeyConstraint, or " + "specify a 'primaryjoin' expression." + % self.prop) + except sa_exc.AmbiguousForeignKeysError, afke: + if self.secondary is not None: + raise sa_exc.AmbiguousForeignKeysError( + "Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are multiple foreign key " + "paths linking the tables via secondary table '%s'. " + "Specify the 'foreign_keys' " + "argument, providing a list of those columns which " + "should be counted as containing a foreign key " + "reference from the secondary table to each of the " + "parent and child tables." + % (self.prop, self.secondary)) + else: + raise sa_exc.AmbiguousForeignKeysError( + "Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are multiple foreign key " + "paths linking the tables. Specify the " + "'foreign_keys' argument, providing a list of those " + "columns which should be counted as containing a " + "foreign key reference to the parent table." + % self.prop) + + @util.memoized_property + def primaryjoin_reverse_remote(self): + """Return the primaryjoin condition suitable for the + "reverse" direction. + + If the primaryjoin was delivered here with pre-existing + "remote" annotations, the local/remote annotations + are reversed. Otherwise, the local/remote annotations + are removed. + + """ + if self._has_remote_annotations: + def replace(element): + if "remote" in element._annotations: + v = element._annotations.copy() + del v['remote'] + v['local'] = True + return element._with_annotations(v) + elif "local" in element._annotations: + v = element._annotations.copy() + del v['local'] + v['remote'] = True + return element._with_annotations(v) + return visitors.replacement_traverse( + self.primaryjoin, {}, replace) + else: + if self._has_foreign_annotations: + # TODO: coverage + return _deep_deannotate(self.primaryjoin, + values=("local", "remote")) + else: + return _deep_deannotate(self.primaryjoin) + + def _has_annotation(self, clause, annotation): + for col in visitors.iterate(clause, {}): + if annotation in col._annotations: + return True + else: + return False + + @util.memoized_property + def _has_foreign_annotations(self): + return self._has_annotation(self.primaryjoin, "foreign") + + @util.memoized_property + def _has_remote_annotations(self): + return self._has_annotation(self.primaryjoin, "remote") + + def _annotate_fks(self): + """Annotate the primaryjoin and secondaryjoin + structures with 'foreign' annotations marking columns + considered as foreign. + + """ + if self._has_foreign_annotations: + return + + if self.consider_as_foreign_keys: + self._annotate_from_fk_list() + else: + self._annotate_present_fks() + + def _annotate_from_fk_list(self): + def check_fk(col): + if col in self.consider_as_foreign_keys: + return col._annotate({"foreign":True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, + {}, + check_fk + ) + if self.secondaryjoin is not None: + self.secondaryjoin = visitors.replacement_traverse( + self.secondaryjoin, + {}, + check_fk + ) + + def _annotate_present_fks(self): + if self.secondary is not None: + secondarycols = util.column_set(self.secondary.c) + else: + secondarycols = set() + def is_foreign(a, b): + if isinstance(a, schema.Column) and \ + isinstance(b, schema.Column): + if a.references(b): + return a + elif b.references(a): + return b + + if secondarycols: + if a in secondarycols and b not in secondarycols: + return a + elif b in secondarycols and a not in secondarycols: + return b + + def visit_binary(binary): + if not isinstance(binary.left, sql.ColumnElement) or \ + not isinstance(binary.right, sql.ColumnElement): + return + + if "foreign" not in binary.left._annotations and \ + "foreign" not in binary.right._annotations: + col = is_foreign(binary.left, binary.right) + if col is not None: + if col.compare(binary.left): + binary.left = binary.left._annotate( + {"foreign":True}) + elif col.compare(binary.right): + binary.right = binary.right._annotate( + {"foreign":True}) + + self.primaryjoin = visitors.cloned_traverse( + self.primaryjoin, + {}, + {"binary":visit_binary} + ) + if self.secondaryjoin is not None: + self.secondaryjoin = visitors.cloned_traverse( + self.secondaryjoin, + {}, + {"binary":visit_binary} + ) + + def _refers_to_parent_table(self): + """Return True if the join condition contains column + comparisons where both columns are in both tables. + + """ + pt = self.parent_selectable + mt = self.child_selectable + result = [False] + def visit_binary(binary): + c, f = binary.left, binary.right + if ( + isinstance(c, expression.ColumnClause) and \ + isinstance(f, expression.ColumnClause) and \ + pt.is_derived_from(c.table) and \ + pt.is_derived_from(f.table) and \ + mt.is_derived_from(c.table) and \ + mt.is_derived_from(f.table) + ): + result[0] = True + visitors.traverse( + self.primaryjoin, + {}, + {"binary":visit_binary} + ) + return result[0] + + def _tables_overlap(self): + """Return True if parent/child tables have some overlap.""" + + return self.parent_selectable.is_derived_from( + self.child_local_selectable) or \ + self.child_selectable.is_derived_from( + self.parent_local_selectable) + + def _annotate_remote(self): + """Annotate the primaryjoin and secondaryjoin + structures with 'remote' annotations marking columns + considered as part of the 'remote' side. + + """ + if self._has_remote_annotations: + return + + parentcols = util.column_set(self.parent_selectable.c) + + if self.secondary is not None: + self._annotate_remote_secondary() + elif self._local_remote_pairs or self._remote_side: + self._annotate_remote_from_args() + elif self._refers_to_parent_table(): + self._annotate_selfref(lambda col:"foreign" in col._annotations) + elif self._tables_overlap(): + self._annotate_remote_with_overlap() + else: + self._annotate_remote_distinct_selectables() + + def _annotate_remote_secondary(self): + """annotate 'remote' in primaryjoin, secondaryjoin + when 'secondary' is present. + + """ + def repl(element): + if self.secondary.c.contains_column(element): + return element._annotate({"remote":True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, repl) + self.secondaryjoin = visitors.replacement_traverse( + self.secondaryjoin, {}, repl) + + def _annotate_selfref(self, fn): + """annotate 'remote' in primaryjoin, secondaryjoin + when the relationship is detected as self-referential. + + """ + def visit_binary(binary): + equated = binary.left.compare(binary.right) + if isinstance(binary.left, expression.ColumnClause) and \ + isinstance(binary.right, expression.ColumnClause): + # assume one to many - FKs are "remote" + if fn(binary.left): + binary.left = binary.left._annotate({"remote":True}) + if fn(binary.right) and \ + not equated: + binary.right = binary.right._annotate( + {"remote":True}) + else: + self._warn_non_column_elements() + + self.primaryjoin = visitors.cloned_traverse( + self.primaryjoin, {}, + {"binary":visit_binary}) + + def _annotate_remote_from_args(self): + """annotate 'remote' in primaryjoin, secondaryjoin + when the 'remote_side' or '_local_remote_pairs' + arguments are used. + + """ + if self._local_remote_pairs: + if self._remote_side: + raise sa_exc.ArgumentError( + "remote_side argument is redundant " + "against more detailed _local_remote_side " + "argument.") + + remote_side = [r for (l, r) in self._local_remote_pairs] + else: + remote_side = self._remote_side + + if self._refers_to_parent_table(): + self._annotate_selfref(lambda col:col in remote_side) + else: + def repl(element): + if element in remote_side: + return element._annotate({"remote":True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, repl) + + def _annotate_remote_with_overlap(self): + """annotate 'remote' in primaryjoin, secondaryjoin + when the parent/child tables have some set of + tables in common, though is not a fully self-referential + relationship. + + """ + def visit_binary(binary): + binary.left, binary.right = proc_left_right(binary.left, + binary.right) + binary.right, binary.left = proc_left_right(binary.right, + binary.left) + def proc_left_right(left, right): + if isinstance(left, expression.ColumnClause) and \ + isinstance(right, expression.ColumnClause): + if self.child_selectable.c.contains_column(right) and \ + self.parent_selectable.c.contains_column(left): + right = right._annotate({"remote":True}) + else: + self._warn_non_column_elements() + + return left, right + + self.primaryjoin = visitors.cloned_traverse( + self.primaryjoin, {}, + {"binary":visit_binary}) + + def _annotate_remote_distinct_selectables(self): + """annotate 'remote' in primaryjoin, secondaryjoin + when the parent/child tables are entirely + separate. + + """ + def repl(element): + if self.child_selectable.c.contains_column(element) and \ + ( + not self.parent_local_selectable.c.\ + contains_column(element) + or self.child_local_selectable.c.\ + contains_column(element) + ): + return element._annotate({"remote":True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, repl) + + def _warn_non_column_elements(self): + util.warn( + "Non-simple column elements in primary " + "join condition for property %s - consider using " + "remote() annotations to mark the remote side." + % self.prop + ) + + def _annotate_local(self): + """Annotate the primaryjoin and secondaryjoin + structures with 'local' annotations. + + This annotates all column elements found + simultaneously in the parent table + and the join condition that don't have a + 'remote' annotation set up from + _annotate_remote() or user-defined. + + """ + if self._has_annotation(self.primaryjoin, "local"): + return + + parentcols = util.column_set(self.parent_selectable.c) + + if self._local_remote_pairs: + local_side = util.column_set([l for (l, r) + in self._local_remote_pairs]) + else: + local_side = util.column_set(self.parent_selectable.c) + + def locals_(elem): + if "remote" not in elem._annotations and \ + elem in local_side: + return elem._annotate({"local":True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, locals_ + ) + + def _check_remote_side(self): + if not self.local_remote_pairs: + raise sa_exc.ArgumentError('Relationship %s could ' + 'not determine any unambiguous local/remote column ' + 'pairs based on join condition and remote_side ' + 'arguments. ' + 'Consider using the remote() annotation to ' + 'accurately mark those elements of the join ' + 'condition that are on the remote side of ' + 'the relationship.' + % (self.prop, )) + + def _check_foreign_cols(self, join_condition, primary): + """Check the foreign key columns collected and emit error + messages.""" + + can_sync = False + + foreign_cols = self._gather_columns_with_annotation( + join_condition, "foreign") + + has_foreign = bool(foreign_cols) + + if primary: + can_sync = bool(self.synchronize_pairs) + else: + can_sync = bool(self.secondary_synchronize_pairs) + + if self.support_sync and can_sync or \ + (not self.support_sync and has_foreign): + return + + # from here below is just determining the best error message + # to report. Check for a join condition using any operator + # (not just ==), perhaps they need to turn on "viewonly=True". + if self.support_sync and has_foreign and not can_sync: + err = "Could not locate any simple equality expressions "\ + "involving locally mapped foreign key columns for "\ + "%s join condition "\ + "'%s' on relationship %s." % ( + primary and 'primary' or 'secondary', + join_condition, + self.prop + ) + err += \ + " Ensure that referencing columns are associated "\ + "with a ForeignKey or ForeignKeyConstraint, or are "\ + "annotated in the join condition with the foreign() "\ + "annotation. To allow comparison operators other than "\ + "'==', the relationship can be marked as viewonly=True." + + raise sa_exc.ArgumentError(err) + else: + err = "Could not locate any relevant foreign key columns "\ + "for %s join condition '%s' on relationship %s." % ( + primary and 'primary' or 'secondary', + join_condition, + self.prop + ) + err += \ + ' Ensure that referencing columns are associated '\ + 'with a ForeignKey or ForeignKeyConstraint, or are '\ + 'annotated in the join condition with the foreign() '\ + 'annotation.' + raise sa_exc.ArgumentError(err) + + def _determine_direction(self): + """Determine if this relationship is one to many, many to one, + many to many. + + """ + if self.secondaryjoin is not None: + self.direction = MANYTOMANY + else: + parentcols = util.column_set(self.parent_selectable.c) + targetcols = util.column_set(self.child_selectable.c) + + # fk collection which suggests ONETOMANY. + onetomany_fk = targetcols.intersection( + self.foreign_key_columns) + + # fk collection which suggests MANYTOONE. + + manytoone_fk = parentcols.intersection( + self.foreign_key_columns) + + if onetomany_fk and manytoone_fk: + # fks on both sides. test for overlap of local/remote + # with foreign key + self_equated = self.remote_columns.intersection( + self.local_columns + ) + onetomany_local = self.remote_columns.\ + intersection(self.foreign_key_columns).\ + difference(self_equated) + manytoone_local = self.local_columns.\ + intersection(self.foreign_key_columns).\ + difference(self_equated) + if onetomany_local and not manytoone_local: + self.direction = ONETOMANY + elif manytoone_local and not onetomany_local: + self.direction = MANYTOONE + else: + raise sa_exc.ArgumentError( + "Can't determine relationship" + " direction for relationship '%s' - foreign " + "key columns within the join condition are present " + "in both the parent and the child's mapped tables. " + "Ensure that only those columns referring " + "to a parent column are marked as foreign, " + "either via the foreign() annotation or " + "via the foreign_keys argument." + % self.prop) + elif onetomany_fk: + self.direction = ONETOMANY + elif manytoone_fk: + self.direction = MANYTOONE + else: + raise sa_exc.ArgumentError("Can't determine relationship " + "direction for relationship '%s' - foreign " + "key columns are present in neither the parent " + "nor the child's mapped tables" % self.prop) + + def _deannotate_pairs(self, collection): + """provide deannotation for the various lists of + pairs, so that using them in hashes doesn't incur + high-overhead __eq__() comparisons against + original columns mapped. + + """ + return [(x._deannotate(), y._deannotate()) + for x, y in collection] + + def _setup_pairs(self): + sync_pairs = [] + lrp = util.OrderedSet([]) + secondary_sync_pairs = [] + + def go(joincond, collection): + def visit_binary(binary, left, right): + if "remote" in right._annotations and \ + "remote" not in left._annotations and \ + self.can_be_synced_fn(left): + lrp.add((left, right)) + elif "remote" in left._annotations and \ + "remote" not in right._annotations and \ + self.can_be_synced_fn(right): + lrp.add((right, left)) + if binary.operator is operators.eq and \ + self.can_be_synced_fn(left, right): + if "foreign" in right._annotations: + collection.append((left, right)) + elif "foreign" in left._annotations: + collection.append((right, left)) + visit_binary_product(visit_binary, joincond) + + for joincond, collection in [ + (self.primaryjoin, sync_pairs), + (self.secondaryjoin, secondary_sync_pairs) + ]: + if joincond is None: + continue + go(joincond, collection) + + self.local_remote_pairs = self._deannotate_pairs(lrp) + self.synchronize_pairs = self._deannotate_pairs(sync_pairs) + self.secondary_synchronize_pairs = self._deannotate_pairs(secondary_sync_pairs) + + @util.memoized_property + def remote_columns(self): + return self._gather_join_annotations("remote") + + @util.memoized_property + def local_columns(self): + return self._gather_join_annotations("local") + + @util.memoized_property + def foreign_key_columns(self): + return self._gather_join_annotations("foreign") + + @util.memoized_property + def deannotated_primaryjoin(self): + return _deep_deannotate(self.primaryjoin) + + @util.memoized_property + def deannotated_secondaryjoin(self): + if self.secondaryjoin is not None: + return _deep_deannotate(self.secondaryjoin) + else: + return None + + def _gather_join_annotations(self, annotation): + s = set( + self._gather_columns_with_annotation( + self.primaryjoin, annotation) + ) + if self.secondaryjoin is not None: + s.update( + self._gather_columns_with_annotation( + self.secondaryjoin, annotation) + ) + return set([x._deannotate() for x in s]) + + def _gather_columns_with_annotation(self, clause, *annotation): + annotation = set(annotation) + return set([ + col for col in visitors.iterate(clause, {}) + if annotation.issubset(col._annotations) + ]) + + + def join_targets(self, source_selectable, + dest_selectable, + aliased, + single_crit=None): + """Given a source and destination selectable, create a + join between them. + + This takes into account aliasing the join clause + to reference the appropriate corresponding columns + in the target objects, as well as the extra child + criterion, equivalent column sets, etc. + + """ + + # place a barrier on the destination such that + # replacement traversals won't ever dig into it. + # its internal structure remains fixed + # regardless of context. + dest_selectable = _shallow_annotate( + dest_selectable, + {'no_replacement_traverse':True}) + + primaryjoin, secondaryjoin, secondary = self.primaryjoin, \ + self.secondaryjoin, self.secondary + + # adjust the join condition for single table inheritance, + # in the case that the join is to a subclass + # this is analogous to the + # "_adjust_for_single_table_inheritance()" method in Query. + + if single_crit is not None: + if secondaryjoin is not None: + secondaryjoin = secondaryjoin & single_crit + else: + primaryjoin = primaryjoin & single_crit + + if aliased: + if secondary is not None: + secondary = secondary.alias() + primary_aliasizer = ClauseAdapter(secondary) + secondary_aliasizer = \ + ClauseAdapter(dest_selectable, + equivalents=self.child_equivalents).\ + chain(primary_aliasizer) + if source_selectable is not None: + primary_aliasizer = \ + ClauseAdapter(secondary).\ + chain(ClauseAdapter(source_selectable, + equivalents=self.parent_equivalents)) + secondaryjoin = \ + secondary_aliasizer.traverse(secondaryjoin) + else: + primary_aliasizer = ClauseAdapter(dest_selectable, + exclude_fn=lambda c: "local" in c._annotations, + equivalents=self.child_equivalents) + if source_selectable is not None: + primary_aliasizer.chain( + ClauseAdapter(source_selectable, + exclude_fn=lambda c: "remote" in c._annotations, + equivalents=self.parent_equivalents)) + secondary_aliasizer = None + + primaryjoin = primary_aliasizer.traverse(primaryjoin) + target_adapter = secondary_aliasizer or primary_aliasizer + target_adapter.exclude_fn = None + else: + target_adapter = None + return primaryjoin, secondaryjoin, secondary, \ + target_adapter, dest_selectable + + def create_lazy_clause(self, reverse_direction=False): + binds = util.column_dict() + lookup = util.column_dict() + equated_columns = util.column_dict() + + if reverse_direction and self.secondaryjoin is None: + for l, r in self.local_remote_pairs: + _list = lookup.setdefault(r, []) + _list.append((r, l)) + equated_columns[l] = r + else: + for l, r in self.local_remote_pairs: + _list = lookup.setdefault(l, []) + _list.append((l, r)) + equated_columns[r] = l + + def col_to_bind(col): + if col in lookup: + for tobind, equated in lookup[col]: + if equated in binds: + return None + if col not in binds: + binds[col] = sql.bindparam( + None, None, type_=col.type, unique=True) + return binds[col] + return None + + lazywhere = self.deannotated_primaryjoin + + if self.deannotated_secondaryjoin is None or not reverse_direction: + lazywhere = visitors.replacement_traverse( + lazywhere, {}, col_to_bind) + + if self.deannotated_secondaryjoin is not None: + secondaryjoin = self.deannotated_secondaryjoin + if reverse_direction: + secondaryjoin = visitors.replacement_traverse( + secondaryjoin, {}, col_to_bind) + lazywhere = sql.and_(lazywhere, secondaryjoin) + + bind_to_col = dict((binds[col].key, col) for col in binds) + + return lazywhere, bind_to_col, equated_columns + + + diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 37980e111..2e09ccbbe 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -324,14 +324,14 @@ class LazyLoader(AbstractRelationshipLoader): def init(self): super(LazyLoader, self).init() + join_condition = self.parent_property._join_condition self._lazywhere, \ self._bind_to_col, \ - self._equated_columns = self._create_lazy_clause(self.parent_property) + self._equated_columns = join_condition.create_lazy_clause() self._rev_lazywhere, \ self._rev_bind_to_col, \ - self._rev_equated_columns = self._create_lazy_clause( - self.parent_property, + self._rev_equated_columns = join_condition.create_lazy_clause( reverse_direction=True) self.logger.info("%s lazy loading clause %s", self, self._lazywhere) @@ -617,49 +617,6 @@ class LazyLoader(AbstractRelationshipLoader): return reset_for_lazy_callable, None, None - @classmethod - def _create_lazy_clause(cls, prop, reverse_direction=False): - binds = util.column_dict() - lookup = util.column_dict() - equated_columns = util.column_dict() - - if reverse_direction and prop.secondaryjoin is None: - for l, r in prop.local_remote_pairs: - _list = lookup.setdefault(r, []) - _list.append((r, l)) - equated_columns[l] = r - else: - for l, r in prop.local_remote_pairs: - _list = lookup.setdefault(l, []) - _list.append((l, r)) - equated_columns[r] = l - - def col_to_bind(col): - if col in lookup: - for tobind, equated in lookup[col]: - if equated in binds: - return None - if col not in binds: - binds[col] = sql.bindparam(None, None, type_=col.type, unique=True) - return binds[col] - return None - - lazywhere = prop.primaryjoin - - if prop.secondaryjoin is None or not reverse_direction: - lazywhere = visitors.replacement_traverse( - lazywhere, {}, col_to_bind) - - if prop.secondaryjoin is not None: - secondaryjoin = prop.secondaryjoin - if reverse_direction: - secondaryjoin = visitors.replacement_traverse( - secondaryjoin, {}, col_to_bind) - lazywhere = sql.and_(lazywhere, secondaryjoin) - - bind_to_col = dict((binds[col].key, col) for col in binds) - - return lazywhere, bind_to_col, equated_columns log.class_logger(LazyLoader) @@ -785,7 +742,8 @@ class SubqueryLoader(AbstractRelationshipLoader): leftmost_mapper, leftmost_prop = \ subq_mapper, \ subq_mapper._props[subq_path[1]] - leftmost_cols, remote_cols = self._local_remote_columns(leftmost_prop) + + leftmost_cols = leftmost_prop.local_columns leftmost_attr = [ leftmost_mapper._columntoproperty[c].class_attribute @@ -846,8 +804,7 @@ class SubqueryLoader(AbstractRelationshipLoader): # self.parent is more specific than subq_path[-2] parent_alias = mapperutil.AliasedClass(self.parent) - local_cols, remote_cols = \ - self._local_remote_columns(self.parent_property) + local_cols = self.parent_property.local_columns local_attr = [ getattr(parent_alias, self.parent._columntoproperty[c].key) @@ -881,17 +838,6 @@ class SubqueryLoader(AbstractRelationshipLoader): q = q.join(attr, aliased=middle, from_joinpoint=True) return q - def _local_remote_columns(self, prop): - if prop.secondary is None: - return zip(*prop.local_remote_pairs) - else: - return \ - [p[0] for p in prop.synchronize_pairs],\ - [ - p[0] for p in prop. - secondary_synchronize_pairs - ] - def _setup_options(self, q, subq_path, orig_query): # propagate loader options etc. to the new query. # these will fire relative to subq_path. @@ -930,7 +876,7 @@ class SubqueryLoader(AbstractRelationshipLoader): if ('subquery', reduced_path) not in context.attributes: return None, None, None - local_cols, remote_cols = self._local_remote_columns(self.parent_property) + local_cols = self.parent_property.local_columns q = context.attributes[('subquery', reduced_path)] diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 197c0c4c1..c14c22bac 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -393,7 +393,21 @@ def _orm_annotate(element, exclude=None): """ return sql_util._deep_annotate(element, {'_orm_adapt':True}, exclude) -_orm_deannotate = sql_util._deep_deannotate +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, + values=("_orm_adapt", "parententity") + ) + +def _orm_full_deannotate(element): + return sql_util._deep_deannotate(element) class _ORMJoin(expression.Join): """Extend Join to support ORM constructs as input.""" diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index f9a3863da..d8ad7c3fa 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -1584,18 +1584,35 @@ class ClauseElement(Visitable): return id(self) def _annotate(self, values): - """return a copy of this ClauseElement with the given annotations - dictionary. + """return a copy of this ClauseElement with annotations + updated by the given dictionary. """ return sqlutil.Annotated(self, values) - def _deannotate(self): - """return a copy of this ClauseElement with an empty annotations - dictionary. + def _with_annotations(self, values): + """return a copy of this ClauseElement with annotations + replaced by the given dictionary. """ - return self._clone() + return sqlutil.Annotated(self, values) + + def _deannotate(self, values=None, clone=False): + """return a copy of this :class:`.ClauseElement` with annotations + removed. + + :param values: optional tuple of individual values + to remove. + + """ + if clone: + # clone is used when we are also copying + # the expression for a deep deannotation + return self._clone() + else: + # if no clone, since we have no annotations we return + # self + return self def unique_params(self, *optionaldict, **kwargs): """Return a copy with :func:`bindparam()` elements replaced. @@ -2195,7 +2212,7 @@ class ColumnElement(ClauseElement, _CompareMixin): for oth in to_compare: if use_proxies and self.shares_lineage(oth): return True - elif oth is self: + elif hash(oth) == hash(self): return True else: return False @@ -3403,6 +3420,10 @@ class _BinaryExpression(ColumnElement): raise TypeError("Boolean value of this clause is not defined") @property + def is_comparison(self): + return operators.is_comparison(self.operator) + + @property def _from_objects(self): return self.left._from_objects + self.right._from_objects diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index 89f0aaee1..b86b50db4 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -521,6 +521,11 @@ def nullslast_op(a): _commutative = set([eq, ne, add, mul]) +_comparison = set([eq, ne, lt, gt, ge, le]) + +def is_comparison(op): + return op in _comparison + def is_commutative(op): return op in _commutative diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 8d2b5ecfd..cb8359048 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -62,6 +62,65 @@ def find_join_source(clauses, join_to): else: return None, None + +def visit_binary_product(fn, expr): + """Produce a traversal of the given expression, delivering + column comparisons to the given function. + + The function is of the form:: + + def my_fn(binary, left, right) + + For each binary expression located which has a + comparison operator, the product of "left" and + "right" will be delivered to that function, + in terms of that binary. + + Hence an expression like:: + + and_( + (a + b) == q + func.sum(e + f), + j == r + ) + + would have the traversal:: + + a <eq> q + a <eq> e + a <eq> f + b <eq> q + b <eq> e + b <eq> f + j <eq> r + + That is, every combination of "left" and + "right" that doesn't further contain + a binary comparison is passed as pairs. + + """ + stack = [] + def visit(element): + if isinstance(element, (expression._ScalarSelect)): + # we dont want to dig into correlated subqueries, + # those are just column elements by themselves + yield element + elif element.__visit_name__ == 'binary' and \ + operators.is_comparison(element.operator): + stack.insert(0, element) + for l in visit(element.left): + for r in visit(element.right): + fn(stack[0], l, r) + stack.pop(0) + for elem in element.get_children(): + visit(elem) + else: + if isinstance(element, expression.ColumnClause): + yield element + for elem in element.get_children(): + for e in visit(elem): + yield e + list(visit(expr)) + def find_tables(clause, check_columns=False, include_aliases=False, include_joins=False, include_selects=False, include_crud=False): @@ -225,7 +284,10 @@ def adapt_criterion_to_null(crit, nulls): return visitors.cloned_traverse(crit, {}, {'binary':visit_binary}) -def join_condition(a, b, ignore_nonexistent_tables=False, a_subset=None): + +def join_condition(a, b, ignore_nonexistent_tables=False, + a_subset=None, + consider_as_foreign_keys=None): """create a join condition between two tables or selectables. e.g.:: @@ -261,6 +323,9 @@ def join_condition(a, b, ignore_nonexistent_tables=False, a_subset=None): for fk in sorted( b.foreign_keys, key=lambda fk:fk.parent._creation_order): + if consider_as_foreign_keys is not None and \ + fk.parent not in consider_as_foreign_keys: + continue try: col = fk.get_referent(left) except exc.NoReferenceError, nrte: @@ -276,6 +341,9 @@ def join_condition(a, b, ignore_nonexistent_tables=False, a_subset=None): for fk in sorted( left.foreign_keys, key=lambda fk:fk.parent._creation_order): + if consider_as_foreign_keys is not None and \ + fk.parent not in consider_as_foreign_keys: + continue try: col = fk.get_referent(b) except exc.NoReferenceError, nrte: @@ -298,11 +366,11 @@ def join_condition(a, b, ignore_nonexistent_tables=False, a_subset=None): "subquery using alias()?" else: hint = "" - raise exc.ArgumentError( + raise exc.NoForeignKeysError( "Can't find any foreign key relationships " "between '%s' and '%s'.%s" % (a.description, b.description, hint)) elif len(constraints) > 1: - raise exc.ArgumentError( + raise exc.AmbiguousForeignKeysError( "Can't determine join between '%s' and '%s'; " "tables have more than one foreign key " "constraint relationship between them. " @@ -356,13 +424,22 @@ class Annotated(object): def _annotate(self, values): _values = self._annotations.copy() _values.update(values) + return self._with_annotations(_values) + + def _with_annotations(self, values): clone = self.__class__.__new__(self.__class__) clone.__dict__ = self.__dict__.copy() - clone._annotations = _values + clone._annotations = values return clone - def _deannotate(self): - return self.__element + def _deannotate(self, values=None, clone=True): + if values is None: + return self.__element + else: + _values = self._annotations.copy() + for v in values: + _values.pop(v, None) + return self._with_annotations(_values) def _compiler_dispatch(self, visitor, **kw): return self.__element.__class__._compiler_dispatch(self, visitor, **kw) @@ -410,14 +487,8 @@ def _deep_annotate(element, annotations, exclude=None): Elements within the exclude collection will be cloned but not annotated. """ - cloned = util.column_dict() - def clone(elem): - # check if element is present in the exclude list. - # take into account proxying relationships. - if elem in cloned: - return cloned[elem] - elif exclude and \ + if exclude and \ hasattr(elem, 'proxy_set') and \ elem.proxy_set.intersection(exclude): newelem = elem._clone() @@ -426,24 +497,32 @@ def _deep_annotate(element, annotations, exclude=None): else: newelem = elem newelem._copy_internals(clone=clone) - cloned[elem] = newelem return newelem if element is not None: element = clone(element) return element -def _deep_deannotate(element): - """Deep copy the given element, removing all annotations.""" +def _deep_deannotate(element, values=None): + """Deep copy the given element, removing annotations.""" cloned = util.column_dict() def clone(elem): - if elem not in cloned: - newelem = elem._deannotate() + # if a values dict is given, + # the elem must be cloned each time it appears, + # as there may be different annotations in source + # elements that are remaining. if totally + # removing all annotations, can assume the same + # slate... + if values or elem not in cloned: + newelem = elem._deannotate(values=values, clone=True) newelem._copy_internals(clone=clone) - cloned[elem] = newelem - return cloned[elem] + if not values: + cloned[elem] = newelem + return newelem + else: + return cloned[elem] if element is not None: element = clone(element) @@ -547,6 +626,10 @@ def criterion_as_pairs(expression, consider_as_foreign_keys=None, "'consider_as_foreign_keys' or " "'consider_as_referenced_keys'") + def col_is(a, b): + #return a is b + return a.compare(b) + def visit_binary(binary): if not any_operator and binary.operator is not operators.eq: return @@ -556,20 +639,20 @@ def criterion_as_pairs(expression, consider_as_foreign_keys=None, if consider_as_foreign_keys: if binary.left in consider_as_foreign_keys and \ - (binary.right is binary.left or + (col_is(binary.right, binary.left) or binary.right not in consider_as_foreign_keys): pairs.append((binary.right, binary.left)) elif binary.right in consider_as_foreign_keys and \ - (binary.left is binary.right or + (col_is(binary.left, binary.right) or binary.left not in consider_as_foreign_keys): pairs.append((binary.left, binary.right)) elif consider_as_referenced_keys: if binary.left in consider_as_referenced_keys and \ - (binary.right is binary.left or + (col_is(binary.right, binary.left) or binary.right not in consider_as_referenced_keys): pairs.append((binary.left, binary.right)) elif binary.right in consider_as_referenced_keys and \ - (binary.left is binary.right or + (col_is(binary.left, binary.right) or binary.left not in consider_as_referenced_keys): pairs.append((binary.right, binary.left)) else: @@ -681,11 +764,22 @@ class ClauseAdapter(visitors.ReplacingCloningVisitor): s.c.col1 == table2.c.col1 """ - def __init__(self, selectable, equivalents=None, include=None, exclude=None, adapt_on_names=False): + def __init__(self, selectable, equivalents=None, + include=None, exclude=None, + include_fn=None, exclude_fn=None, + adapt_on_names=False): self.__traverse_options__ = {'stop_on':[selectable]} self.selectable = selectable - self.include = include - self.exclude = exclude + if include: + assert not include_fn + self.include_fn = lambda e: e in include + else: + self.include_fn = include_fn + if exclude: + assert not exclude_fn + self.exclude_fn = lambda e: e in exclude + else: + self.exclude_fn = exclude_fn self.equivalents = util.column_dict(equivalents or {}) self.adapt_on_names = adapt_on_names @@ -705,19 +799,17 @@ class ClauseAdapter(visitors.ReplacingCloningVisitor): return newcol def replace(self, col): - if isinstance(col, expression.FromClause): - if self.selectable.is_derived_from(col): + if isinstance(col, expression.FromClause) and \ + self.selectable.is_derived_from(col): return self.selectable - - if not isinstance(col, expression.ColumnElement): + elif not isinstance(col, expression.ColumnElement): return None - - if self.include and col not in self.include: + elif self.include_fn and not self.include_fn(col): return None - elif self.exclude and col in self.exclude: + elif self.exclude_fn and self.exclude_fn(col): return None - - return self._corresponding_column(col, True) + else: + return self._corresponding_column(col, True) class ColumnAdapter(ClauseAdapter): """Extends ClauseAdapter with extra utility functions. diff --git a/lib/sqlalchemy/sql/visitors.py b/lib/sqlalchemy/sql/visitors.py index 5354fbcbb..8a06982fc 100644 --- a/lib/sqlalchemy/sql/visitors.py +++ b/lib/sqlalchemy/sql/visitors.py @@ -242,13 +242,13 @@ def cloned_traverse(obj, opts, visitors): if elem in stop_on: return elem else: - if elem not in cloned: - cloned[elem] = newelem = elem._clone() + if id(elem) not in cloned: + cloned[id(elem)] = newelem = elem._clone() newelem._copy_internals(clone=clone) meth = visitors.get(newelem.__visit_name__, None) if meth: meth(newelem) - return cloned[elem] + return cloned[id(elem)] if obj is not None: obj = clone(obj) @@ -260,16 +260,16 @@ def replacement_traverse(obj, opts, replace): replacement by a given replacement function.""" cloned = util.column_dict() - stop_on = util.column_set(opts.get('stop_on', [])) + stop_on = util.column_set([id(x) for x in opts.get('stop_on', [])]) def clone(elem, **kw): - if elem in stop_on or \ + if id(elem) in stop_on or \ 'no_replacement_traverse' in elem._annotations: return elem else: newelem = replace(elem) if newelem is not None: - stop_on.add(newelem) + stop_on.add(id(newelem)) return newelem else: if elem not in cloned: diff --git a/test/aaa_profiling/test_orm.py b/test/aaa_profiling/test_orm.py index 8cc2e0087..8dea18359 100644 --- a/test/aaa_profiling/test_orm.py +++ b/test/aaa_profiling/test_orm.py @@ -95,6 +95,7 @@ class MergeTest(fixtures.MappedTest): '3':1050} ) def go(): + print "GO" p2 = sess2.merge(p1) go() diff --git a/test/ext/test_declarative.py b/test/ext/test_declarative.py index 69042b5c8..5e185f664 100644 --- a/test/ext/test_declarative.py +++ b/test/ext/test_declarative.py @@ -368,6 +368,28 @@ class DeclarativeTest(DeclarativeTestBase): assert class_mapper(User).get_property('props').secondary \ is user_to_prop + def test_string_dependency_resolution_annotations(self): + Base = decl.declarative_base() + + class Parent(Base): + __tablename__ = 'parent' + id = Column(Integer, primary_key=True) + name = Column(String) + children = relationship("Child", + primaryjoin="Parent.name==remote(foreign(func.lower(Child.name_upper)))" + ) + + class Child(Base): + __tablename__ = 'child' + id = Column(Integer, primary_key=True) + name_upper = Column(String) + + configure_mappers() + eq_( + Parent.children.property._calculated_foreign_keys, + set([Child.name_upper.property.columns[0]]) + ) + def test_shared_class_registry(self): reg = {} Base1 = decl.declarative_base(testing.db, class_registry=reg) diff --git a/test/ext/test_serializer.py b/test/ext/test_serializer.py index 5134d71ee..87b7a2f67 100644 --- a/test/ext/test_serializer.py +++ b/test/ext/test_serializer.py @@ -112,13 +112,18 @@ class SerializeTest(fixtures.MappedTest): eq_(q2.all(), [Address(email='ed@wood.com'), Address(email='ed@lala.com'), Address(email='ed@bettyboop.com')]) - q = \ - Session.query(User).join(User.addresses).\ - filter(Address.email.like('%fred%')) - q2 = serializer.loads(serializer.dumps(q, -1), users.metadata, - Session) - eq_(q2.all(), [User(name='fred')]) - eq_(list(q2.values(User.id, User.name)), [(9, u'fred')]) + + # unfortunately pickle just doesn't have the horsepower + # to pickle annotated joins, both cpickle and pickle + # get confused likely since identity-unequal/hash equal + # objects with cycles being used + #q = \ + # Session.query(User).join(User.addresses).\ + # filter(Address.email.like('%fred%')) + #q2 = serializer.loads(serializer.dumps(q, -1), users.metadata, + # Session) + #eq_(q2.all(), [User(name='fred')]) + #eq_(list(q2.values(User.id, User.name)), [(9, u'fred')]) @testing.exclude('sqlite', '<=', (3, 5, 9), 'id comparison failing on the buildbot') diff --git a/test/orm/inheritance/test_abc_inheritance.py b/test/orm/inheritance/test_abc_inheritance.py index 6a2f579ae..e1304e26e 100644 --- a/test/orm/inheritance/test_abc_inheritance.py +++ b/test/orm/inheritance/test_abc_inheritance.py @@ -111,7 +111,11 @@ def produce_test(parent, child, direction): parent_class = parent_mapper.class_ child_class = child_mapper.class_ - parent_mapper.add_property("collection", relationship(child_mapper, primaryjoin=relationshipjoin, foreign_keys=foreign_keys, remote_side=remote_side, uselist=True)) + parent_mapper.add_property("collection", + relationship(child_mapper, + primaryjoin=relationshipjoin, + foreign_keys=foreign_keys, + remote_side=remote_side, uselist=True)) sess = create_session() diff --git a/test/orm/test_joins.py b/test/orm/test_joins.py index db7c78cdd..6c43a2f39 100644 --- a/test/orm/test_joins.py +++ b/test/orm/test_joins.py @@ -1700,21 +1700,29 @@ class SelfReferentialTest(fixtures.MappedTest, AssertsCompiledSQL): sess.flush() sess.close() - def test_join(self): + def test_join_1(self): Node = self.classes.Node - sess = create_session() node = sess.query(Node).join('children', aliased=True).filter_by(data='n122').first() assert node.data=='n12' + def test_join_2(self): + Node = self.classes.Node + sess = create_session() ret = sess.query(Node.data).join(Node.children, aliased=True).filter_by(data='n122').all() assert ret == [('n12',)] + def test_join_3(self): + Node = self.classes.Node + sess = create_session() node = sess.query(Node).join('children', 'children', aliased=True).filter_by(data='n122').first() assert node.data=='n1' + def test_join_4(self): + Node = self.classes.Node + sess = create_session() node = sess.query(Node).filter_by(data='n122').join('parent', aliased=True).filter_by(data='n12').\ join('parent', aliased=True, from_joinpoint=True).filter_by(data='n1').first() assert node.data == 'n122' diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py index 79ae7ff59..4478e5d80 100644 --- a/test/orm/test_mapper.py +++ b/test/orm/test_mapper.py @@ -522,6 +522,8 @@ class MapperTest(_fixtures.FixtureTest, AssertsCompiledSQL): assert User.x.property.columns[0] is not expr assert User.x.property.columns[0].element.left is users.c.name + # a deannotate needs to clone the base, in case + # the original one referenced annotated elements. assert User.x.property.columns[0].element.right is not expr.right assert User.y.property.columns[0] is not expr2 diff --git a/test/orm/test_query.py b/test/orm/test_query.py index bcc976816..1b57299f0 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -623,6 +623,14 @@ class OperatorTest(QueryTest, AssertsCompiledSQL): self._test(Address.user != None, "addresses.user_id IS NOT NULL") + def test_foo(self): + Node = self.classes.Node + nalias = aliased(Node) + self._test( + nalias.parent.has(Node.data=='some data'), + "EXISTS (SELECT 1 FROM nodes WHERE nodes.id = nodes_1.parent_id AND nodes.data = :data_1)" + ) + def test_selfref_relationship(self): Node = self.classes.Node diff --git a/test/orm/test_rel_fn.py b/test/orm/test_rel_fn.py new file mode 100644 index 000000000..e35dd925a --- /dev/null +++ b/test/orm/test_rel_fn.py @@ -0,0 +1,927 @@ +from test.lib.testing import assert_raises, assert_raises_message, eq_, \ + AssertsCompiledSQL, is_ +from test.lib import fixtures +from sqlalchemy.orm import relationships, foreign, remote, remote_foreign +from sqlalchemy import MetaData, Table, Column, ForeignKey, Integer, \ + select, ForeignKeyConstraint, exc, func, and_ +from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE, MANYTOMANY + + +class _JoinFixtures(object): + @classmethod + def setup_class(cls): + m = MetaData() + cls.left = Table('lft', m, + Column('id', Integer, primary_key=True), + Column('x', Integer), + Column('y', Integer), + ) + cls.right = Table('rgt', m, + Column('id', Integer, primary_key=True), + Column('lid', Integer, ForeignKey('lft.id')), + Column('x', Integer), + Column('y', Integer), + ) + cls.right_multi_fk = Table('rgt_multi_fk', m, + Column('id', Integer, primary_key=True), + Column('lid1', Integer, ForeignKey('lft.id')), + Column('lid2', Integer, ForeignKey('lft.id')), + ) + + cls.selfref = Table('selfref', m, + Column('id', Integer, primary_key=True), + Column('sid', Integer, ForeignKey('selfref.id')) + ) + cls.composite_selfref = Table('composite_selfref', m, + Column('id', Integer, primary_key=True), + Column('group_id', Integer, primary_key=True), + Column('parent_id', Integer), + ForeignKeyConstraint( + ['parent_id', 'group_id'], + ['composite_selfref.id', 'composite_selfref.group_id'] + ) + ) + cls.m2mleft = Table('m2mlft', m, + Column('id', Integer, primary_key=True), + ) + cls.m2mright = Table('m2mrgt', m, + Column('id', Integer, primary_key=True), + ) + cls.m2msecondary = Table('m2msecondary', m, + Column('lid', Integer, ForeignKey('m2mlft.id'), primary_key=True), + Column('rid', Integer, ForeignKey('m2mrgt.id'), primary_key=True), + ) + cls.m2msecondary_no_fks = Table('m2msecondary_no_fks', m, + Column('lid', Integer, primary_key=True), + Column('rid', Integer, primary_key=True), + ) + cls.m2msecondary_ambig_fks = Table('m2msecondary_ambig_fks', m, + Column('lid1', Integer, ForeignKey('m2mlft.id'), primary_key=True), + Column('rid1', Integer, ForeignKey('m2mrgt.id'), primary_key=True), + Column('lid2', Integer, ForeignKey('m2mlft.id'), primary_key=True), + Column('rid2', Integer, ForeignKey('m2mrgt.id'), primary_key=True), + ) + cls.base_w_sub_rel = Table('base_w_sub_rel', m, + Column('id', Integer, primary_key=True), + Column('sub_id', Integer, ForeignKey('rel_sub.id')) + ) + cls.rel_sub = Table('rel_sub', m, + Column('id', Integer, ForeignKey('base_w_sub_rel.id'), + primary_key=True) + ) + cls.base = Table('base', m, + Column('id', Integer, primary_key=True), + ) + cls.sub = Table('sub', m, + Column('id', Integer, ForeignKey('base.id'), + primary_key=True), + ) + cls.sub_w_base_rel = Table('sub_w_base_rel', m, + Column('id', Integer, ForeignKey('base.id'), + primary_key=True), + Column('base_id', Integer, ForeignKey('base.id')) + ) + cls.right_w_base_rel = Table('right_w_base_rel', m, + Column('id', Integer, primary_key=True), + Column('base_id', Integer, ForeignKey('base.id')) + ) + + cls.three_tab_a = Table('three_tab_a', m, + Column('id', Integer, primary_key=True), + ) + cls.three_tab_b = Table('three_tab_b', m, + Column('id', Integer, primary_key=True), + Column('aid', Integer, ForeignKey('three_tab_a.id')) + ) + cls.three_tab_c = Table('three_tab_c', m, + Column('id', Integer, primary_key=True), + Column('aid', Integer, ForeignKey('three_tab_a.id')), + Column('bid', Integer, ForeignKey('three_tab_b.id')) + ) + + def _join_fixture_overlapping_three_tables(self, **kw): + def _can_sync(*cols): + for c in cols: + if self.three_tab_c.c.contains_column(c): + return False + else: + return True + return relationships.JoinCondition( + self.three_tab_a, + self.three_tab_b, + self.three_tab_a, + self.three_tab_b, + support_sync=False, + can_be_synced_fn=_can_sync, + primaryjoin=and_( + self.three_tab_a.c.id==self.three_tab_b.c.aid, + self.three_tab_c.c.bid==self.three_tab_b.c.id, + self.three_tab_c.c.aid==self.three_tab_a.c.id + ) + ) + + def _join_fixture_m2m(self, **kw): + return relationships.JoinCondition( + self.m2mleft, + self.m2mright, + self.m2mleft, + self.m2mright, + secondary=self.m2msecondary, + **kw + ) + + def _join_fixture_o2m(self, **kw): + return relationships.JoinCondition( + self.left, + self.right, + self.left, + self.right, + **kw + ) + + def _join_fixture_m2o(self, **kw): + return relationships.JoinCondition( + self.right, + self.left, + self.right, + self.left, + **kw + ) + + def _join_fixture_o2m_selfref(self, **kw): + return relationships.JoinCondition( + self.selfref, + self.selfref, + self.selfref, + self.selfref, + **kw + ) + + def _join_fixture_m2o_selfref(self, **kw): + return relationships.JoinCondition( + self.selfref, + self.selfref, + self.selfref, + self.selfref, + remote_side=set([self.selfref.c.id]), + **kw + ) + + def _join_fixture_o2m_composite_selfref(self, **kw): + return relationships.JoinCondition( + self.composite_selfref, + self.composite_selfref, + self.composite_selfref, + self.composite_selfref, + **kw + ) + + def _join_fixture_m2o_composite_selfref(self, **kw): + return relationships.JoinCondition( + self.composite_selfref, + self.composite_selfref, + self.composite_selfref, + self.composite_selfref, + remote_side=set([self.composite_selfref.c.id, + self.composite_selfref.c.group_id]), + **kw + ) + + def _join_fixture_o2m_composite_selfref_func(self, **kw): + return relationships.JoinCondition( + self.composite_selfref, + self.composite_selfref, + self.composite_selfref, + self.composite_selfref, + primaryjoin=and_( + self.composite_selfref.c.group_id== + func.foo(self.composite_selfref.c.group_id), + self.composite_selfref.c.parent_id== + self.composite_selfref.c.id + ), + **kw + ) + + def _join_fixture_o2m_composite_selfref_func_annotated(self, **kw): + return relationships.JoinCondition( + self.composite_selfref, + self.composite_selfref, + self.composite_selfref, + self.composite_selfref, + primaryjoin=and_( + remote(self.composite_selfref.c.group_id)== + func.foo(self.composite_selfref.c.group_id), + remote(self.composite_selfref.c.parent_id)== + self.composite_selfref.c.id + ), + **kw + ) + + def _join_fixture_compound_expression_1(self, **kw): + return relationships.JoinCondition( + self.left, + self.right, + self.left, + self.right, + primaryjoin=(self.left.c.x + self.left.c.y) == \ + relationships.remote_foreign( + self.right.c.x * self.right.c.y + ), + **kw + ) + + def _join_fixture_compound_expression_2(self, **kw): + return relationships.JoinCondition( + self.left, + self.right, + self.left, + self.right, + primaryjoin=(self.left.c.x + self.left.c.y) == \ + relationships.foreign( + self.right.c.x * self.right.c.y + ), + **kw + ) + + def _join_fixture_compound_expression_1_non_annotated(self, **kw): + return relationships.JoinCondition( + self.left, + self.right, + self.left, + self.right, + primaryjoin=(self.left.c.x + self.left.c.y) == \ + ( + self.right.c.x * self.right.c.y + ), + **kw + ) + + def _join_fixture_base_to_joined_sub(self, **kw): + # see test/orm/inheritance/test_abc_inheritance:TestaTobM2O + # and others there + right = self.base_w_sub_rel.join(self.rel_sub, + self.base_w_sub_rel.c.id==self.rel_sub.c.id + ) + return relationships.JoinCondition( + self.base_w_sub_rel, + right, + self.base_w_sub_rel, + self.rel_sub, + primaryjoin=self.base_w_sub_rel.c.sub_id==\ + self.rel_sub.c.id, + **kw + ) + + def _join_fixture_o2m_joined_sub_to_base(self, **kw): + left = self.base.join(self.sub_w_base_rel, + self.base.c.id==self.sub_w_base_rel.c.id) + return relationships.JoinCondition( + left, + self.base, + self.sub_w_base_rel, + self.base, + primaryjoin=self.sub_w_base_rel.c.base_id==self.base.c.id + ) + + def _join_fixture_m2o_sub_to_joined_sub(self, **kw): + # see test.orm.test_mapper:MapperTest.test_add_column_prop_deannotate, + right = self.base.join(self.right_w_base_rel, + self.base.c.id==self.right_w_base_rel.c.id) + return relationships.JoinCondition( + self.right_w_base_rel, + right, + self.right_w_base_rel, + self.right_w_base_rel, + ) + + def _join_fixture_m2o_sub_to_joined_sub_func(self, **kw): + # see test.orm.test_mapper:MapperTest.test_add_column_prop_deannotate, + right = self.base.join(self.right_w_base_rel, + self.base.c.id==self.right_w_base_rel.c.id) + return relationships.JoinCondition( + self.right_w_base_rel, + right, + self.right_w_base_rel, + self.right_w_base_rel, + primaryjoin=self.right_w_base_rel.c.base_id==\ + func.foo(self.base.c.id) + ) + + def _join_fixture_o2o_joined_sub_to_base(self, **kw): + left = self.base.join(self.sub, + self.base.c.id==self.sub.c.id) + + # see test_relationships->AmbiguousJoinInterpretedAsSelfRef + return relationships.JoinCondition( + left, + self.sub, + left, + self.sub, + ) + + def _join_fixture_o2m_to_annotated_func(self, **kw): + return relationships.JoinCondition( + self.left, + self.right, + self.left, + self.right, + primaryjoin=self.left.c.id== + foreign(func.foo(self.right.c.lid)), + **kw + ) + + def _join_fixture_o2m_to_oldstyle_func(self, **kw): + return relationships.JoinCondition( + self.left, + self.right, + self.left, + self.right, + primaryjoin=self.left.c.id== + func.foo(self.right.c.lid), + consider_as_foreign_keys=[self.right.c.lid], + **kw + ) + + def _assert_non_simple_warning(self, fn): + assert_raises_message( + exc.SAWarning, + "Non-simple column elements in " + "primary join condition for property " + r"None - consider using remote\(\) " + "annotations to mark the remote side.", + fn + ) + + def _assert_raises_no_relevant_fks(self, fn, expr, relname, + primary, *arg, **kw): + assert_raises_message( + exc.ArgumentError, + r"Could not locate any relevant foreign key columns " + r"for %s join condition '%s' on relationship %s. " + r"Ensure that referencing columns are associated with " + r"a ForeignKey or ForeignKeyConstraint, or are annotated " + r"in the join condition with the foreign\(\) annotation." + % ( + primary, expr, relname + ), + fn, *arg, **kw + ) + + def _assert_raises_no_equality(self, fn, expr, relname, + primary, *arg, **kw): + assert_raises_message( + sa.exc.ArgumentError, + "Could not locate any simple equality expressions " + "involving locally mapped foreign key columns for %s join " + "condition '%s' on relationship %s. " + "Ensure that referencing columns are associated with a " + "ForeignKey or ForeignKeyConstraint, or are annotated in " + r"the join condition with the foreign\(\) annotation. " + "To allow comparison operators other than '==', " + "the relationship can be marked as viewonly=True." % ( + primary, expr, relname + ), + fn, *arg, **kw + ) + + def _assert_raises_ambig_join(self, fn, relname, secondary_arg, + *arg, **kw): + if secondary_arg is not None: + assert_raises_message( + exc.AmbiguousForeignKeysError, + "Could not determine join condition between " + "parent/child tables on relationship %s - " + "there are multiple foreign key paths linking the " + "tables via secondary table '%s'. " + "Specify the 'foreign_keys' argument, providing a list " + "of those columns which should be counted as " + "containing a foreign key reference from the " + "secondary table to each of the parent and child tables." + % (relname, secondary_arg), + fn, *arg, **kw) + else: + assert_raises_message( + exc.AmbiguousForeignKeysError, + "Could not determine join condition between " + "parent/child tables on relationship %s - " + "there are no foreign keys linking these tables. " + % (relname,), + fn, *arg, **kw) + + def _assert_raises_no_join(self, fn, relname, secondary_arg, + *arg, **kw): + if secondary_arg is not None: + assert_raises_message( + exc.NoForeignKeysError, + "Could not determine join condition between " + "parent/child tables on relationship %s - " + "there are no foreign keys linking these tables " + "via secondary table '%s'. " + "Ensure that referencing columns are associated with a ForeignKey " + "or ForeignKeyConstraint, or specify 'primaryjoin' and " + "'secondaryjoin' expressions" + % (relname, secondary_arg), + fn, *arg, **kw) + else: + assert_raises_message( + exc.NoForeignKeysError, + "Could not determine join condition between " + "parent/child tables on relationship %s - " + "there are no foreign keys linking these tables. " + "Ensure that referencing columns are associated with a ForeignKey " + "or ForeignKeyConstraint, or specify a 'primaryjoin' " + "expression." + % (relname,), + fn, *arg, **kw) + + +class ColumnCollectionsTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL): + def test_determine_local_remote_pairs_o2o_joined_sub_to_base(self): + joincond = self._join_fixture_o2o_joined_sub_to_base() + eq_( + joincond.local_remote_pairs, + [(self.base.c.id, self.sub.c.id)] + ) + + def test_determine_synchronize_pairs_o2m_to_annotated_func(self): + joincond = self._join_fixture_o2m_to_annotated_func() + eq_( + joincond.synchronize_pairs, + [(self.left.c.id, self.right.c.lid)] + ) + + def test_determine_synchronize_pairs_o2m_to_oldstyle_func(self): + joincond = self._join_fixture_o2m_to_oldstyle_func() + eq_( + joincond.synchronize_pairs, + [(self.left.c.id, self.right.c.lid)] + ) + + def test_determine_local_remote_base_to_joined_sub(self): + joincond = self._join_fixture_base_to_joined_sub() + eq_( + joincond.local_remote_pairs, + [ + (self.base_w_sub_rel.c.sub_id, self.rel_sub.c.id) + ] + ) + + def test_determine_local_remote_o2m_joined_sub_to_base(self): + joincond = self._join_fixture_o2m_joined_sub_to_base() + eq_( + joincond.local_remote_pairs, + [ + (self.sub_w_base_rel.c.base_id, self.base.c.id) + ] + ) + + def test_determine_local_remote_m2o_sub_to_joined_sub(self): + joincond = self._join_fixture_m2o_sub_to_joined_sub() + eq_( + joincond.local_remote_pairs, + [ + (self.right_w_base_rel.c.base_id, self.base.c.id) + ] + ) + + def test_determine_remote_columns_compound_1(self): + joincond = self._join_fixture_compound_expression_1( + support_sync=False) + eq_( + joincond.remote_columns, + set([self.right.c.x, self.right.c.y]) + ) + + def test_determine_local_remote_compound_1(self): + joincond = self._join_fixture_compound_expression_1( + support_sync=False) + eq_( + joincond.local_remote_pairs, + [ + (self.left.c.x, self.right.c.x), + (self.left.c.x, self.right.c.y), + (self.left.c.y, self.right.c.x), + (self.left.c.y, self.right.c.y) + ] + ) + + def test_determine_local_remote_compound_2(self): + joincond = self._join_fixture_compound_expression_2( + support_sync=False) + eq_( + joincond.local_remote_pairs, + [ + (self.left.c.x, self.right.c.x), + (self.left.c.x, self.right.c.y), + (self.left.c.y, self.right.c.x), + (self.left.c.y, self.right.c.y) + ] + ) + + def test_determine_local_remote_compound_1(self): + joincond = self._join_fixture_compound_expression_1() + eq_( + joincond.local_remote_pairs, + [ + (self.left.c.x, self.right.c.x), + (self.left.c.x, self.right.c.y), + (self.left.c.y, self.right.c.x), + (self.left.c.y, self.right.c.y), + ] + ) + + def test_err_local_remote_compound_1(self): + self._assert_raises_no_relevant_fks( + self._join_fixture_compound_expression_1_non_annotated, + r'lft.x \+ lft.y = rgt.x \* rgt.y', + "None", "primary" + ) + + def test_determine_remote_columns_compound_2(self): + joincond = self._join_fixture_compound_expression_2( + support_sync=False) + eq_( + joincond.remote_columns, + set([self.right.c.x, self.right.c.y]) + ) + + + def test_determine_remote_columns_o2m(self): + joincond = self._join_fixture_o2m() + eq_( + joincond.remote_columns, + set([self.right.c.lid]) + ) + + def test_determine_remote_columns_o2m_selfref(self): + joincond = self._join_fixture_o2m_selfref() + eq_( + joincond.remote_columns, + set([self.selfref.c.sid]) + ) + + def test_determine_local_remote_pairs_o2m_composite_selfref(self): + joincond = self._join_fixture_o2m_composite_selfref() + eq_( + joincond.local_remote_pairs, + [ + (self.composite_selfref.c.group_id, self.composite_selfref.c.group_id), + (self.composite_selfref.c.id, self.composite_selfref.c.parent_id), + ] + ) + + def test_determine_local_remote_pairs_o2m_composite_selfref_func_warning(self): + self._assert_non_simple_warning( + self._join_fixture_o2m_composite_selfref_func + ) + + def test_determine_local_remote_pairs_o2m_overlap_func_warning(self): + self._assert_non_simple_warning( + self._join_fixture_m2o_sub_to_joined_sub_func + ) + + def test_determine_local_remote_pairs_o2m_composite_selfref_func_annotated(self): + joincond = self._join_fixture_o2m_composite_selfref_func_annotated() + eq_( + joincond.local_remote_pairs, + [ + (self.composite_selfref.c.group_id, self.composite_selfref.c.group_id), + (self.composite_selfref.c.id, self.composite_selfref.c.parent_id), + ] + ) + + def test_determine_remote_columns_m2o_composite_selfref(self): + joincond = self._join_fixture_m2o_composite_selfref() + eq_( + joincond.remote_columns, + set([self.composite_selfref.c.id, + self.composite_selfref.c.group_id]) + ) + + def test_determine_remote_columns_m2o(self): + joincond = self._join_fixture_m2o() + eq_( + joincond.remote_columns, + set([self.left.c.id]) + ) + + def test_determine_local_remote_pairs_o2m(self): + joincond = self._join_fixture_o2m() + eq_( + joincond.local_remote_pairs, + [(self.left.c.id, self.right.c.lid)] + ) + + def test_determine_synchronize_pairs_m2m(self): + joincond = self._join_fixture_m2m() + eq_( + joincond.synchronize_pairs, + [(self.m2mleft.c.id, self.m2msecondary.c.lid)] + ) + eq_( + joincond.secondary_synchronize_pairs, + [(self.m2mright.c.id, self.m2msecondary.c.rid)] + ) + + def test_determine_local_remote_pairs_o2m_backref(self): + joincond = self._join_fixture_o2m() + joincond2 = self._join_fixture_m2o( + primaryjoin=joincond.primaryjoin_reverse_remote, + ) + eq_( + joincond2.local_remote_pairs, + [(self.right.c.lid, self.left.c.id)] + ) + + def test_determine_local_remote_pairs_m2m(self): + joincond = self._join_fixture_m2m() + eq_( + joincond.local_remote_pairs, + [(self.m2mleft.c.id, self.m2msecondary.c.lid), + (self.m2mright.c.id, self.m2msecondary.c.rid)] + ) + + def test_determine_local_remote_pairs_m2m_backref(self): + joincond = self._join_fixture_m2m() + joincond2 = self._join_fixture_m2m( + primaryjoin=joincond.secondaryjoin, + secondaryjoin=joincond.primaryjoin + ) + eq_( + joincond.local_remote_pairs, + [(self.m2mleft.c.id, self.m2msecondary.c.lid), + (self.m2mright.c.id, self.m2msecondary.c.rid)] + ) + + def test_determine_remote_columns_m2o_selfref(self): + joincond = self._join_fixture_m2o_selfref() + eq_( + joincond.remote_columns, + set([self.selfref.c.id]) + ) + + def test_determine_local_remote_cols_three_tab_viewonly(self): + joincond = self._join_fixture_overlapping_three_tables() + eq_( + joincond.local_remote_pairs, + [(self.three_tab_a.c.id, self.three_tab_b.c.aid)] + ) + eq_( + joincond.remote_columns, + set([self.three_tab_b.c.id, self.three_tab_b.c.aid]) + ) + +class DirectionTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL): + def test_determine_direction_compound_2(self): + joincond = self._join_fixture_compound_expression_2( + support_sync=False) + is_( + joincond.direction, + ONETOMANY + ) + + def test_determine_direction_o2m(self): + joincond = self._join_fixture_o2m() + is_(joincond.direction, ONETOMANY) + + def test_determine_direction_o2m_selfref(self): + joincond = self._join_fixture_o2m_selfref() + is_(joincond.direction, ONETOMANY) + + def test_determine_direction_m2o_selfref(self): + joincond = self._join_fixture_m2o_selfref() + is_(joincond.direction, MANYTOONE) + + def test_determine_direction_o2m_composite_selfref(self): + joincond = self._join_fixture_o2m_composite_selfref() + is_(joincond.direction, ONETOMANY) + + def test_determine_direction_m2o_composite_selfref(self): + joincond = self._join_fixture_m2o_composite_selfref() + is_(joincond.direction, MANYTOONE) + + def test_determine_direction_m2o(self): + joincond = self._join_fixture_m2o() + is_(joincond.direction, MANYTOONE) + + +class DetermineJoinTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL): + __dialect__ = 'default' + + def test_determine_join_o2m(self): + joincond = self._join_fixture_o2m() + self.assert_compile( + joincond.primaryjoin, + "lft.id = rgt.lid" + ) + + def test_determine_join_o2m_selfref(self): + joincond = self._join_fixture_o2m_selfref() + self.assert_compile( + joincond.primaryjoin, + "selfref.id = selfref.sid" + ) + + def test_determine_join_m2o_selfref(self): + joincond = self._join_fixture_m2o_selfref() + self.assert_compile( + joincond.primaryjoin, + "selfref.id = selfref.sid" + ) + + def test_determine_join_o2m_composite_selfref(self): + joincond = self._join_fixture_o2m_composite_selfref() + self.assert_compile( + joincond.primaryjoin, + "composite_selfref.group_id = composite_selfref.group_id " + "AND composite_selfref.id = composite_selfref.parent_id" + ) + + def test_determine_join_m2o_composite_selfref(self): + joincond = self._join_fixture_m2o_composite_selfref() + self.assert_compile( + joincond.primaryjoin, + "composite_selfref.group_id = composite_selfref.group_id " + "AND composite_selfref.id = composite_selfref.parent_id" + ) + + def test_determine_join_m2o(self): + joincond = self._join_fixture_m2o() + self.assert_compile( + joincond.primaryjoin, + "lft.id = rgt.lid" + ) + + def test_determine_join_ambiguous_fks_o2m(self): + assert_raises_message( + exc.AmbiguousForeignKeysError, + "Could not determine join condition between " + "parent/child tables on relationship None - " + "there are multiple foreign key paths linking " + "the tables. Specify the 'foreign_keys' argument, " + "providing a list of those columns which " + "should be counted as containing a foreign " + "key reference to the parent table.", + relationships.JoinCondition, + self.left, + self.right_multi_fk, + self.left, + self.right_multi_fk, + ) + + def test_determine_join_no_fks_o2m(self): + self._assert_raises_no_join( + relationships.JoinCondition, + "None", None, + self.left, + self.selfref, + self.left, + self.selfref, + ) + + + def test_determine_join_ambiguous_fks_m2m(self): + + self._assert_raises_ambig_join( + relationships.JoinCondition, + "None", self.m2msecondary_ambig_fks, + self.m2mleft, + self.m2mright, + self.m2mleft, + self.m2mright, + secondary=self.m2msecondary_ambig_fks + ) + + def test_determine_join_no_fks_m2m(self): + self._assert_raises_no_join( + relationships.JoinCondition, + "None", self.m2msecondary_no_fks, + self.m2mleft, + self.m2mright, + self.m2mleft, + self.m2mright, + secondary=self.m2msecondary_no_fks + ) + + def _join_fixture_fks_ambig_m2m(self): + return relationships.JoinCondition( + self.m2mleft, + self.m2mright, + self.m2mleft, + self.m2mright, + secondary=self.m2msecondary_ambig_fks, + consider_as_foreign_keys=[ + self.m2msecondary_ambig_fks.c.lid1, + self.m2msecondary_ambig_fks.c.rid1] + ) + + def test_determine_join_w_fks_ambig_m2m(self): + joincond = self._join_fixture_fks_ambig_m2m() + self.assert_compile( + joincond.primaryjoin, + "m2mlft.id = m2msecondary_ambig_fks.lid1" + ) + self.assert_compile( + joincond.secondaryjoin, + "m2mrgt.id = m2msecondary_ambig_fks.rid1" + ) + +class AdaptedJoinTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL): + __dialect__ = 'default' + + def test_join_targets_o2m_selfref(self): + joincond = self._join_fixture_o2m_selfref() + left = select([joincond.parent_selectable]).alias('pj') + pj, sj, sec, adapter, ds = joincond.join_targets( + left, + joincond.child_selectable, + True) + self.assert_compile( + pj, "pj.id = selfref.sid" + ) + + right = select([joincond.child_selectable]).alias('pj') + pj, sj, sec, adapter, ds = joincond.join_targets( + joincond.parent_selectable, + right, + True) + self.assert_compile( + pj, "selfref.id = pj.sid" + ) + + + def test_join_targets_o2m_plain(self): + joincond = self._join_fixture_o2m() + pj, sj, sec, adapter, ds = joincond.join_targets( + joincond.parent_selectable, + joincond.child_selectable, + False) + self.assert_compile( + pj, "lft.id = rgt.lid" + ) + + def test_join_targets_o2m_left_aliased(self): + joincond = self._join_fixture_o2m() + left = select([joincond.parent_selectable]).alias('pj') + pj, sj, sec, adapter, ds = joincond.join_targets( + left, + joincond.child_selectable, + True) + self.assert_compile( + pj, "pj.id = rgt.lid" + ) + + def test_join_targets_o2m_right_aliased(self): + joincond = self._join_fixture_o2m() + right = select([joincond.child_selectable]).alias('pj') + pj, sj, sec, adapter, ds = joincond.join_targets( + joincond.parent_selectable, + right, + True) + self.assert_compile( + pj, "lft.id = pj.lid" + ) + + def test_join_targets_o2m_composite_selfref(self): + joincond = self._join_fixture_o2m_composite_selfref() + right = select([joincond.child_selectable]).alias('pj') + pj, sj, sec, adapter, ds = joincond.join_targets( + joincond.parent_selectable, + right, + True) + self.assert_compile( + pj, + "pj.group_id = composite_selfref.group_id " + "AND composite_selfref.id = pj.parent_id" + ) + + def test_join_targets_m2o_composite_selfref(self): + joincond = self._join_fixture_m2o_composite_selfref() + right = select([joincond.child_selectable]).alias('pj') + pj, sj, sec, adapter, ds = joincond.join_targets( + joincond.parent_selectable, + right, + True) + self.assert_compile( + pj, + "pj.group_id = composite_selfref.group_id " + "AND pj.id = composite_selfref.parent_id" + ) + +class LazyClauseTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL): + + def _test_lazy_clause_o2m(self): + joincond = self._join_fixture_o2m() + self.assert_compile( + relationships.create_lazy_clause(joincond), + "" + ) + + def _test_lazy_clause_o2m_reverse(self): + joincond = self._join_fixture_o2m() + self.assert_compile( + relationships.create_lazy_clause(joincond, + reverse_direction=True), + "" + ) + diff --git a/test/orm/test_relationships.py b/test/orm/test_relationships.py index d718c9d2d..6e610b0cf 100644 --- a/test/orm/test_relationships.py +++ b/test/orm/test_relationships.py @@ -7,11 +7,127 @@ from test.lib.schema import Table, Column from sqlalchemy.orm import mapper, relationship, relation, \ backref, create_session, configure_mappers, \ clear_mappers, sessionmaker, attributes,\ - Session, composite, column_property -from test.lib.testing import eq_, startswith_ + Session, composite, column_property, foreign,\ + remote +from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE, MANYTOMANY +from test.lib.testing import eq_, startswith_, AssertsCompiledSQL, is_ from test.lib import fixtures from test.orm import _fixtures +from sqlalchemy import exc +class _RelationshipErrors(object): + def _assert_raises_no_relevant_fks(self, fn, expr, relname, + primary, *arg, **kw): + assert_raises_message( + sa.exc.ArgumentError, + "Could not locate any relevant foreign key columns " + "for %s join condition '%s' on relationship %s. " + "Ensure that referencing columns are associated with " + "a ForeignKey or ForeignKeyConstraint, or are annotated " + r"in the join condition with the foreign\(\) annotation." + % ( + primary, expr, relname + ), + fn, *arg, **kw + ) + + def _assert_raises_no_equality(self, fn, expr, relname, + primary, *arg, **kw): + assert_raises_message( + sa.exc.ArgumentError, + "Could not locate any simple equality expressions " + "involving locally mapped foreign key columns for %s join " + "condition '%s' on relationship %s. " + "Ensure that referencing columns are associated with a " + "ForeignKey or ForeignKeyConstraint, or are annotated in " + r"the join condition with the foreign\(\) annotation. " + "To allow comparison operators other than '==', " + "the relationship can be marked as viewonly=True." % ( + primary, expr, relname + ), + fn, *arg, **kw + ) + + def _assert_raises_ambig_join(self, fn, relname, secondary_arg, + *arg, **kw): + if secondary_arg is not None: + assert_raises_message( + exc.ArgumentError, + "Could not determine join condition between " + "parent/child tables on relationship %s - " + "there are multiple foreign key paths linking the " + "tables via secondary table '%s'. " + "Specify the 'foreign_keys' argument, providing a list " + "of those columns which should be counted as " + "containing a foreign key reference from the " + "secondary table to each of the parent and child tables." + % (relname, secondary_arg), + fn, *arg, **kw) + else: + assert_raises_message( + exc.ArgumentError, + "Could not determine join " + "condition between parent/child tables on " + "relationship %s - there are multiple foreign key " + "paths linking the tables. Specify the " + "'foreign_keys' argument, providing a list of those " + "columns which should be counted as containing a " + "foreign key reference to the parent table." + % (relname,), + fn, *arg, **kw) + + def _assert_raises_no_join(self, fn, relname, secondary_arg, + *arg, **kw): + if secondary_arg is not None: + assert_raises_message( + exc.NoForeignKeysError, + "Could not determine join condition between " + "parent/child tables on relationship %s - " + "there are no foreign keys linking these tables " + "via secondary table '%s'. " + "Ensure that referencing columns are associated with a ForeignKey " + "or ForeignKeyConstraint, or specify 'primaryjoin' and " + "'secondaryjoin' expressions" + % (relname, secondary_arg), + fn, *arg, **kw) + else: + assert_raises_message( + exc.NoForeignKeysError, + "Could not determine join condition between " + "parent/child tables on relationship %s - " + "there are no foreign keys linking these tables. " + "Ensure that referencing columns are associated with a ForeignKey " + "or ForeignKeyConstraint, or specify a 'primaryjoin' " + "expression." + % (relname,), + fn, *arg, **kw) + + def _assert_raises_ambiguous_direction(self, fn, relname, *arg, **kw): + assert_raises_message( + sa.exc.ArgumentError, + "Can't determine relationship" + " direction for relationship '%s' - foreign " + "key columns within the join condition are present " + "in both the parent and the child's mapped tables. " + "Ensure that only those columns referring to a parent column " + r"are marked as foreign, either via the foreign\(\) annotation or " + "via the foreign_keys argument." + % relname, + fn, *arg, **kw + ) + + def _assert_raises_no_local_remote(self, fn, relname, *arg, **kw): + assert_raises_message( + sa.exc.ArgumentError, + "Relationship %s could not determine " + "any unambiguous local/remote column " + "pairs based on join condition and remote_side arguments. " + r"Consider using the remote\(\) annotation to " + "accurately mark those elements of the join " + "condition that are on the remote side of the relationship." % relname, + + fn, *arg, **kw + ) class DependencyTwoParentTest(fixtures.MappedTest): """Test flush() when a mapper is dependent on multiple relationships""" @@ -158,7 +274,8 @@ class CompositeSelfRefFKTest(fixtures.MappedTest): @classmethod def setup_classes(cls): class Company(cls.Basic): - pass + def __init__(self, name): + self.name = name class Employee(cls.Basic): def __init__(self, name, company, emp_id, reports_to=None): @@ -185,8 +302,10 @@ class CompositeSelfRefFKTest(fixtures.MappedTest): employee_t.c.company_id==employee_t.c.company_id ), remote_side=[employee_t.c.emp_id, employee_t.c.company_id], - foreign_keys=[employee_t.c.reports_to_id], - backref=backref('employees', foreign_keys=None)) + foreign_keys=[employee_t.c.reports_to_id, employee_t.c.company_id], + backref=backref('employees', + foreign_keys=[employee_t.c.reports_to_id, + employee_t.c.company_id])) }) self._test() @@ -202,8 +321,10 @@ class CompositeSelfRefFKTest(fixtures.MappedTest): 'company':relationship(Company, backref='employees'), 'reports_to':relationship(Employee, remote_side=[employee_t.c.emp_id, employee_t.c.company_id], - foreign_keys=[employee_t.c.reports_to_id], - backref=backref('employees', foreign_keys=None) + foreign_keys=[employee_t.c.reports_to_id, + employee_t.c.company_id], + backref=backref('employees', foreign_keys= + [employee_t.c.reports_to_id, employee_t.c.company_id]) ) }) @@ -240,19 +361,70 @@ class CompositeSelfRefFKTest(fixtures.MappedTest): (employee_t.c.reports_to_id, employee_t.c.emp_id), (employee_t.c.company_id, employee_t.c.company_id) ], - foreign_keys=[employee_t.c.reports_to_id], - backref=backref('employees', foreign_keys=None) + foreign_keys=[employee_t.c.reports_to_id, + employee_t.c.company_id], + backref=backref('employees', foreign_keys= + [employee_t.c.reports_to_id, employee_t.c.company_id]) + ) + }) + + self._test() + + def test_annotated(self): + Employee, Company, employee_t, company_t = (self.classes.Employee, + self.classes.Company, + self.tables.employee_t, + self.tables.company_t) + + mapper(Company, company_t) + mapper(Employee, employee_t, properties= { + 'company':relationship(Company, backref='employees'), + 'reports_to':relationship(Employee, + primaryjoin=sa.and_( + remote(employee_t.c.emp_id)==employee_t.c.reports_to_id, + remote(employee_t.c.company_id)==employee_t.c.company_id + ), + backref=backref('employees') ) }) self._test() def _test(self): + self._test_relationships() + sess = Session() + self._setup_data(sess) + self._test_lazy_relations(sess) + self._test_join_aliasing(sess) + + def _test_relationships(self): + configure_mappers() + Employee = self.classes.Employee + employee_t = self.tables.employee_t + eq_( + set(Employee.employees.property.local_remote_pairs), + set([ + (employee_t.c.company_id, employee_t.c.company_id), + (employee_t.c.emp_id, employee_t.c.reports_to_id), + ]) + ) + eq_( + Employee.employees.property.remote_side, + set([employee_t.c.company_id, employee_t.c.reports_to_id]) + ) + eq_( + set(Employee.reports_to.property.local_remote_pairs), + set([ + (employee_t.c.company_id, employee_t.c.company_id), + (employee_t.c.reports_to_id, employee_t.c.emp_id), + ]) + ) + + def _setup_data(self, sess): Employee, Company = self.classes.Employee, self.classes.Company - sess = create_session() - c1 = Company() - c2 = Company() + c1 = Company('c1') + c2 = Company('c2') e1 = Employee(u'emp1', c1, 1) e2 = Employee(u'emp2', c1, 2, e1) @@ -263,10 +435,17 @@ class CompositeSelfRefFKTest(fixtures.MappedTest): e7 = Employee(u'emp7', c2, 3, e5) sess.add_all((c1, c2)) - sess.flush() - sess.expunge_all() + sess.commit() + sess.close() + + def _test_lazy_relations(self, sess): + Employee, Company = self.classes.Employee, self.classes.Company + + c1 = sess.query(Company).filter_by(name='c1').one() + c2 = sess.query(Company).filter_by(name='c2').one() + e1 = sess.query(Employee).filter_by(name='emp1').one() + e5 = sess.query(Employee).filter_by(name='emp5').one() - test_c1 = sess.query(Company).get(c1.company_id) test_e1 = sess.query(Employee).get([c1.company_id, e1.emp_id]) assert test_e1.name == 'emp1', test_e1.name test_e5 = sess.query(Employee).get([c2.company_id, e5.emp_id]) @@ -277,17 +456,63 @@ class CompositeSelfRefFKTest(fixtures.MappedTest): assert sess.query(Employee).\ get([c2.company_id, 3]).reports_to.name == 'emp5' - @testing.fails_if(lambda: True, "This will be fixed by #1401") - def go(): - eq_( - [n for n, in sess.query(Employee.name).\ - join(Employee.reports_to, aliased=True).\ - filter_by(name='emp5').\ - reset_joinpoint().\ - order_by(Employee.name)], - ['emp6', 'emp7'] + def _test_join_aliasing(self, sess): + Employee, Company = self.classes.Employee, self.classes.Company + eq_( + [n for n, in sess.query(Employee.name).\ + join(Employee.reports_to, aliased=True).\ + filter_by(name='emp5').\ + reset_joinpoint().\ + order_by(Employee.name)], + ['emp6', 'emp7'] + ) + +class CompositeJoinPartialFK(fixtures.MappedTest, AssertsCompiledSQL): + __dialect__ = 'default' + @classmethod + def define_tables(cls, metadata): + Table("parent", metadata, + Column('x', Integer, primary_key=True), + Column('y', Integer, primary_key=True), + Column('z', Integer), + ) + Table("child", metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('x', Integer), + Column('y', Integer), + Column('z', Integer), + # note 'z' is not here + sa.ForeignKeyConstraint( + ["x", "y"], + ["parent.x", "parent.y"] ) - go() + ) + @classmethod + def setup_mappers(cls): + parent, child = cls.tables.parent, cls.tables.child + class Parent(cls.Comparable): + pass + + class Child(cls.Comparable): + pass + mapper(Parent, parent, properties={ + 'children':relationship(Child, primaryjoin=and_( + parent.c.x==child.c.x, + parent.c.y==child.c.y, + parent.c.z==child.c.z, + )) + }) + mapper(Child, child) + + def test_joins_fully(self): + Parent, Child = self.classes.Parent, self.classes.Child + s = Session() + self.assert_compile( + Parent.children.property.strategy._lazywhere, + ":param_1 = child.x AND :param_2 = child.y AND :param_3 = child.z" + ) + class FKsAsPksTest(fixtures.MappedTest): """Syncrules on foreign keys that are also primary""" @@ -768,7 +993,6 @@ class AmbiguousJoinInterpretedAsSelfRef(fixtures.MappedTest): subscriber_table = Table('subscriber', metadata, Column('id', Integer, primary_key=True, test_needs_autoincrement=True), - Column('dummy', String(10)) # to appease older sqlite version ) address_table = Table('address', @@ -803,7 +1027,6 @@ class AmbiguousJoinInterpretedAsSelfRef(fixtures.MappedTest): def test_mapping(self): Subscriber, Address = self.classes.Subscriber, self.classes.Address - from sqlalchemy.orm.interfaces import ONETOMANY, MANYTOONE sess = create_session() assert Subscriber.addresses.property.direction is ONETOMANY assert Address.customer.property.direction is MANYTOONE @@ -1587,7 +1810,7 @@ class ViewOnlyRepeatedLocalColumn(fixtures.MappedTest): eq_(sess.query(Foo).filter_by(id=f2.id).one(), Foo(bars=[Bar(data='b3'), Bar(data='b4')])) -class ViewOnlyComplexJoin(fixtures.MappedTest): +class ViewOnlyComplexJoin(_RelationshipErrors, fixtures.MappedTest): """'viewonly' mappings with a complex join condition.""" @classmethod @@ -1671,9 +1894,7 @@ class ViewOnlyComplexJoin(fixtures.MappedTest): 't1':relationship(T1), 't3s':relationship(T3, secondary=t2tot3)}) mapper(T3, t3) - assert_raises_message(sa.exc.ArgumentError, - "Specify remote_side argument", - sa.orm.configure_mappers) + self._assert_raises_no_local_remote(configure_mappers, "T1.t3s") class ExplicitLocalRemoteTest(fixtures.MappedTest): @@ -1697,21 +1918,45 @@ class ExplicitLocalRemoteTest(fixtures.MappedTest): class T2(cls.Comparable): pass - def test_onetomany_funcfk(self): + def test_onetomany_funcfk_oldstyle(self): T2, T1, t2, t1 = (self.classes.T2, self.classes.T1, self.tables.t2, self.tables.t1) - # use a function within join condition. but specifying - # local_remote_pairs overrides all parsing of the join condition. + # old _local_remote_pairs mapper(T1, t1, properties={ 't2s':relationship(T2, primaryjoin=t1.c.id==sa.func.lower(t2.c.t1id), _local_remote_pairs=[(t1.c.id, t2.c.t1id)], - foreign_keys=[t2.c.t1id])}) + foreign_keys=[t2.c.t1id] + ) + }) + mapper(T2, t2) + self._test_onetomany() + + def test_onetomany_funcfk_annotated(self): + T2, T1, t2, t1 = (self.classes.T2, + self.classes.T1, + self.tables.t2, + self.tables.t1) + + # use annotation + mapper(T1, t1, properties={ + 't2s':relationship(T2, + primaryjoin=t1.c.id== + foreign(sa.func.lower(t2.c.t1id)), + )}) mapper(T2, t2) + self._test_onetomany() + def _test_onetomany(self): + T2, T1, t2, t1 = (self.classes.T2, + self.classes.T1, + self.tables.t2, + self.tables.t1) + is_(T1.t2s.property.direction, ONETOMANY) + eq_(T1.t2s.property.local_remote_pairs, [(t1.c.id, t2.c.t1id)]) sess = create_session() a1 = T1(id='number1', data='a1') a2 = T1(id='number2', data='a2') @@ -1910,8 +2155,135 @@ class InvalidRemoteSideTest(fixtures.MappedTest): "mean to set remote_side on the many-to-one side ?", configure_mappers) +class AmbiguousFKResolutionTest(_RelationshipErrors, fixtures.MappedTest): + @classmethod + def define_tables(cls, metadata): + Table("a", metadata, + Column('id', Integer, primary_key=True) + ) + Table("b", metadata, + Column('id', Integer, primary_key=True), + Column('aid_1', Integer, ForeignKey('a.id')), + Column('aid_2', Integer, ForeignKey('a.id')), + ) + Table("atob", metadata, + Column('aid', Integer), + Column('bid', Integer), + ) + Table("atob_ambiguous", metadata, + Column('aid1', Integer, ForeignKey('a.id')), + Column('bid1', Integer, ForeignKey('b.id')), + Column('aid2', Integer, ForeignKey('a.id')), + Column('bid2', Integer, ForeignKey('b.id')), + ) + + @classmethod + def setup_classes(cls): + class A(cls.Basic): + pass + class B(cls.Basic): + pass + + def test_ambiguous_fks_o2m(self): + A, B = self.classes.A, self.classes.B + a, b = self.tables.a, self.tables.b + mapper(A, a, properties={ + 'bs':relationship(B) + }) + mapper(B, b) + self._assert_raises_ambig_join( + configure_mappers, + "A.bs", + None + ) + + def test_with_fks_o2m(self): + A, B = self.classes.A, self.classes.B + a, b = self.tables.a, self.tables.b + mapper(A, a, properties={ + 'bs':relationship(B, foreign_keys=b.c.aid_1) + }) + mapper(B, b) + sa.orm.configure_mappers() + assert A.bs.property.primaryjoin.compare( + a.c.id==b.c.aid_1 + ) + eq_( + A.bs.property._calculated_foreign_keys, + set([b.c.aid_1]) + ) + + def test_with_pj_o2m(self): + A, B = self.classes.A, self.classes.B + a, b = self.tables.a, self.tables.b + mapper(A, a, properties={ + 'bs':relationship(B, primaryjoin=a.c.id==b.c.aid_1) + }) + mapper(B, b) + sa.orm.configure_mappers() + assert A.bs.property.primaryjoin.compare( + a.c.id==b.c.aid_1 + ) + eq_( + A.bs.property._calculated_foreign_keys, + set([b.c.aid_1]) + ) + + def test_with_annotated_pj_o2m(self): + A, B = self.classes.A, self.classes.B + a, b = self.tables.a, self.tables.b + mapper(A, a, properties={ + 'bs':relationship(B, primaryjoin=a.c.id==foreign(b.c.aid_1)) + }) + mapper(B, b) + sa.orm.configure_mappers() + assert A.bs.property.primaryjoin.compare( + a.c.id==b.c.aid_1 + ) + eq_( + A.bs.property._calculated_foreign_keys, + set([b.c.aid_1]) + ) + + def test_no_fks_m2m(self): + A, B = self.classes.A, self.classes.B + a, b, a_to_b = self.tables.a, self.tables.b, self.tables.atob + mapper(A, a, properties={ + 'bs':relationship(B, secondary=a_to_b) + }) + mapper(B, b) + self._assert_raises_no_join( + sa.orm.configure_mappers, + "A.bs", a_to_b, + ) + + def test_ambiguous_fks_m2m(self): + A, B = self.classes.A, self.classes.B + a, b, a_to_b = self.tables.a, self.tables.b, self.tables.atob_ambiguous + mapper(A, a, properties={ + 'bs':relationship(B, secondary=a_to_b) + }) + mapper(B, b) + + self._assert_raises_ambig_join( + configure_mappers, + "A.bs", + "atob_ambiguous" + ) + + + def test_with_fks_m2m(self): + A, B = self.classes.A, self.classes.B + a, b, a_to_b = self.tables.a, self.tables.b, self.tables.atob_ambiguous + mapper(A, a, properties={ + 'bs':relationship(B, secondary=a_to_b, + foreign_keys=[a_to_b.c.aid1, a_to_b.c.bid1]) + }) + mapper(B, b) + sa.orm.configure_mappers() + -class InvalidRelationshipEscalationTest(fixtures.MappedTest): +class InvalidRelationshipEscalationTest(_RelationshipErrors, fixtures.MappedTest): @classmethod def define_tables(cls, metadata): @@ -1936,6 +2308,7 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): class Bar(cls.Basic): pass + def test_no_join(self): bars, Foo, Bar, foos = (self.tables.bars, self.classes.Foo, @@ -1946,10 +2319,9 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): 'bars':relationship(Bar)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine join condition between parent/child " - "tables on relationship", sa.orm.configure_mappers) + self._assert_raises_no_join(sa.orm.configure_mappers, + "Foo.bars", None + ) def test_no_join_self_ref(self): bars, Foo, Bar, foos = (self.tables.bars, @@ -1961,10 +2333,11 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): 'foos':relationship(Foo)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine join condition between parent/child " - "tables on relationship", sa.orm.configure_mappers) + self._assert_raises_no_join( + configure_mappers, + "Foo.foos", + None + ) def test_no_equated(self): bars, Foo, Bar, foos = (self.tables.bars, @@ -1977,11 +2350,10 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): primaryjoin=foos.c.id>bars.c.fid)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine relationship direction " - "for primaryjoin condition", - configure_mappers) + self._assert_raises_no_relevant_fks( + configure_mappers, + "foos.id > bars.fid", "Foo.bars", "primary" + ) def test_no_equated_fks(self): bars, Foo, Bar, foos = (self.tables.bars, @@ -1994,15 +2366,10 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): primaryjoin=foos.c.id>bars.c.fid, foreign_keys=bars.c.fid)}) mapper(Bar, bars) - - assert_raises_message( - sa.exc.ArgumentError, - "Could not locate any foreign-key-equated, " - "locally mapped column pairs for primaryjoin " - "condition 'foos.id > bars.fid' on relationship " - "Foo.bars. For more relaxed rules on join " - "conditions, the relationship may be marked as viewonly=True.", - sa.orm.configure_mappers) + self._assert_raises_no_equality( + sa.orm.configure_mappers, + "foos.id > bars.fid", "Foo.bars", "primary" + ) def test_no_equated_wo_fks_works_on_relaxed(self): foos_with_fks, Foo, Bar, bars_with_fks, foos = (self.tables.foos_with_fks, @@ -2026,19 +2393,12 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): )}) mapper(Bar, bars_with_fks) - assert_raises_message( - sa.exc.ArgumentError, - "Could not locate any foreign-key-equated, locally mapped " - "column pairs for primaryjoin condition " - "'bars_with_fks.fid = foos_with_fks.id AND " - "foos_with_fks.id = foos.id' on relationship Foo.bars. " - "Ensure that the referencing Column objects have a " - "ForeignKey present, or are otherwise part of a " - "ForeignKeyConstraint on their parent Table, or specify " - "the foreign_keys parameter to this relationship. For " - "more relaxed rules on join conditions, the relationship " - "may be marked as viewonly=True.", - sa.orm.configure_mappers) + self._assert_raises_no_equality( + sa.orm.configure_mappers, + "bars_with_fks.fid = foos_with_fks.id " + "AND foos_with_fks.id = foos.id", + "Foo.bars", "primary" + ) def test_ambiguous_fks(self): bars, Foo, Bar, foos = (self.tables.bars, @@ -2052,20 +2412,10 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): foreign_keys=[foos.c.id, bars.c.fid])}) mapper(Bar, bars) - assert_raises_message(sa.exc.ArgumentError, - "Could not determine relationship " - "direction for primaryjoin condition " - "'foos.id = bars.fid', on relationship " - "Foo.bars, using manual 'foreign_keys' " - "setting. Do the columns in " - "'foreign_keys' represent all, and only, " - "the 'foreign' columns in this join " - r"condition\? Does the mapped Table " - "already have adequate ForeignKey and/or " - "ForeignKeyConstraint objects " - r"established \(in which case " - r"'foreign_keys' is usually unnecessary\)\?" - , sa.orm.configure_mappers) + self._assert_raises_ambiguous_direction( + sa.orm.configure_mappers, + "Foo.bars" + ) def test_ambiguous_remoteside_o2m(self): bars, Foo, Bar, foos = (self.tables.bars, @@ -2082,10 +2432,11 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): )}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "could not determine any local/remote column pairs", - sa.orm.configure_mappers) + self._assert_raises_no_local_remote( + configure_mappers, + "Foo.bars", + ) + def test_ambiguous_remoteside_m2o(self): bars, Foo, Bar, foos = (self.tables.bars, @@ -2102,13 +2453,13 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): )}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "could not determine any local/remote column pairs", - sa.orm.configure_mappers) + self._assert_raises_no_local_remote( + configure_mappers, + "Foo.bars", + ) - def test_no_equated_self_ref(self): + def test_no_equated_self_ref_no_fks(self): bars, Foo, Bar, foos = (self.tables.bars, self.classes.Foo, self.classes.Bar, @@ -2119,13 +2470,12 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): primaryjoin=foos.c.id>foos.c.fid)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine relationship direction for primaryjoin " - "condition", - configure_mappers) + self._assert_raises_no_relevant_fks(configure_mappers, + "foos.id > foos.fid", "Foo.foos", "primary" + ) - def test_no_equated_self_ref(self): + + def test_no_equated_self_ref_no_equality(self): bars, Foo, Bar, foos = (self.tables.bars, self.classes.Foo, self.classes.Bar, @@ -2137,14 +2487,9 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): foreign_keys=[foos.c.fid])}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not locate any foreign-key-equated, " - "locally mapped column pairs for primaryjoin " - "condition 'foos.id > foos.fid' on relationship " - "Foo.foos. For more relaxed rules on join " - "conditions, the relationship may be marked as viewonly=True.", - sa.orm.configure_mappers) + self._assert_raises_no_equality(configure_mappers, + "foos.id > foos.fid", "Foo.foos", "primary" + ) def test_no_equated_viewonly(self): bars, Bar, bars_with_fks, foos_with_fks, Foo, foos = (self.tables.bars, @@ -2160,10 +2505,10 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): viewonly=True)}) mapper(Bar, bars) - assert_raises_message(sa.exc.ArgumentError, - 'Could not determine relationship ' - 'direction for primaryjoin condition', - sa.orm.configure_mappers) + self._assert_raises_no_relevant_fks( + sa.orm.configure_mappers, + "foos.id > bars.fid", "Foo.bars", "primary" + ) sa.orm.clear_mappers() mapper(Foo, foos_with_fks, properties={ @@ -2187,15 +2532,10 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): viewonly=True)}) mapper(Bar, bars) - assert_raises_message(sa.exc.ArgumentError, - "Could not determine relationship " - "direction for primaryjoin condition " - "'foos.id > foos.fid', on relationship " - "Foo.foos. Ensure that the referencing " - "Column objects have a ForeignKey " - "present, or are otherwise part of a " - "ForeignKeyConstraint on their parent " - "Table.", sa.orm.configure_mappers) + self._assert_raises_no_relevant_fks( + sa.orm.configure_mappers, + "foos.id > foos.fid", "Foo.foos", "primary" + ) sa.orm.clear_mappers() mapper(Foo, foos_with_fks, properties={ @@ -2230,11 +2570,10 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): primaryjoin=foos.c.id==bars.c.fid)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine relationship direction for primaryjoin " - "condition", - configure_mappers) + self._assert_raises_no_relevant_fks( + configure_mappers, + "foos.id = bars.fid", "Foo.bars", "primary" + ) sa.orm.clear_mappers() mapper(Foo, foos_with_fks, properties={ @@ -2250,11 +2589,10 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): 'foos':relationship(Foo, primaryjoin=foos.c.id==foos.c.fid)}) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine relationship direction for primaryjoin " - "condition", - configure_mappers) + self._assert_raises_no_relevant_fks( + configure_mappers, + "foos.id = foos.fid", "Foo.foos", "primary" + ) def test_equated_self_ref_wrong_fks(self): @@ -2267,14 +2605,13 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest): primaryjoin=foos.c.id==foos.c.fid, foreign_keys=[bars.c.id])}) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine relationship direction for primaryjoin " - "condition", - configure_mappers) + self._assert_raises_no_relevant_fks( + configure_mappers, + "foos.id = foos.fid", "Foo.foos", "primary" + ) -class InvalidRelationshipEscalationTestM2M(fixtures.MappedTest): +class InvalidRelationshipEscalationTestM2M(_RelationshipErrors, fixtures.MappedTest): @classmethod def define_tables(cls, metadata): @@ -2317,10 +2654,11 @@ class InvalidRelationshipEscalationTestM2M(fixtures.MappedTest): 'bars': relationship(Bar, secondary=foobars)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine join condition between parent/child tables " - "on relationship", sa.orm.configure_mappers) + self._assert_raises_no_join( + configure_mappers, + "Foo.bars", + "foobars" + ) def test_no_secondaryjoin(self): foobars, bars, Foo, Bar, foos = (self.tables.foobars, @@ -2335,62 +2673,13 @@ class InvalidRelationshipEscalationTestM2M(fixtures.MappedTest): primaryjoin=foos.c.id > foobars.c.fid)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine join condition between parent/child tables " - "on relationship", - sa.orm.configure_mappers) - - def test_no_fks_warning_1(self): - foobars_with_many_columns, bars, Bar, foobars, Foo, foos = (self.tables.foobars_with_many_columns, - self.tables.bars, - self.classes.Bar, - self.tables.foobars, - self.classes.Foo, - self.tables.foos) - - mapper(Foo, foos, properties={ - 'bars': relationship(Bar, secondary=foobars, - primaryjoin=foos.c.id==foobars.c.fid, - secondaryjoin=foobars.c.bid==bars.c.id)}) - mapper(Bar, bars) - - assert_raises_message(sa.exc.SAWarning, - "No ForeignKey objects were present in " - "secondary table 'foobars'. Assumed " - "referenced foreign key columns " - "'foobars.bid', 'foobars.fid' for join " - "condition 'foos.id = foobars.fid' on " - "relationship Foo.bars", - sa.orm.configure_mappers) - - sa.orm.clear_mappers() - mapper(Foo, foos, properties={ - 'bars': relationship(Bar, - secondary=foobars_with_many_columns, - primaryjoin=foos.c.id== - foobars_with_many_columns.c.fid, - secondaryjoin=foobars_with_many_columns.c.bid== - bars.c.id)}) - mapper(Bar, bars) + self._assert_raises_no_join( + configure_mappers, + "Foo.bars", + "foobars" + ) - assert_raises_message(sa.exc.SAWarning, - "No ForeignKey objects were present in " - "secondary table 'foobars_with_many_colum" - "ns'. Assumed referenced foreign key " - "columns 'foobars_with_many_columns.bid'," - " 'foobars_with_many_columns.bid1', " - "'foobars_with_many_columns.bid2', " - "'foobars_with_many_columns.fid', " - "'foobars_with_many_columns.fid1', " - "'foobars_with_many_columns.fid2' for " - "join condition 'foos.id = " - "foobars_with_many_columns.fid' on " - "relationship Foo.bars", - sa.orm.configure_mappers) - - @testing.emits_warning(r'No ForeignKey objects.*') - def test_no_fks_warning_2(self): + def test_no_fks(self): foobars_with_many_columns, bars, Bar, foobars, Foo, foos = (self.tables.foobars_with_many_columns, self.tables.bars, self.classes.Bar, @@ -2448,11 +2737,11 @@ class InvalidRelationshipEscalationTestM2M(fixtures.MappedTest): secondaryjoin=foobars.c.bid<=bars.c.id)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not determine relationship direction for " - "primaryjoin condition", - configure_mappers) + self._assert_raises_no_equality( + configure_mappers, + 'foos.id > foobars.fid', + "Foo.bars", + "primary") sa.orm.clear_mappers() mapper(Foo, foos, properties={ @@ -2461,18 +2750,11 @@ class InvalidRelationshipEscalationTestM2M(fixtures.MappedTest): primaryjoin=foos.c.id > foobars_with_fks.c.fid, secondaryjoin=foobars_with_fks.c.bid<=bars.c.id)}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - r"Could not locate any foreign-key-equated, locally mapped " - "column pairs for primaryjoin condition 'foos.id > " - "foobars_with_fks.fid' on relationship Foo.bars. Ensure " - "that the referencing Column objects have a ForeignKey " - "present, or are otherwise part of a ForeignKeyConstraint " - "on their parent Table, or specify the foreign_keys " - "parameter to this relationship. For more relaxed " - "rules on join conditions, the relationship may be marked " - "as viewonly=True.", - configure_mappers) + self._assert_raises_no_equality( + configure_mappers, + 'foos.id > foobars_with_fks.fid', + "Foo.bars", + "primary") sa.orm.clear_mappers() mapper(Foo, foos, properties={ @@ -2498,21 +2780,12 @@ class InvalidRelationshipEscalationTestM2M(fixtures.MappedTest): secondaryjoin=foobars.c.bid <= bars.c.id, foreign_keys=[foobars.c.fid])}) mapper(Bar, bars) - - assert_raises_message(sa.exc.ArgumentError, - "Could not determine relationship " - "direction for secondaryjoin condition " - r"'foobars.bid \<\= bars.id', on " - "relationship Foo.bars, using manual " - "'foreign_keys' setting. Do the columns " - "in 'foreign_keys' represent all, and only, the " - "'foreign' columns in this join " - r"condition\? Does the " - "secondary Table already have adequate " - "ForeignKey and/or ForeignKeyConstraint " - r"objects established \(in which case " - r"'foreign_keys' is usually unnecessary\)?" - , sa.orm.configure_mappers) + self._assert_raises_no_relevant_fks( + configure_mappers, + "foobars.bid <= bars.id", + "Foo.bars", + "secondary" + ) def test_no_equated_secondaryjoin(self): foobars, bars, Foo, Bar, foos = (self.tables.foobars, @@ -2529,10 +2802,12 @@ class InvalidRelationshipEscalationTestM2M(fixtures.MappedTest): foreign_keys=[foobars.c.fid, foobars.c.bid])}) mapper(Bar, bars) - assert_raises_message( - sa.exc.ArgumentError, - "Could not locate any foreign-key-equated, locally mapped column pairs for " - "secondaryjoin condition", sa.orm.configure_mappers) + self._assert_raises_no_equality( + configure_mappers, + "foobars.bid <= bars.id", + "Foo.bars", + "secondary" + ) class ActiveHistoryFlagTest(_fixtures.FixtureTest): run_inserts = None diff --git a/test/sql/test_generative.py b/test/sql/test_generative.py index 98e783ede..29b7cd482 100644 --- a/test/sql/test_generative.py +++ b/test/sql/test_generative.py @@ -1,5 +1,5 @@ from sqlalchemy import * -from sqlalchemy.sql import table, column, ClauseElement +from sqlalchemy.sql import table, column, ClauseElement, operators from sqlalchemy.sql.expression import _clone, _from_objects from test.lib import * from sqlalchemy.sql.visitors import * @@ -166,6 +166,102 @@ class TraversalTest(fixtures.TestBase, AssertsExecutionResults): s = set(ClauseVisitor().iterate(bin)) assert set(ClauseVisitor().iterate(bin)) == set([foo, bar, bin]) +class BinaryEndpointTraversalTest(fixtures.TestBase): + """test the special binary product visit""" + + def _assert_traversal(self, expr, expected): + canary = [] + def visit(binary, l, r): + canary.append((binary.operator, l, r)) + print binary.operator, l, r + sql_util.visit_binary_product(visit, expr) + eq_( + canary, expected + ) + + def test_basic(self): + a, b = column("a"), column("b") + self._assert_traversal( + a == b, + [ + (operators.eq, a, b) + ] + ) + + def test_with_tuples(self): + a, b, c, d, b1, b1a, b1b, e, f = ( + column("a"), + column("b"), + column("c"), + column("d"), + column("b1"), + column("b1a"), + column("b1b"), + column("e"), + column("f") + ) + expr = tuple_( + a, b, b1==tuple_(b1a, b1b == d), c + ) > tuple_( + func.go(e + f) + ) + self._assert_traversal( + expr, + [ + (operators.gt, a, e), + (operators.gt, a, f), + (operators.gt, b, e), + (operators.gt, b, f), + (operators.eq, b1, b1a), + (operators.eq, b1b, d), + (operators.gt, c, e), + (operators.gt, c, f) + ] + ) + + def test_composed(self): + a, b, e, f, q, j, r = ( + column("a"), + column("b"), + column("e"), + column("f"), + column("q"), + column("j"), + column("r"), + ) + expr = and_( + (a + b) == q + func.sum(e + f), + and_( + j == r, + f == q + ) + ) + self._assert_traversal( + expr, + [ + (operators.eq, a, q), + (operators.eq, a, e), + (operators.eq, a, f), + (operators.eq, b, q), + (operators.eq, b, e), + (operators.eq, b, f), + (operators.eq, j, r), + (operators.eq, f, q), + ] + ) + + def test_subquery(self): + a, b, c = column("a"), column("b"), column("c") + subq = select([c]).where(c == a).as_scalar() + expr = and_(a == b, b == subq) + self._assert_traversal( + expr, + [ + (operators.eq, a, b), + (operators.eq, b, subq), + ] + ) + class ClauseTest(fixtures.TestBase, AssertsCompiledSQL): """test copy-in-place behavior of various ClauseElements.""" diff --git a/test/sql/test_selectable.py b/test/sql/test_selectable.py index bbb9131a5..dde832e7d 100644 --- a/test/sql/test_selectable.py +++ b/test/sql/test_selectable.py @@ -1023,6 +1023,25 @@ class AnnotationsTest(fixtures.TestBase): annot = obj._annotate({}) eq_(set([obj]), set([annot])) + def test_compare(self): + t = table('t', column('x'), column('y')) + x_a = t.c.x._annotate({}) + assert t.c.x.compare(x_a) + assert x_a.compare(t.c.x) + assert not x_a.compare(t.c.y) + assert not t.c.y.compare(x_a) + assert (t.c.x == 5).compare(x_a == 5) + assert not (t.c.y == 5).compare(x_a == 5) + + s = select([t]) + x_p = s.c.x + assert not x_a.compare(x_p) + assert not t.c.x.compare(x_p) + x_p_a = x_p._annotate({}) + assert x_p_a.compare(x_p) + assert x_p.compare(x_p_a) + assert not x_p_a.compare(x_a) + def test_custom_constructions(self): from sqlalchemy.schema import Column class MyColumn(Column): @@ -1132,13 +1151,18 @@ class AnnotationsTest(fixtures.TestBase): assert b2.left is not bin.left assert b3.left is not b2.left is not bin.left assert b4.left is bin.left # since column is immutable - assert b4.right is not bin.right is not b2.right is not b3.right + # deannotate copies the element + assert bin.right is not b2.right is not b3.right is not b4.right def test_annotate_unique_traversal(self): """test that items are copied only once during annotate, deannotate traversal - #2453 + #2453 - however note this was modified by + #1401, and it's likely that re49563072578 + is helping us with the str() comparison + case now, as deannotate is making + clones again in some cases. """ table1 = table('table1', column('x')) table2 = table('table2', column('y')) @@ -1146,21 +1170,81 @@ class AnnotationsTest(fixtures.TestBase): s = select([a1.c.x]).select_from( a1.join(table2, a1.c.x==table2.c.y) ) - for sel in ( sql_util._deep_deannotate(s), - sql_util._deep_annotate(s, {'foo':'bar'}), visitors.cloned_traverse(s, {}, {}), visitors.replacement_traverse(s, {}, lambda x:None) ): # the columns clause isn't changed at all assert sel._raw_columns[0].table is a1 - # the from objects are internally consistent, - # i.e. the Alias at position 0 is the same - # Alias in the Join object in position 1 assert sel._froms[0] is sel._froms[1].left + + eq_(str(s), str(sel)) + + # when we are modifying annotations sets only + # partially, each element is copied unconditionally + # when encountered. + for sel in ( + sql_util._deep_deannotate(s, {"foo":"bar"}), + sql_util._deep_annotate(s, {'foo':'bar'}), + ): + assert sel._froms[0] is not sel._froms[1].left + + # but things still work out due to + # re49563072578 eq_(str(s), str(sel)) + + def test_annotate_varied_annot_same_col(self): + """test two instances of the same column with different annotations + preserving them when deep_annotate is run on them. + + """ + t1 = table('table1', column("col1"), column("col2")) + s = select([t1.c.col1._annotate({"foo":"bar"})]) + s2 = select([t1.c.col1._annotate({"bat":"hoho"})]) + s3 = s.union(s2) + sel = sql_util._deep_annotate(s3, {"new":"thing"}) + + eq_( + sel.selects[0]._raw_columns[0]._annotations, + {"foo":"bar", "new":"thing"} + ) + + eq_( + sel.selects[1]._raw_columns[0]._annotations, + {"bat":"hoho", "new":"thing"} + ) + + def test_deannotate_2(self): + table1 = table('table1', column("col1"), column("col2")) + j = table1.c.col1._annotate({"remote":True}) == \ + table1.c.col2._annotate({"local":True}) + j2 = sql_util._deep_deannotate(j) + eq_( + j.left._annotations, {"remote":True} + ) + eq_( + j2.left._annotations, {} + ) + + def test_deannotate_3(self): + table1 = table('table1', column("col1"), column("col2"), + column("col3"), column("col4")) + j = and_( + table1.c.col1._annotate({"remote":True})== + table1.c.col2._annotate({"local":True}), + table1.c.col3._annotate({"remote":True})== + table1.c.col4._annotate({"local":True}) + ) + j2 = sql_util._deep_deannotate(j) + eq_( + j.clauses[0].left._annotations, {"remote":True} + ) + eq_( + j2.clauses[0].left._annotations, {} + ) + def test_annotate_fromlist_preservation(self): """test the FROM list in select still works even when multiple annotate runs have created |
