diff options
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 742 |
1 files changed, 742 insertions, 0 deletions
diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py new file mode 100644 index 000000000..bb70f8a11 --- /dev/null +++ b/lib/sqlalchemy/orm/relationships.py @@ -0,0 +1,742 @@ +# 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 +from sqlalchemy.sql import operators, expression, visitors +from sqlalchemy.orm.interfaces import MANYTOMANY, MANYTOONE, ONETOMANY + +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, + extra_child_criterion=None, + self_referential=False, + prop=None, + support_sync=True + ): + self.parent_selectable = parent_selectable + self.parent_local_selectable = parent_local_selectable + self.child_selectable = child_selectable + self.child_local_selecatble = child_local_selectable + self.parent_equivalents = parent_equivalents + self.child_equivalents = child_equivalents + self.primaryjoin = primaryjoin + self.secondaryjoin = secondaryjoin + self.secondary = secondary + self.extra_child_criterion = extra_child_criterion + 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._determine_joins() + self._parse_joins() + + 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) + if self.primaryjoin is None: + self.primaryjoin = \ + join_condition( + self.parent_selectable, + self.secondary, + a_subset=parent_local_selectable) + else: + if self.primaryjoin is None: + self.primaryjoin = \ + join_condition( + self.parent_selectable, + self.child_selectable, + a_subset=self.parent_local_selectable) + 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.prop) + + def _parse_joins(self): + """Apply 'remote', 'local' and 'foreign' annotations + to the primary and secondary join conditions. + + """ + parentcols = util.column_set(self.parent_selectable.c) + targetcols = util.column_set(self.child_selectable.c) + + def col_is(a, b): + return a.compare(b) + + def is_foreign(a, b): + if self.consider_as_foreign_keys: + if a in self.consider_as_foreign_keys and ( + col_is(a, b) or + b not in self.consider_as_foreign_keys + ): + return a + elif b in self.consider_as_foreign_keys and ( + col_is(a, b) or + a not in self.consider_as_foreign_keys + ): + return b + elif isinstance(a, schema.Column) and \ + isinstance(b, schema.Column): + if a.references(b): + return a + elif b.references(a): + return b + + any_operator = not self.support_sync + + def visit_binary(binary): + #if not any_operator and binary.operator is not operators.eq: + # return + 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 is binary.left: + binary.left = binary.left._annotate( + {"foreign":True}) + elif col is binary.right: + binary.right = binary.right._annotate( + {"foreign":True}) + # TODO: when the two cols are the same. + + if "remote" not in binary.left._annotations and \ + "remote" not in binary.right._annotations: + if self.local_remote_pairs: + raise NotImplementedError() + elif self.remote_side: + raise NotImplementedError() + elif self.self_referential: + # assume one to many - FKs are "Remote" + if "foreign" in binary.left._annotations: + binary.left = binary.left._annotate( + {"remote":True}) + if binary.right in parentcols: + binary.right = binary.right._annotate( + {"local":True}) + elif "foreign" in binary.right._annotations: + binary.right = binary.right._annotate( + {"remote":True}) + if binary.left in parentcols: + binary.left = binary.left._annotate( + {"local":True}) + else: + if binary.left in targetcols: + binary.left = binary.left._annotate( + {"remote":True}) + if binary.right in parentcols: + binary.right = binary.right._annotate( + {"local":True}) + elif binary.right in targetcols: + binary.right = binary.right._annotate( + {"remote":True}) + if binary.left in parentcols: + binary.left = binary.left._annotate( + {"local":True}) + + self.primaryjoin = visitors.cloned_traverse( + self.primaryjoin, + {}, + {"binary":visit_binary} + ) + + def join_targets(self, source_selectable, + dest_selectable, + aliased): + """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. + + single_crit = self.extra_child_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.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 + +################# everything below is TODO ################################ + +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 + +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) + + # fk collection which suggests MANYTOONE. + + manytoone_fk = parentcols.intersection( + self._calculated_foreign_keys) + + 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) + + 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) + + if self.cascade.delete_orphan and not self.single_parent \ + and (self.direction is MANYTOMANY or self.direction + is MANYTOONE): + util.warn('On %s, delete-orphan cascade is not supported ' + 'on a many-to-many or many-to-one relationship ' + 'when single_parent is not set. Set ' + 'single_parent=True on the relationship().' + % self) + if self.direction is MANYTOONE and self.passive_deletes: + util.warn("On %s, 'passive_deletes' is normally configured " + "on one-to-many, one-to-one, many-to-many " + "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. + + """ + 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)) + + count = [0] + def clone(elem): + if set(['local', 'remote']).intersection(elem._annotations): + return None + elif elem in self.local_side and elem in self.remote_side: + # TODO: OK this still sucks. this is basically, + # refuse, refuse, refuse the temptation to guess! + # but crap we really have to guess don't we. we + # might want to traverse here with cloned_traverse + # so we can see the binary exprs and do it at that + # level.... + if count[0] % 2 == 0: + elem = elem._annotate({'local':True}) + else: + elem = elem._annotate({'remote':True}) + count[0] += 1 + elif elem in self.local_side: + elem = elem._annotate({'local':True}) + elif elem in self.remote_side: + elem = elem._annotate({'remote':True}) + else: + elem = None + return elem + + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, clone + ) + + +def _criterion_exists(self, criterion=None, **kwargs): + if getattr(self, '_of_type', None): + target_mapper = self._of_type + to_selectable = target_mapper._with_polymorphic_selectable + if self.property._is_self_referential: + to_selectable = to_selectable.alias() + + single_crit = target_mapper._single_table_criterion + if single_crit is not None: + if criterion is not None: + criterion = single_crit & criterion + else: + criterion = single_crit + else: + to_selectable = None + + if self.adapter: + source_selectable = self.__clause_element__() + else: + source_selectable = None + + pj, sj, source, dest, secondary, target_adapter = \ + self.property._create_joins(dest_polymorphic=True, + dest_selectable=to_selectable, + source_selectable=source_selectable) + + for k in kwargs: + crit = getattr(self.property.mapper.class_, k) == kwargs[k] + if criterion is None: + criterion = crit + else: + criterion = criterion & crit + + # annotate the *local* side of the join condition, in the case + # of pj + sj this is the full primaryjoin, in the case of just + # pj its the local side of the primaryjoin. + if sj is not None: + j = _orm_annotate(pj) & sj + 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) + + # only have the "joined left side" of what we + # return be subject to Query adaption. The right + # side of it is used for an exists() subquery and + # should not correlate or otherwise reach out + # to anything in the enclosing query. + if criterion is not None: + criterion = criterion._annotate({'no_replacement_traverse': True}) + + crit = j & criterion + + return sql.exists([1], crit, from_obj=dest).\ + correlate(source._annotate({'_orm_adapt':True})) + + +def __negated_contains_or_equals(self, other): + if self.property.direction == MANYTOONE: + state = attributes.instance_state(other) + + def state_bindparam(x, state, col): + o = state.obj() # strong ref + return sql.bindparam(x, unique=True, callable_=lambda : \ + self.property.mapper._get_committed_attr_by_column(o, + col)) + + def adapt(col): + if self.adapter: + return self.adapter(col) + else: + return col + + if self.property._use_get: + return sql.and_(*[ + sql.or_( + adapt(x) != state_bindparam(adapt(x), state, y), + adapt(x) == None) + for (x, y) in self.property.local_remote_pairs]) + + criterion = sql.and_(*[x==y for (x, y) in + zip( + self.property.mapper.primary_key, + self.property.\ + mapper.\ + primary_key_from_instance(other)) + ]) + return ~self._criterion_exists(criterion) |
