diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-02-27 20:03:33 +0000 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-02-27 20:03:33 +0000 |
commit | cd6af2e03b8ffba0c7d6b8906c178fd1aa742920 (patch) | |
tree | 4da300a9f8441274624b3db49236322476954c07 | |
parent | 8b1096eea4fdc081a803279c57226ac07481d788 (diff) | |
download | sqlalchemy-cd6af2e03b8ffba0c7d6b8906c178fd1aa742920.tar.gz |
working on pyodbc / mxodbc
-rw-r--r-- | CHANGES | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/connectors/mxodbc.py | 17 | ||||
-rw-r--r-- | lib/sqlalchemy/connectors/pyodbc.py | 1 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/mssql/base.py | 77 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/mssql/mxodbc.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/mssql/pyodbc.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/default.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 1 | ||||
-rw-r--r-- | lib/sqlalchemy/test/requires.py | 6 | ||||
-rw-r--r-- | test/dialect/test_mssql.py | 24 | ||||
-rw-r--r-- | test/engine/test_reflection.py | 5 | ||||
-rw-r--r-- | test/sql/test_query.py | 13 | ||||
-rw-r--r-- | test/sql/test_returning.py | 3 | ||||
-rw-r--r-- | test/sql/test_rowcount.py | 7 | ||||
-rw-r--r-- | test/sql/test_types.py | 8 |
15 files changed, 121 insertions, 56 deletions
@@ -255,6 +255,10 @@ CHANGES - mssql - Re-established initial support for pymssql. + - Various fixes for implicit returning, reflection, + etc. - the MS-SQL dialects aren't quite complete + in 0.6 yet (but are close) + - Added basic support for mxODBC [ticket:1710]. - Removed the text_as_varchar option. diff --git a/lib/sqlalchemy/connectors/mxodbc.py b/lib/sqlalchemy/connectors/mxodbc.py index 93e323fb9..ef7852f61 100644 --- a/lib/sqlalchemy/connectors/mxodbc.py +++ b/lib/sqlalchemy/connectors/mxodbc.py @@ -1,5 +1,7 @@ import sys +import re + from sqlalchemy.connectors import Connector class MxODBCConnector(Connector): @@ -8,7 +10,8 @@ class MxODBCConnector(Connector): supports_sane_multi_rowcount = False supports_unicode_statements = False supports_unicode_binds = False - + supports_native_decimal = False + @classmethod def dbapi(cls): platform = sys.platform @@ -61,4 +64,14 @@ class MxODBCConnector(Connector): else: return False - + def _get_server_version_info(self, connection): + dbapi_con = connection.connection + version = [] + r = re.compile('[.\-]') + # 18 == pyodbc.SQL_DBMS_VER + for n in r.split(dbapi_con.getinfo(18)[1]): + try: + version.append(int(n)) + except ValueError: + version.append(n) + return tuple(version) diff --git a/lib/sqlalchemy/connectors/pyodbc.py b/lib/sqlalchemy/connectors/pyodbc.py index 46b0556d5..6abdbf0dd 100644 --- a/lib/sqlalchemy/connectors/pyodbc.py +++ b/lib/sqlalchemy/connectors/pyodbc.py @@ -12,6 +12,7 @@ class PyODBCConnector(Connector): # PyODBC unicode is broken on UCS-4 builds supports_unicode = sys.maxunicode == 65535 supports_unicode_statements = supports_unicode + supports_native_decimal = True default_paramstyle = 'named' # for non-DSN connections, this should diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 1ce3cbde8..dc767882b 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -279,13 +279,17 @@ RESERVED_WORDS = set( class _MSNumeric(sqltypes.Numeric): def result_processor(self, dialect, coltype): if self.asdecimal: - return processors.to_decimal_processor_factory(decimal.Decimal) + if getattr(self, 'scale', None) is not None and dialect.supports_native_decimal: + return None + else: + return processors.to_decimal_processor_factory(decimal.Decimal) else: #XXX: if the DBAPI returns a float (this is likely, given the # processor when asdecimal is True), this should be a None # processor instead. return processors.to_float - + return None + def bind_processor(self, dialect): def process(value): if isinstance(value, decimal.Decimal): @@ -797,14 +801,18 @@ class MSExecutionContext(default.DefaultExecutionContext): else: self.cursor.execute("SELECT @@identity AS lastrowid") # fetchall() ensures the cursor is consumed without closing it - row = self.cursor.fetchall()[0] + row = self.cursor.fetchall()[0] self._lastrowid = int(row[0]) if (self.isinsert or self.isupdate or self.isdelete) and self.compiled.returning: self._result_proxy = base.FullyBufferedResultProxy(self) if self._enable_identity_insert: - self.cursor.execute("SET IDENTITY_INSERT %s OFF" % self.dialect.identifier_preparer.format_table(self.compiled.statement.table)) + self.cursor.execute( + "SET IDENTITY_INSERT %s OFF" % + self.dialect.identifier_preparer. + format_table(self.compiled.statement.table) + ) def get_lastrowid(self): return self._lastrowid @@ -1080,7 +1088,7 @@ class MSDialect(default.DefaultDialect): query_timeout=None, use_scope_identity=True, max_identifier_length=None, - schema_name="dbo", **opts): + schema_name=u"dbo", **opts): self.query_timeout = int(query_timeout or 0) self.schema_name = schema_name @@ -1099,7 +1107,8 @@ class MSDialect(default.DefaultDialect): def initialize(self, connection): super(MSDialect, self).initialize(connection) - if self.server_version_info >= MS_2005_VERSION and 'implicit_returning' not in self.__dict__: + if self.server_version_info >= MS_2005_VERSION and \ + 'implicit_returning' not in self.__dict__: self.implicit_returning = True def _get_default_schema_name(self, connection): @@ -1115,7 +1124,7 @@ class MSDialect(default.DefaultDialect): try: default_schema_name = connection.scalar(query, [user_name]) if default_schema_name is not None: - return default_schema_name + return unicode(default_schema_name) except: pass return self.schema_name @@ -1282,34 +1291,34 @@ class MSDialect(default.DefaultDialect): name='%s_identity' % col_name) break cursor.close() - if ic is not None: - try: - # is this table_fullname reliable? - table_fullname = "%s.%s" % (current_schema, tablename) - cursor = connection.execute( - sql.text("select ident_seed(:seed), ident_incr(:incr)"), - {'seed':table_fullname, 'incr':table_fullname} + + if ic is not None and self.server_version_info >= MS_2005_VERSION: + table_fullname = "%s.%s" % (current_schema, tablename) + cursor = connection.execute( + sql.text("select ident_seed(:tname), ident_incr(:tname)", + bindparams=[ + sql.bindparam('tname', table_fullname) + ] ) - row = cursor.first() - if not row is None: - colmap[ic]['sequence'].update({ - 'start' : int(row[0]), - 'increment' : int(row[1]) - }) - except: - # ignoring it, works just like before - pass + ) + row = cursor.first() + if not row is None: + colmap[ic]['sequence'].update({ + 'start' : int(row[0]), + 'increment' : int(row[1]) + }) return cols @reflection.cache def get_primary_keys(self, connection, tablename, schema=None, **kw): current_schema = schema or self.default_schema_name pkeys = [] - # Add constraints - RR = ischema.ref_constraints #information_schema.referential_constraints - TC = ischema.constraints #information_schema.table_constraints - C = ischema.key_constraints.alias('C') #information_schema.constraint_column_usage: the constrained column - R = ischema.key_constraints.alias('R') #information_schema.constraint_column_usage: the referenced column + RR = ischema.ref_constraints # information_schema.referential_constraints + TC = ischema.constraints # information_schema.table_constraints + C = ischema.key_constraints.alias('C') # information_schema.constraint_column_usage: + # the constrained column + R = ischema.key_constraints.alias('R') # information_schema.constraint_column_usage: + # the referenced column # Primary key constraints s = sql.select([C.c.column_name, TC.c.constraint_type], @@ -1329,13 +1338,16 @@ class MSDialect(default.DefaultDialect): # Add constraints RR = ischema.ref_constraints #information_schema.referential_constraints TC = ischema.constraints #information_schema.table_constraints - C = ischema.key_constraints.alias('C') #information_schema.constraint_column_usage: the constrained column - R = ischema.key_constraints.alias('R') #information_schema.constraint_column_usage: the referenced column + C = ischema.key_constraints.alias('C') # information_schema.constraint_column_usage: + # the constrained column + R = ischema.key_constraints.alias('R') # information_schema.constraint_column_usage: + # the referenced column # Foreign key constraints s = sql.select([C.c.column_name, R.c.table_schema, R.c.table_name, R.c.column_name, - RR.c.constraint_name, RR.c.match_option, RR.c.update_rule, RR.c.delete_rule], + RR.c.constraint_name, RR.c.match_option, RR.c.update_rule, + RR.c.delete_rule], sql.and_(C.c.table_name == tablename, C.c.table_schema == current_schema, C.c.constraint_name == RR.c.constraint_name, @@ -1378,6 +1390,3 @@ class MSDialect(default.DefaultDialect): return fkeys.values() - -# fixme. I added this for the tests to run. -Randall -MSSQLDialect = MSDialect diff --git a/lib/sqlalchemy/dialects/mssql/mxodbc.py b/lib/sqlalchemy/dialects/mssql/mxodbc.py index bbaccd328..38d559e2b 100644 --- a/lib/sqlalchemy/dialects/mssql/mxodbc.py +++ b/lib/sqlalchemy/dialects/mssql/mxodbc.py @@ -10,7 +10,8 @@ from sqlalchemy.dialects.mssql.pyodbc import MSExecutionContext_pyodbc MSExecutionContext_mxodbc = MSExecutionContext_pyodbc class MSDialect_mxodbc(MxODBCConnector, MSDialect): - supports_sane_rowcount = True + # FIXME: yikes, plain rowcount doesn't work ? + supports_sane_rowcount = False #True supports_sane_multi_rowcount = False execution_ctx_cls = MSExecutionContext_mxodbc diff --git a/lib/sqlalchemy/dialects/mssql/pyodbc.py b/lib/sqlalchemy/dialects/mssql/pyodbc.py index 9a2a9e4e7..23ab0320c 100644 --- a/lib/sqlalchemy/dialects/mssql/pyodbc.py +++ b/lib/sqlalchemy/dialects/mssql/pyodbc.py @@ -35,7 +35,8 @@ class MSExecutionContext_pyodbc(MSExecutionContext): # We may have to skip over a number of result sets with no data (due to triggers, etc.) while True: try: - # fetchall() ensures the cursor is consumed without closing it (FreeTDS particularly) + # fetchall() ensures the cursor is consumed + # without closing it (FreeTDS particularly) row = self.cursor.fetchall()[0] break except self.dialect.dbapi.Error, e: diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 74562a70e..ac933bdf4 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -39,6 +39,11 @@ class DefaultDialect(base.Dialect): supports_native_enum = False supports_native_boolean = False + # if the NUMERIC type + # returns decimal.Decimal. + # *not* the FLOAT type however. + supports_native_decimal = False + # Py3K #supports_unicode_statements = True #supports_unicode_binds = True diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 32aa2a992..60f74e923 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -852,6 +852,7 @@ class SQLCompiler(engine.Compiled): if c.primary_key and \ need_pks and \ ( + implicit_returning or not postfetch_lastrowid or c is not stmt.table._autoincrement_column ): diff --git a/lib/sqlalchemy/test/requires.py b/lib/sqlalchemy/test/requires.py index c97e6f5bb..0bf4689df 100644 --- a/lib/sqlalchemy/test/requires.py +++ b/lib/sqlalchemy/test/requires.py @@ -190,6 +190,12 @@ def unicode_ddl(fn): exclude('mysql', '<', (4, 1, 1), 'no unicode connection support'), ) +def sane_rowcount(fn): + return _chain_decorators_on( + fn, + skip_if(lambda: not testing.db.dialect.supports_sane_rowcount) + ) + def python2(fn): return _chain_decorators_on( fn, diff --git a/test/dialect/test_mssql.py b/test/dialect/test_mssql.py index aa5ecf8cd..caf71ab10 100644 --- a/test/dialect/test_mssql.py +++ b/test/dialect/test_mssql.py @@ -214,7 +214,7 @@ class CompileTest(TestBase, AssertsCompiledSQL): class IdentityInsertTest(TestBase, AssertsCompiledSQL): __only_on__ = 'mssql' - __dialect__ = mssql.MSSQLDialect() + __dialect__ = mssql.MSDialect() @classmethod def setup_class(cls): @@ -322,7 +322,8 @@ class ReflectionTest(TestBase, ComparesTables): meta2 = MetaData(testing.db) try: table2 = Table('identity_test', meta2, autoload=True) - sequence = isinstance(table2.c['col1'].default, schema.Sequence) and table2.c['col1'].default + sequence = isinstance(table2.c['col1'].default, schema.Sequence) \ + and table2.c['col1'].default assert sequence.start == 2 assert sequence.increment == 3 finally: @@ -704,7 +705,8 @@ class TypesTest(TestBase, AssertsExecutionResults, ComparesTables): '0.0000000000000000002', '0.2', '-0.0000000000000000002', '-2E-2', '156666.458923543', '-156666.458923543', '1', '-1', '-1234', '1234', '2E-12', '4E8', '3E-6', '3E-7', '4.1', '1E-1', '1E-2', '1E-3', - '1E-4', '1E-5', '1E-6', '1E-7', '1E-1', '1E-8', '0.2732E2', '-0.2432E2', '4.35656E2', + '1E-4', '1E-5', '1E-6', '1E-7', '1E-1', '1E-8', '0.2732E2', + '-0.2432E2', '4.35656E2', '-02452E-2', '45125E-2', '1234.58965E-2', '1.521E+15', '-1E-25', '1E-25', '1254E-25', '-1203E-25', '0', '-0.00', '-0', '4585E12', '000000000000000000012', '000000000000.32E12', @@ -714,7 +716,7 @@ class TypesTest(TestBase, AssertsExecutionResults, ComparesTables): numeric_table.insert().execute(numericcol=value) for value in select([numeric_table.c.numericcol]).execute(): - assert value[0] in test_items, "%s not in test_items" % value[0] + assert value[0] in test_items, "%r not in test_items" % value[0] def test_float(self): float_table = Table('float_table', metadata, @@ -1071,16 +1073,17 @@ class TypesTest(TestBase, AssertsExecutionResults, ComparesTables): testing.eq_(gen.get_column_specification(t.c.t), "t %s" % expected) self.assert_(repr(t.c.t)) t.create(checkfirst=True) - + + @testing.crashes("+mxodbc", "mxODBC doesn't do scope_identity() with DEFAULT VALUES") def test_autoincrement(self): Table('ai_1', metadata, Column('int_y', Integer, primary_key=True), Column('int_n', Integer, DefaultClause('0'), - primary_key=True)) + primary_key=True, autoincrement=False)) Table('ai_2', metadata, Column('int_y', Integer, primary_key=True), Column('int_n', Integer, DefaultClause('0'), - primary_key=True)) + primary_key=True, autoincrement=False)) Table('ai_3', metadata, Column('int_n', Integer, DefaultClause('0'), primary_key=True, autoincrement=False), @@ -1117,11 +1120,14 @@ class TypesTest(TestBase, AssertsExecutionResults, ComparesTables): for name in table_names: tbl = Table(name, mr, autoload=True) + tbl = metadata.tables[name] for c in tbl.c: if c.name.startswith('int_y'): - assert c.autoincrement + assert c.autoincrement, name + assert tbl._autoincrement_column is c, name elif c.name.startswith('int_n'): - assert not c.autoincrement + assert not c.autoincrement, name + assert tbl._autoincrement_column is not c, name for counter, engine in enumerate([ engines.testing_engine(options={'implicit_returning':False}), diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py index 5d3f0ca86..bf94cce65 100644 --- a/test/engine/test_reflection.py +++ b/test/engine/test_reflection.py @@ -804,12 +804,13 @@ class UnicodeReflectionTest(TestBase): metadata = MetaData(bind) if testing.against('sybase', 'maxdb', 'oracle', 'mssql'): - names = set(['plain']) + names = set([u'plain']) else: names = set([u'plain', u'Unit\u00e9ble', u'\u6e2c\u8a66']) for name in names: - Table(name, metadata, Column('id', sa.Integer, sa.Sequence(name + "_id_seq"), primary_key=True)) + Table(name, metadata, Column('id', sa.Integer, sa.Sequence(name + "_id_seq"), + primary_key=True)) metadata.create_all() reflected = set(bind.table_names()) diff --git a/test/sql/test_query.py b/test/sql/test_query.py index d7bca1af4..62da772a4 100644 --- a/test/sql/test_query.py +++ b/test/sql/test_query.py @@ -78,6 +78,13 @@ class QueryTest(TestBase): detects rows that had defaults and post-fetches. """ + # verify implicit_returning is working + if engine.dialect.implicit_returning: + ins = table.insert() + comp = ins.compile(engine, column_keys=list(values)) + if not set(values).issuperset(c.key for c in table.primary_key): + assert comp.returning + result = engine.execute(table.insert(), **values) ret = values.copy() @@ -85,13 +92,17 @@ class QueryTest(TestBase): ret[col.key] = id if result.lastrow_has_defaults(): - criterion = and_(*[col==id for col, id in zip(table.primary_key, result.inserted_primary_key)]) + criterion = and_(*[col==id for col, id in + zip(table.primary_key, result.inserted_primary_key)]) row = engine.execute(table.select(criterion)).first() for c in table.c: ret[c.key] = row[c] return ret if testing.against('firebird', 'postgresql', 'oracle', 'mssql'): + assert testing.db.dialect.implicit_returning + + if testing.db.dialect.implicit_returning: test_engines = [ engines.testing_engine(options={'implicit_returning':False}), engines.testing_engine(options={'implicit_returning':True}), diff --git a/test/sql/test_returning.py b/test/sql/test_returning.py index a36ce3cd8..481eba825 100644 --- a/test/sql/test_returning.py +++ b/test/sql/test_returning.py @@ -95,6 +95,7 @@ class ReturningTest(TestBase, AssertsExecutionResults): @testing.fails_on('postgresql', '') @testing.fails_on('oracle', '') + @testing.crashes('mssql+mxodbc', 'Raises an error') def test_executemany(): # return value is documented as failing with psycopg2/executemany result2 = table.insert().returning(table).execute( @@ -112,8 +113,6 @@ class ReturningTest(TestBase, AssertsExecutionResults): test_executemany() - result3 = table.insert().returning(table.c.id).execute({'persons': 4, 'full': False}) - eq_([dict(row) for row in result3], [{'id': 4}]) @testing.exclude('firebird', '<', (2, 1), '2.1+ feature') diff --git a/test/sql/test_rowcount.py b/test/sql/test_rowcount.py index 82301a4a5..6da25b914 100644 --- a/test/sql/test_rowcount.py +++ b/test/sql/test_rowcount.py @@ -4,6 +4,9 @@ from sqlalchemy.test import * class FoundRowsTest(TestBase, AssertsExecutionResults): """tests rowcount functionality""" + + __requires__ = ('sane_rowcount', ) + @classmethod def setup_class(cls): metadata = MetaData(testing.db) @@ -11,7 +14,9 @@ class FoundRowsTest(TestBase, AssertsExecutionResults): global employees_table employees_table = Table('employees', metadata, - Column('employee_id', Integer, Sequence('employee_id_seq', optional=True), primary_key=True), + Column('employee_id', Integer, + Sequence('employee_id_seq', optional=True), + primary_key=True), Column('name', String(50)), Column('department', String(1)), ) diff --git a/test/sql/test_types.py b/test/sql/test_types.py index 53f4d8d91..29b337eda 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -112,7 +112,7 @@ class PickleMetadataTest(TestBase): class UserDefinedTest(TestBase): """tests user-defined types.""" - def testprocessing(self): + def test_processing(self): global users users.insert().execute( @@ -132,7 +132,7 @@ class UserDefinedTest(TestBase): [1800, 2250, 1350], l ): - for col in row[1:5]: + for col in list(row)[1:5]: eq_(col, assertstr) eq_(row[5], assertint) eq_(row[6], assertint2) @@ -1113,7 +1113,7 @@ class BooleanTest(TestBase, AssertsExecutionResults): global bool_table metadata = MetaData(testing.db) bool_table = Table('booltest', metadata, - Column('id', Integer, primary_key=True), + Column('id', Integer, primary_key=True, autoincrement=False), Column('value', Boolean), Column('unconstrained_value', Boolean(create_constraint=False)), ) @@ -1156,6 +1156,8 @@ class BooleanTest(TestBase, AssertsExecutionResults): @testing.fails_on('mysql', "The CHECK clause is parsed but ignored by all storage engines.") + @testing.fails_on('mssql', + "FIXME: MS-SQL 2005 doesn't honor CHECK ?!?") @testing.skip_if(lambda: testing.db.dialect.supports_native_boolean) def test_constraint(self): assert_raises((exc.IntegrityError, exc.ProgrammingError), |