From: Mike Bayer Date: Sun, 2 Sep 2012 19:14:09 +0000 (-0400) Subject: - fixes for mxODBC, some pyodbc X-Git-Tag: rel_0_8_0b1~180 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=626ae5b7a1625835011c25469937549a74186157;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - fixes for mxODBC, some pyodbc - enhancements to test suite including ability to set up a testing engine for a whole test class, fixes to how noseplugin sets up/tears down per-class context --- diff --git a/lib/sqlalchemy/connectors/mxodbc.py b/lib/sqlalchemy/connectors/mxodbc.py index 4456f351f4..b65a43074b 100644 --- a/lib/sqlalchemy/connectors/mxodbc.py +++ b/lib/sqlalchemy/connectors/mxodbc.py @@ -28,8 +28,8 @@ class MxODBCConnector(Connector): driver='mxodbc' supports_sane_multi_rowcount = False - supports_unicode_statements = False - supports_unicode_binds = False + supports_unicode_statements = True + supports_unicode_binds = True supports_native_decimal = True @@ -130,21 +130,28 @@ class MxODBCConnector(Connector): version.append(n) return tuple(version) - def do_execute(self, cursor, statement, parameters, context=None): + def _get_direct(self, context): + return True if context: native_odbc_execute = context.execution_options.\ get('native_odbc_execute', 'auto') if native_odbc_execute is True: # user specified native_odbc_execute=True - cursor.execute(statement, parameters) + return False elif native_odbc_execute is False: # user specified native_odbc_execute=False - cursor.executedirect(statement, parameters) + return True elif context.is_crud: # statement is UPDATE, DELETE, INSERT - cursor.execute(statement, parameters) + return False else: # all other statements - cursor.executedirect(statement, parameters) + return True else: - cursor.executedirect(statement, parameters) + return True + + def do_executemany(self, cursor, statement, parameters, context=None): + cursor.executemany(statement, parameters, direct=self._get_direct(context)) + + def do_execute(self, cursor, statement, parameters, context=None): + cursor.execute(statement, parameters, direct=self._get_direct(context)) diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 9c2c1555d9..03bbf6446d 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -287,6 +287,7 @@ class TIME(sqltypes.TIME): else: return value return process +_MSTime = TIME class _DateTimeBase(object): def bind_processor(self, dialect): @@ -973,10 +974,6 @@ class MSSQLStrictCompiler(MSSQLCompiler): self.process(binary.right, **kw) ) - def visit_function(self, func, **kw): - kw['literal_binds'] = True - return super(MSSQLStrictCompiler, self).visit_function(func, **kw) - def render_literal_value(self, value, type_): """ For date and datetime values, convert to a string diff --git a/lib/sqlalchemy/dialects/mssql/mxodbc.py b/lib/sqlalchemy/dialects/mssql/mxodbc.py index 3f0c106c29..0044b5f4fc 100644 --- a/lib/sqlalchemy/dialects/mssql/mxodbc.py +++ b/lib/sqlalchemy/dialects/mssql/mxodbc.py @@ -57,10 +57,28 @@ from ...connectors.mxodbc import MxODBCConnector from .pyodbc import MSExecutionContext_pyodbc from .base import (MSDialect, MSSQLStrictCompiler, - _MSDateTime, _MSDate, TIME) + _MSDateTime, _MSDate, _MSTime) +class _MSDate_mxodbc(_MSDate): + def bind_processor(self, dialect): + def process(value): + if value is not None: + return "%s-%s-%s" % (value.year, value.month, value.day) + else: + return None + return process + +class _MSTime_mxodbc(_MSTime): + def bind_processor(self, dialect): + def process(value): + if value is not None: + return "%s:%s:%s" % (value.hour, value.minute, value.second) + else: + return None + return process + class MSExecutionContext_mxodbc(MSExecutionContext_pyodbc): """ The pyodbc execution context is useful for enabling @@ -80,12 +98,12 @@ class MSDialect_mxodbc(MxODBCConnector, MSDialect): colspecs = { #sqltypes.Numeric : _MSNumeric, sqltypes.DateTime : _MSDateTime, - sqltypes.Date : _MSDate, - sqltypes.Time : TIME, + sqltypes.Date : _MSDate_mxodbc, + sqltypes.Time : _MSTime_mxodbc, } - def __init__(self, description_encoding='latin-1', **params): + def __init__(self, description_encoding=None, **params): super(MSDialect_mxodbc, self).__init__(**params) self.description_encoding = description_encoding diff --git a/lib/sqlalchemy/dialects/mssql/pyodbc.py b/lib/sqlalchemy/dialects/mssql/pyodbc.py index 616c906cd5..83bf7ee6b0 100644 --- a/lib/sqlalchemy/dialects/mssql/pyodbc.py +++ b/lib/sqlalchemy/dialects/mssql/pyodbc.py @@ -242,7 +242,7 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect): } ) - def __init__(self, description_encoding='latin-1', **params): + def __init__(self, description_encoding=None, **params): super(MSDialect_pyodbc, self).__init__(**params) self.description_encoding = description_encoding self.use_scope_identity = self.use_scope_identity and \ diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index af7341081c..47594407f2 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -947,7 +947,9 @@ class Connection(Connectable): ex_text = str(e) except TypeError: ex_text = repr(e) - self.connection._logger.warn("Error closing cursor: %s", ex_text) + if not self.closed: + self.connection._logger.warn( + "Error closing cursor: %s", ex_text) if isinstance(e, (SystemExit, KeyboardInterrupt)): raise diff --git a/test/bootstrap/noseplugin.py b/test/bootstrap/noseplugin.py index dc14b48b33..e664552dc5 100644 --- a/test/bootstrap/noseplugin.py +++ b/test/bootstrap/noseplugin.py @@ -132,17 +132,6 @@ class NoseSQLAlchemy(Plugin): elif cls.__name__.startswith('_'): return False else: - if hasattr(cls, 'setup_class'): - existing_setup = cls.setup_class.im_func - else: - existing_setup = None - @classmethod - def setup_class(cls): - self._do_skips(cls) - if existing_setup: - existing_setup(cls) - cls.setup_class = setup_class - return True def _do_skips(self, cls): @@ -177,7 +166,8 @@ class NoseSQLAlchemy(Plugin): ) for db, op, spec in getattr(cls, '__excluded_on__', ()): - testing.exclude(db, op, spec, "'%s' unsupported on DB %s version %s" % ( + testing.exclude(db, op, spec, + "'%s' unsupported on DB %s version %s" % ( cls.__name__, testing.db.name, testing._server_version())) @@ -189,7 +179,30 @@ class NoseSQLAlchemy(Plugin): engines.testing_reaper._after_test_ctx() testing.resetwarnings() + def _setup_cls_engines(self, cls): + engine_opts = getattr(cls, '__testing_engine__', None) + if engine_opts: + self._save_testing_db = testing.db + testing.db = engines.testing_engine(options=engine_opts) + + def _teardown_cls_engines(self, cls): + engine_opts = getattr(cls, '__testing_engine__', None) + if engine_opts: + testing.db = self._save_testing_db + del self._save_testing_db + + def startContext(self, ctx): + if not isinstance(ctx, type) \ + or not issubclass(ctx, fixtures.TestBase): + return + self._do_skips(ctx) + self._setup_cls_engines(ctx) + def stopContext(self, ctx): + if not isinstance(ctx, type) \ + or not issubclass(ctx, fixtures.TestBase): + return engines.testing_reaper._stop_test_ctx() + self._teardown_cls_engines(ctx) if not config.options.low_connections: testing.global_cleanup_assertions() diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index 37cb9965c7..94b9dfb7b3 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -1144,10 +1144,10 @@ class EngineEventsTest(fixtures.TestBase): for engine in [ engines.testing_engine(options=dict(implicit_returning=False)), - #engines.testing_engine(options=dict(implicit_returning=False, - # strategy='threadlocal')), - #engines.testing_engine(options=dict(implicit_returning=False)).\ - # connect() + engines.testing_engine(options=dict(implicit_returning=False, + strategy='threadlocal')), + engines.testing_engine(options=dict(implicit_returning=False)).\ + connect() ]: event.listen(engine, 'before_execute', execute) event.listen(engine, 'before_cursor_execute', cursor_execute) @@ -1181,7 +1181,9 @@ class EngineEventsTest(fixtures.TestBase): ('INSERT INTO t1 (c1, c2)', { 'c2': 'some data', 'c1': 5}, (5, 'some data')), - ('SELECT lower', {'lower_2': 'Foo'}, ('Foo', )), + ('SELECT lower', {'lower_2': 'Foo'}, + () if testing.against('mssql+mxodbc') else + ('Foo', )), ('INSERT INTO t1 (c1, c2)', {'c2': 'foo', 'c1': 6}, (6, 'foo')), @@ -1446,7 +1448,9 @@ class ProxyConnectionTest(fixtures.TestBase): ('CREATE TABLE t1', {}, ()), ('INSERT INTO t1 (c1, c2)', {'c2': 'some data', 'c1' : 5}, (5, 'some data')), - ('SELECT lower', {'lower_2': 'Foo'}, ('Foo', )), + ('SELECT lower', {'lower_2': 'Foo'}, + () if testing.against('mssql+mxodbc') + else ('Foo', )), ('INSERT INTO t1 (c1, c2)', {'c2': 'foo', 'c1': 6}, (6, 'foo')), ('select * from t1', {}, ()), diff --git a/test/lib/fixtures.py b/test/lib/fixtures.py index 00d35df752..3281e1a00a 100644 --- a/test/lib/fixtures.py +++ b/test/lib/fixtures.py @@ -26,6 +26,11 @@ class TestBase(object): # skipped. __skip_if__ = None + # replace testing.db with a testing.engine() + # for the duration of this suite, using the given + # arguments + __testing_engine__ = None + def assert_(self, val, msg=None): assert val, msg diff --git a/test/lib/requires.py b/test/lib/requires.py index 3fb5e7e14b..c91c87cd74 100644 --- a/test/lib/requires.py +++ b/test/lib/requires.py @@ -58,6 +58,17 @@ def boolean_col_expressions(fn): no_support('informix', 'not supported by database'), ) +def standalone_binds(fn): + """target database/driver supports bound parameters as column expressions + without being in the context of a typed column. + + """ + return _chain_decorators_on( + fn, + no_support('firebird', 'not supported by driver'), + no_support('mssql+mxodbc', 'not supported by driver') + ) + def identity(fn): """Target database must support GENERATED AS IDENTITY or a facsimile. @@ -89,6 +100,16 @@ def reflectable_autoincrement(fn): no_support('sybase', 'not supported by database'), ) +def binary_comparisons(fn): + """target database/driver can allow BLOB/BINARY fields to be compared + against a bound parameter value. + """ + return _chain_decorators_on( + fn, + no_support('oracle', 'not supported by database/driver'), + no_support('mssql', 'not supported by database/driver') + ) + def independent_cursors(fn): """Target must support simultaneous, independent database cursors on a single connection.""" @@ -317,7 +338,21 @@ def cextensions(fn): ) +def emulated_lastrowid(fn): + """"target dialect retrieves cursor.lastrowid or an equivalent + after an insert() construct executes. + """ + return _chain_decorators_on( + fn, + fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', + 'sqlite+pysqlite', 'mysql+pymysql', + 'mssql+pyodbc', 'mssql+mxodbc'), + ) + def dbapi_lastrowid(fn): + """"target backend includes a 'lastrowid' accessor on the DBAPI + cursor object. + """ return _chain_decorators_on( fn, fails_on_everything_except('mysql+mysqldb', 'mysql+oursql', diff --git a/test/sql/test_defaults.py b/test/sql/test_defaults.py index 55aa86633b..b5277c6239 100644 --- a/test/sql/test_defaults.py +++ b/test/sql/test_defaults.py @@ -13,6 +13,7 @@ from sqlalchemy.dialects import sqlite from test.lib import fixtures class DefaultTest(fixtures.TestBase): + __testing_engine__ = {'execution_options':{'native_odbc_execute':False}} @classmethod def setup_class(cls): @@ -404,6 +405,7 @@ class DefaultTest(fixtures.TestBase): class PKDefaultTest(fixtures.TablesTest): __requires__ = ('subqueries',) + __testing_engine__ = {'execution_options':{'native_odbc_execute':False}} @classmethod def define_tables(cls, metadata): @@ -439,6 +441,7 @@ class PKDefaultTest(fixtures.TablesTest): class PKIncrementTest(fixtures.TablesTest): run_define_tables = 'each' + __testing_engine__ = {'execution_options':{'native_odbc_execute':False}} @classmethod def define_tables(cls, metadata): diff --git a/test/sql/test_query.py b/test/sql/test_query.py index 67c0fec225..a0c849de53 100644 --- a/test/sql/test_query.py +++ b/test/sql/test_query.py @@ -710,6 +710,8 @@ class QueryTest(fixtures.TestBase): use_labels=labels), [(3, 'a'), (2, 'b'), (1, None)]) + @testing.fails_on('mssql+pyodbc', + "pyodbc result row doesn't support slicing") def test_column_slices(self): users.insert().execute(user_id=1, user_name='john') users.insert().execute(user_id=2, user_name='jack') @@ -1276,6 +1278,7 @@ class RequiredBindTest(fixtures.TablesTest): stmt, {'data': 'data'} ) + @testing.requires.standalone_binds def test_select_columns(self): stmt = select([bindparam('data'), bindparam('x')]) self._assert_raises( @@ -1394,6 +1397,26 @@ class TableInsertTest(fixtures.TablesTest): inserted_primary_key=[1] ) + def test_uppercase_direct_params(self): + t = self.tables.foo + self._test( + t.insert().values(id=1, data='data', x=5), + (1, 'data', 5), + inserted_primary_key=[1] + ) + + @testing.requires.returning + def test_uppercase_direct_params_returning(self): + t = self.tables.foo + self._test( + t.insert().values( + id=1, data='data', x=5).returning(t.c.id, t.c.x), + (1, 'data', 5), + returning=(1, 5) + ) + + @testing.fails_on('mssql', + "lowercase table doesn't support identity insert disable") def test_direct_params(self): t = self._fixture() self._test( @@ -1402,6 +1425,8 @@ class TableInsertTest(fixtures.TablesTest): inserted_primary_key=[] ) + @testing.fails_on('mssql', + "lowercase table doesn't support identity insert disable") @testing.requires.returning def test_direct_params_returning(self): t = self._fixture() @@ -1412,7 +1437,7 @@ class TableInsertTest(fixtures.TablesTest): returning=(1, 5) ) - @testing.requires.dbapi_lastrowid + @testing.requires.emulated_lastrowid def test_implicit_pk(self): t = self._fixture() self._test( @@ -1422,7 +1447,7 @@ class TableInsertTest(fixtures.TablesTest): inserted_primary_key=[] ) - @testing.requires.dbapi_lastrowid + @testing.requires.emulated_lastrowid def test_implicit_pk_multi_rows(self): t = self._fixture() self._test_multi( @@ -1439,7 +1464,7 @@ class TableInsertTest(fixtures.TablesTest): ], ) - @testing.requires.dbapi_lastrowid + @testing.requires.emulated_lastrowid def test_implicit_pk_inline(self): t = self._fixture() self._test( diff --git a/test/sql/test_types.py b/test/sql/test_types.py index 223cba064b..e00c08ad24 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -678,7 +678,8 @@ class UnicodeTest(fixtures.TestBase, AssertsExecutionResults): """assert expected values for 'native unicode' mode""" if \ - (testing.against('mssql+pyodbc') and not testing.db.dialect.freetds): + (testing.against('mssql+pyodbc') and not testing.db.dialect.freetds) \ + or testing.against('mssql+mxodbc'): assert testing.db.dialect.returns_unicode_strings == 'conditional' return @@ -817,6 +818,8 @@ class UnicodeTest(fixtures.TestBase, AssertsExecutionResults): # lambda: testing.db_spec("postgresql")(testing.db), # "pg8000 and psycopg2 both have issues here in py3k" # ) + @testing.skip_if(lambda: testing.db_spec('mssql+mxodbc'), + "unsupported behavior") def test_ignoring_unicode_error(self): """checks String(unicode_error='ignore') is passed to underlying codec.""" @@ -1019,8 +1022,8 @@ class EnumTest(fixtures.TestBase): class BinaryTest(fixtures.TestBase, AssertsExecutionResults): __excluded_on__ = ( ('mysql', '<', (4, 1, 1)), # screwy varbinary types - ) - + ) + @classmethod def setup_class(cls): global binary_table, MyPickleType, metadata @@ -1102,8 +1105,7 @@ class BinaryTest(fixtures.TestBase, AssertsExecutionResults): eq_(testobj3.moredata, l[0]['mypickle'].moredata) eq_(l[0]['mypickle'].stuff, 'this is the right stuff') - @testing.fails_on('oracle+cx_oracle', 'oracle fairly grumpy about binary ' - 'data, not really known how to make this work') + @testing.requires.binary_comparisons def test_comparison(self): """test that type coercion occurs on comparison for binary""" @@ -1138,7 +1140,8 @@ class ExpressionTest(fixtures.TestBase, AssertsExecutionResults, AssertsCompiled return value / 10 return process def adapt_operator(self, op): - return {operators.add:operators.sub, operators.sub:operators.add}.get(op, op) + return {operators.add:operators.sub, + operators.sub:operators.add}.get(op, op) class MyTypeDec(types.TypeDecorator): impl = String diff --git a/test/sql/test_update.py b/test/sql/test_update.py index 79079e5127..0c629f9aa9 100644 --- a/test/sql/test_update.py +++ b/test/sql/test_update.py @@ -133,6 +133,7 @@ class UpdateFromCompileTest(_UpdateFromTestBase, fixtures.TablesTest, AssertsCom ) class UpdateFromRoundTripTest(_UpdateFromTestBase, fixtures.TablesTest): + __testing_engine__ = {'execution_options':{'native_odbc_execute':False}} @testing.requires.update_from def test_exec_two_table(self):