From: Mike Bayer Date: Mon, 18 Jan 2010 03:00:05 +0000 (+0000) Subject: - added native INTERVAL type to the dialect. This supports X-Git-Tag: rel_0_6beta1~59 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9680e6483f4a811e147dd75bf3f5ccab989f01e0;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - added native INTERVAL type to the dialect. This supports only the DAY TO SECOND interval type so far due to lack of support in cx_oracle for YEAR TO MONTH. [ticket:1467] - The Interval type includes a "native" flag which controls if native INTERVAL types (postgresql + oracle) are selected if available, or not. "day_precision" and "second_precision" arguments are also added which propagate as appropriately to these native types. Related to [ticket:1467]. - DefaultDialect.type_descriptor moves back to being per-dialect. TypeEngine/TypeDecorator key type impls to the dialect class + server_version_info so that the colspecs dict can be modified per-dialect based on server version. - Fixed TypeDecorator's incorrect usage of _impl_dict --- diff --git a/CHANGES b/CHANGES index 4c1201ca1d..84127d71f8 100644 --- a/CHANGES +++ b/CHANGES @@ -668,6 +668,10 @@ CHANGES - an NCLOB type is added to the base types. + - added native INTERVAL type to the dialect. This supports + only the DAY TO SECOND interval type so far due to lack + of support in cx_oracle for YEAR TO MONTH. [ticket:1467] + - usage of the CHAR type results in cx_oracle's FIXED_CHAR dbapi type being bound to statements. @@ -799,6 +803,12 @@ CHANGES constraint to enforce the enum. [ticket:1109] [ticket:1511] + - The Interval type includes a "native" flag which controls + if native INTERVAL types (postgresql + oracle) are selected + if available, or not. "day_precision" and "second_precision" + arguments are also added which propagate as appropriately + to these native types. Related to [ticket:1467]. + - The Boolean type, when used on a backend that doesn't have native boolean support, will generate a CHECK constraint "col IN (0, 1)" along with the int/smallint- diff --git a/lib/sqlalchemy/dialects/oracle/__init__.py b/lib/sqlalchemy/dialects/oracle/__init__.py index 7b4d6aeabf..eb47e80cb2 100644 --- a/lib/sqlalchemy/dialects/oracle/__init__.py +++ b/lib/sqlalchemy/dialects/oracle/__init__.py @@ -5,11 +5,11 @@ base.dialect = cx_oracle.dialect from sqlalchemy.dialects.oracle.base import \ VARCHAR, NVARCHAR, CHAR, DATE, DATETIME, NUMBER,\ BLOB, BFILE, CLOB, NCLOB, TIMESTAMP, RAW,\ - FLOAT, DOUBLE_PRECISION, LONG, dialect + FLOAT, DOUBLE_PRECISION, LONG, dialect, INTERVAL __all__ = ( 'VARCHAR', 'NVARCHAR', 'CHAR', 'DATE', 'DATETIME', 'NUMBER', 'BLOB', 'BFILE', 'CLOB', 'NCLOB', 'TIMESTAMP', 'RAW', -'FLOAT', 'DOUBLE_PRECISION', 'LONG', 'dialect' +'FLOAT', 'DOUBLE_PRECISION', 'LONG', 'dialect', 'INTERVAL' ) diff --git a/lib/sqlalchemy/dialects/oracle/base.py b/lib/sqlalchemy/dialects/oracle/base.py index b639539595..882505a405 100644 --- a/lib/sqlalchemy/dialects/oracle/base.py +++ b/lib/sqlalchemy/dialects/oracle/base.py @@ -162,6 +162,40 @@ class BFILE(sqltypes.Binary): class LONG(sqltypes.Text): __visit_name__ = 'LONG' + +class INTERVAL(sqltypes.TypeEngine): + __visit_name__ = 'INTERVAL' + + def __init__(self, + day_precision=None, + second_precision=None): + """Construct an INTERVAL. + + Note that only DAY TO SECOND intervals are currently supported. + This is due to a lack of support for YEAR TO MONTH intervals + within available DBAPIs (cx_oracle and zxjdbc). + + :param day_precision: the day precision value. this is the number of digits + to store for the day field. Defaults to "2" + :param second_precision: the second precision value. this is the number of digits + to store for the fractional seconds field. Defaults to "6". + + """ + self.day_precision = day_precision + self.second_precision = second_precision + + @classmethod + def _adapt_from_generic_interval(cls, interval): + return INTERVAL(day_precision=interval.day_precision, + second_precision=interval.second_precision) + + def adapt(self, impltype): + return impltype(day_precision=self.day_precision, + second_precision=self.second_precision) + + @property + def _type_affinity(self): + return sqltypes.Interval class _OracleBoolean(sqltypes.Boolean): def get_dbapi_type(self, dbapi): @@ -169,6 +203,7 @@ class _OracleBoolean(sqltypes.Boolean): colspecs = { sqltypes.Boolean : _OracleBoolean, + sqltypes.Interval : INTERVAL, } ischema_names = { @@ -204,7 +239,17 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler): def visit_unicode(self, type_): return self.visit_NVARCHAR(type_) - + + def visit_INTERVAL(self, type_): + return "INTERVAL DAY%s TO SECOND%s" % ( + type_.day_precision is not None and + "(%d)" % type_.day_precision or + "", + type_.second_precision is not None and + "(%d)" % type_.second_precision or + "", + ) + def visit_DOUBLE_PRECISION(self, type_): return self._generate_numeric(type_, "DOUBLE PRECISION") @@ -512,6 +557,10 @@ class OracleDialect(default.DefaultDialect): self.implicit_returning = self.server_version_info > (10, ) and \ self.__dict__.get('implicit_returning', True) + if self.server_version_info < (9,): + self.colspecs = self.colspecs.copy() + self.colspecs.pop(sqltypes.Interval) + def do_release_savepoint(self, connection, name): # Oracle does not support RELEASE SAVEPOINT pass diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index 5a94efccb1..6b1d7e5b99 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -167,15 +167,19 @@ class _OracleBinary(_LOBMixin, sqltypes.Binary): def bind_processor(self, dialect): return None - +class _OracleInterval(oracle.INTERVAL): + def get_dbapi_type(self, dbapi): + return dbapi.INTERVAL + class _OracleRaw(oracle.RAW): pass - colspecs = { sqltypes.Date : _OracleDate, sqltypes.Binary : _OracleBinary, sqltypes.Boolean : oracle._OracleBoolean, + sqltypes.Interval : _OracleInterval, + oracle.INTERVAL : _OracleInterval, sqltypes.Text : _OracleText, sqltypes.UnicodeText : _OracleUnicodeText, sqltypes.CHAR : _OracleChar, diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 308e23bae8..2c8f896e9c 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -109,6 +109,10 @@ class INTERVAL(sqltypes.TypeEngine): def adapt(self, impltype): return impltype(self.precision) + @classmethod + def _adapt_from_generic_interval(cls, interval): + return INTERVAL(precision=interval.second_precision) + @property def _type_affinity(self): return sqltypes.Interval diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 5d37741bdd..4b2e3b6818 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -57,6 +57,8 @@ class DefaultDialect(base.Dialect): supports_default_values = False supports_empty_insert = True + server_version_info = None + # indicates symbol names are # UPPERCASEd if they are case insensitive # within the database. @@ -142,8 +144,7 @@ class DefaultDialect(base.Dialect): cursor.close() return result - @classmethod - def type_descriptor(cls, typeobj): + def type_descriptor(self, typeobj): """Provide a database-specific ``TypeEngine`` object, given the generic object which comes from the types module. @@ -152,7 +153,7 @@ class DefaultDialect(base.Dialect): and passes on to ``types.adapt_type()``. """ - return sqltypes.adapt_type(typeobj, cls.colspecs) + return sqltypes.adapt_type(typeobj, self.colspecs) def reflecttable(self, connection, table, include_columns): insp = reflection.Inspector.from_engine(connection) diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py index 47cc37c2db..5ed631a867 100644 --- a/lib/sqlalchemy/types.py +++ b/lib/sqlalchemy/types.py @@ -31,7 +31,7 @@ import sys schema.types = expression.sqltypes =sys.modules['sqlalchemy.types'] from sqlalchemy.util import pickle from sqlalchemy.sql.visitors import Visitable -import sqlalchemy.util as util +from sqlalchemy import util NoneType = type(None) if util.jython: import array @@ -131,10 +131,12 @@ class TypeEngine(AbstractType): return {} def dialect_impl(self, dialect, **kwargs): + key = (dialect.__class__, dialect.server_version_info) + try: - return self._impl_dict[dialect.__class__] + return self._impl_dict[key] except KeyError: - return self._impl_dict.setdefault(dialect.__class__, dialect.__class__.type_descriptor(self)) + return self._impl_dict.setdefault(key, dialect.type_descriptor(self)) def __getstate__(self): d = self.__dict__.copy() @@ -256,19 +258,18 @@ class TypeDecorator(AbstractType): return cls() def dialect_impl(self, dialect): + key = (dialect.__class__, dialect.server_version_info) try: - return self._impl_dict[dialect.__class__] - except AttributeError: - self._impl_dict = {} + return self._impl_dict[key] except KeyError: pass # adapt the TypeDecorator first, in # the case that the dialect maps the TD # to one of its native types (i.e. PGInterval) - adapted = dialect.__class__.type_descriptor(self) + adapted = dialect.type_descriptor(self) if adapted is not self: - self._impl_dict[dialect] = adapted + self._impl_dict[key] = adapted return adapted # otherwise adapt the impl type, link @@ -280,7 +281,7 @@ class TypeDecorator(AbstractType): raise AssertionError("Type object %s does not properly implement the copy() " "method, it must return an object of type %s" % (self, self.__class__)) tt.impl = typedesc - self._impl_dict[dialect] = tt + self._impl_dict[key] = tt return tt @util.memoized_property @@ -304,7 +305,7 @@ class TypeDecorator(AbstractType): if isinstance(self.impl, TypeDecorator): return self.impl.dialect_impl(dialect) else: - return dialect.__class__.type_descriptor(self.impl) + return dialect.type_descriptor(self.impl) def __getattr__(self, key): """Proxy all other undefined accessors to the underlying implementation.""" @@ -348,6 +349,7 @@ class TypeDecorator(AbstractType): def copy(self): instance = self.__class__.__new__(self.__class__) instance.__dict__.update(self.__dict__) + instance._impl_dict = {} return instance def get_dbapi_type(self, dbapi): @@ -938,7 +940,6 @@ class SchemaType(object): self._on_metadata_create) table.metadata.append_ddl_listener('after-drop', self._on_metadata_drop) - @property def bind(self): @@ -1237,6 +1238,36 @@ class Interval(TypeDecorator): impl = DateTime epoch = dt.datetime.utcfromtimestamp(0) + def __init__(self, native=True, + second_precision=None, + day_precision=None): + """Construct an Interval object. + + :param native: when True, use the actual + INTERVAL type provided by the database, if + supported (currently Postgresql, Oracle). + Otherwise, represent the interval data as + an epoch value regardless. + + :param second_precision: For native interval types + which support a "fractional seconds precision" parameter, + i.e. Oracle and Postgresql + + :param day_precision: for native interval types which + support a "day precision" parameter, i.e. Oracle. + + """ + super(Interval, self).__init__() + self.native = native + self.second_precision = second_precision + self.day_precision = day_precision + + def adapt(self, cls): + if self.native: + return cls._adapt_from_generic_interval(self) + else: + return self + def bind_processor(self, dialect): impl_processor = self.impl.bind_processor(dialect) epoch = self.epoch diff --git a/lib/sqlalchemy/util.py b/lib/sqlalchemy/util.py index bd988dd203..cfa891554f 100644 --- a/lib/sqlalchemy/util.py +++ b/lib/sqlalchemy/util.py @@ -338,7 +338,7 @@ def get_cls_kwargs(cls): if has_kw: stack.update(class_.__bases__) args.discard('self') - return list(args) + return args def get_func_kwargs(func): """Return the full set of legal kwargs for the given `func`.""" diff --git a/test/dialect/test_oracle.py b/test/dialect/test_oracle.py index 0dea6c719d..daad18e46b 100644 --- a/test/dialect/test_oracle.py +++ b/test/dialect/test_oracle.py @@ -11,6 +11,7 @@ from sqlalchemy.dialects.oracle import cx_oracle, base as oracle from sqlalchemy.engine import default from sqlalchemy.util import jython from decimal import Decimal +import datetime import os @@ -384,6 +385,7 @@ class ConstraintTest(TestBase): class TypesTest(TestBase, AssertsCompiledSQL): __only_on__ = 'oracle' + __dialect__ = oracle.OracleDialect() def test_no_clobs_for_string_params(self): """test that simple string params get a DBAPI type of VARCHAR, not CLOB. @@ -466,6 +468,31 @@ class TypesTest(TestBase, AssertsCompiledSQL): finally: t1.drop() + def test_interval(self): + + for type_, expected in [ + (oracle.INTERVAL(), "INTERVAL DAY TO SECOND"), + (oracle.INTERVAL(day_precision=3), "INTERVAL DAY(3) TO SECOND"), + (oracle.INTERVAL(second_precision=5), "INTERVAL DAY TO SECOND(5)"), + (oracle.INTERVAL(day_precision=2, second_precision=5), "INTERVAL DAY(2) TO SECOND(5)"), + ]: + self.assert_compile(type_, expected) + + metadata = MetaData(testing.db) + interval_table = Table("intervaltable", metadata, + Column("id", Integer, primary_key=True, test_needs_autoincrement=True), + Column("day_interval", oracle.INTERVAL(day_precision=3)), + ) + metadata.create_all() + try: + interval_table.insert().execute( + day_interval=datetime.timedelta(days=35, seconds=5743), + ) + row = interval_table.select().execute().first() + eq_(row['day_interval'], datetime.timedelta(days=35, seconds=5743)) + finally: + metadata.drop_all() + def test_numerics(self): m = MetaData(testing.db) t1 = Table('t1', m, diff --git a/test/sql/test_types.py b/test/sql/test_types.py index 065e272759..b31015d85b 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -8,7 +8,7 @@ from sqlalchemy.sql import operators from sqlalchemy.test.testing import eq_ import sqlalchemy.engine.url as url from sqlalchemy.databases import * - +from sqlalchemy.test.schema import Table, Column from sqlalchemy.test import * @@ -827,8 +827,10 @@ class IntervalTest(TestBase, AssertsExecutionResults): global interval_table, metadata metadata = MetaData(testing.db) interval_table = Table("intervaltable", metadata, - Column("id", Integer, Sequence('interval_id_seq', optional=True), primary_key=True), - Column("interval", Interval), + Column("id", Integer, primary_key=True, test_needs_autoincrement=True), + Column("native_interval", Interval()), + Column("native_interval_args", Interval(day_precision=3, second_precision=6)), + Column("non_native_interval", Interval(native=False)), ) metadata.create_all() @@ -843,13 +845,24 @@ class IntervalTest(TestBase, AssertsExecutionResults): @testing.fails_on("+pg8000", "Not yet known how to pass values of the INTERVAL type") @testing.fails_on("postgresql+zxjdbc", "Not yet known how to pass values of the INTERVAL type") def test_roundtrip(self): - delta = datetime.datetime(2006, 10, 5) - datetime.datetime(2005, 8, 17) - interval_table.insert().execute(interval=delta) - assert interval_table.select().execute().first()['interval'] == delta + small_delta = datetime.timedelta(days=15, seconds=5874) + delta = datetime.timedelta(414) + interval_table.insert().execute( + native_interval=small_delta, + native_interval_args=delta, + non_native_interval=delta + ) + row = interval_table.select().execute().first() + eq_(row['native_interval'], small_delta) + eq_(row['native_interval_args'], delta) + eq_(row['non_native_interval'], delta) def test_null(self): - interval_table.insert().execute(id=1, inverval=None) - assert interval_table.select().execute().first()['interval'] is None + interval_table.insert().execute(id=1, native_inverval=None, non_native_interval=None) + row = interval_table.select().execute().first() + eq_(row['native_interval'], None) + eq_(row['native_interval_args'], None) + eq_(row['non_native_interval'], None) class BooleanTest(TestBase, AssertsExecutionResults): @classmethod