]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- Changed the handling in determination of join
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 7 May 2011 16:52:25 +0000 (12:52 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 7 May 2011 16:52:25 +0000 (12:52 -0400)
conditions such that foreign key errors are
only considered between the two given tables.
That is, t1.join(t2) will report FK errors
that involve 't1' or 't2', but anything
involving 't3' will be skipped.   This affects
join(), as well as ORM relationship and
inherit condition logic.  Will keep the more conservative
approach to [ticket:2153] in 0.6.

CHANGES
lib/sqlalchemy/exc.py
lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/schema.py
lib/sqlalchemy/sql/util.py
test/orm/inheritance/test_basic.py
test/orm/test_relationships.py
test/sql/test_selectable.py

diff --git a/CHANGES b/CHANGES
index 2a06242fe716df7c738bb95f24c65a0a0b7fb6c2..daffcfd1315a2351726281eeef600d38c498bfe7 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -35,10 +35,15 @@ CHANGES
 
   - mapper() will ignore non-configured foreign keys
     to unrelated tables when determining inherit
-    condition between parent and child class.
-    This is equivalent to behavior already 
-    applied to declarative.  [ticket:2153]
-    Also in 0.6.8.
+    condition between parent and child class,
+    but will raise as usual for unresolved
+    columns and table names regarding the inherited
+    table.  This is an enhanced generalization of 
+    behavior that was already applied to declarative
+    previously.  [ticket:2153]   0.6.8 has a more
+    conservative version of this which doesn't
+    fundamentally alter how join conditions
+    are determined.
 
   - It is an error to call query.get() when the
     given entity is not a single, full class 
@@ -70,6 +75,15 @@ CHANGES
     Also in 0.6.8.
 
 - sql
+  - Changed the handling in determination of join
+    conditions such that foreign key errors are
+    only considered between the two given tables.
+    That is, t1.join(t2) will report FK errors
+    that involve 't1' or 't2', but anything 
+    involving 't3' will be skipped.   This affects
+    join(), as well as ORM relationship and
+    inherit condition logic.
+
   - Some improvements to error handling inside
     of the execute procedure to ensure auto-close
     connections are really closed when very 
index 99214dfddace9364c37dbd16380e6f95b1224012..c52924cfb747d959ae401561a9da12387fff7269 100644 (file)
@@ -78,9 +78,18 @@ class NoReferenceError(InvalidRequestError):
 class NoReferencedTableError(NoReferenceError):
     """Raised by ``ForeignKey`` when the referred ``Table`` cannot be located."""
 
+    def __init__(self, message, tname):
+        super(NoReferencedTableError, self).__init__(message)
+        self.table_name = tname
+
 class NoReferencedColumnError(NoReferenceError):
     """Raised by ``ForeignKey`` when the referred ``Column`` cannot be located."""
 
+    def __init__(self, message, tname, cname):
+        super(NoReferencedColumnError, self).__init__(message)
+        self.table_name = tname
+        self.column_name = cname
+
 class NoSuchTableError(InvalidRequestError):
     """Table does not exist or is not visible to a connection."""
 
index 03f3e90db2fcdfd92ebb12d00ce90e906b578117..53f8af0b4e67d9cfe2c22f6dba095d0a5dc233dd 100644 (file)
@@ -470,8 +470,7 @@ class Mapper(object):
                         # want (allows test/inheritance.InheritTest4 to pass)
                         self.inherit_condition = sqlutil.join_condition(
                                                     self.inherits.local_table,
-                                                    self.local_table,
-                                                    ignore_nonexistent_tables=True)
+                                                    self.local_table)
                     self.mapped_table = sql.join(
                                                 self.inherits.mapped_table, 
                                                 self.local_table,
index 47fc7b08c9fe11e326d55eca7f4b306cc61ab71a..e85c82ad72970f175712029350a919ce5eaab369 100644 (file)
@@ -1277,7 +1277,8 @@ class ForeignKey(SchemaItem):
                 raise exc.NoReferencedTableError(
                     "Foreign key associated with column '%s' could not find "
                     "table '%s' with which to generate a "
-                    "foreign key to target column '%s'" % (self.parent, tname, colname))
+                    "foreign key to target column '%s'" % (self.parent, tname, colname),
+                    tname)
             table = Table(tname, parenttable.metadata,
                           mustexist=True, schema=schema)
 
