From: Mike Bayer Date: Thu, 2 Jun 2011 23:52:26 +0000 (-0400) Subject: - Unit tests pass 100% on MySQL installed X-Git-Tag: rel_0_7_1~13 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9211ecb6cfdc8e213a6e8154aaffc6c81da5820f;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Unit tests pass 100% on MySQL installed on windows, after aggressive exclusion of a wide variety of tests. Not clear to what degree the failures are related to version 5.5 vs. the usage of windows, in particular the ON UPDATE CASCADE immediately crashes the server. The features being tested here are all edge cases not likely to be used in typical MySQL environments. - Removed the "adjust casing" step that would fail when reflecting a table on MySQL on windows with a mixed case name. After some experimenting with a windows MySQL server, it's been determined that this step wasn't really helping the situation much; MySQL does not return FK names with proper casing on non-windows platforms either, and removing the step at least allows the reflection to act more like it does on other OSes. A warning here has been considered but its difficult to determine under what conditions such a warning can be raised, so punted on that for now - added some docs instead. [ticket:2181] - supports_sane_rowcount will be set to False if using MySQLdb and the DBAPI doesn't provide the constants.CLIENT module. --- diff --git a/CHANGES b/CHANGES index 4c8a86db12..36b4fe275c 100644 --- a/CHANGES +++ b/CHANGES @@ -25,6 +25,29 @@ CHANGES and are redundant: reflecttable(), create(), drop(), text(), engine.func +- mysql + - Unit tests pass 100% on MySQL installed + on windows. + + - Removed the "adjust casing" step that would + fail when reflecting a table on MySQL + on windows with a mixed case name. After some + experimenting with a windows MySQL server, it's + been determined that this step wasn't really + helping the situation much; MySQL does not return + FK names with proper casing on non-windows + platforms either, and removing the step at + least allows the reflection to act more like + it does on other OSes. A warning here + has been considered but its difficult to + determine under what conditions such a warning + can be raised, so punted on that for now - + added some docs instead. [ticket:2181] + + - supports_sane_rowcount will be set to False + if using MySQLdb and the DBAPI doesn't provide + the constants.CLIENT module. + 0.7.0 ======= - This section documents those changes from 0.7b4 diff --git a/README.unittests b/README.unittests index 0ae6e155e5..d06452d359 100644 --- a/README.unittests +++ b/README.unittests @@ -102,6 +102,9 @@ expect them to be present will fail. Additional steps specific to individual databases are as follows: + MYSQL: Default storage engine should be "MyISAM". Tests that require + "InnoDB" as the engine will specify this explicitly. + ORACLE: a user named "test_schema" is created. The primary database user needs to be able to create and drop tables, diff --git a/lib/sqlalchemy/connectors/mysqldb.py b/lib/sqlalchemy/connectors/mysqldb.py index 189c412a00..b5a9f05a8d 100644 --- a/lib/sqlalchemy/connectors/mysqldb.py +++ b/lib/sqlalchemy/connectors/mysqldb.py @@ -93,7 +93,7 @@ class MySQLDBConnector(Connector): ).constants.CLIENT client_flag |= CLIENT_FLAGS.FOUND_ROWS except (AttributeError, ImportError): - pass + self.supports_sane_rowcount = False opts['client_flag'] = client_flag return [[], opts] diff --git a/lib/sqlalchemy/dialects/mysql/base.py b/lib/sqlalchemy/dialects/mysql/base.py index 33dc8a73e4..bb23c12afa 100644 --- a/lib/sqlalchemy/dialects/mysql/base.py +++ b/lib/sqlalchemy/dialects/mysql/base.py @@ -68,6 +68,22 @@ creation option can be specified in this syntax:: mysql_charset='utf8' ) +Case Sensitivity and Table Reflection +------------------------------------- + +MySQL has inconsistent support for case-sensitive identifier +names, basing support on specific details of the underlying +operating system. However, it has been observed that no matter +what case sensitivity behavior is present, the names of tables in +foreign key declarations are *always* received from the database +as all-lower case, making it impossible to accurately reflect a +schema where inter-related tables use mixed-case identifier names. + +Therefore it is strongly advised that table names be declared as +all lower case both within SQLAlchemy as well as on the MySQL +database itself, especially if database reflection features are +to be used. + Keys ---- @@ -1974,17 +1990,6 @@ class MySQLDialect(default.DefaultDialect): sql = parser._describe_to_create(table_name, columns) return parser.parse(sql, charset) - def _adjust_casing(self, table, charset=None): - """Adjust Table name to the server case sensitivity, if needed.""" - - casing = self._server_casing - - # For winxx database hosts. TODO: is this really needed? - if casing == 1 and table.name != table.name.lower(): - table.name = table.name.lower() - lc_alias = sa_schema._get_table_key(table.name, table.schema) - table.metadata.tables[lc_alias] = table - def _detect_charset(self, connection): raise NotImplementedError() @@ -2114,7 +2119,7 @@ class MySQLTableDefinitionParser(object): self.dialect = dialect self.preparer = preparer self._prep_regexes() - + def parse(self, show_create, charset): state = ReflectedState() state.charset = charset diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index ca43603253..e87c1ce0fd 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -346,12 +346,6 @@ class Inspector(object): """ dialect = self.bind.dialect - # MySQL dialect does this. Applicable with other dialects? - if hasattr(dialect, '_connection_charset') \ - and hasattr(dialect, '_adjust_casing'): - charset = dialect._connection_charset - dialect._adjust_casing(table) - # table attributes we might need. reflection_options = dict( (k, table.kwargs.get(k)) for k in dialect.reflection_options if k in table.kwargs) diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 5a684eaa50..febd83c1b1 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -394,6 +394,7 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): finally: meta.drop_all() + @testing.provide_metadata def test_nonreflected_fk_raises(self): """test that a NoReferencedColumnError is raised when reflecting a table with an FK to another table which has not included the target @@ -401,7 +402,7 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): """ - meta = MetaData(testing.db) + meta = self.metadata a1 = Table('a', meta, Column('x', sa.Integer, primary_key=True), Column('z', sa.Integer), @@ -412,15 +413,11 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): test_needs_fk=True ) meta.create_all() - try: - m2 = MetaData(testing.db) - a2 = Table('a', m2, include_columns=['z'], autoload=True) - b2 = Table('b', m2, autoload=True) - - assert_raises(sa.exc.NoReferencedColumnError, a2.join, b2) - finally: - meta.drop_all() + m2 = MetaData(testing.db) + a2 = Table('a', m2, include_columns=['z'], autoload=True) + b2 = Table('b', m2, autoload=True) + assert_raises(sa.exc.NoReferencedColumnError, a2.join, b2) @testing.exclude('mysql', '<', (4, 1, 1), 'innodb funkiness') def test_override_existing_fk(self): @@ -585,7 +582,10 @@ class ReflectionTest(fixtures.TestBase, ComparesTables): @testing.crashes('oracle', 'FIXME: unknown, confirm not fails_on') - @testing.fails_on('+informixdb', 'FIXME: should be supported via the DELIMITED env var but that breaks everything else for now') + @testing.fails_on('+informixdb', + "FIXME: should be supported via the " + "DELIMITED env var but that breaks " + "everything else for now") def test_reserved(self): # check a table that uses an SQL reserved name doesn't cause an @@ -1136,6 +1136,48 @@ class ReverseCasingReflectTest(fixtures.TestBase, AssertsCompiledSQL): 'weird_casing."Col2", weird_casing."col3" ' 'FROM weird_casing') +class CaseSensitiveTest(fixtures.TablesTest): + """Nail down case sensitive behaviors, mostly on MySQL.""" + + @classmethod + def define_tables(cls, metadata): + Table('SomeTable', metadata, + Column('x', Integer, primary_key=True), + test_needs_fk=True + ) + Table('SomeOtherTable', metadata, + Column('x', Integer, primary_key=True), + Column('y', Integer, sa.ForeignKey("SomeTable.x")), + test_needs_fk=True + ) + + @testing.fails_if(testing.requires._has_mysql_on_windows) + def test_table_names(self): + x = testing.db.run_callable( + testing.db.dialect.get_table_names + ) + assert set(["SomeTable", "SomeOtherTable"]).issubset(x) + + def test_reflect_exact_name(self): + m = MetaData() + t1 = Table("SomeTable", m, autoload=True, autoload_with=testing.db) + eq_(t1.name, "SomeTable") + assert t1.c.x is not None + + @testing.fails_on('mysql', 'FKs come back as lower case no matter what') + def test_reflect_via_fk(self): + m = MetaData() + t2 = Table("SomeOtherTable", m, autoload=True, autoload_with=testing.db) + eq_(t2.name, "SomeOtherTable") + assert "SomeTable" in m.tables + + @testing.fails_on_everything_except('sqlite', 'mysql') + def test_reflect_case_insensitive(self): + m = MetaData() + t2 = Table("sOmEtAbLe", m, autoload=True, autoload_with=testing.db) + eq_(t2.name, "sOmEtAbLe") + + class ComponentReflectionTest(fixtures.TestBase): @testing.requires.schemas diff --git a/test/engine/test_transaction.py b/test/engine/test_transaction.py index e4046f4d36..1184befda9 100644 --- a/test/engine/test_transaction.py +++ b/test/engine/test_transaction.py @@ -276,6 +276,7 @@ class TransactionTest(fixtures.TestBase): # PG emergency shutdown: # select * from pg_prepared_xacts # ROLLBACK PREPARED '' + @testing.requires.skip_mysql_on_windows @testing.requires.two_phase_transactions @testing.requires.savepoints def test_mixed_two_phase_transaction(self): diff --git a/test/lib/requires.py b/test/lib/requires.py index f79c53c44c..42ac82988b 100644 --- a/test/lib/requires.py +++ b/test/lib/requires.py @@ -129,11 +129,11 @@ def savepoints(fn): return _chain_decorators_on( fn, emits_warning_on('mssql', 'Savepoint support in mssql is experimental and may lead to data loss.'), - no_support('access', 'not supported by database'), - no_support('sqlite', 'not supported by database'), - no_support('sybase', 'FIXME: guessing, needs confirmation'), - exclude('mysql', '<', (5, 0, 3), 'not supported by database'), - exclude('informix', '<', (11, 55, 'xC3'), 'not supported by database'), + no_support('access', 'savepoints not supported'), + no_support('sqlite', 'savepoints not supported'), + no_support('sybase', 'savepoints not supported'), + exclude('mysql', '<', (5, 0, 3), 'savepoints not supported'), + exclude('informix', '<', (11, 55, 'xC3'), 'savepoints not supported'), ) def denormalized_names(fn): @@ -369,6 +369,10 @@ def _has_sqlite(): except ImportError: return False +def _has_mysql_on_windows(): + return testing.against('mysql+mysqldb') and \ + testing.db.dialect._server_casing == 1 + def sqlite(fn): return _chain_decorators_on( fn, @@ -387,3 +391,13 @@ def ad_hoc_engines(fn): fn, skip_if(lambda: config.options.low_connections) ) + +def skip_mysql_on_windows(fn): + """Catchall for a large variety of MySQL on Windows failures""" + + return _chain_decorators_on( + fn, + skip_if(_has_mysql_on_windows, + "Not supported on MySQL + Windows" + ) + ) \ No newline at end of file diff --git a/test/lib/testing.py b/test/lib/testing.py index f5babc19c3..90e8b7746d 100644 --- a/test/lib/testing.py +++ b/test/lib/testing.py @@ -267,6 +267,7 @@ def _is_excluded(db, op, spec): _is_excluded('bigdb', '==', (9,0,9)) _is_excluded('yikesdb', 'in', ((0, 3, 'alpha2'), (0, 3, 'alpha3'))) """ + vendor_spec = db_spec(db) if not vendor_spec(config.db): diff --git a/test/orm/test_naturalpks.py b/test/orm/test_naturalpks.py index 34b392cd14..f0f3ebb211 100644 --- a/test/orm/test_naturalpks.py +++ b/test/orm/test_naturalpks.py @@ -15,6 +15,9 @@ from test.lib import fixtures from test.orm import _fixtures class NaturalPKTest(fixtures.MappedTest): + # MySQL 5.5 on Windows crashes (the entire server, not the client) + # if you screw around with ON UPDATE CASCADE type of stuff. + __requires__ = 'skip_mysql_on_windows', @classmethod def define_tables(cls, metadata): @@ -616,6 +619,8 @@ class SelfReferentialTest(fixtures.MappedTest): class NonPKCascadeTest(fixtures.MappedTest): + __requires__ = 'skip_mysql_on_windows', + @classmethod def define_tables(cls, metadata): if testing.against('oracle'): @@ -983,6 +988,8 @@ class JoinedInheritanceTest(fixtures.MappedTest): # mssql doesn't allow ON UPDATE on self-referential keys __unsupported_on__ = ('mssql',) + __requires__ = 'skip_mysql_on_windows', + @classmethod def define_tables(cls, metadata): if testing.against('oracle'): diff --git a/test/sql/test_types.py b/test/sql/test_types.py index 6ccb67f919..4fbafe6848 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -761,8 +761,9 @@ class EnumTest(fixtures.TestBase): eq_(e1.adapt(ENUM).name, 'foo') eq_(e1.adapt(ENUM).schema, 'bar') - @testing.fails_on('mysql+mysqldb', "MySQL seems to issue a 'data truncated' warning.") - @testing.fails_on('mysql+pymysql', "MySQL seems to issue a 'data truncated' warning.") + @testing.crashes('mysql', + 'Inconsistent behavior across various OS/drivers' + ) def test_constraint(self): assert_raises(exc.DBAPIError, enum_table.insert().execute,