]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Use case insensitive matching on lower_case_table_names=1,2
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 10 Nov 2018 04:18:55 +0000 (23:18 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 10 Nov 2018 04:22:22 +0000 (23:22 -0500)
Fixed regression caused by :ticket:`4344` released in 1.2.13, where the fix
for MySQL 8.0's case sensitivity problem with referenced column names when
reflecting foreign key referents is worked around using the
``information_schema.columns`` view.  The workaround was failing on OSX /
``lower_case_table_names=2`` which produces non-matching casing for the
``information_schema.columns`` vs. that of ``SHOW CREATE TABLE``, so in
case-insensitive SQL modes case-insensitive matching is now used.

Fixes: #4361
Change-Id: I748549bc4c27fad6394593f8ec93fc22bfd01f6c

doc/build/changelog/unreleased_12/4361.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mysql/base.py
test/dialect/mysql/test_reflection.py

diff --git a/doc/build/changelog/unreleased_12/4361.rst b/doc/build/changelog/unreleased_12/4361.rst
new file mode 100644 (file)
index 0000000..3ce8c7a
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, mysql
+    :tickets: 4361
+
+    Fixed regression caused by :ticket:`4344` released in 1.2.13, where the fix
+    for MySQL 8.0's case sensitivity problem with referenced column names when
+    reflecting foreign key referents is worked around using the
+    ``information_schema.columns`` view.  The workaround was failing on OSX /
+    ``lower_case_table_names=2`` which produces non-matching casing for the
+    ``information_schema.columns`` vs. that of ``SHOW CREATE TABLE``, so in
+    case-insensitive SQL modes case-insensitive matching is now used.
index 23e482d2bbb352fc420b65ed8f51367d3f915234..a0215519977df10f30465d3b5acb491a0d3390d6 100644 (file)
@@ -1982,6 +1982,7 @@ class MySQLDialect(default.DefaultDialect):
         self._connection_charset = self._detect_charset(connection)
         self._detect_sql_mode(connection)
         self._detect_ansiquotes(connection)
+        self._detect_casing(connection)
         if self._server_ansiquotes:
             # if ansiquotes == True, build a new IdentifierPreparer
             # with the new setting
@@ -2156,11 +2157,24 @@ class MySQLDialect(default.DefaultDialect):
         # https://bugs.mysql.com/bug.php?id=88718
         # issue #4344 for SQLAlchemy
 
+        # for lower_case_table_names=2, information_schema.columns
+        # preserves the original table/schema casing, but SHOW CREATE
+        # TABLE does not.   this problem is not in lower_case_table_names=1,
+        # but use case-insensitive matching for these two modes in any case.
+        if self._casing in (1, 2):
+            lower = str.lower
+        else:
+            # if on case sensitive, there can be two tables referenced
+            # with the same name different casing, so we need to use
+            # case-sensitive matching.
+            def lower(s):
+                return s
+
         default_schema_name = connection.dialect.default_schema_name
         col_tuples = [
             (
-                rec['referred_schema'] or default_schema_name,
-                rec['referred_table'],
+                lower(rec['referred_schema'] or default_schema_name),
+                lower(rec['referred_table']),
                 col_name
             )
             for rec in fkeys
@@ -2180,16 +2194,28 @@ class MySQLDialect(default.DefaultDialect):
                 ), table_data=col_tuples
             )
 
+            # in casing=0, table name and schema name come back in their
+            # exact case.
+            # in casing=1, table name and schema name come back in lower
+            # case.
+            # in casing=2, table name and schema name come back from the
+            # information_schema.columns view in the case
+            # that was used in CREATE DATABASE and CREATE TABLE, but
+            # SHOW CREATE TABLE converts them to *lower case*, therefore
+            # not matching.  So for this case, case-insensitive lookup
+            # is necessary
             d = defaultdict(dict)
             for schema, tname, cname in correct_for_wrong_fk_case:
