]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Reflect unique constraints when reflecting a Table object
authorJohannes Erdfelt <johannes@erdfelt.com>
Wed, 10 Sep 2014 14:37:59 +0000 (07:37 -0700)
committerJohannes Erdfelt <johannes@erdfelt.com>
Wed, 17 Sep 2014 20:19:50 +0000 (13:19 -0700)
Calls to reflect a table did not create any UniqueConstraint objects.
The reflection core made no calls to get_unique_constraints and as
a result, the sqlite dialect would never reflect any unique constraints.

MySQL transparently converts unique constraints into unique indexes, but
SQLAlchemy would reflect those as an Index object and as a
UniqueConstraint. The reflection core will now deduplicate the unique
constraints.

PostgreSQL would reflect unique constraints as an Index object and as
a UniqueConstraint object. The reflection core will now deduplicate
the unique indexes.

lib/sqlalchemy/dialects/mysql/base.py
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/engine/reflection.py
lib/sqlalchemy/testing/suite/test_reflection.py
test/dialect/mysql/test_reflection.py
test/dialect/postgresql/test_reflection.py

index 7ccd59abb33cd3ec757e58c51388e211614a6b07..2f85a362636a46977a159d2b519012265b76226e 100644 (file)
@@ -2590,7 +2590,8 @@ class MySQLDialect(default.DefaultDialect):
         return [
             {
                 'name': key['name'],
-                'column_names': [col[0] for col in key['columns']]
+                'column_names': [col[0] for col in key['columns']],
+                'duplicates_index': key['name'],
             }
             for key in parsed_state.keys
             if key['type'] == 'UNIQUE'
index b9a0d461bcee2da756f8524463f328a81c6e7119..556493b3c336cc22293354bcc6dab3c025368b85 100644 (file)
@@ -2471,14 +2471,19 @@ class PGDialect(default.DefaultDialect):
           SELECT
               i.relname as relname,
               ix.indisunique, ix.indexprs, ix.indpred,
-              a.attname, a.attnum, ix.indkey%s
+              a.attname, a.attnum, c.conrelid, ix.indkey%s
           FROM
               pg_class t
                     join pg_index ix on t.oid = ix.indrelid
-                    join pg_class i on i.oid=ix.indexrelid
+                    join pg_class i on i.oid = ix.indexrelid
                     left outer join
                         pg_attribute a
-                        on t.oid=a.attrelid and %s
+                        on t.oid = a.attrelid and %s
+                    left outer join
+                        pg_constraint c
+                        on (ix.indrelid = c.conrelid and
+                            ix.indexrelid = c.conindid and
+                            c.contype in ('p', 'u', 'x'))
           WHERE
               t.relkind IN ('r', 'v', 'f', 'm')
               and t.oid = :table_oid
@@ -2501,7 +2506,7 @@ class PGDialect(default.DefaultDialect):
 
         sv_idx_name = None
         for row in c.fetchall():
-            idx_name, unique, expr, prd, col, col_num, idx_key = row
+            idx_name, unique, expr, prd, col, col_num, conrelid, idx_key = row
 
             if expr:
                 if idx_name != sv_idx_name:
@@ -2523,11 +2528,14 @@ class PGDialect(default.DefaultDialect):
                 index['cols'][col_num] = col
             index['key'] = [int(k.strip()) for k in idx_key.split()]
             index['unique'] = unique
+            index['duplicates_constraint'] = (None if conrelid is None
+                                                   else idx_name)
 
         return [
             {'name': name,
              'unique': idx['unique'],
-             'column_names': [idx['cols'][i] for i in idx['key']]}
+             'column_names': [idx['cols'][i] for i in idx['key']],
+             'duplicates_constraint': idx['duplicates_constraint']}
             for name, idx in indexes.items()
         ]
 
index c0a3240a53aea39c850cb95fff609ef9095f57ea..330fc2b196c7a31d3b2ca36087ff38fb33fca6ea 100644 (file)
@@ -603,12 +603,15 @@ class Inspector(object):
             columns = index_d['column_names']
             unique = index_d['unique']
             flavor = index_d.get('type', 'index')
+            duplicates = index_d.get('duplicates_constraint')
             if include_columns and \
                     not set(columns).issubset(include_columns):
                 util.warn(
                     "Omitting %s key for (%s), key covers omitted columns." %
                     (flavor, ', '.join(columns)))
                 continue
+            if duplicates:
+                continue
             # look for columns by orig name in cols_by_orig_name,
             # but support columns that are in-Python only as fallback
             idx_cols = []
@@ -626,3 +629,34 @@ class Inspector(object):
                     idx_cols.append(idx_col)
 
             sa_schema.Index(name, *idx_cols, **dict(unique=unique))
+
+        # Unique Constraints
+        constraints = self.get_unique_constraints(table_name, schema)
+        for const_d in constraints:
+            conname = const_d['name']
+            columns = const_d['column_names']
+            duplicates = const_d.get('duplicates_index')
+            if include_columns and \
+                    not set(columns).issubset(include_columns):
+                util.warn(
+                    "Omitting unique constraint key for (%s), "
+                    "key covers omitted columns." %
+                    ', '.join(columns))
+                continue
+            if duplicates:
+                continue
+            # look for columns by orig name in cols_by_orig_name,
+            # but support columns that are in-Python only as fallback
+            constrained_cols = []
+            for c in columns:
+                try:
+                    constrained_col = cols_by_orig_name[c] \
+                        if c in cols_by_orig_name else table.c[c]
+                except KeyError:
+                    util.warn(
+                        "unique constraint key '%s' was not located in "
+                        "columns for table '%s'" % (c, table_name))
+                else:
+                    constrained_cols.append(constrained_col)
+            table.append_constraint(
+                sa_schema.UniqueConstraint(*constrained_cols, name=conname))
index 690a880bb7176c33717c88da211088660e80b0ee..bd0be57380f37ded5660539f64c4696d9cf032e4 100644 (file)
@@ -487,10 +487,12 @@ class ComponentReflectionTest(fixtures.TablesTest):
     @testing.requires.temp_table_reflection
     def test_get_temp_table_unique_constraints(self):
         insp = inspect(self.metadata.bind)
