From: Mike Bayer Date: Sat, 10 Nov 2018 04:18:55 +0000 (-0500) Subject: Use case insensitive matching on lower_case_table_names=1,2 X-Git-Tag: rel_1_3_0b1~20 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=af159c56957b26c523e7fe324edf7d17882f88be;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Use case insensitive matching on lower_case_table_names=1,2 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 --- diff --git a/doc/build/changelog/unreleased_12/4361.rst b/doc/build/changelog/unreleased_12/4361.rst new file mode 100644 index 0000000000..3ce8c7ad1b --- /dev/null +++ b/doc/build/changelog/unreleased_12/4361.rst @@ -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. diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 23e482d2bb..a021551997 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -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): diff --git a/test/dialect/mysql/test_reflection.py b/test/dialect/mysql/test_reflection.py index c739d3c4f1..5de855838f 100644 --- a/test/dialect/mysql/test_reflection.py +++ b/test/dialect/mysql/test_reflection.py @@ -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': {} + }, + } )