@@ -1302,7 +1303,8 @@ class ForeignKey(SchemaItem):
                 raise exc.NoReferencedColumnError(
                     "Could not create ForeignKey '%s' on table '%s': "
                     "table '%s' has no column named '%s'" % (
-                    self._colspec, parenttable.name, table.name, key))
+                    self._colspec, parenttable.name, table.name, key), 
+                    table.name, key)
 
         elif hasattr(self._colspec, '__clause_element__'):
             _column = self._colspec.__clause_element__()
index 853ec9c5f52f1b73d17c6da382d3ee56505baba3..1a3f7d2f8fdf299395daf5de798143893734973b 100644 (file)
@@ -187,7 +187,6 @@ 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):
     """create a join condition between two tables or selectables.
 
@@ -203,11 +202,9 @@ def join_condition(a, b, ignore_nonexistent_tables=False, a_subset=None):
     between the two selectables.   If there are multiple ways
     to join, or no way to join, an error is raised.
 
-    :param ignore_nonexistent_tables: This flag will cause the
-    function to silently skip over foreign key resolution errors
-    due to nonexistent tables - the assumption is that these
-    tables have not yet been defined within an initialization process
-    and are not significant to the operation.
+    :param ignore_nonexistent_tables:  Deprecated - this
+    flag is no longer used.  Only resolution errors regarding
+    the two given tables are propagated.
 
     :param a_subset: An optional expression that is a sub-component
     of ``a``.  An attempt will be made to join to just this sub-component
@@ -228,11 +225,11 @@ def join_condition(a, b, ignore_nonexistent_tables=False, a_subset=None):
                     key=lambda fk:fk.parent._creation_order):
             try:
                 col = fk.get_referent(left)
-            except exc.NoReferencedTableError:
-                if ignore_nonexistent_tables:
-                    continue
-                else:
+            except exc.NoReferenceError, nrte:
+                if nrte.table_name == left.name:
                     raise
+                else:
+                    continue
 
             if col is not None:
                 crit.append(col == fk.parent)
@@ -243,11 +240,13 @@ def join_condition(a, b, ignore_nonexistent_tables=False, a_subset=None):
                         key=lambda fk:fk.parent._creation_order):
                 try:
                     col = fk.get_referent(b)
-                except exc.NoReferencedTableError:
-                    if ignore_nonexistent_tables:
-                        continue
-                    else:
+                except exc.NoReferenceError, nrte:
+                    if nrte.table_name == b.name:
                         raise
+                    else:
+                        # this is totally covered.  can't get
+                        # coverage to mark it.
+                        continue
 
                 if col is not None:
                     crit.append(col == fk.parent)
index 5b0deaf9d7802e92ba23119d902f402a6c52b7e7..ee408373a6267bd6f62790b9bf0a1e57acd19b37 100644 (file)
@@ -1508,7 +1508,7 @@ class NoPKOnSubTableWarningTest(fixtures.TestBase):
         eq_(mc.primary_key, (parent.c.id,))
 
 class InhCondTest(fixtures.TestBase):
-    def test_inh_cond_ignores_others(self):
+    def test_inh_cond_nonexistent_table_unrelated(self):
         metadata = MetaData()
         base_table = Table("base", metadata,
             Column("id", Integer, primary_key=True)
@@ -1532,7 +1532,32 @@ class InhCondTest(fixtures.TestBase):
                     base_table.c.id==derived_table.c.id
                 )
 
-    def test_inh_cond_fails_notfound(self):
+    def test_inh_cond_nonexistent_col_unrelated(self):
+        m = MetaData()
+        base_table = Table("base", m,
+            Column("id", Integer, primary_key=True)
+        )
+        derived_table = Table("derived", m,
+            Column("id", Integer, ForeignKey('base.id'), 
+                primary_key=True),
+            Column('order_id', Integer, ForeignKey('order.foo'))
+        )
+        order_table = Table('order', m, Column('id', Integer, primary_key=True))
+        class Base(object):
+            pass
+
+        class Derived(Base):
+            pass
+
+        mapper(Base, base_table)
+
+        # succeeds, despite "order.foo" doesn't exist
+        m2 = mapper(Derived, derived_table, inherits=Base)
+        assert m2.inherit_condition.compare(
+                    base_table.c.id==derived_table.c.id
+                )
+
+    def test_inh_cond_no_fk(self):
         metadata = MetaData()
         base_table = Table("base", metadata,
             Column("id", Integer, primary_key=True)
@@ -1556,7 +1581,7 @@ class InhCondTest(fixtures.TestBase):
             Derived, derived_table,  inherits=Base
         )
 
-    def test_inh_cond_fails_separate_metas(self):
+    def test_inh_cond_nonexistent_table_related(self):
         m1 = MetaData()
         m2 = MetaData()
         base_table = Table("base", m1,
@@ -1575,21 +1600,46 @@ class InhCondTest(fixtures.TestBase):
 
         mapper(Base, base_table)
 
-        # this used to be "can't resolve foreign key base.id",
-        # but with the flag on, we just get "can't find".  this is
-        # the less-than-ideal case that prevented us from doing this
-        # for mapper(), not just declarative, in the first place.  
-        # there is no case where the failure would be silent - 
-        # there is either a single join condition between the two tables 
-        # or there's not.
+        # the ForeignKey def is correct but there are two
+        # different metadatas.  Would like the traditional
+        # "noreferencedtable" error to raise so that the
+        # user is directed towards the FK definition in question.
         assert_raises_message(
-            sa_exc.ArgumentError,
-            "Can't find any foreign key relationships between "
-            "'base' and 'derived'.",
+            sa_exc.NoReferencedTableError,
+            "Foreign key associated with column 'derived.id' "
+            "could not find table 'base' with which to generate "
+            "a foreign key to target column 'id'",
             mapper,
             Derived, derived_table,  inherits=Base
         )
 
+    def test_inh_cond_nonexistent_col_related(self):
+        m = MetaData()
+        base_table = Table("base", m,
+            Column("id", Integer, primary_key=True)
+        )
+        derived_table = Table("derived", m,
+            Column("id", Integer, ForeignKey('base.q'), 
+                primary_key=True),
+        )
+
+        class Base(object):
+            pass
+
+        class Derived(Base):
+            pass
+
+        mapper(Base, base_table)
+
+        assert_raises_message(
+            sa_exc.NoReferencedColumnError,
+            "Could not create ForeignKey 'base.q' on table "
+            "'derived': table 'base' has no column named 'q'",
+            mapper,
+            Derived, derived_table,  inherits=Base
+        )
+
+
 class PKDiscriminatorTest(fixtures.MappedTest):
     @classmethod
     def define_tables(cls, metadata):
index 0d19d6150897c385fd9cfe511c004d7ac0b06ac6..6781d71045f4ad5619620a32851cd9a5437f757d 100644 (file)
@@ -1125,7 +1125,7 @@ class JoinConditionErrorTest(fixtures.TestBase):
                 configure_mappers)
 
 
-    def test_fk_error_raised(self):
+    def test_fk_error_not_raised_unrelated(self):
         m = MetaData()
         t1 = Table('t1', m, 
             Column('id', Integer, primary_key=True),
@@ -1147,8 +1147,7 @@ class JoinConditionErrorTest(fixtures.TestBase):
 
         mapper(C1, t1, properties={'c2':relationship(C2)})
         mapper(C2, t3)
-
-        assert_raises(sa.exc.NoReferencedColumnError, configure_mappers)
+        assert C1.c2.property.primaryjoin.compare(t1.c.id==t3.c.t1id)
 
     def test_join_error_raised(self):
         m = MetaData()
index 0e98a430aa67ad1dc3ce9ad45fcf44ae0e8ea4fc..a529842bfe5727ef442518d7cee474133e41f544 100644 (file)
@@ -302,6 +302,7 @@ class SelectableTest(fixtures.TestBase, AssertsExecutionResults):
 
         assert_raises(exc.NoReferencedTableError, s.join, t1)
 
+class JoinConditionTest(fixtures.TestBase, AssertsExecutionResults):
 
     def test_join_condition(self):
         m = MetaData()
@@ -373,6 +374,66 @@ class SelectableTest(fixtures.TestBase, AssertsExecutionResults):
                               t1t2.join, t2t3.select(use_labels=True))
 
 
+    def test_join_cond_no_such_unrelated_table(self):
+        m = MetaData()
+        # bounding the "good" column with two "bad" ones is so to 
+        # try to get coverage to get the "continue" statements
+        # in the loop...
+        t1 = Table('t1', m, 
+                            Column('y', Integer, ForeignKey('t22.id')),
+                            Column('x', Integer, ForeignKey('t2.id')), 
+                            Column('q', Integer, ForeignKey('t22.id')), 
+                            )
+        t2 = Table('t2', m, Column('id', Integer))
+        assert sql_util.join_condition(t1, t2).compare(t1.c.x==t2.c.id)
+        assert sql_util.join_condition(t2, t1).compare(t1.c.x==t2.c.id)
+
+    def test_join_cond_no_such_unrelated_column(self):
+        m = MetaData()
+        t1 = Table('t1', m, Column('x', Integer, ForeignKey('t2.id')), 
+                            Column('y', Integer, ForeignKey('t3.q')))
+        t2 = Table('t2', m, Column('id', Integer))
+        t3 = Table('t3', m, Column('id', Integer))
+        assert sql_util.join_condition(t1, t2).compare(t1.c.x==t2.c.id)
+        assert sql_util.join_condition(t2, t1).compare(t1.c.x==t2.c.id)
+
+    def test_join_cond_no_such_related_table(self):
+        m1 = MetaData()
+        m2 = MetaData()
+        t1 = Table('t1', m1, Column('x', Integer, ForeignKey('t2.id')))
+        t2 = Table('t2', m2, Column('id', Integer))
+        assert_raises_message(
+            exc.NoReferencedTableError,
+            "Foreign key associated with column 't1.x' could not find "
+            "table 't2' with which to generate a foreign key to "
+            "target column 'id'",
+            sql_util.join_condition, t1, t2
+        )
+
+        assert_raises_message(
+            exc.NoReferencedTableError,
+            "Foreign key associated with column 't1.x' could not find "
+            "table 't2' with which to generate a foreign key to "
+            "target column 'id'",
+            sql_util.join_condition, t2, t1
+        )
+
+    def test_join_cond_no_such_related_column(self):
+        m = MetaData()
+        t1 = Table('t1', m, Column('x', Integer, ForeignKey('t2.q')))
+        t2 = Table('t2', m, Column('id', Integer))
+        assert_raises_message(
+            exc.NoReferencedColumnError,
+            "Could not create ForeignKey 't2.q' on table 't1': table 't2' has no column named 'q'",
+            sql_util.join_condition, t1, t2
+        )
+
+        assert_raises_message(
+            exc.NoReferencedColumnError,
+            "Could not create ForeignKey 't2.q' on table 't1': table 't2' has no column named 'q'",
+            sql_util.join_condition, t2, t1
+        )
+
 class PrimaryKeyTest(fixtures.TestBase, AssertsExecutionResults):
 
     def test_join_pk_collapse_implicit(self):