diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-04-04 10:38:29 -0400 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-04-04 10:38:29 -0400 |
| commit | f05b5b0bda7b21a585f3252016d2e74c361b6233 (patch) | |
| tree | 56f94f0b1088b9738ae363832b1466f7a98befdd /lib | |
| parent | 3d3095497b872696e8860763f54217f425c7d35b (diff) | |
| parent | dbc582a43fb8e5d3c0b05cb62b6a77c4e5bc27ac (diff) | |
| download | sqlalchemy-f05b5b0bda7b21a585f3252016d2e74c361b6233.tar.gz | |
merge default branch
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/cextension/resultproxy.c | 35 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/oracle/cx_oracle.py | 104 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/compiler.py | 19 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/declarative.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 24 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/attributes.py | 11 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 298 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/properties.py | 25 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/schema.py | 5 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/expression.py | 10 | ||||
| -rw-r--r-- | lib/sqlalchemy/topological.py | 1 | ||||
| -rw-r--r-- | lib/sqlalchemy/types.py | 10 |
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 |
