diff options
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r-- | lib/sqlalchemy/dialects/oracle/cx_oracle.py | 123 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 69 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/pg8000.py | 22 | ||||
-rw-r--r-- | lib/sqlalchemy/dialects/postgresql/psycopg2.py | 15 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 22 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/interfaces.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/properties.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 7 |
9 files changed, 230 insertions, 39 deletions
diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index eb25e614e..87a84e514 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -66,6 +66,52 @@ Two Phase Transaction Support Two Phase transactions are implemented using XA transactions. Success has been reported with this feature but it should be regarded as experimental. +Precision Numerics +------------------ + +The SQLAlchemy dialect goes thorugh a lot of steps to ensure +that decimal numbers are sent and received with full accuracy. +An "outputtypehandler" callable is associated with each +cx_oracle connection object which detects numeric types and +receives them as string values, instead of receiving a Python +``float`` directly, which is then passed to the Python +``Decimal`` constructor. The :class:`.Numeric` and +:class:`.Float` types under the cx_oracle dialect are aware of +this behavior, and will coerce the ``Decimal`` to ``float`` if +the ``asdecimal`` flag is ``False`` (default on :class:`.Float`, +optional on :class:`.Numeric`). + +The handler attempts to use the "precision" and "scale" +attributes of the result set column to best determine if +subsequent incoming values should be received as ``Decimal`` as +opposed to int (in which case no processing is added). There are +several scenarios where OCI_ does not provide unambiguous data +as to the numeric type, including some situations where +individual rows may return a combination of floating point and +integer values. Certain values for "precision" and "scale" have +been observed to determine this scenario. When it occurs, the +outputtypehandler receives as string and then passes off to a +processing function which detects, for each returned value, if a +decimal point is present, and if so converts to ``Decimal``, +otherwise to int. The intention is that simple int-based +statements like "SELECT my_seq.nextval() FROM DUAL" continue to +return ints and not ``Decimal`` objects, and that any kind of +floating point value is received as a string so that there is no +floating point loss of precision. + +The "decimal point is present" logic itself is also sensitive to +locale. Under OCI_, this is controlled by the NLS_LANG +environment variable. Upon first connection, the dialect runs a +test to determine the current "decimal" character, which can be +a comma "," for european locales. From that point forward the +outputtypehandler uses that character to represent a decimal +point (this behavior is new in version 0.6.6). Note that +cx_oracle 5.0.3 or greater is required when dealing with +numerics with locale settings that don't use a period "." as the +decimal character. + +.. _OCI: http://www.oracle.com/technetwork/database/features/oci/index.html + """ from sqlalchemy.dialects.oracle.base import OracleCompiler, OracleDialect, \ @@ -76,6 +122,7 @@ from sqlalchemy import types as sqltypes, util, exc, processors from datetime import datetime import random from decimal import Decimal +import re class _OracleNumeric(sqltypes.Numeric): def bind_processor(self, dialect): @@ -473,37 +520,80 @@ class OracleDialect_cx_oracle(OracleDialect): self.dbapi.BLOB: oracle.BLOB(), self.dbapi.BINARY: oracle.RAW(), } + @classmethod + def dbapi(cls): + import cx_Oracle + return cx_Oracle def initialize(self, connection): super(OracleDialect_cx_oracle, self).initialize(connection) if self._is_oracle_8: self.supports_unicode_binds = False + self._detect_decimal_char(connection) + + def _detect_decimal_char(self, connection): + """detect if the decimal separator character is not '.', as + is the case with european locale settings for NLS_LANG. + + cx_oracle itself uses similar logic when it formats Python + Decimal objects to strings on the bind side (as of 5.0.3), + as Oracle sends/receives string numerics only in the + current locale. + + """ + if self.cx_oracle_ver < (5,): + # no output type handlers before version 5 + return + + cx_Oracle = self.dbapi + conn = connection.connection + + # override the output_type_handler that's + # on the cx_oracle connection with a plain + # one on the cursor + + def output_type_handler(cursor, name, defaultType, + size, precision, scale): + return cursor.var( + cx_Oracle.STRING, + 255, arraysize=cursor.arraysize) + + cursor = conn.cursor() + cursor.outputtypehandler = output_type_handler + cursor.execute("SELECT 0.1 FROM DUAL") + val = cursor.fetchone()[0] + cursor.close() + char = re.match(r"([\.,])", val).group(1) + if char != '.': + _detect_decimal = self._detect_decimal + self._detect_decimal = \ + lambda value: _detect_decimal(value.replace(char, '.')) + self._to_decimal = \ + lambda value: Decimal(value.replace(char, '.')) + + def _detect_decimal(self, value): + if "." in value: + return Decimal(value) + else: + return int(value) + + _to_decimal = Decimal - @classmethod - def dbapi(cls): - import cx_Oracle - return cx_Oracle - def on_connect(self): if self.cx_oracle_ver < (5,): # no output type handlers before version 5 return - def maybe_decimal(value): - if "." in value: - return Decimal(value) - else: - return int(value) - cx_Oracle = self.dbapi - def output_type_handler(cursor, name, defaultType, size, precision, scale): + def output_type_handler(cursor, name, defaultType, + size, precision, scale): # convert all NUMBER with precision + positive scale to Decimal # this almost allows "native decimal" mode. if defaultType == cx_Oracle.NUMBER and precision and scale > 0: return cursor.var( cx_Oracle.STRING, 255, - outconverter=Decimal, + outconverter=self._to_decimal, arraysize=cursor.arraysize) # if NUMBER with zero precision and 0 or neg scale, this appears # to indicate "ambiguous". Use a slower converter that will @@ -515,7 +605,7 @@ class OracleDialect_cx_oracle(OracleDialect): return cursor.var( cx_Oracle.STRING, 255, - outconverter=maybe_decimal, + outconverter=self._detect_decimal, arraysize=cursor.arraysize) # allow all strings to come back natively as Unicode elif defaultType in (cx_Oracle.STRING, cx_Oracle.FIXED_CHAR): @@ -578,7 +668,10 @@ class OracleDialect_cx_oracle(OracleDialect): return ([], opts) def _get_server_version_info(self, connection): - return tuple(int(x) for x in connection.connection.version.split('.')) + return tuple( + int(x) + for x in connection.connection.version.split('.') + ) def is_disconnect(self, e): if isinstance(e, self.dbapi.InterfaceError): diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 0d103cb0d..7b1a97c32 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -94,13 +94,18 @@ from sqlalchemy.sql import compiler, expression, util as sql_util from sqlalchemy.sql import operators as sql_operators from sqlalchemy import types as sqltypes +try: + from uuid import UUID as _python_UUID +except ImportError: + _python_UUID = None + from sqlalchemy.types import INTEGER, BIGINT, SMALLINT, VARCHAR, \ CHAR, TEXT, FLOAT, NUMERIC, \ DATE, BOOLEAN -_DECIMAL_TYPES = (1700, 1231) +_DECIMAL_TYPES = (1231, 1700) _FLOAT_TYPES = (700, 701, 1021, 1022) - +_INT_TYPES = (20, 21, 23, 26, 1005, 1007, 1016) class REAL(sqltypes.Float): __visit_name__ = "REAL" @@ -134,6 +139,12 @@ class TIME(sqltypes.TIME): self.precision = precision class INTERVAL(sqltypes.TypeEngine): + """Postgresql INTERVAL type. + + The INTERVAL type may not be supported on all DBAPIs. + It is known to work on psycopg2 and not pg8000 or zxjdbc. + + """ __visit_name__ = 'INTERVAL' def __init__(self, precision=None): self.precision = precision @@ -156,17 +167,67 @@ class BIT(sqltypes.TypeEngine): PGBit = BIT class UUID(sqltypes.TypeEngine): + """Postgresql UUID type. + + Represents the UUID column type, interpreting + data either as natively returned by the DBAPI + or as Python uuid objects. + + The UUID type may not be supported on all DBAPIs. + It is known to work on psycopg2 and not pg8000. + + """ __visit_name__ = 'UUID' + + def __init__(self, as_uuid=False): + """Construct a UUID type. + + + :param as_uuid=False: if True, values will be interpreted + as Python uuid objects, converting to/from string via the + DBAPI. + + """ + if as_uuid and _python_UUID is None: + raise NotImplementedError( + "This version of Python does not support the native UUID type." + ) + self.as_uuid = as_uuid + + def bind_processor(self, dialect): + if self.as_uuid: + def process(value): + if value is not None: + value = str(value) + return value + return process + else: + return None + + def result_processor(self, dialect, coltype): + if self.as_uuid: + def process(value): + if value is not None: + value = _python_UUID(value) + return value + return process + else: + return None + PGUuid = UUID class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine): """Postgresql ARRAY type. Represents values as Python lists. + + The ARRAY type may not be supported on all DBAPIs. + It is known to work on psycopg2 and not pg8000. **Note:** be sure to read the notes for - :class:`~sqlalchemy.types.MutableType` regarding ORM - performance implications. + :class:`.MutableType` regarding ORM + performance implications. The :class:`.ARRAY` type's + mutability can be disabled using the "mutable" flag. """ __visit_name__ = 'ARRAY' diff --git a/lib/sqlalchemy/dialects/postgresql/pg8000.py b/lib/sqlalchemy/dialects/postgresql/pg8000.py index 6af2cbd76..7b1d8e6a7 100644 --- a/lib/sqlalchemy/dialects/postgresql/pg8000.py +++ b/lib/sqlalchemy/dialects/postgresql/pg8000.py @@ -9,14 +9,16 @@ URLs are of the form Unicode ------- -pg8000 requires that the postgresql client encoding be configured in the postgresql.conf file -in order to use encodings other than ascii. Set this value to the same value as -the "encoding" parameter on create_engine(), usually "utf-8". +pg8000 requires that the postgresql client encoding be +configured in the postgresql.conf file in order to use encodings +other than ascii. Set this value to the same value as the +"encoding" parameter on create_engine(), usually "utf-8". Interval -------- -Passing data from/to the Interval type is not supported as of yet. +Passing data from/to the Interval type is not supported as of +yet. """ import decimal @@ -27,26 +29,28 @@ from sqlalchemy import processors from sqlalchemy import types as sqltypes from sqlalchemy.dialects.postgresql.base import PGDialect, \ PGCompiler, PGIdentifierPreparer, PGExecutionContext,\ - _DECIMAL_TYPES, _FLOAT_TYPES + _DECIMAL_TYPES, _FLOAT_TYPES, _INT_TYPES class _PGNumeric(sqltypes.Numeric): def result_processor(self, dialect, coltype): if self.asdecimal: if coltype in _FLOAT_TYPES: return processors.to_decimal_processor_factory(decimal.Decimal) - elif coltype in _DECIMAL_TYPES: + elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: # pg8000 returns Decimal natively for 1700 return None else: - raise exc.InvalidRequestError("Unknown PG numeric type: %d" % coltype) + raise exc.InvalidRequestError( + "Unknown PG numeric type: %d" % coltype) else: if coltype in _FLOAT_TYPES: # pg8000 returns float natively for 701 return None - elif coltype in _DECIMAL_TYPES: + elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: return processors.to_float else: - raise exc.InvalidRequestError("Unknown PG numeric type: %d" % coltype) + raise exc.InvalidRequestError( + "Unknown PG numeric type: %d" % coltype) class PGExecutionContext_pg8000(PGExecutionContext): pass diff --git a/lib/sqlalchemy/dialects/postgresql/psycopg2.py b/lib/sqlalchemy/dialects/postgresql/psycopg2.py index 6e1ea92c1..88e6ce670 100644 --- a/lib/sqlalchemy/dialects/postgresql/psycopg2.py +++ b/lib/sqlalchemy/dialects/postgresql/psycopg2.py @@ -96,8 +96,9 @@ from sqlalchemy.sql import expression from sqlalchemy.sql import operators as sql_operators from sqlalchemy import types as sqltypes from sqlalchemy.dialects.postgresql.base import PGDialect, PGCompiler, \ - PGIdentifierPreparer, PGExecutionContext, \ - ENUM, ARRAY, _DECIMAL_TYPES, _FLOAT_TYPES + PGIdentifierPreparer, PGExecutionContext, \ + ENUM, ARRAY, _DECIMAL_TYPES, _FLOAT_TYPES,\ + _INT_TYPES logger = logging.getLogger('sqlalchemy.dialects.postgresql') @@ -111,19 +112,21 @@ class _PGNumeric(sqltypes.Numeric): if self.asdecimal: if coltype in _FLOAT_TYPES: return processors.to_decimal_processor_factory(decimal.Decimal) - elif coltype in _DECIMAL_TYPES: + elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: # pg8000 returns Decimal natively for 1700 return None else: - raise exc.InvalidRequestError("Unknown PG numeric type: %d" % coltype) + raise exc.InvalidRequestError( + "Unknown PG numeric type: %d" % coltype) else: if coltype in _FLOAT_TYPES: # pg8000 returns float natively for 701 return None - elif coltype in _DECIMAL_TYPES: + elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES: return processors.to_float else: - raise exc.InvalidRequestError("Unknown PG numeric type: %d" % coltype) + raise exc.InvalidRequestError( + "Unknown PG numeric type: %d" % coltype) class _PGEnum(ENUM): def __init__(self, *arg, **kw): diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index c2417d138..b51142909 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -206,6 +206,16 @@ def relationship(argument, secondary=None, **kwargs): generally mutually exclusive with the use of the *secondary* keyword argument. + :param active_history=False: + When ``True``, indicates that the "previous" value for a + many-to-one reference should be loaded when replaced, if + not already loaded. Normally, history tracking logic for + simple many-to-ones only needs to be aware of the "new" + value in order to perform a flush. This flag is available + for applications that make use of + :func:`.attributes.get_history` which also need to know + the "previous" value of the attribute. (New in 0.6.6) + :param backref: indicates the string name of a property to be placed on the related mapper's class that will handle this relationship in the other @@ -576,7 +586,7 @@ def column_property(*args, **kwargs): """Provide a column-level property for use with a Mapper. Column-based properties can normally be applied to the mapper's - ``properties`` dictionary using the ``schema.Column`` element directly. + ``properties`` dictionary using the :class:`.Column` element directly. Use this function when the given column is not directly present within the mapper's selectable; examples include SQL expressions, functions, and scalar SELECT queries. @@ -587,6 +597,16 @@ def column_property(*args, **kwargs): :param \*cols: list of Column objects to be mapped. + :param active_history=False: + When ``True``, indicates that the "previous" value for a + scalar attribute should be loaded when replaced, if not + already loaded. Normally, history tracking logic for + simple non-primary-key scalar values only needs to be + aware of the "new" value in order to perform a flush. This + flag is available for applications that make use of + :func:`.attributes.get_history` which also need to know + the "previous" value of the attribute. (new in 0.6.6) + :param comparator_factory: a class which extends :class:`.ColumnProperty.Comparator` which provides custom SQL clause generation for comparison operations. diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index c3c9c754f..a6fe153e5 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -888,6 +888,10 @@ class AttributeExtension(object): active_history = True """indicates that the set() method would like to receive the 'old' value, even if it means firing lazy callables. + + Note that ``active_history`` can also be set directly via + :func:`.column_property` and :func:`.relationship`. + """ def append(self, state, value, initiator): diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index e9da4f533..c1045226c 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -107,7 +107,7 @@ class Mapper(object): self.class_manager = None - self.primary_key_argument = primary_key + self.primary_key_argument = util.to_list(primary_key) self.non_primary = non_primary if order_by is not False: diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py index feee041ce..edfb861f4 100644 --- a/lib/sqlalchemy/orm/properties.py +++ b/lib/sqlalchemy/orm/properties.py @@ -60,6 +60,7 @@ class ColumnProperty(StrategizedProperty): self.__class__.Comparator) self.descriptor = kwargs.pop('descriptor', None) self.extension = kwargs.pop('extension', None) + self.active_history = kwargs.pop('active_history', False) if 'doc' in kwargs: self.doc = kwargs.pop('doc') @@ -114,6 +115,7 @@ class ColumnProperty(StrategizedProperty): return ColumnProperty( deferred=self.deferred, group=self.group, + active_history=self.active_history, *self.columns) def _getattr(self, state, dict_, column, passive=False): @@ -184,6 +186,7 @@ class CompositeProperty(ColumnProperty): deferred=self.deferred, group=self.group, composite_class=self.composite_class, + active_history=self.active_history, *self.columns) def do_init(self): @@ -444,6 +447,7 @@ class RelationshipProperty(StrategizedProperty): comparator_factory=None, single_parent=False, innerjoin=False, doc=None, + active_history=False, cascade_backrefs=True, load_on_pending=False, strategy_class=None, _local_remote_pairs=None, @@ -469,6 +473,7 @@ class RelationshipProperty(StrategizedProperty): self.query_class = query_class self.innerjoin = innerjoin self.doc = doc + self.active_history = active_history self.join_depth = join_depth self.local_remote_pairs = _local_remote_pairs self.extension = extension diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 398a63e7a..04a23f000 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -108,7 +108,8 @@ class ColumnLoader(LoaderStrategy): self.is_class_level = True coltype = self.columns[0].type # TODO: check all columns ? check for foreign key as well? - active_history = self.columns[0].primary_key + active_history = self.parent_property.active_history or \ + self.columns[0].primary_key _register_attribute(self, mapper, useobject=False, compare_function=coltype.compare_values, @@ -163,8 +164,7 @@ class CompositeColumnLoader(ColumnLoader): _register_attribute(self, mapper, useobject=False, compare_function=compare, copy_function=copy, - mutable_scalars=True - #active_history ? + mutable_scalars=True, ) def create_row_processor(self, selectcontext, path, mapper, @@ -398,6 +398,7 @@ class LazyLoader(AbstractRelationshipLoader): uselist = self.parent_property.uselist, typecallable = self.parent_property.collection_class, active_history = \ + self.parent_property.active_history or \ self.parent_property.direction is not \ interfaces.MANYTOONE or \ not self.use_get, |