diff options
-rw-r--r-- | lib/sqlalchemy/orm/properties.py | 417 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/relationships.py | 128 | ||||
-rw-r--r-- | test/orm/test_query.py | 2 | ||||
-rw-r--r-- | test/orm/test_rel_fn.py | 54 |
4 files changed, 152 insertions, 449 deletions
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index 95a309357..77da33b5f 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -914,19 +914,15 @@ class RelationshipProperty(StrategizedProperty): def do_init(self): self._check_conflicts() self._process_dependent_arguments() - self._create_new_thing() - self._determine_joins() - self._determine_synchronize_pairs() - self._determine_direction() - self._determine_local_remote_pairs() - self._test_new_thing() + self._setup_join_conditions() + self._extra_determine_direction() self._post_init() self._generate_backref() super(RelationshipProperty, self).do_init() - def _create_new_thing(self): + def _setup_join_conditions(self): import relationships - self.jc = relationships.JoinCondition( + 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, @@ -944,12 +940,14 @@ class RelationshipProperty(StrategizedProperty): support_sync=not self.viewonly, can_be_synced_fn=self._columns_are_mapped ) - - def _test_new_thing(self): - assert self.jc.direction is self.direction - assert self.jc.remote_side.issuperset(self.remote_side) -# assert self.jc.local_remote_pairs.issuperset(self.local_remote_pairs) - pass + self.primaryjoin = jc.primaryjoin + self.secondaryjoin = jc.secondaryjoin + self.direction = jc.direction + self.local_remote_pairs = jc.local_remote_pairs + self.remote_side = jc.remote_columns + self.synchronize_pairs = jc.synchronize_pairs + self.secondary_synchronize_pairs = jc.secondary_synchronize_pairs + self._calculated_foreign_keys = jc.foreign_key_columns def _check_conflicts(self): """Test that this relationship is legal, warn about @@ -1037,284 +1035,7 @@ 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) - - # 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) - + def _extra_determine_direction(self): if self.cascade.delete_orphan and not self.single_parent \ and (self.direction is MANYTOMANY or self.direction is MANYTOONE): @@ -1329,110 +1050,20 @@ 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)) - - 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 + 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 - self.primaryjoin = visitors.replacement_traverse( - self.primaryjoin, {}, clone - ) def _generate_backref(self): if not self.is_primary(): diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index cb07f234a..723d45295 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -241,36 +241,69 @@ class JoinCondition(object): return result[0] def _annotate_remote(self): + parentcols = util.column_set(self.parent_selectable.c) + for col in visitors.iterate(self.primaryjoin, {}): if "remote" in col._annotations: - return - - if self._local_remote_pairs: - raise NotImplementedError() - elif self._remote_side: - def repl(element): - if element in self._remote_side: - return element._annotate({"remote":True}) - elif self.secondary is not None: - def repl(element): - if self.secondary.c.contains_column(element): - return element._annotate({"remote":True}) - elif self._refers_to_parent_table(): - def repl(element): - # assume one to many - FKs are "remote" - if "foreign" in element._annotations: - return element._annotate({"remote":True}) + has_remote_annotations = True + break else: - def repl(element): - if self.child_selectable.c.contains_column(element): - return element._annotate({"remote":True}) + has_remote_annotations = False + + def _annotate_selfref(fn): + def visit_binary(binary): + equated = binary.left is binary.right + if isinstance(binary.left, sql.ColumnElement) and \ + isinstance(binary.right, sql.ColumnElement): + # 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}) + + self.primaryjoin = visitors.cloned_traverse( + self.primaryjoin, {}, + {"binary":visit_binary}) + + if not has_remote_annotations: + if self._local_remote_pairs: + raise NotImplementedError() + elif self._remote_side: + if self._refers_to_parent_table(): + _annotate_selfref(lambda col:col in self._remote_side) + else: + def repl(element): + if element in self._remote_side: + return element._annotate({"remote":True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, repl) + elif self.secondary is not None: + 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) + elif self._refers_to_parent_table(): + _annotate_selfref(lambda col:"foreign" in col._annotations) + else: + def repl(element): + if self.child_selectable.c.contains_column(element): + return element._annotate({"remote":True}) - self.primaryjoin = visitors.replacement_traverse( - self.primaryjoin, {}, repl) - if self.secondaryjoin is not None: - self.secondaryjoin = visitors.replacement_traverse( - self.secondaryjoin, {}, repl) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, repl) + def locals_(elem): + if "remote" not in elem._annotations and \ + elem in parentcols: + return elem._annotate({"local":True}) + self.primaryjoin = visitors.replacement_traverse( + self.primaryjoin, {}, locals_ + ) def _check_foreign_cols(self, join_condition, primary): """Check the foreign key columns collected and emit error messages.""" @@ -375,30 +408,49 @@ class JoinCondition(object): "nor the child's mapped tables" % self.prop) @util.memoized_property - def liberal_remote_columns(self): - # this is temporary until we figure out - # which version of "remote" to use + def remote_columns(self): return self._gather_join_annotations("remote") @util.memoized_property - def remote_columns(self): - return set([r for l, r in self.local_remote_pairs]) - #return self._gather_join_annotations("remote") + def local_columns(self): + return self._gather_join_annotations("local") - remote_side = remote_columns + @util.memoized_property + def synchronize_pairs(self): + parentcols = util.column_set(self.parent_selectable.c) + targetcols = util.column_set(self.child_selectable.c) + result = [] + for l, r in self.local_remote_pairs: + if self.secondary is not None: + if "foreign" in r._annotations and \ + l in parentcols: + result.append((l, r)) + elif "foreign" in r._annotations and \ + "can_be_synced" in r._annotations: + result.append((l, r)) + elif "foreign" in l._annotations and \ + "can_be_synced" in l._annotations: + result.append((r, l)) + return result @util.memoized_property - def local_columns(self): - return set([l for l, r in self.local_remote_pairs]) + def secondary_synchronize_pairs(self): + parentcols = util.column_set(self.parent_selectable.c) + targetcols = util.column_set(self.child_selectable.c) + result = [] + if self.secondary is None: + return result + + for l, r in self.local_remote_pairs: + if "foreign" in l._annotations and \ + r in targetcols: + result.append((l, r)) + return result @util.memoized_property def foreign_key_columns(self): return self._gather_join_annotations("foreign") - @util.memoized_property - def referent_columns(self): - return self._gather_join_annotations("referent") - def _gather_join_annotations(self, annotation): s = set( self._gather_columns_with_annotation(self.primaryjoin, diff --git a/test/orm/test_query.py b/test/orm/test_query.py index 155f7c68d..852c50099 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -622,7 +622,7 @@ class OperatorTest(QueryTest, AssertsCompiledSQL): self._test(Address.user == None, "addresses.user_id IS NULL") self._test(Address.user != None, "addresses.user_id IS NOT NULL") - + def test_foo(self): Node = self.classes.Node nalias = aliased(Node) diff --git a/test/orm/test_rel_fn.py b/test/orm/test_rel_fn.py index 862149bc1..d3d346bba 100644 --- a/test/orm/test_rel_fn.py +++ b/test/orm/test_rel_fn.py @@ -120,11 +120,11 @@ class JoinCondTest(fixtures.TestBase, AssertsCompiledSQL): **kw ) - def test_determine_remote_side_compound_1(self): + def test_determine_remote_columns_compound_1(self): joincond = self._join_fixture_compound_expression_1( support_sync=False) eq_( - joincond.liberal_remote_columns, + joincond.remote_columns, set([self.right.c.x, self.right.c.y]) ) @@ -152,12 +152,12 @@ class JoinCondTest(fixtures.TestBase, AssertsCompiledSQL): self._join_fixture_compound_expression_1 ) - def test_determine_remote_side_compound_2(self): + def test_determine_remote_columns_compound_2(self): joincond = self._join_fixture_compound_expression_2( support_sync=False) eq_( - joincond.remote_side, - set([]) + joincond.remote_columns, + set([self.right.c.x, self.right.c.y]) ) def test_determine_local_remote_compound_2(self): @@ -187,10 +187,10 @@ class JoinCondTest(fixtures.TestBase, AssertsCompiledSQL): joincond = self._join_fixture_o2m() is_(joincond.direction, ONETOMANY) - def test_determine_remote_side_o2m(self): + def test_determine_remote_columns_o2m(self): joincond = self._join_fixture_o2m() eq_( - joincond.remote_side, + joincond.remote_columns, set([self.right.c.lid]) ) @@ -205,13 +205,33 @@ class JoinCondTest(fixtures.TestBase, AssertsCompiledSQL): joincond = self._join_fixture_o2m_selfref() is_(joincond.direction, ONETOMANY) - def test_determine_remote_side_o2m_selfref(self): + def test_determine_remote_columns_o2m_selfref(self): joincond = self._join_fixture_o2m_selfref() eq_( - joincond.remote_side, + joincond.remote_columns, set([self.selfref.c.sid]) ) + def test_join_targets_o2m_selfref(self): + joincond = self._join_fixture_o2m_selfref() + left = select([joincond.parent_selectable]).alias('pj') + pj, sj, sec, adapter = 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 = joincond.join_targets( + joincond.parent_selectable, + right, + True) + self.assert_compile( + pj, "selfref.id = pj.sid" + ) + def test_determine_join_m2o_selfref(self): joincond = self._join_fixture_m2o_selfref() self.assert_compile( @@ -223,10 +243,10 @@ class JoinCondTest(fixtures.TestBase, AssertsCompiledSQL): joincond = self._join_fixture_m2o_selfref() is_(joincond.direction, MANYTOONE) - def test_determine_remote_side_m2o_selfref(self): + def test_determine_remote_columns_m2o_selfref(self): joincond = self._join_fixture_m2o_selfref() eq_( - joincond.remote_side, + joincond.remote_columns, set([self.selfref.c.id]) ) @@ -242,10 +262,10 @@ class JoinCondTest(fixtures.TestBase, AssertsCompiledSQL): joincond = self._join_fixture_o2m_composite_selfref() is_(joincond.direction, ONETOMANY) - def test_determine_remote_side_o2m_composite_selfref(self): + def test_determine_remote_columns_o2m_composite_selfref(self): joincond = self._join_fixture_o2m_composite_selfref() eq_( - joincond.remote_side, + joincond.remote_columns, set([self.composite_selfref.c.parent_id, self.composite_selfref.c.group_id]) ) @@ -262,10 +282,10 @@ class JoinCondTest(fixtures.TestBase, AssertsCompiledSQL): joincond = self._join_fixture_m2o_composite_selfref() is_(joincond.direction, MANYTOONE) - def test_determine_remote_side_m2o_composite_selfref(self): + def test_determine_remote_columns_m2o_composite_selfref(self): joincond = self._join_fixture_m2o_composite_selfref() eq_( - joincond.liberal_remote_columns, + joincond.remote_columns, set([self.composite_selfref.c.id, self.composite_selfref.c.group_id]) ) @@ -281,10 +301,10 @@ class JoinCondTest(fixtures.TestBase, AssertsCompiledSQL): joincond = self._join_fixture_m2o() is_(joincond.direction, MANYTOONE) - def test_determine_remote_side_m2o(self): + def test_determine_remote_columns_m2o(self): joincond = self._join_fixture_m2o() eq_( - joincond.remote_side, + joincond.remote_columns, set([self.left.c.id]) ) |