summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2010-04-04 10:38:29 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2010-04-04 10:38:29 -0400
commitf05b5b0bda7b21a585f3252016d2e74c361b6233 (patch)
tree56f94f0b1088b9738ae363832b1466f7a98befdd /lib
parent3d3095497b872696e8860763f54217f425c7d35b (diff)
parentdbc582a43fb8e5d3c0b05cb62b6a77c4e5bc27ac (diff)
downloadsqlalchemy-f05b5b0bda7b21a585f3252016d2e74c361b6233.tar.gz
merge default branch
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/cextension/resultproxy.c35
-rw-r--r--lib/sqlalchemy/dialects/oracle/cx_oracle.py104
-rw-r--r--lib/sqlalchemy/ext/compiler.py19
-rw-r--r--lib/sqlalchemy/ext/declarative.py8
-rw-r--r--lib/sqlalchemy/orm/__init__.py24
-rw-r--r--lib/sqlalchemy/orm/attributes.py11
-rw-r--r--lib/sqlalchemy/orm/mapper.py298
-rw-r--r--lib/sqlalchemy/orm/properties.py25
-rw-r--r--lib/sqlalchemy/orm/strategies.py1
-rw-r--r--lib/sqlalchemy/schema.py5
-rw-r--r--lib/sqlalchemy/sql/expression.py10
-rw-r--r--lib/sqlalchemy/topological.py1
-rw-r--r--lib/sqlalchemy/types.py10
13 files changed, 363 insertions, 188 deletions
diff --git a/lib/sqlalchemy/cextension/resultproxy.c b/lib/sqlalchemy/cextension/resultproxy.c
index b530b65f7..5d9100469 100644
--- a/lib/sqlalchemy/cextension/resultproxy.c
+++ b/lib/sqlalchemy/cextension/resultproxy.c
@@ -69,8 +69,8 @@ BaseRowProxy_init(BaseRowProxy *self, PyObject *args, PyObject *kwds)
Py_INCREF(parent);
self->parent = parent;
- if (!PyTuple_CheckExact(row)) {
- PyErr_SetString(PyExc_TypeError, "row must be a tuple");
+ if (!PySequence_Check(row)) {
+ PyErr_SetString(PyExc_TypeError, "row must be a sequence");
return -1;
}
Py_INCREF(row);
@@ -148,13 +148,15 @@ BaseRowProxy_processvalues(PyObject *values, PyObject *processors, int astuple)
{
Py_ssize_t num_values, num_processors;
PyObject **valueptr, **funcptr, **resultptr;
- PyObject *func, *result, *processed_value;
+ PyObject *func, *result, *processed_value, *values_fastseq;
num_values = Py_SIZE(values);
num_processors = Py_SIZE(processors);
if (num_values != num_processors) {
- PyErr_SetString(PyExc_RuntimeError,
- "number of values in row differ from number of column processors");
+ PyErr_Format(PyExc_RuntimeError,
+ "number of values in row (%d) differ from number of column "
+ "processors (%d)",
+ num_values, num_processors);
return NULL;
}
@@ -166,9 +168,11 @@ BaseRowProxy_processvalues(PyObject *values, PyObject *processors, int astuple)
if (result == NULL)
return NULL;
- /* we don't need to use PySequence_Fast as long as values, processors and
- * result are simple tuple or lists. */
- valueptr = PySequence_Fast_ITEMS(values);
+ values_fastseq = PySequence_Fast(values, "row must be a sequence");
+ if (values_fastseq == NULL)
+ return NULL;
+
+ valueptr = PySequence_Fast_ITEMS(values_fastseq);
funcptr = PySequence_Fast_ITEMS(processors);
resultptr = PySequence_Fast_ITEMS(result);
while (--num_values >= 0) {
@@ -177,6 +181,7 @@ BaseRowProxy_processvalues(PyObject *values, PyObject *processors, int astuple)
processed_value = PyObject_CallFunctionObjArgs(func, *valueptr,
NULL);
if (processed_value == NULL) {
+ Py_DECREF(values_fastseq);
Py_DECREF(result);
return NULL;
}
@@ -189,6 +194,7 @@ BaseRowProxy_processvalues(PyObject *values, PyObject *processors, int astuple)
funcptr++;
resultptr++;
}
+ Py_DECREF(values_fastseq);
return result;
}
@@ -199,19 +205,12 @@ BaseRowProxy_values(BaseRowProxy *self)
self->processors, 0);
}
-static PyTupleObject *
-BaseRowProxy_tuplevalues(BaseRowProxy *self)
-{
- return (PyTupleObject *)BaseRowProxy_processvalues(self->row,
- self->processors, 1);
-}
-
static PyObject *
BaseRowProxy_iter(BaseRowProxy *self)
{
PyObject *values, *result;
- values = (PyObject *)BaseRowProxy_tuplevalues(self);
+ values = BaseRowProxy_processvalues(self->row, self->processors, 1);
if (values == NULL)
return NULL;
@@ -393,9 +392,9 @@ BaseRowProxy_setrow(BaseRowProxy *self, PyObject *value, void *closure)
return -1;
}
- if (!PyTuple_CheckExact(value)) {
+ if (!PySequence_Check(value)) {
PyErr_SetString(PyExc_TypeError,
- "The 'row' attribute value must be a tuple");
+ "The 'row' attribute value must be a sequence");
return -1;
}
diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py
index 91af6620b..7502ed1d5 100644
--- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py
+++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py
@@ -6,6 +6,9 @@ Driver
The Oracle dialect uses the cx_oracle driver, available at
http://cx-oracle.sourceforge.net/ . The dialect has several behaviors
which are specifically tailored towards compatibility with this module.
+Version 5.0 or greater is **strongly** recommended, as SQLAlchemy makes
+extensive use of the cx_oracle output converters for numeric and
+string conversions.
Connecting
----------
@@ -38,33 +41,21 @@ URL, or as keyword arguments to :func:`~sqlalchemy.create_engine()` are:
Unicode
-------
-As of cx_oracle 5, Python unicode objects can be bound directly to statements,
-and it appears that cx_oracle can handle these even without NLS_LANG being set.
-SQLAlchemy tests for version 5 and will pass unicode objects straight to cx_oracle
-if this is the case. For older versions of cx_oracle, SQLAlchemy will encode bind
-parameters normally using dialect.encoding as the encoding.
+cx_oracle 5 fully supports Python unicode objects. SQLAlchemy will pass
+all unicode strings directly to cx_oracle, and additionally uses an output
+handler so that all string based result values are returned as unicode as well.
LOB Objects
-----------
-cx_oracle presents some challenges when fetching LOB objects. A LOB object in a result set
-is presented by cx_oracle as a cx_oracle.LOB object which has a read() method. By default,
-SQLAlchemy converts these LOB objects into Python strings. This is for two reasons. First,
-the LOB object requires an active cursor association, meaning if you were to fetch many rows
-at once such that cx_oracle had to go back to the database and fetch a new batch of rows,
-the LOB objects in the already-fetched rows are now unreadable and will raise an error.
-SQLA "pre-reads" all LOBs so that their data is fetched before further rows are read.
-The size of a "batch of rows" is controlled by the cursor.arraysize value, which SQLAlchemy
-defaults to 50 (cx_oracle normally defaults this to one).
-
-Secondly, the LOB object is not a standard DBAPI return value so SQLAlchemy seeks to
-"normalize" the results to look more like that of other DBAPIs.
-
-The conversion of LOB objects by this dialect is unique in SQLAlchemy in that it takes place
-for all statement executions, even plain string-based statements for which SQLA has no awareness
-of result typing. This is so that calls like fetchmany() and fetchall() can work in all cases
-without raising cursor errors. The conversion of LOB in all cases, as well as the "prefetch"
-of LOB objects, can be disabled using auto_convert_lobs=False.
+cx_oracle returns oracle LOBs using the cx_oracle.LOB object. SQLAlchemy converts
+these to strings so that the interface of the Binary type is consistent with that of
+other backends, and so that the linkage to a live cursor is not needed in scenarios
+like result.fetchmany() and result.fetchall(). This means that by default, LOB
+objects are fully fetched unconditionally by SQLAlchemy, and the linkage to a live
+cursor is broken.
+
+To disable this processing, pass ``auto_convert_lobs=False`` to :func:`create_engine()`.
Two Phase Transaction Support
-----------------------------
@@ -78,16 +69,33 @@ from sqlalchemy.dialects.oracle.base import OracleCompiler, OracleDialect, \
RESERVED_WORDS, OracleExecutionContext
from sqlalchemy.dialects.oracle import base as oracle
from sqlalchemy.engine import base
-from sqlalchemy import types as sqltypes, util, exc
+from sqlalchemy import types as sqltypes, util, exc, processors
from datetime import datetime
import random
+from decimal import Decimal
class _OracleNumeric(sqltypes.Numeric):
- # cx_oracle accepts Decimal objects, but returns
- # floats
def bind_processor(self, dialect):
+ # cx_oracle accepts Decimal objects and floats
return None
-
+
+ def result_processor(self, dialect, coltype):
+ # 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:
+ return processors.to_float
+ else:
+ return None
+ else:
+ # cx_oracle 4 behavior, will assume
+ # floats
+ return super(_OracleNumeric, self).\
+ result_processor(dialect, coltype)
+
class _OracleDate(sqltypes.Date):
def bind_processor(self, dialect):
return None
@@ -127,17 +135,9 @@ class _NativeUnicodeMixin(object):
return super(_NativeUnicodeMixin, self).bind_processor(dialect)
# end Py2K
- def result_processor(self, dialect, coltype):
- # if we know cx_Oracle will return unicode,
- # don't process results
- if dialect._cx_oracle_with_unicode:
- return None
- elif self.convert_unicode != 'force' and \
- dialect._cx_oracle_native_nvarchar and \
- coltype in dialect._cx_oracle_unicode_types:
- return None
- else:
- return super(_NativeUnicodeMixin, self).result_processor(dialect, coltype)
+ # we apply a connection output handler that returns
+ # unicode in all cases, so the "native_unicode" flag
+ # will be set for the default String.result_processor.
class _OracleChar(_NativeUnicodeMixin, sqltypes.CHAR):
def get_dbapi_type(self, dbapi):
@@ -163,7 +163,7 @@ class _OracleUnicodeText(_LOBMixin, _NativeUnicodeMixin, sqltypes.UnicodeText):
if lob_processor is None:
return None
- string_processor = _NativeUnicodeMixin.result_processor(self, dialect, coltype)
+ string_processor = sqltypes.UnicodeText.result_processor(self, dialect, coltype)
if string_processor is None:
return lob_processor
@@ -253,6 +253,7 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext):
c = self._connection.connection.cursor()
if self.dialect.arraysize:
c.arraysize = self.dialect.arraysize
+
return c
def get_result_proxy(self):
@@ -345,6 +346,7 @@ class ReturningResultProxy(base.FullyBufferedResultProxy):
class OracleDialect_cx_oracle(OracleDialect):
execution_ctx_cls = OracleExecutionContext_cx_oracle
statement_compiler = OracleCompiler_cx_oracle
+
driver = "cx_oracle"
colspecs = colspecs = {
@@ -361,7 +363,6 @@ class OracleDialect_cx_oracle(OracleDialect):
sqltypes.CHAR : _OracleChar,
sqltypes.Integer : _OracleInteger, # this is only needed for OUT parameters.
# it would be nice if we could not use it otherwise.
- oracle.NUMBER : oracle.NUMBER, # don't let this get converted
oracle.RAW: _OracleRaw,
sqltypes.Unicode: _OracleNVarChar,
sqltypes.NVARCHAR : _OracleNVarChar,
@@ -388,7 +389,7 @@ class OracleDialect_cx_oracle(OracleDialect):
cx_oracle_ver = tuple([int(x) for x in self.dbapi.version.split('.')])
else:
cx_oracle_ver = (0, 0, 0)
-
+
def types(*names):
return set([
getattr(self.dbapi, name, None) for name in names
@@ -398,6 +399,7 @@ class OracleDialect_cx_oracle(OracleDialect):
self._cx_oracle_unicode_types = types("UNICODE", "NCLOB")
self._cx_oracle_binary_types = types("BFILE", "CLOB", "NCLOB", "BLOB")
self.supports_unicode_binds = cx_oracle_ver >= (5, 0)
+ self.supports_native_decimal = cx_oracle_ver >= (5, 0)
self._cx_oracle_native_nvarchar = cx_oracle_ver >= (5, 0)
if cx_oracle_ver is None:
@@ -446,6 +448,26 @@ class OracleDialect_cx_oracle(OracleDialect):
import cx_Oracle
return cx_Oracle
+ def on_connect(self):
+ cx_Oracle = self.dbapi
+ def output_type_handler(cursor, name, defaultType, size, precision, scale):
+ # convert all NUMBER with precision + positive scale to Decimal.
+ # this effectively allows "native decimal" mode.
+ if defaultType == cx_Oracle.NUMBER and precision and scale > 0:
+ return cursor.var(
+ cx_Oracle.STRING,
+ 255,
+ outconverter=Decimal,
+ arraysize=cursor.arraysize)
+ # allow all strings to come back natively as Unicode
+ elif defaultType in (cx_Oracle.STRING, cx_Oracle.FIXED_CHAR):
+ return cursor.var(unicode, size, cursor.arraysize)
+
+ def on_connect(conn):
+ conn.outputtypehandler = output_type_handler
+
+ return on_connect
+
def create_connect_args(self, url):
dialect_opts = dict(url.query)
for opt in ('use_ansi', 'auto_setinputsizes', 'auto_convert_lobs',
diff --git a/lib/sqlalchemy/ext/compiler.py b/lib/sqlalchemy/ext/compiler.py
index dde49e232..20d6aa05f 100644
--- a/lib/sqlalchemy/ext/compiler.py
+++ b/lib/sqlalchemy/ext/compiler.py
@@ -147,8 +147,23 @@ A big part of using the compiler extension is subclassing SQLAlchemy expression
function or stored procedure type of call. Since most databases support
statements along the line of "SELECT FROM <some function>"
``FunctionElement`` adds in the ability to be used in the FROM clause of a
- ``select()`` construct.
-
+ ``select()`` construct::
+
+ from sqlalchemy.sql.expression import FunctionElement
+
+ class coalesce(FunctionElement):
+ name = 'coalesce'
+
+ @compiles(coalesce)
+ def compile(element, compiler, **kw):
+ return "coalesce(%s)" % compiler.process(element.clauses)
+
+ @compiles(coalesce, 'oracle')
+ def compile(element, compiler, **kw):
+ if len(element.clauses) > 2:
+ raise TypeError("coalesce only supports two arguments on Oracle")
+ return "nvl(%s)" % compiler.process(element.clauses)
+
* :class:`~sqlalchemy.schema.DDLElement` - The root of all DDL expressions,
like CREATE TABLE, ALTER TABLE, etc. Compilation of ``DDLElement``
subclasses is issued by a ``DDLCompiler`` instead of a ``SQLCompiler``.
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py
index 1f4658b60..407de1004 100644
--- a/lib/sqlalchemy/ext/declarative.py
+++ b/lib/sqlalchemy/ext/declarative.py
@@ -749,8 +749,12 @@ class _GetColumns(object):
mapper = class_mapper(self.cls, compile=False)
if mapper:
- prop = mapper.get_property(key)
- if not isinstance(prop, ColumnProperty):
+ prop = mapper.get_property(key, raiseerr=False)
+ if prop is None:
+ raise exceptions.InvalidRequestError(
+ "Class %r does not have a mapped column named %r"
+ % (self.cls, key))
+ elif not isinstance(prop, ColumnProperty):
raise exceptions.InvalidRequestError(
"Property %r is not an instance of"
" ColumnProperty (i.e. does not correspond"
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 206c8d0c2..c2f6337bc 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -266,6 +266,9 @@ def relationship(argument, secondary=None, **kwargs):
a class which extends :class:`RelationshipProperty.Comparator` which
provides custom SQL clause generation for comparison operations.
+ :param doc:
+ docstring which will be applied to the resulting descriptor.
+
:param extension:
an :class:`AttributeExtension` instance, or list of extensions,
which will be prepended to the list of attribute listeners for
@@ -469,7 +472,7 @@ def relation(*arg, **kw):
def dynamic_loader(argument, secondary=None, primaryjoin=None,
secondaryjoin=None, foreign_keys=None, backref=None,
post_update=False, cascade=False, remote_side=None,
- enable_typechecks=True, passive_deletes=False,
+ enable_typechecks=True, passive_deletes=False, doc=None,
order_by=None, comparator_factory=None, query_class=None):
"""Construct a dynamically-loading mapper property.
@@ -508,7 +511,7 @@ def dynamic_loader(argument, secondary=None, primaryjoin=None,
secondaryjoin=secondaryjoin, foreign_keys=foreign_keys, backref=backref,
post_update=post_update, cascade=cascade, remote_side=remote_side,
enable_typechecks=enable_typechecks, passive_deletes=passive_deletes,
- order_by=order_by, comparator_factory=comparator_factory,
+ order_by=order_by, comparator_factory=comparator_factory,doc=doc,
strategy_class=DynaLoader, query_class=query_class)
def column_property(*args, **kwargs):
@@ -538,7 +541,11 @@ def column_property(*args, **kwargs):
it does not load immediately, and is instead loaded when the
attribute is first accessed on an instance. See also
:func:`~sqlalchemy.orm.deferred`.
-
+
+ doc
+ optional string that will be applied as the doc on the
+ class-bound descriptor.
+
extension
an :class:`~sqlalchemy.orm.interfaces.AttributeExtension` instance,
or list of extensions, which will be prepended to the list of
@@ -612,6 +619,10 @@ def composite(class_, *cols, **kwargs):
a class which extends ``sqlalchemy.orm.properties.CompositeProperty.Comparator``
which provides custom SQL clause generation for comparison operations.
+ doc
+ optional string that will be applied as the doc on the
+ class-bound descriptor.
+
extension
an :class:`~sqlalchemy.orm.interfaces.AttributeExtension` instance,
or list of extensions, which will be prepended to the list of
@@ -813,7 +824,7 @@ def mapper(class_, local_table=None, *args, **params):
"""
return Mapper(class_, local_table, *args, **params)
-def synonym(name, map_column=False, descriptor=None, comparator_factory=None):
+def synonym(name, map_column=False, descriptor=None, comparator_factory=None, doc=None):
"""Set up `name` as a synonym to another mapped property.
Used with the ``properties`` dictionary sent to :func:`~sqlalchemy.orm.mapper`.
@@ -851,7 +862,10 @@ def synonym(name, map_column=False, descriptor=None, comparator_factory=None):
proxy access to the column-based attribute.
"""
- return SynonymProperty(name, map_column=map_column, descriptor=descriptor, comparator_factory=comparator_factory)
+ return SynonymProperty(name, map_column=map_column,
+ descriptor=descriptor,
+ comparator_factory=comparator_factory,
+ doc=doc)
def comparable_property(comparator_factory, descriptor=None):
"""Provide query semantics for an unmanaged attribute.
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 887d9a9c1..b631ea2c9 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -1366,12 +1366,12 @@ def unregister_class(class_):
instrumentation_registry.unregister(class_)
def register_attribute(class_, key, **kw):
-
proxy_property = kw.pop('proxy_property', None)
comparator = kw.pop('comparator', None)
parententity = kw.pop('parententity', None)
- register_descriptor(class_, key, proxy_property, comparator, parententity)
+ doc = kw.pop('doc', None)
+ register_descriptor(class_, key, proxy_property, comparator, parententity, doc=doc)
if not proxy_property:
register_attribute_impl(class_, key, **kw)
@@ -1405,7 +1405,8 @@ def register_attribute_impl(class_, key,
manager.post_configure_attribute(key)
-def register_descriptor(class_, key, proxy_property=None, comparator=None, parententity=None, property_=None):
+def register_descriptor(class_, key, proxy_property=None, comparator=None,
+ parententity=None, property_=None, doc=None):
manager = manager_of_class(class_)
if proxy_property:
@@ -1413,7 +1414,9 @@ def register_descriptor(class_, key, proxy_property=None, comparator=None, paren
descriptor = proxy_type(key, proxy_property, comparator, parententity)
else:
descriptor = InstrumentedAttribute(key, comparator=comparator, parententity=parententity)
-
+
+ descriptor.__doc__ = doc
+
manager.instrument_attribute(key, descriptor)
def unregister_attribute(class_, key):
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index ac35ce49b..fbb66a7dd 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -1224,11 +1224,13 @@ class Mapper(object):
try:
if item_type == 'property':
prop = iterator.next()
- visitables.append((prop.cascade_iterator(type_, parent_state, visited_instances, halt_on), 'mapper', None))
+ visitables.append((prop.cascade_iterator(type_, parent_state,
+ visited_instances, halt_on), 'mapper', None))
elif item_type == 'mapper':
instance, instance_mapper, corresponding_state = iterator.next()
yield (instance, instance_mapper)
- visitables.append((instance_mapper._props.itervalues(), 'property', corresponding_state))
+ visitables.append((instance_mapper._props.itervalues(),
+ 'property', corresponding_state))
except StopIteration:
visitables.pop()
@@ -1297,52 +1299,43 @@ class Mapper(object):
# if batch=false, call _save_obj separately for each object
if not single and not self.batch:
for state in _sort_states(states):
- self._save_obj([state], uowtransaction, postupdate=postupdate, post_update_cols=post_update_cols, single=True)
+ self._save_obj([state],
+ uowtransaction,
+ postupdate=postupdate,
+ post_update_cols=post_update_cols,
+ single=True)
return
-
+
# if session has a connection callable,
- # organize individual states with the connection to use for insert/update
- tups = []
+ # organize individual states with the connection
+ # to use for insert/update
if 'connection_callable' in uowtransaction.mapper_flush_opts:
- connection_callable = uowtransaction.mapper_flush_opts['connection_callable']
- for state in _sort_states(states):
- m = _state_mapper(state)
- tups.append(
- (
- state,
- m,
- connection_callable(self, state.obj()),
- _state_has_identity(state),
- state.key or m._identity_key_from_state(state)
- )
- )
+ connection_callable = \
+ uowtransaction.mapper_flush_opts['connection_callable']
else:
connection = uowtransaction.transaction.connection(self)
- for state in _sort_states(states):
- m = _state_mapper(state)
- tups.append(
- (
- state,
- m,
- connection,
- _state_has_identity(state),
- state.key or m._identity_key_from_state(state)
- )
- )
+ connection_callable = None
- if not postupdate:
- # call before_XXX extensions
- for state, mapper, connection, has_identity, instance_key in tups:
+ tups = []
+ for state in _sort_states(states):
+ conn = connection_callable and \
+ connection_callable(self, state.obj()) or \
+ connection
+
+ has_identity = _state_has_identity(state)
+ mapper = _state_mapper(state)
+ instance_key = state.key or mapper._identity_key_from_state(state)
+
+ row_switch = None
+ if not postupdate:
+ # call before_XXX extensions
if not has_identity:
if 'before_insert' in mapper.extension:
- mapper.extension.before_insert(mapper, connection, state.obj())
+ mapper.extension.before_insert(mapper, conn, state.obj())
else:
if 'before_update' in mapper.extension:
- mapper.extension.before_update(mapper, connection, state.obj())
+ mapper.extension.before_update(mapper, conn, state.obj())
- row_switches = {}
- if not postupdate:
- for state, mapper, connection, has_identity, instance_key in tups:
# detect if we have a "pending" instance (i.e. has no instance_key attached to it),
# and another instance with the same identity key already exists as persistent.
# convert to an UPDATE if so.
@@ -1354,28 +1347,42 @@ class Mapper(object):
"New instance %s with identity key %s conflicts "
"with persistent instance %s" %
(state_str(state), instance_key, state_str(existing)))
-
+
self._log_debug(
- "detected row switch for identity %s. will update %s, remove %s from "
- "transaction", instance_key, state_str(state), state_str(existing))
-
+ "detected row switch for identity %s. "
+ "will update %s, remove %s from "
+ "transaction", instance_key,
+ state_str(state), state_str(existing))
+
# remove the "delete" flag from the existing element
uowtransaction.remove_state_actions(existing)
- row_switches[state] = existing
-
+ row_switch = existing
+
+ tups.append(
+ (state,
+ mapper,
+ conn,
+ has_identity,
+ instance_key,
+ row_switch)
+ )
+
table_to_mapper = self._sorted_tables
- for table in table_to_mapper.iterkeys():
+ for table in table_to_mapper:
insert = []
update = []
- for state, mapper, connection, has_identity, instance_key in tups:
+ for state, mapper, connection, has_identity, \
+ instance_key, row_switch in tups:
if table not in mapper._pks_by_table:
continue
pks = mapper._pks_by_table[table]
- isinsert = not has_identity and not postupdate and state not in row_switches
+ isinsert = not has_identity and \
+ not postupdate and \
+ not row_switch
params = {}
value_params = {}
@@ -1405,23 +1412,36 @@ class Mapper(object):
value_params[col] = value
else:
params[col.key] = value
- insert.append((state, params, mapper, connection, value_params))
+ insert.append((state, params, mapper,
+ connection, value_params))
else:
for col in mapper._cols_by_table[table]:
if col is mapper.version_id_col:
- params[col._label] = mapper._get_state_attr_by_column(row_switches.get(state, state), col)
- params[col.key] = mapper.version_id_generator(params[col._label])
+ params[col._label] = \
+ mapper._get_state_attr_by_column(
+ row_switch or state,
+ col)
+ params[col.key] = \
+ mapper.version_id_generator(params[col._label])
+
+ # HACK: check for history, in case the history is only
+ # in a different table than the one where the version_id_col
+ # is.
for prop in mapper._columntoproperty.itervalues():
- history = attributes.get_state_history(state, prop.key, passive=True)
+ history = attributes.get_state_history(
+ state, prop.key, passive=True)
if history.added:
hasdata = True
elif mapper.polymorphic_on is not None and \
- mapper.polymorphic_on.shares_lineage(col) and col not in pks:
+ mapper.polymorphic_on.shares_lineage(col) and \
+ col not in pks:
pass
else:
- if post_update_cols is not None and col not in post_update_cols:
+ if post_update_cols is not None and \
+ col not in post_update_cols:
if col in pks:
- params[col._label] = mapper._get_state_attr_by_column(state, col)
+ params[col._label] = \
+ mapper._get_state_attr_by_column(state, col)
continue
prop = mapper._columntoproperty[col]
@@ -1458,27 +1478,32 @@ class Mapper(object):
elif col in pks:
params[col._label] = mapper._get_state_attr_by_column(state, col)
if hasdata:
- update.append((state, params, mapper, connection, value_params))
+ update.append((state, params, mapper,
+ connection, value_params))
if update:
mapper = table_to_mapper[table]
clause = sql.and_()
for col in mapper._pks_by_table[table]:
- clause.clauses.append(col == sql.bindparam(col._label, type_=col.type))
+ clause.clauses.append(
+ col ==
+ sql.bindparam(col._label, type_=col.type)
+ )
- if mapper.version_id_col is not None and \
- table.c.contains_column(mapper.version_id_col):
-
+ needs_version_id = mapper.version_id_col is not None and \
+ table.c.contains_column(mapper.version_id_col)
+
+ if needs_version_id:
clause.clauses.append(mapper.version_id_col ==\
sql.bindparam(mapper.version_id_col._label, type_=col.type))
statement = table.update(clause)
-
+
rows = 0
for state, params, mapper, connection, value_params in update:
c = connection.execute(statement.values(value_params), params)
- mapper._postfetch(uowtransaction, connection, table,
+ mapper._postfetch(uowtransaction, table,
state, c, c.last_updated_params(), value_params)
rows += c.rowcount
@@ -1486,13 +1511,15 @@ class Mapper(object):
if connection.dialect.supports_sane_rowcount:
if rows != len(update):
raise orm_exc.ConcurrentModificationError(
- "Updated rowcount %d does not match number of objects updated %d" %
+ "Updated rowcount %d does not match number "
+ "of objects updated %d" %
(rows, len(update)))
-
- elif mapper.version_id_col is not None:
+
+ elif needs_version_id:
util.warn("Dialect %s does not support updated rowcount "
- "- versioning cannot be verified." % c.dialect.dialect_description,
- stacklevel=12)
+ "- versioning cannot be verified." %
+ c.dialect.dialect_description,
+ stacklevel=12)
if insert:
statement = table.insert()
@@ -1507,12 +1534,12 @@ class Mapper(object):
len(primary_key) > i:
mapper._set_state_attr_by_column(state, col, primary_key[i])
- mapper._postfetch(uowtransaction, connection, table,
+ mapper._postfetch(uowtransaction, table,
state, c, c.last_inserted_params(), value_params)
-
if not postupdate:
- for state, mapper, connection, has_identity, instance_key in tups:
+ for state, mapper, connection, has_identity, \
+ instance_key, row_switch in tups:
# expire readonly attributes
readonly = state.unmodified.intersection(
@@ -1522,8 +1549,8 @@ class Mapper(object):
if readonly:
_expire_state(state, state.dict, readonly)
- # if specified, eagerly refresh whatever has
- # been expired.
+ # if eager_defaults option is enabled,
+ # refresh whatever has been expired.
if self.eager_defaults and state.unloaded:
state.key = self._identity_key_from_state(state)
uowtransaction.session.query(self)._get(
@@ -1538,7 +1565,7 @@ class Mapper(object):
if 'after_update' in mapper.extension:
mapper.extension.after_update(mapper, connection, state.obj())
- def _postfetch(self, uowtransaction, connection, table,
+ def _postfetch(self, uowtransaction, table,
state, resultproxy, params, value_params):
"""Expire attributes in need of newly persisted database state."""
@@ -1557,23 +1584,37 @@ class Mapper(object):
if c.key in params and c in self._columntoproperty:
self._set_state_attr_by_column(state, c, params[c.key])
- deferred_props = [prop.key for prop in [self._columntoproperty[c] for c in postfetch_cols]]
-
- if deferred_props:
- _expire_state(state, state.dict, deferred_props)
+ if postfetch_cols:
+ _expire_state(state, state.dict,
+ [self._columntoproperty[c].key
+ for c in postfetch_cols]
+ )
# synchronize newly inserted ids from one table to the next
# TODO: this still goes a little too often. would be nice to
# have definitive list of "columns that changed" here
- cols = set(table.c)
- for m in self.iterate_to_root():
- if m._inherits_equated_pairs and \
- cols.intersection([l for l, r in m._inherits_equated_pairs]):
- sync.populate(state, m, state, m,
- m._inherits_equated_pairs,
- uowtransaction,
- self.passive_updates)
-
+ for m, equated_pairs in self._table_to_equated[table]:
+ sync.populate(state, m, state, m,
+ equated_pairs,
+ uowtransaction,
+ self.passive_updates)
+
+ @util.memoized_property
+ def _table_to_equated(self):
+ """memoized map of tables to collections of columns to be
+ synchronized upwards to the base mapper."""
+
+ result = util.defaultdict(list)
+
+ for table in self._sorted_tables:
+ cols = set(table.c)
+ for m in self.iterate_to_root():
+ if m._inherits_equated_pairs and \
+ cols.intersection([l for l, r in m._inherits_equated_pairs]):
+ result[table].append((m, m._inherits_equated_pairs))
+
+ return result
+
def _delete_obj(self, states, uowtransaction):
"""Issue ``DELETE`` statements for a list of objects.
@@ -1582,50 +1623,95 @@ class Mapper(object):
"""
if 'connection_callable' in uowtransaction.mapper_flush_opts:
- connection_callable = uowtransaction.mapper_flush_opts['connection_callable']
- tups = [(state, _state_mapper(state), connection_callable(self, state.obj())) for state in _sort_states(states)]
+ connection_callable = \
+ uowtransaction.mapper_flush_opts['connection_callable']
else:
connection = uowtransaction.transaction.connection(self)
- tups = [(state, _state_mapper(state), connection) for state in _sort_states(states)]
-
- for state, mapper, connection in tups:
+ connection_callable = None
+
+ tups = []
+ for state in _sort_states(states):
+ mapper = _state_mapper(state)
+
+ conn = connection_callable and \
+ connection_callable(self, state.obj()) or \
+ connection
+
if 'before_delete' in mapper.extension:
- mapper.extension.before_delete(mapper, connection, state.obj())
+ mapper.extension.before_delete(mapper, conn, state.obj())
+
+ tups.append((state,
+ _state_mapper(state),
+ _state_has_identity(state),
+ conn))
table_to_mapper = self._sorted_tables
for table in reversed(table_to_mapper.keys()):
- delete = {}
- for state, mapper, connection in tups:
- if table not in mapper._pks_by_table:
+ delete = util.defaultdict(list)
+ for state, mapper, has_identity, connection in tups:
+ if not has_identity or table not in mapper._pks_by_table:
continue
params = {}
- if not _state_has_identity(state):
- continue
- else:
- delete.setdefault(connection, []).append(params)
+ delete[connection].append(params)
for col in mapper._pks_by_table[table]:
params[col.key] = mapper._get_state_attr_by_column(state, col)
- if mapper.version_id_col is not None and table.c.contains_column(mapper.version_id_col):
- params[mapper.version_id_col.key] = mapper._get_state_attr_by_column(state, mapper.version_id_col)
+ if mapper.version_id_col is not None and \
+ table.c.contains_column(mapper.version_id_col):
+ params[mapper.version_id_col.key] = \
+ mapper._get_state_attr_by_column(state, mapper.version_id_col)
for connection, del_objects in delete.iteritems():
mapper = table_to_mapper[table]
clause = sql.and_()
for col in mapper._pks_by_table[table]:
clause.clauses.append(col == sql.bindparam(col.key, type_=col.type))
- if mapper.version_id_col is not None and table.c.contains_column(mapper.version_id_col):
+
+ need_version_id = mapper.version_id_col is not None and \
+ table.c.contains_column(mapper.version_id_col)
+
+ if need_version_id:
clause.clauses.append(
mapper.version_id_col ==
- sql.bindparam(mapper.version_id_col.key, type_=mapper.version_id_col.type))
+ sql.bindparam(
+ mapper.version_id_col.key,
+ type_=mapper.version_id_col.type
+ )
+ )
+
statement = table.delete(clause)
- c = connection.execute(statement, del_objects)
- if c.supports_sane_multi_rowcount() and c.rowcount != len(del_objects):
- raise orm_exc.ConcurrentModificationError("Deleted rowcount %d does not match "
- "number of objects deleted %d" % (c.rowcount, len(del_objects)))
+ rows = -1
+
+ if need_version_id and \
+ not connection.dialect.supports_sane_multi_rowcount:
+ # TODO: need test coverage for this [ticket:1761]
+ if connection.dialect.supports_sane_rowcount:
+ rows = 0
+ # execute deletes individually so that versioned
+ # rows can be verified
+ for params in del_objects:
+ c = connection.execute(statement, params)
+ rows += c.rowcount
+ else:
+ util.warn("Dialect %s does not support deleted rowcount "
+ "- versioning cannot be verified." %
+ c.dialect.dialect_description,
+ stacklevel=12)
+ connection.execute(statement, del_objects)
+ else:
+ c = connection.execute(statement, del_objects)
+ if connection.dialect.supports_sane_multi_rowcount:
+ rows = c.rowcount
+
+ if rows != -1 and rows != len(del_objects):
+ raise orm_exc.ConcurrentModificationError(
+ "Deleted rowcount %d does not match "
+ "number of objects deleted %d" %
+ (c.rowcount, len(del_objects))
+ )
- for state, mapper, connection in tups:
+ for state, mapper, has_identity, connection in tups:
if 'after_delete' in mapper.extension:
mapper.extension.after_delete(mapper, connection, state.obj())
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index 754cdc118..f6fc4b81a 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -58,6 +58,8 @@ class ColumnProperty(StrategizedProperty):
self.comparator_factory = kwargs.pop('comparator_factory', self.__class__.Comparator)
self.descriptor = kwargs.pop('descriptor', None)
self.extension = kwargs.pop('extension', None)
+ self.doc = kwargs.pop('doc', getattr(columns[0], 'doc', None))
+
if kwargs:
raise TypeError(
"%s received unexpected keyword argument(s): %s" % (
@@ -80,7 +82,8 @@ class ColumnProperty(StrategizedProperty):
self.key,
comparator=self.comparator_factory(self, mapper),
parententity=mapper,
- property_=self
+ property_=self,
+ doc=self.doc
)
def do_init(self):
@@ -259,11 +262,12 @@ class SynonymProperty(MapperProperty):
extension = None
- def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None):
+ def __init__(self, name, map_column=None, descriptor=None, comparator_factory=None, doc=None):
self.name = name
self.map_column = map_column
self.descriptor = descriptor
self.comparator_factory = comparator_factory
+ self.doc = doc or (descriptor and descriptor.__doc__) or None
util.set_creation_order(self)
def setup(self, context, entity, path, adapter, **kwargs):
@@ -303,7 +307,8 @@ class SynonymProperty(MapperProperty):
comparator=comparator_callable(self, mapper),
parententity=mapper,
property_=self,
- proxy_property=self.descriptor
+ proxy_property=self.descriptor,
+ doc=self.doc
)
def merge(self, session, source_state, source_dict, dest_state, dest_dict, load, _recursive):
@@ -316,9 +321,10 @@ class ComparableProperty(MapperProperty):
extension = None
- def __init__(self, comparator_factory, descriptor=None):
+ def __init__(self, comparator_factory, descriptor=None, doc=None):
self.descriptor = descriptor
self.comparator_factory = comparator_factory
+ self.doc = doc or (descriptor and descriptor.__doc__) or None
util.set_creation_order(self)
def instrument_class(self, mapper):
@@ -330,7 +336,8 @@ class ComparableProperty(MapperProperty):
comparator=self.comparator_factory(self, mapper),
parententity=mapper,
property_=self,
- proxy_property=self.descriptor
+ proxy_property=self.descriptor,
+ doc=self.doc,
)
def setup(self, context, entity, path, adapter, **kwargs):
@@ -364,6 +371,7 @@ class RelationshipProperty(StrategizedProperty):
enable_typechecks=True, join_depth=None,
comparator_factory=None,
single_parent=False, innerjoin=False,
+ doc=None,
strategy_class=None, _local_remote_pairs=None, query_class=None):
self.uselist = uselist
@@ -384,7 +392,7 @@ class RelationshipProperty(StrategizedProperty):
self.enable_typechecks = enable_typechecks
self.query_class = query_class
self.innerjoin = innerjoin
-
+ self.doc = doc
self.join_depth = join_depth
self.local_remote_pairs = _local_remote_pairs
self.extension = extension
@@ -433,7 +441,8 @@ class RelationshipProperty(StrategizedProperty):
self.key,
comparator=self.comparator_factory(self, mapper),
parententity=mapper,
- property_=self
+ property_=self,
+ doc=self.doc,
)
class Comparator(PropComparator):
@@ -1149,7 +1158,7 @@ class RelationshipProperty(StrategizedProperty):
parent = self.parent.primary_mapper()
kwargs.setdefault('viewonly', self.viewonly)
kwargs.setdefault('post_update', self.post_update)
-
+
self.back_populates = backref_key
relationship = RelationshipProperty(
parent,
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 93b1170f4..39657564a 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -66,6 +66,7 @@ def _register_attribute(strategy, mapper, useobject,
callable_=callable_,
active_history=active_history,
impl_class=impl_class,
+ doc=prop.doc,
**kw
)
diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py
index 8ffb68a4e..0e03be686 100644
--- a/lib/sqlalchemy/schema.py
+++ b/lib/sqlalchemy/schema.py
@@ -535,6 +535,10 @@ class Column(SchemaItem, expression.ColumnClause):
Contrast this argument to ``server_default`` which creates a
default generator on the database side.
+ :param doc: optional String that can be used by the ORM or similar
+ to document attributes. This attribute does not render SQL
+ comments (a future attribute 'comment' will achieve that).
+
:param key: An optional string identifier which will identify this
``Column`` object on the :class:`Table`. When a key is provided,
this is the only identifier referencing the ``Column`` within the
@@ -651,6 +655,7 @@ class Column(SchemaItem, expression.ColumnClause):
self.index = kwargs.pop('index', None)
self.unique = kwargs.pop('unique', None)
self.quote = kwargs.pop('quote', None)
+ self.doc = kwargs.pop('doc', None)
self.onupdate = kwargs.pop('onupdate', None)
self.autoincrement = kwargs.pop('autoincrement', True)
self.constraints = set()
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 3aaa06fd6..5958a0bc4 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -3185,6 +3185,16 @@ class ColumnClause(_Immutable, ColumnElement):
label = _escape_for_generated(self.table.name) + "_" + \
_escape_for_generated(self.name)
+ # ensure the label name doesn't conflict with that
+ # of an existing column
+ if label in self.table.c:
+ _label = label
+ counter = 1
+ while _label in self.table.c:
+ _label = label + "_" + str(counter)
+ counter += 1
+ label = _label
+
return _generated_label(label)
else:
diff --git a/lib/sqlalchemy/topological.py b/lib/sqlalchemy/topological.py
index 8886bcbf7..5fc982ae0 100644
--- a/lib/sqlalchemy/topological.py
+++ b/lib/sqlalchemy/topological.py
@@ -132,4 +132,3 @@ def find_cycles(tuples, allitems):
else:
node = stack.pop()
return output
-
diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py
index 16cd57f26..dba75b36e 100644
--- a/lib/sqlalchemy/types.py
+++ b/lib/sqlalchemy/types.py
@@ -939,6 +939,13 @@ class Numeric(_DateAffinity, TypeEngine):
# we're a "numeric", DBAPI will give us Decimal directly
return None
else:
+ util.warn("Dialect %s+%s does *not* support Decimal objects natively, "
+ "and SQLAlchemy must convert from floating point - "
+ "rounding errors and other issues may occur. "
+ "Please consider storing Decimal numbers as strings or "
+ "integers on this platform for lossless storage." %
+ (dialect.name, dialect.driver))
+
# we're a "numeric", DBAPI returns floats, convert.
if self.scale is not None:
return processors.to_decimal_processor_factory(_python_Decimal, self.scale)
@@ -976,7 +983,8 @@ class Float(Numeric):
:param precision: the numeric precision for use in DDL ``CREATE TABLE``.
:param asdecimal: the same flag as that of :class:`Numeric`, but
- defaults to ``False``.
+ defaults to ``False``. Note that setting this flag to ``True``
+ results in floating point conversion.
"""
self.precision = precision