-        eq_(
-            insp.get_unique_constraints('user_tmp'),
-            [{'column_names': ['name'], 'name': 'user_tmp_uq'}]
-        )
+        reflected = insp.get_unique_constraints('user_tmp')
+        for refl in reflected:
+            # Different dialects handle duplicate index and constraints
+            # differently, so ignore this flag
+            refl.pop('duplicates_index', None)
+        eq_(reflected, [{'column_names': ['name'], 'name': 'user_tmp_uq'}])
 
     @testing.requires.temp_table_reflection
     def test_get_temp_table_indexes(self):
@@ -544,6 +546,9 @@ class ComponentReflectionTest(fixtures.TablesTest):
         )
 
         for orig, refl in zip(uniques, reflected):
+            # Different dialects handle duplicate index and constraints
+            # differently, so ignore this flag
+            refl.pop('duplicates_index', None)
             eq_(orig, refl)
 
     @testing.provide_metadata
index bf35a2c6bf4af3c49b1ab18f260c07ea0cb74f2b..b8f72b942611c2306db730e5ad5faa6ed1b6ad32 100644 (file)
@@ -283,6 +283,37 @@ class ReflectionTest(fixtures.TestBase, AssertsExecutionResults):
         view_names = dialect.get_view_names(connection, "information_schema")
         self.assert_('TABLES' in view_names)
 
+    def test_reflection_with_unique_constraint(self):
+        insp = inspect(testing.db)
+
+        uc_table = Table('mysql_uc', MetaData(testing.db),
+                         Column('a', String(10)),
+                         UniqueConstraint('a', name='uc_a'))
+
+        try:
+            uc_table.create()
+
+            # MySQL converts unique constraints into unique indexes and
+            # the 0.9 API returns it as both an index and a constraint
+            indexes = set(i['name'] for i in insp.get_indexes('mysql_uc'))
+            constraints = set(i['name']
+                              for i in insp.get_unique_constraints('mysql_uc'))
+
+            self.assert_('uc_a' in indexes)
+            self.assert_('uc_a' in constraints)
+
+            # However, upon creating a Table object via reflection, it should
+            # only appear as a unique index and not a constraint
+            reflected = Table('mysql_uc', MetaData(testing.db), autoload=True)
+
+            indexes = set(i.name for i in reflected.indexes)
+            constraints = set(uc.name for uc in reflected.constraints)
+
+            self.assert_('uc_a' in indexes)
+            self.assert_('uc_a' not in constraints)
+        finally:
+            uc_table.drop()
+
 
 class RawReflectionTest(fixtures.TestBase):
     def setup(self):
index b8b9be3dec912009ee6db0d33587c8b128428638..fc013c72aac8b1a2c9780ceb125873aba9fbaf3b 100644 (file)
@@ -7,7 +7,7 @@ from sqlalchemy.testing import fixtures
 from sqlalchemy import testing
 from sqlalchemy import inspect
 from sqlalchemy import Table, Column, MetaData, Integer, String, \
-    PrimaryKeyConstraint, ForeignKey, join, Sequence
+    PrimaryKeyConstraint, ForeignKey, join, Sequence, UniqueConstraint
 from sqlalchemy import exc
 import sqlalchemy as sa
 from sqlalchemy.dialects.postgresql import base as postgresql
@@ -656,7 +656,8 @@ class ReflectionTest(fixtures.TestBase):
         conn.execute("ALTER TABLE t RENAME COLUMN x to y")
 
         ind = testing.db.dialect.get_indexes(conn, "t", None)
-        eq_(ind, [{'unique': False, 'column_names': ['y'], 'name': 'idx1'}])
+        eq_(ind, [{'unique': False, 'duplicates_constraint': None,
+                   'column_names': ['y'], 'name': 'idx1'}])
         conn.close()
 
     @testing.provide_metadata
@@ -803,6 +804,38 @@ class ReflectionTest(fixtures.TestBase):
                 'labels': ['sad', 'ok', 'happy']
             }])
 
+    def test_reflection_with_unique_constraint(self):
+        insp = inspect(testing.db)
+
+        uc_table = Table('pgsql_uc', MetaData(testing.db),
+                         Column('a', String(10)),
+                         UniqueConstraint('a', name='uc_a'))
+
+        try:
+            uc_table.create()
+
+            # PostgreSQL will create an implicit index for a unique
+            # constraint. As a result, the 0.9 API returns it as both
+            # an index and a constraint
+            indexes = set(i['name'] for i in insp.get_indexes('pgsql_uc'))
+            constraints = set(i['name']
+                              for i in insp.get_unique_constraints('pgsql_uc'))
+
+            self.assert_('uc_a' in indexes)
+            self.assert_('uc_a' in constraints)
+
+            # However, upon creating a Table object via reflection, it should
+            # only appear as a unique constraint and not an index
+            reflected = Table('pgsql_uc', MetaData(testing.db), autoload=True)
+
+            indexes = set(i.name for i in reflected.indexes)
+            constraints = set(uc.name for uc in reflected.constraints)
+
+            self.assert_('uc_a' not in indexes)
+            self.assert_('uc_a' in constraints)
+        finally:
+            uc_table.drop()
+
 
 class CustomTypeReflectionTest(fixtures.TestBase):