-                d[(schema, tname)][cname.lower()] = cname
+                d[(lower(schema), lower(tname))][cname.lower()] = cname
 
             for fkey in fkeys:
                 fkey['referred_columns'] = [
                     d[
                         (
-                            fkey['referred_schema'] or default_schema_name,
-                            fkey['referred_table']
+                            lower(
+                                fkey['referred_schema'] or
+                                default_schema_name),
+                            lower(fkey['referred_table'])
                         )
                     ][col.lower()]
                     for col in fkey['referred_columns']
@@ -2345,6 +2371,7 @@ class MySQLDialect(default.DefaultDialect):
                 cs = 1
             else:
                 cs = int(row[1])
+        self._casing = cs
         return cs
 
     def _detect_collations(self, connection):
index c739d3c4f11245aab35d5f0489b0edcf97432c6c..5de855838fae7a98e514cce340c8b2bd6377318d 100644 (file)
@@ -655,11 +655,13 @@ class ReflectionTest(fixtures.TestBase, AssertsCompiledSQL):
         m1 = self.metadata
 
         Table(
-            'Track', m1, Column('TrackID', Integer, primary_key=True)
+            'Track', m1, Column('TrackID', Integer, primary_key=True),
+            mysql_engine='InnoDB'
         )
         Table(
             'Track', m1, Column('TrackID', Integer, primary_key=True),
-            schema=testing.config.test_schema
+            schema=testing.config.test_schema,
+            mysql_engine='InnoDB'
         )
         Table(
             'PlaylistTrack', m1, Column('id', Integer, primary_key=True),
@@ -671,24 +673,94 @@ class ReflectionTest(fixtures.TestBase, AssertsCompiledSQL):
                     '%s.Track.TrackID' % (testing.config.test_schema,),
                     name='FK_PlaylistTTrackId'
                 )
-            )
+            ),
+            mysql_engine='InnoDB'
         )
         m1.create_all()
 
+        if testing.db.dialect._casing in (1, 2):
+            eq_(
+                inspect(testing.db).get_foreign_keys('PlaylistTrack'),
+                [
+                    {'name': 'FK_PlaylistTTrackId',
+                     'constrained_columns': ['TTrackID'],
+                     'referred_schema': testing.config.test_schema,
+                     'referred_table': 'track',
+                     'referred_columns': ['TrackID'], 'options': {}},
+                    {'name': 'FK_PlaylistTrackId',
+                     'constrained_columns': ['TrackID'],
+                     'referred_schema': None,
+                     'referred_table': 'track',
+                     'referred_columns': ['TrackID'], 'options': {}}
+                ]
+            )
+        else:
+            eq_(
+                inspect(testing.db).get_foreign_keys('PlaylistTrack'),
+                [
+                    {'name': 'FK_PlaylistTTrackId',
+                     'constrained_columns': ['TTrackID'],
+                     'referred_schema': testing.config.test_schema,
+                     'referred_table': 'Track',
+                     'referred_columns': ['TrackID'], 'options': {}},
+                    {'name': 'FK_PlaylistTrackId',
+                     'constrained_columns': ['TrackID'],
+                     'referred_schema': None,
+                     'referred_table': 'Track',
+                     'referred_columns': ['TrackID'], 'options': {}}
+                ]
+            )
+
+    @testing.requires.mysql_fully_case_sensitive
+    @testing.provide_metadata
+    def test_case_sensitive_reflection_dual_case_references(self):
+        # this tests that within the fix we do for MySQL bug
+        # 88718, we don't do case-insensitive logic if the backend
+        # is case sensitive
+        m = self.metadata
+        Table(
+            't1', m,
+            Column('some_id', Integer, primary_key=True),
+            mysql_engine='InnoDB'
+
+        )
+
+        Table(
+            'T1', m,
+            Column('Some_Id', Integer, primary_key=True),
+            mysql_engine='InnoDB'
+        )
+
+        Table(
+            't2', m,
+            Column('id', Integer, primary_key=True),
+            Column('t1id', ForeignKey('t1.some_id', name='t1id_fk')),
+            Column('cap_t1id', ForeignKey('T1.Some_Id', name='cap_t1id_fk')),
+            mysql_engine='InnoDB'
+        )
+        m.create_all(testing.db)
+
         eq_(
-            inspect(testing.db).get_foreign_keys('PlaylistTrack'),
-            [
-                {'name': 'FK_PlaylistTTrackId',
-                 'constrained_columns': ['TTrackID'],
-                 'referred_schema': testing.config.test_schema,
-                 'referred_table': 'Track',
-                 'referred_columns': ['TrackID'], 'options': {}},
-                {'name': 'FK_PlaylistTrackId',
-                 'constrained_columns': ['TrackID'],
-                 'referred_schema': None,
-                 'referred_table': 'Track',
-                 'referred_columns': ['TrackID'], 'options': {}}
-            ]
+            dict(
+                (rec['name'], rec)
+                for rec in inspect(testing.db).get_foreign_keys('t2')
+            ),
+            {
+                'cap_t1id_fk': {
+                    'name': 'cap_t1id_fk',
+                    'constrained_columns': ['cap_t1id'],
+                    'referred_schema': None,
+                    'referred_table': 'T1',
+                    'referred_columns': ['Some_Id'], 'options': {}
+                },
+                't1id_fk': {
+                    'name': 't1id_fk',
+                    'constrained_columns': ['t1id'],
+                    'referred_schema': None,
+                    'referred_table': 't1',
+                    'referred_columns': ['some_id'], 'options': {}
+                },
+            }
         )