summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy')
-rw-r--r--lib/sqlalchemy/dialects/oracle/cx_oracle.py123
-rw-r--r--lib/sqlalchemy/dialects/postgresql/base.py69
-rw-r--r--lib/sqlalchemy/dialects/postgresql/pg8000.py22
-rw-r--r--lib/sqlalchemy/dialects/postgresql/psycopg2.py15
-rw-r--r--lib/sqlalchemy/orm/__init__.py22
-rw-r--r--lib/sqlalchemy/orm/interfaces.py4
-rw-r--r--lib/sqlalchemy/orm/mapper.py2
-rw-r--r--lib/sqlalchemy/orm/properties.py5
-rw-r--r--lib/sqlalchemy/orm/strategies.py7
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,