]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- fixes for mxODBC, some pyodbc
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 2 Sep 2012 19:14:09 +0000 (15:14 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 2 Sep 2012 19:14:09 +0000 (15:14 -0400)
- 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

13 files changed:
lib/sqlalchemy/connectors/mxodbc.py
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mssql/mxodbc.py
lib/sqlalchemy/dialects/mssql/pyodbc.py
lib/sqlalchemy/engine/base.py
test/bootstrap/noseplugin.py
test/engine/test_execute.py
test/lib/fixtures.py
test/lib/requires.py
test/sql/test_defaults.py
test/sql/test_query.py
test/sql/test_types.py
test/sql/test_update.py

index 4456f351f4c21760619edc4d843698f3b66db4ca..b65a43074b08c82d7356cd0780162a6ac080021e 100644 (file)
@@ -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))
index 9c2c1555d956ee1939ab5a8476689747296704e8..03bbf6446d388697b2fc7b2e1ecd154e02e941fc 100644 (file)
@@ -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
index 3f0c106c295385a06be61fdbd497740100cbcbfc..0044b5f4fce5a57d149975d86cd48d2bdce90b96 100644 (file)
@@ -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
 
index 616c906cd517e01f8f82815707fc99e524965c99..83bf7ee6b0676d4a23d9629d1b676934c99ad809 100644 (file)
@@ -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 \
index af7341081ce47398edeaa1d31303ed2be15ac67b..47594407f2c09bf0b37818c6b11b6d020580fcaa 100644 (file)
@@ -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
index dc14b48b332c697256bece4e2c4021fa2b4260c5..e664552dc541767f330ac4ab2381acf06c524bf0 100644 (file)
@@ -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()
index 37cb9965c7387744cbe4e9a74ec389cdd368fbc5..94b9dfb7b33805a32f8d2c1694f2ed86b1dbb65d 100644 (file)
@@ -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', {}, ()),
index 00d35df75213edb25fa1bcd25547b8ac093f2ad7..3281e1a00ab938ed15811860a7858236434e1a30 100644 (file)
@@ -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
 
index 3fb5e7e14b6eb9b986fadc2b427def25d17b4375..c91c87cd74de4e3f5dbb27e48178901d2b5e3ad0 100644 (file)
@@ -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',
index 55aa86633b2d3aea8eeb3d1b34d11dd798f46e3d..b5277c62399654770498c1910d071b19ff6bd15d 100644 (file)
@@ -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):
index 67c0fec225b41324bef06abfe76ae1ada721be82..a0c849de531fc8cb38be5dc6930322a83c07dacf 100644 (file)
@@ -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(
index 223cba064bd80edb2eac6765939941cac0cc6102..e00c08ad247c3301868a22aa05f77a1939bcf52d 100644 (file)
@@ -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
index 79079e5127977d4c16e25953d05ecee1c25c9dd5..0c629f9aa9776d905d4a490291838a32190eb97f 100644 (file)
@@ -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):