From: Mike Bayer Date: Thu, 1 Jul 2010 20:57:02 +0000 (-0400) Subject: - Oracle's "native decimal" metadata begins to return X-Git-Tag: rel_0_6_2~15 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=25db56bc0c1ee30fc0ad9166ac48de7dc8328762;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Oracle's "native decimal" metadata begins to return ambiguous typing information about numerics when columns are embedded in subqueries as well as when ROWNUM is consulted with subqueries, as we do for limit/offset. We've added these ambiguous conditions to the cx_oracle "convert to Decimal()" handler, so that we receive numerics as Decimal in more cases instead of as floats. These are then converted, if requested, into Integer or Float, or otherwise kept as the lossless Decimal [ticket:1840]. --- diff --git a/CHANGES b/CHANGES index 721cc4d732..b3b0d65242 100644 --- a/CHANGES +++ b/CHANGES @@ -131,6 +131,18 @@ CHANGES - Fixed ora-8 compatibility flags such that they don't cache a stale value from before the first database connection actually occurs. [ticket:1819] + + - Oracle's "native decimal" metadata begins to return + ambiguous typing information about numerics + when columns are embedded in subqueries as well + as when ROWNUM is consulted with subqueries, as we + do for limit/offset. We've added these ambiguous + conditions to the cx_oracle "convert to Decimal()" + handler, so that we receive numerics as Decimal + in more cases instead of as floats. These are + then converted, if requested, into Integer + or Float, or otherwise kept as the lossless + Decimal [ticket:1840]. - firebird - Fixed incorrect signature in do_execute(), error diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index dd32f32016..01ac1a6855 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -86,10 +86,11 @@ class _OracleNumeric(sqltypes.Numeric): # we apply a connection output handler that # returns Decimal for positive precision + scale NUMBER # types + if dialect.supports_native_decimal: if self.asdecimal and self.scale is None: processors.to_decimal_processor_factory(Decimal) - elif not self.asdecimal and self.scale > 0: + elif not self.asdecimal: return processors.to_float else: return None @@ -465,9 +466,15 @@ class OracleDialect_cx_oracle(OracleDialect): cx_Oracle = self.dbapi def output_type_handler(cursor, name, defaultType, size, precision, scale): - # convert all NUMBER with precision + positive scale to Decimal. + # convert all NUMBER with precision + positive scale to Decimal, + # or zero precision and 0 or neg scale, indicates "don't know", # this effectively allows "native decimal" mode. - if defaultType == cx_Oracle.NUMBER and precision and scale > 0: + + if defaultType == cx_Oracle.NUMBER \ + and ( + (precision and scale > 0) or \ + (not precision and scale <= 0) + ): return cursor.var( cx_Oracle.STRING, 255, diff --git a/test/dialect/test_oracle.py b/test/dialect/test_oracle.py index cc4bbab228..27459bb3f4 100644 --- a/test/dialect/test_oracle.py +++ b/test/dialect/test_oracle.py @@ -758,6 +758,90 @@ class TypesTest(TestBase, AssertsCompiledSQL): finally: t1.drop() + @testing.provide_metadata + def test_numerics_broken_inspection(self): + """Numeric scenarios where Oracle type info is 'broken', + returning us precision, scale of the form (0, 0) or (0, -127). + We convert to Decimal and let int()/float() processors take over. + + """ + + # this test requires cx_oracle 5 + + foo = Table('foo', metadata, + Column('idata', Integer), + Column('ndata', Numeric(20, 2)), + Column('fdata', Float()), + ) + foo.create() + + foo.insert().execute( + {'idata':5, 'ndata':Decimal("45.6"), 'fdata':45.68392} + ) + + stmt = """ + SELECT + idata, + ndata, + fdata + FROM foo + """ + eq_( + testing.db.execute(stmt).fetchall(), + [(5, Decimal('45.6'), 45.683920000000001)] + ) + + stmt = """ + SELECT + (SELECT (SELECT idata FROM foo) FROM DUAL) AS idata, + (SELECT CAST((SELECT ndata FROM foo) AS NUMERIC(20, 2)) FROM DUAL) AS ndata, + (SELECT CAST((SELECT fdata FROM foo) AS FLOAT) FROM DUAL) AS fdata + FROM dual + """ + eq_( + testing.db.execute(stmt).fetchall(), + [(Decimal('5'), Decimal('45.6'), Decimal('45.68392'))] + ) + eq_( + testing.db.execute(text(stmt, + typemap={ + 'idata':Integer(), + 'ndata':Numeric(20, 2), + 'fdata':Float() + })).fetchall(), + [(5, Decimal('45.6'), 45.683920000000001)] + ) + + stmt = """ + SELECT + anon_1.idata AS anon_1_idata, + anon_1.ndata AS anon_1_ndata, + anon_1.fdata AS anon_1_fdata + FROM (SELECT idata, ndata, fdata + FROM ( + SELECT + (SELECT (SELECT idata FROM foo) FROM DUAL) AS idata, + (SELECT CAST((SELECT ndata FROM foo) AS NUMERIC(20, 2)) FROM DUAL) AS ndata, + (SELECT CAST((SELECT fdata FROM foo) AS FLOAT) FROM DUAL) AS fdata + FROM dual + ) + WHERE ROWNUM >= 0) anon_1 + """ + eq_( + testing.db.execute(stmt).fetchall(), + [(Decimal('5'), Decimal('45.6'), Decimal('45.68392'))] + ) + eq_( + testing.db.execute(text(stmt, + typemap={ + 'anon_1_idata':Integer(), + 'anon_1_ndata':Numeric(20, 2), + 'anon_1_fdata':Float() + })).fetchall(), + [(5, Decimal('45.6'), 45.683920000000001)] + ) + + def test_reflect_dates(self): metadata = MetaData(testing.db) Table(