]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
start to work on error messages, allow foreign_keys as only argument
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 25 Feb 2012 22:10:06 +0000 (17:10 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 25 Feb 2012 22:10:06 +0000 (17:10 -0500)
if otherwise can't determine join condition due to no fks

lib/sqlalchemy/exc.py
lib/sqlalchemy/orm/relationships.py
lib/sqlalchemy/sql/util.py
test/orm/test_rel_fn.py
test/orm/test_relationships.py

index 91ffc2811b523d347ecb3775c05aa27eb0a71ade..f28bd8a079188e150ec791e675d9b54bae85af46 100644 (file)
@@ -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.
index edb7498e035be1e5415fcc2f7af1e2f821c67728..76e219efee17078e0cb967d84fe512f420bb397a 100644 (file)
@@ -80,11 +80,11 @@ class JoinCondition(object):
         self._annotate_fks()
         self._annotate_remote()
         self._annotate_local()
-        self._determine_direction()
         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()
 
@@ -134,27 +134,68 @@ class JoinCondition(object):
                         join_condition(
                                 self.child_selectable, 
                                 self.secondary,
-                                a_subset=self.child_local_selectable)
+                                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)
+                                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)
-        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)
+                                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):
@@ -515,7 +556,7 @@ class JoinCondition(object):
             err = "Could not locate any simple equality expressions "\
                     "involving foreign key columns for %s join condition "\
                     "'%s' on relationship %s." % (
-                        primary and 'primaryjoin' or 'secondaryjoin', 
+                        primary and 'primary' or 'secondary', 
                         join_condition, 
                         self.prop
                     )
@@ -529,12 +570,12 @@ class JoinCondition(object):
         else:
             err = "Could not locate any relevant foreign key columns "\
                     "for %s join condition '%s' on relationship %s." % (
-                        primary and 'primaryjoin' or 'secondaryjoin', 
+                        primary and 'primary' or 'secondary', 
                         join_condition, 
                         self.prop
                     )
-            err += "Ensure that referencing columns are associated with a "\
-                    "ForeignKey or ForeignKeyConstraint, or are annotated "\
+            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)
 
index 2862e9af927ae3218df3ba292ca2acbf1809a2cc..38d95dde5d41f5bed458133ea7fa0d12bff477a6 100644 (file)
@@ -284,8 +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):
+                            a_subset=None,
+                            consider_as_foreign_keys=None):
     """create a join condition between two tables or selectables.
 
     e.g.::
@@ -321,6 +323,9 @@ def join_condition(a, b, ignore_nonexistent_tables=False,
         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:
@@ -336,6 +341,9 @@ def join_condition(a, b, ignore_nonexistent_tables=False,
             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:
@@ -358,11 +366,11 @@ def join_condition(a, b, ignore_nonexistent_tables=False,
                                 "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. "
index decf086d2f5a6f1355ec56f5a19be0509bd26c13..9fe6ce170630a4c6e36753545a8df14ecd0cc416 100644 (file)
@@ -22,6 +22,12 @@ class _JoinFixtures(object):
             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'))
@@ -45,6 +51,16 @@ class _JoinFixtures(object):
             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'))
@@ -302,6 +318,89 @@ class _JoinFixtures(object):
             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 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()
@@ -398,13 +497,10 @@ class ColumnCollectionsTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL
         )
 
     def test_err_local_remote_compound_1(self):
-        assert_raises_message(
-            exc.ArgumentError,
-            "Can't determine relationship direction for "
-            "relationship 'None' - foreign key "
-            "columns are present in neither the "
-            "parent nor the child's mapped tables",
-            self._join_fixture_compound_expression_1_non_annotated
+        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):
@@ -612,6 +708,80 @@ class DetermineJoinTest(_JoinFixtures, fixtures.TestBase, AssertsCompiledSQL):
                 "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'
 
index 266f7a924a973ec5940a4c9e2ea1e428febe39e6..859aa5b4e02ab846f3bb40b9683dd07c82379237 100644 (file)
@@ -13,7 +13,7 @@ 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 DependencyTwoParentTest(fixtures.MappedTest):
     """Test flush() when a mapper is dependent on multiple relationships"""
@@ -879,7 +879,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',
@@ -2044,6 +2043,148 @@ class InvalidRemoteSideTest(fixtures.MappedTest):
             "mean to set remote_side on the many-to-one side ?", 
             configure_mappers)
 
+class AmbiguousFKResolutionTest(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)
+        assert_raises_message(
+            sa.exc.AmbiguousForeignKeysError,
+            "Could not determine join condition between "
+            "parent/child tables on relationship A.bs - "
+            "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.",
+            sa.orm.configure_mappers
+        )
+
+    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)
+        assert_raises_message(
+            sa.exc.NoForeignKeysError,
+            "Could not determine join condition between "
+            "parent/child tables on relationship A.bs - "
+            "there are no foreign keys linking these "
+            "tables via secondary table 'atob'.  "
+            "Specify 'primaryjoin' and 'secondaryjoin' expressions.",
+            sa.orm.configure_mappers
+        )
+
+    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)
+        assert_raises_message(
+            sa.exc.AmbiguousForeignKeysError,
+            "Could not determine join condition between "
+            "parent/child tables on relationship A.bs - "
+            "there are multiple foreign key paths linking "
+            "the tables via secondary table 'atob_ambiguous'.  "
+            "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.",
+            sa.orm.configure_mappers
+        )
+
+    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):
 
@@ -2070,6 +2211,88 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest):
         class Bar(cls.Basic):
             pass
 
+    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 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 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)
+
     def test_no_join(self):
         bars, Foo, Bar, foos = (self.tables.bars,
                                 self.classes.Foo,
@@ -2080,10 +2303,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_ambig_join(sa.orm.configure_mappers,
+            "Foo.bars", None
+        )
 
     def test_no_join_self_ref(self):
         bars, Foo, Bar, foos = (self.tables.bars,
@@ -2242,7 +2464,7 @@ class InvalidRelationshipEscalationTest(fixtures.MappedTest):
                 sa.orm.configure_mappers)
 
 
-    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,
@@ -2253,13 +2475,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,
@@ -2271,14 +2492,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,