From b66dad46f31961ad9f2271e6dae377e38fc67979 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sat, 1 Dec 2012 20:12:23 -0500 Subject: - refactor of pathing mechanics, to address #2614, #2617 - paths now store Mapper + MapperProperty now instead of string key, so that the parent mapper for the property is known, supports same-named properties on multiple subclasses - the Mapper within the path is now always relevant to the property to the right of it. PathRegistry does the translation now, instead of having all the outside users of PathRegistry worry about it, to produce a path that is much more consistent. Paths are now consistent with mappings in all cases. Special logic to get at "with_polymorphic" structures and such added also. - AliasedClass now has two modes, "use_mapper_path" and regular; "use_mapper_path" is for all those situations where we put an AliasedClass in for a plain class internally, and want it to "path" with the plain mapper. - The AliasedInsp is now the first class "entity" for an AliasedClass, and is passed around internally and used as attr._parententity and such. it is the AliasedClass analogue for Mapper. --- lib/sqlalchemy/ext/serializer.py | 8 ++ lib/sqlalchemy/orm/interfaces.py | 33 +++---- lib/sqlalchemy/orm/loading.py | 15 ++-- lib/sqlalchemy/orm/mapper.py | 6 +- lib/sqlalchemy/orm/query.py | 37 +++++--- lib/sqlalchemy/orm/strategies.py | 83 ++++++++--------- lib/sqlalchemy/orm/util.py | 189 +++++++++++++++++++++++++++------------ 7 files changed, 235 insertions(+), 136 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/ext/serializer.py b/lib/sqlalchemy/ext/serializer.py index 3ed41f48a..c129b0dcc 100644 --- a/lib/sqlalchemy/ext/serializer.py +++ b/lib/sqlalchemy/ext/serializer.py @@ -54,6 +54,7 @@ needed for: from ..orm import class_mapper from ..orm.session import Session from ..orm.mapper import Mapper +from ..orm.interfaces import MapperProperty from ..orm.attributes import QueryableAttribute from .. import Table, Column from ..engine import Engine @@ -90,6 +91,9 @@ def Serializer(*args, **kw): id = "attribute:" + key + ":" + b64encode(pickle.dumps(cls)) elif isinstance(obj, Mapper) and not obj.non_primary: id = "mapper:" + b64encode(pickle.dumps(obj.class_)) + elif isinstance(obj, MapperProperty) and not obj.parent.non_primary: + id = "mapperprop:" + b64encode(pickle.dumps(obj.parent.class_)) + \ + ":" + obj.key elif isinstance(obj, Table): id = "table:" + str(obj) elif isinstance(obj, Column) and isinstance(obj.table, Table): @@ -134,6 +138,10 @@ def Deserializer(file, metadata=None, scoped_session=None, engine=None): elif type_ == "mapper": cls = pickle.loads(b64decode(args)) return class_mapper(cls) + elif type_ == "mapperprop": + mapper, keyname = args.split(':') + cls = pickle.loads(b64decode(args)) + return class_mapper(cls).attrs[keyname] elif type_ == "table": return metadata.tables[args] elif type_ == "column": diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index c91746da0..55a980b2e 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -219,6 +219,10 @@ class MapperProperty(_MappedAttribute, _InspectionAttr): return operator(self.comparator, value) + def __repr__(self): + return '<%s at 0x%x; %s>' % ( + self.__class__.__name__, + id(self), self.key) class PropComparator(operators.ColumnOperators): """Defines boolean, comparison, and other operators for @@ -413,21 +417,18 @@ class StrategizedProperty(MapperProperty): return None def _get_context_strategy(self, context, path): - # this is essentially performance inlining. - key = ('loaderstrategy', path.reduced_path + (self.key,)) - cls = None - if key in context.attributes: - cls = context.attributes[key] - else: + strategy_cls = path._inlined_get_for(self, context, 'loaderstrategy') + + if not strategy_cls: wc_key = self._wildcard_path if wc_key and wc_key in context.attributes: - cls = context.attributes[wc_key] + strategy_cls = context.attributes[wc_key] - if cls: + if strategy_cls: try: - return self._strategies[cls] + return self._strategies[strategy_cls] except KeyError: - return self.__init_strategy(cls) + return self.__init_strategy(strategy_cls) return self.strategy def _get_strategy(self, cls): @@ -528,10 +529,8 @@ class PropertyOption(MapperOption): def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): if orm_util._is_aliased_class(mapper): searchfor = mapper - isa = False else: searchfor = orm_util._class_to_mapper(mapper) - isa = True for ent in query._mapper_entities: if ent.corresponds_to(searchfor): return ent @@ -600,7 +599,7 @@ class PropertyOption(MapperOption): # exhaust current_path before # matching tokens to entities if current_path: - if current_path[1] == token: + if current_path[1].key == token: current_path = current_path[2:] continue else: @@ -634,7 +633,7 @@ class PropertyOption(MapperOption): # matching tokens to entities if current_path: if current_path[0:2] == \ - [token._parententity, prop.key]: + [token._parententity, prop]: current_path = current_path[2:] continue else: @@ -648,6 +647,7 @@ class PropertyOption(MapperOption): raiseerr) if not entity: return no_result + path_element = entity.entity_zero mapper = entity.mapper else: @@ -659,7 +659,7 @@ class PropertyOption(MapperOption): raise sa_exc.ArgumentError("Attribute '%s' does not " "link from element '%s'" % (token, path_element)) - path = path[path_element][prop.key] + path = path[path_element][prop] paths.append(path) @@ -670,7 +670,8 @@ class PropertyOption(MapperOption): if not ext_info.is_aliased_class: ac = orm_util.with_polymorphic( ext_info.mapper.base_mapper, - ext_info.mapper, aliased=True) + ext_info.mapper, aliased=True, + _use_mapper_path=True) ext_info = inspect(ac) path.set(query, "path_with_polymorphic", ext_info) else: diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index a5d156a1f..4bd80c388 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -271,6 +271,7 @@ def instance_processor(mapper, context, path, adapter, new_populators = [] existing_populators = [] eager_populators = [] + load_path = context.query._current_path + path \ if context.query._current_path.path \ else path @@ -504,9 +505,12 @@ def _populators(mapper, context, path, row, adapter, delayed_populators = [] pops = (new_populators, existing_populators, delayed_populators, eager_populators) + for prop in mapper._props.itervalues(): + for i, pop in enumerate(prop.create_row_processor( - context, path, + context, + path, mapper, row, adapter)): if pop is not None: pops[i].append((prop.key, pop)) @@ -529,17 +533,10 @@ def _configure_subclass_mapper(mapper, context, path, adapter): if sub_mapper is mapper: return None - # replace the tip of the path info with the subclass mapper - # being used, that way accurate "load_path" info is available - # for options invoked during deferred loads, e.g. - # query(Person).options(defer(Engineer.machines, Machine.name)). - # for AliasedClass paths, disregard this step (new in 0.8). return instance_processor( sub_mapper, context, - path.parent[sub_mapper] - if not path.is_aliased_class - else path, + path, adapter, polymorphic_from=mapper) return configure_subclass_mapper diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index b89163340..626105b5e 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -472,7 +472,7 @@ class Mapper(_InspectionAttr): dispatch = event.dispatcher(events.MapperEvents) @util.memoized_property - def _sa_path_registry(self): + def _path_registry(self): return PathRegistry.per_mapper(self) def _configure_inheritance(self): @@ -1403,7 +1403,7 @@ class Mapper(_InspectionAttr): if _new_mappers: configure_mappers() if not self.with_polymorphic: - return [self] + return [] return self._mappers_from_spec(*self.with_polymorphic) @_memoized_configured_property @@ -1458,10 +1458,10 @@ class Mapper(_InspectionAttr): return list(self._iterate_polymorphic_properties( self._with_polymorphic_mappers)) + def _iterate_polymorphic_properties(self, mappers=None): """Return an iterator of MapperProperty objects which will render into a SELECT.""" - if mappers is None: mappers = self._with_polymorphic_mappers diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index ca334e273..d6847177f 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -157,7 +157,7 @@ class Query(object): ent.setup_entity(*d[entity]) def _mapper_loads_polymorphically_with(self, mapper, adapter): - for m2 in mapper._with_polymorphic_mappers: + for m2 in mapper._with_polymorphic_mappers or [mapper]: self._polymorphic_adapters[m2] = adapter for m in m2.iterate_to_root(): self._polymorphic_adapters[m.local_table] = adapter @@ -2744,17 +2744,24 @@ class _MapperEntity(_QueryEntity): self._with_polymorphic = ext_info.with_polymorphic_mappers self._polymorphic_discriminator = \ ext_info.polymorphic_on + self.entity_zero = ext_info if ext_info.is_aliased_class: - self.entity_zero = ext_info.entity - self._label_name = self.entity_zero._sa_label_name + self._label_name = self.entity_zero.name else: - self.entity_zero = self.mapper self._label_name = self.mapper.class_.__name__ - self.path = self.entity_zero._sa_path_registry + self.path = self.entity_zero._path_registry def set_with_polymorphic(self, query, cls_or_mappers, selectable, polymorphic_on): + """Receive an update from a call to query.with_polymorphic(). + + Note the newer style of using a free standing with_polymporphic() + construct doesn't make use of this method. + + + """ if self.is_aliased_class: + # TODO: invalidrequest ? raise NotImplementedError( "Can't use with_polymorphic() against " "an Aliased object" @@ -2785,13 +2792,18 @@ class _MapperEntity(_QueryEntity): return self.entity_zero def corresponds_to(self, entity): - entity_info = inspect(entity) - if entity_info.is_aliased_class or self.is_aliased_class: - return entity is self.entity_zero \ - or \ - entity in self._with_polymorphic - else: - return entity.common_parent(self.entity_zero) + if entity.is_aliased_class: + if self.is_aliased_class: + if entity._base_alias is self.entity_zero._base_alias: + return True + return False + elif self.is_aliased_class: + if self.entity_zero._use_mapper_path: + return entity in self._with_polymorphic + else: + return entity is self.entity_zero + + return entity.common_parent(self.entity_zero) def adapt_to_selectable(self, query, sel): query._entities.append(self) @@ -3008,6 +3020,7 @@ class _ColumnEntity(_QueryEntity): if self.entity_zero is None: return False elif _is_aliased_class(entity): + # TODO: polymorphic subclasses ? return entity is self.entity_zero else: return not _is_aliased_class(self.entity_zero) and \ diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 586ec4b4e..05c7ef37b 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -303,16 +303,6 @@ class AbstractRelationshipLoader(LoaderStrategy): self.uselist = self.parent_property.uselist - def _warn_existing_path(self): - raise sa_exc.InvalidRequestError( - "Eager loading cannot currently function correctly when two or " - "more " - "same-named attributes associated with multiple polymorphic " - "classes " - "of the same base are present. Encountered more than one " - r"eager path for attribute '%s' on mapper '%s'." % - (self.key, self.parent.base_mapper, )) - class NoLoader(AbstractRelationshipLoader): """Provide loading behavior for a :class:`.RelationshipProperty` @@ -564,7 +554,7 @@ class LazyLoader(AbstractRelationshipLoader): q = q.autoflush(False) if state.load_path: - q = q._with_current_path(state.load_path[self.key]) + q = q._with_current_path(state.load_path[self.parent_property]) if state.load_options: q = q._conditional_options(*state.load_options) @@ -694,7 +684,7 @@ class SubqueryLoader(AbstractRelationshipLoader): if not context.query._enable_eagerloads: return - path = path[self.key] + path = path[self.parent_property] # build up a path indicating the path from the leftmost # entity to the thing we're subquery loading. @@ -757,22 +747,20 @@ class SubqueryLoader(AbstractRelationshipLoader): # add new query to attributes to be picked up # by create_row_processor - existing = path.replace(context, "subquery", q) - if existing: - self._warn_existing_path() + path.set(context, "subquery", q) def _get_leftmost(self, subq_path): subq_path = subq_path.path subq_mapper = orm_util._class_to_mapper(subq_path[0]) # determine attributes of the leftmost mapper - if self.parent.isa(subq_mapper) and self.key == subq_path[1]: + if self.parent.isa(subq_mapper) and self.parent_property is subq_path[1]: leftmost_mapper, leftmost_prop = \ self.parent, self.parent_property else: leftmost_mapper, leftmost_prop = \ subq_mapper, \ - subq_mapper._props[subq_path[1]] + subq_path[1] leftmost_cols = leftmost_prop.local_columns @@ -805,23 +793,35 @@ class SubqueryLoader(AbstractRelationshipLoader): # the original query now becomes a subquery # which we'll join onto. embed_q = q.with_labels().subquery() - left_alias = orm_util.AliasedClass(leftmost_mapper, embed_q) + left_alias = orm_util.AliasedClass(leftmost_mapper, embed_q, + use_mapper_path=True) return left_alias def _prep_for_joins(self, left_alias, subq_path): - subq_path = subq_path.path - # figure out what's being joined. a.k.a. the fun part - to_join = [ - (subq_path[i], subq_path[i + 1]) - for i in xrange(0, len(subq_path), 2) - ] + to_join = [] + pairs = list(subq_path.pairs()) + + for i, (mapper, prop) in enumerate(pairs): + if i > 0: + # look at the previous mapper in the chain - + # if it is as or more specific than this prop's + # mapper, use that instead. + # note we have an assumption here that + # the non-first element is always going to be a mapper, + # not an AliasedClass + + prev_mapper = pairs[i - 1][1].mapper + to_append = prev_mapper if prev_mapper.isa(mapper) else mapper + else: + to_append = mapper + + to_join.append((to_append, prop.key)) # determine the immediate parent class we are joining from, # which needs to be aliased. - if len(to_join) > 1: - info = inspect(subq_path[-2]) + info = inspect(to_join[-1][0]) if len(to_join) < 2: # in the case of a one level eager load, this is the @@ -833,11 +833,13 @@ class SubqueryLoader(AbstractRelationshipLoader): # in the vast majority of cases, and [ticket:2014] # illustrates a case where sub_path[-2] is a subclass # of self.parent - parent_alias = orm_util.AliasedClass(subq_path[-2]) + parent_alias = orm_util.AliasedClass(to_join[-1][0], + use_mapper_path=True) else: # if of_type() were used leading to this relationship, # self.parent is more specific than subq_path[-2] - parent_alias = orm_util.AliasedClass(self.parent) + parent_alias = orm_util.AliasedClass(self.parent, + use_mapper_path=True) local_cols = self.parent_property.local_columns @@ -916,9 +918,10 @@ class SubqueryLoader(AbstractRelationshipLoader): "population - eager loading cannot be applied." % self) - path = path[self.key] + path = path[self.parent_property] subq = path.get(context, 'subquery') + if subq is None: return None, None, None @@ -1000,7 +1003,7 @@ class JoinedLoader(AbstractRelationshipLoader): if not context.query._enable_eagerloads: return - path = path[self.key] + path = path[self.parent_property] with_polymorphic = None @@ -1040,6 +1043,7 @@ class JoinedLoader(AbstractRelationshipLoader): with_polymorphic = None path = path[self.mapper] + for value in self.mapper._iterate_polymorphic_properties( mappers=with_polymorphic): value.setup( @@ -1079,7 +1083,8 @@ class JoinedLoader(AbstractRelationshipLoader): if with_poly_info: to_adapt = with_poly_info.entity else: - to_adapt = orm_util.AliasedClass(self.mapper) + to_adapt = orm_util.AliasedClass(self.mapper, + use_mapper_path=True) clauses = orm_util.ORMAdapter( to_adapt, equivalents=self.mapper._equivalent_columns, @@ -1104,9 +1109,8 @@ class JoinedLoader(AbstractRelationshipLoader): ) add_to_collection = context.secondary_columns - existing = path.replace(context, "eager_row_processor", clauses) - if existing: - self._warn_existing_path() + path.set(context, "eager_row_processor", clauses) + return clauses, adapter, add_to_collection, allow_innerjoin def _create_eager_join(self, context, entity, @@ -1154,7 +1158,8 @@ class JoinedLoader(AbstractRelationshipLoader): onclause = getattr( orm_util.AliasedClass( self.parent, - adapter.selectable + adapter.selectable, + use_mapper_path=True ), self.key, self.parent_property ) @@ -1238,7 +1243,7 @@ class JoinedLoader(AbstractRelationshipLoader): "population - eager loading cannot be applied." % self) - our_path = path[self.key] + our_path = path[self.parent_property] eager_adapter = self._create_eager_adapter( context, @@ -1391,15 +1396,13 @@ class LoadEagerFromAliasOption(PropertyOption): def process_query_property(self, query, paths): if self.chained: for path in paths[0:-1]: - (root_mapper, propname) = path.path[-2:] - prop = root_mapper._props[propname] + (root_mapper, prop) = path.path[-2:] adapter = query._polymorphic_adapters.get(prop.mapper, None) path.setdefault(query, "user_defined_eager_row_processor", adapter) - root_mapper, propname = paths[-1].path[-2:] - prop = root_mapper._props[propname] + root_mapper, prop = paths[-1].path[-2:] if self.alias is not None: if isinstance(self.alias, basestring): self.alias = prop.target.alias(self.alias) diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index e5e725138..04838cb64 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -245,6 +245,8 @@ class ORMAdapter(sql_util.ColumnAdapter): else: return None +def _unreduce_path(path): + return PathRegistry.deserialize(path) class PathRegistry(object): """Represent query load paths and registry functions. @@ -277,19 +279,13 @@ class PathRegistry(object): self.path == other.path def set(self, reg, key, value): - reg._attributes[(key, self.reduced_path)] = value - - def replace(self, reg, key, value): - path_key = (key, self.reduced_path) - existing = reg._attributes.get(path_key, None) - reg._attributes[path_key] = value - return existing + reg._attributes[(key, self.path)] = value def setdefault(self, reg, key, value): - reg._attributes.setdefault((key, self.reduced_path), value) + reg._attributes.setdefault((key, self.path), value) def get(self, reg, key, value=None): - key = (key, self.reduced_path) + key = (key, self.path) if key in reg._attributes: return reg._attributes[key] else: @@ -302,17 +298,25 @@ class PathRegistry(object): def length(self): return len(self.path) + def pairs(self): + path = self.path + for i in xrange(0, len(path), 2): + yield path[i], path[i + 1] + def contains_mapper(self, mapper): return mapper in self.path def contains(self, reg, key): - return (key, self.reduced_path) in reg._attributes + return (key, self.path) in reg._attributes + + def __reduce__(self): + return _unreduce_path, (self.serialize(), ) def serialize(self): path = self.path return zip( [m.class_ for m in [path[i] for i in range(0, len(path), 2)]], - [path[i] for i in range(1, len(path), 2)] + [None] + [path[i].key for i in range(1, len(path), 2)] + [None] ) @classmethod @@ -320,7 +324,10 @@ class PathRegistry(object): if path is None: return None - p = tuple(chain(*[(class_mapper(mcls), key) for mcls, key in path])) + p = tuple(chain(*[(class_mapper(mcls), + class_mapper(mcls).attrs[key] + if key is not None else None) + for mcls, key in path])) if p and p[-1] is None: p = p[0:-1] return cls.coerce(p) @@ -337,7 +344,7 @@ class PathRegistry(object): @classmethod def token(cls, token): - return KeyRegistry(cls.root, token) + return TokenRegistry(cls.root, token) def __add__(self, other): return util.reduce( @@ -354,19 +361,36 @@ class RootRegistry(PathRegistry): """ path = () - reduced_path = () - def __getitem__(self, mapper): - return mapper._sa_path_registry + def __getitem__(self, entity): + return entity._path_registry PathRegistry.root = RootRegistry() +class TokenRegistry(PathRegistry): + def __init__(self, parent, token): + self.token = token + self.parent = parent + self.path = parent.path + (token,) -class KeyRegistry(PathRegistry): - def __init__(self, parent, key): - self.key = key + def __getitem__(self, entity): + raise NotImplementedError() + +class PropRegistry(PathRegistry): + def __init__(self, parent, prop): + # restate this path in terms of the + # given MapperProperty's parent. + insp = inspection.inspect(parent[-1]) + if not insp.is_aliased_class or insp._use_mapper_path: + parent = parent.parent[prop.parent] + elif insp.is_aliased_class and insp.with_polymorphic_mappers: + if prop.parent is not insp.mapper and \ + prop.parent in insp.with_polymorphic_mappers: + subclass_entity = parent[-1]._entity_for_mapper(prop.parent) + parent = parent.parent[subclass_entity] + + self.prop = prop self.parent = parent - self.path = parent.path + (key,) - self.reduced_path = parent.reduced_path + (key,) + self.path = parent.path + (prop,) def __getitem__(self, entity): if isinstance(entity, (int, slice)): @@ -381,15 +405,11 @@ class EntityRegistry(PathRegistry, dict): is_aliased_class = False def __init__(self, parent, entity): - self.key = reduced_key = entity + self.key = entity self.parent = parent - if hasattr(entity, 'base_mapper'): - reduced_key = entity.base_mapper - else: - self.is_aliased_class = True + self.is_aliased_class = entity.is_aliased_class self.path = parent.path + (entity,) - self.reduced_path = parent.reduced_path + (reduced_key,) def __nonzero__(self): return True @@ -400,8 +420,26 @@ class EntityRegistry(PathRegistry, dict): else: return dict.__getitem__(self, entity) + def _inlined_get_for(self, prop, context, key): + """an inlined version of: + + cls = path[mapperproperty].get(context, key) + + Skips the isinstance() check in __getitem__ + and the extra method call for get(). + Used by StrategizedProperty for its + very frequent lookup. + + """ + path = dict.__getitem__(self, prop) + path_key = (key, path.path) + if path_key in context._attributes: + return context._attributes[path_key] + else: + return None + def __missing__(self, key): - self[key] = item = KeyRegistry(self, key) + self[key] = item = PropRegistry(self, key) return item @@ -448,8 +486,11 @@ class AliasedClass(object): def __init__(self, cls, alias=None, name=None, adapt_on_names=False, + # TODO: None for default here? with_polymorphic_mappers=(), - with_polymorphic_discriminator=None): + with_polymorphic_discriminator=None, + base_alias=None, + use_mapper_path=False): mapper = _class_to_mapper(cls) if alias is None: alias = mapper._with_polymorphic_selectable.alias(name=name) @@ -458,11 +499,19 @@ class AliasedClass(object): mapper, alias, name, - with_polymorphic_mappers, + with_polymorphic_mappers + if with_polymorphic_mappers + else mapper.with_polymorphic_mappers, with_polymorphic_discriminator + if with_polymorphic_discriminator is not None + else mapper.polymorphic_on, + base_alias, + use_mapper_path ) + self._setup(self._aliased_insp, adapt_on_names) + def _setup(self, aliased_insp, adapt_on_names): self.__adapt_on_names = adapt_on_names mapper = aliased_insp.mapper @@ -473,18 +522,13 @@ class AliasedClass(object): equivalents=mapper._equivalent_columns, adapt_on_names=self.__adapt_on_names) for poly in aliased_insp.with_polymorphic_mappers: - setattr(self, poly.class_.__name__, - AliasedClass(poly.class_, alias)) + if poly is not mapper: + setattr(self, poly.class_.__name__, + AliasedClass(poly.class_, alias, base_alias=self, + use_mapper_path=self._aliased_insp._use_mapper_path)) - # used to assign a name to the RowTuple object - # returned by Query. - self._sa_label_name = aliased_insp.name self.__name__ = 'AliasedClass_%s' % self.__target.__name__ - @util.memoized_property - def _sa_path_registry(self): - return PathRegistry.per_mapper(self) - def __getstate__(self): return { 'mapper': self._aliased_insp.mapper, @@ -494,7 +538,9 @@ class AliasedClass(object): 'with_polymorphic_mappers': self._aliased_insp.with_polymorphic_mappers, 'with_polymorphic_discriminator': - self._aliased_insp.polymorphic_on + self._aliased_insp.polymorphic_on, + 'base_alias': self._aliased_insp._base_alias.entity, + 'use_mapper_path': self._aliased_insp._use_mapper_path } def __setstate__(self, state): @@ -503,8 +549,10 @@ class AliasedClass(object): state['mapper'], state['alias'], state['name'], - state.get('with_polymorphic_mappers'), - state.get('with_polymorphic_discriminator') + state['with_polymorphic_mappers'], + state['with_polymorphic_discriminator'], + state['base_alias'], + state['use_mapper_path'] ) self._setup(self._aliased_insp, state['adapt_on_names']) @@ -521,7 +569,7 @@ class AliasedClass(object): queryattr = attributes.QueryableAttribute( self, key, impl=existing.impl, - parententity=self, + parententity=self._aliased_insp, comparator=comparator) setattr(self, key, queryattr) return queryattr @@ -558,17 +606,7 @@ class AliasedClass(object): id(self), self.__target.__name__) -AliasedInsp = util.namedtuple("AliasedInsp", [ - "entity", - "mapper", - "selectable", - "name", - "with_polymorphic_mappers", - "polymorphic_on" - ]) - - -class AliasedInsp(_InspectionAttr, AliasedInsp): +class AliasedInsp(_InspectionAttr): """Provide an inspection interface for an :class:`.AliasedClass` object. @@ -604,6 +642,22 @@ class AliasedInsp(_InspectionAttr, AliasedInsp): """ + def __init__(self, entity, mapper, selectable, name, + with_polymorphic_mappers, polymorphic_on, + _base_alias, _use_mapper_path): + self.entity = entity + self.mapper = mapper + self.selectable = selectable + self.name = name + self.with_polymorphic_mappers = with_polymorphic_mappers + self.polymorphic_on = polymorphic_on + + # a little dance to get serialization to work + self._base_alias = _base_alias._aliased_insp if _base_alias \ + and _base_alias is not entity else self + self._use_mapper_path = _use_mapper_path + + is_aliased_class = True "always returns True" @@ -613,8 +667,29 @@ class AliasedInsp(_InspectionAttr, AliasedInsp): :class:`.AliasedInsp`.""" return self.mapper.class_ + @util.memoized_property + def _path_registry(self): + if self._use_mapper_path: + return self.mapper._path_registry + else: + return PathRegistry.per_mapper(self) + + def _entity_for_mapper(self, mapper): + self_poly = self.with_polymorphic_mappers + if mapper in self_poly: + return getattr(self.entity, mapper.class_.__name__)._aliased_insp + elif mapper.isa(self.mapper): + return self + else: + assert False, "mapper %s doesn't correspond to %s" % (mapper, self) + + def __repr__(self): + return '' % ( + id(self), self.class_.__name__) + inspection._inspects(AliasedClass)(lambda target: target._aliased_insp) +inspection._inspects(AliasedInsp)(lambda target: target) def aliased(element, alias=None, name=None, adapt_on_names=False): @@ -699,7 +774,7 @@ def aliased(element, alias=None, name=None, adapt_on_names=False): def with_polymorphic(base, classes, selectable=False, polymorphic_on=None, aliased=False, - innerjoin=False): + innerjoin=False, _use_mapper_path=False): """Produce an :class:`.AliasedClass` construct which specifies columns for descendant mappers of the given base. @@ -758,7 +833,8 @@ def with_polymorphic(base, classes, selectable=False, return AliasedClass(base, selectable, with_polymorphic_mappers=mappers, - with_polymorphic_discriminator=polymorphic_on) + with_polymorphic_discriminator=polymorphic_on, + use_mapper_path=_use_mapper_path) def _orm_annotate(element, exclude=None): @@ -1109,6 +1185,7 @@ def _entity_descriptor(entity, key): description = entity entity = insp.c elif insp.is_aliased_class: + entity = insp.entity description = entity elif hasattr(insp, "mapper"): description = entity = insp.mapper.class_ -- cgit v1.2.1 From 4950b85e8384869d3f03498c6914afe5aadbf561 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 2 Dec 2012 12:37:52 -0500 Subject: - BinaryExpression now keeps track of "left" and "right" as passed in, so that they can be compared in ``__nonzero__`` prior to their self_group() step. [ticket:2621] --- lib/sqlalchemy/sql/expression.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 3dc8dfea4..aa912a0f6 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -3723,6 +3723,7 @@ class BinaryExpression(ColumnElement): # refer to BinaryExpression directly and pass strings if isinstance(operator, basestring): operator = operators.custom_op(operator) + self._orig = (left, right) self.left = _literal_as_text(left).self_group(against=operator) self.right = _literal_as_text(right).self_group(against=operator) self.operator = operator @@ -3735,9 +3736,9 @@ class BinaryExpression(ColumnElement): self.modifiers = modifiers def __nonzero__(self): - try: - return self.operator(hash(self.left), hash(self.right)) - except: + if self.operator in (operator.eq, operator.ne): + return self.operator(hash(self._orig[0]), hash(self._orig[1])) + else: raise TypeError("Boolean value of this clause is not defined") @property -- cgit v1.2.1 From e99f8be3712fb6c73fd8151ab8c482b6ac4e122f Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Mon, 3 Dec 2012 07:40:00 -0500 Subject: visit_DECIMAL should include precision and scale (when provided) just like visit_NUMERIC see #2618 --- lib/sqlalchemy/sql/compiler.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 102b44a7e..215ecf7bd 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2084,7 +2084,15 @@ class GenericTypeCompiler(engine.TypeCompiler): 'scale': type_.scale} def visit_DECIMAL(self, type_): - return "DECIMAL" + if type_.precision is None: + return "DECIMAL" + elif type_.scale is None: + return "DECIMAL(%(precision)s)" % \ + {'precision': type_.precision} + else: + return "DECIMAL(%(precision)s, %(scale)s)" % \ + {'precision': type_.precision, + 'scale': type_.scale} def visit_INTEGER(self, type_): return "INTEGER" -- cgit v1.2.1 From d89d71d1dad4b795e14d5395f4008c5027b59baa Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 3 Dec 2012 19:49:42 -0500 Subject: The :class:`.MutableComposite` type did not allow for the :meth:`.MutableBase.coerce` method to be used, even though the code seemed to indicate this intent, so this now works and a brief example is added. As a side-effect, the mechanics of this event handler have been changed so that new :class:`.MutableComposite` types no longer add per-type global event handlers. Also in 0.7.10 [ticket:2624] --- lib/sqlalchemy/event.py | 3 ++ lib/sqlalchemy/ext/mutable.py | 91 ++++++++++++++++++++++++++----------------- 2 files changed, 59 insertions(+), 35 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/event.py b/lib/sqlalchemy/event.py index bf996ae3c..6453a3987 100644 --- a/lib/sqlalchemy/event.py +++ b/lib/sqlalchemy/event.py @@ -245,6 +245,9 @@ class _DispatchDescriptor(object): self._clslevel = util.defaultdict(list) self._empty_listeners = {} + def _contains(self, cls, evt): + return evt in self._clslevel[cls] + def insert(self, obj, target, propagate): assert isinstance(target, type), \ "Class-level Event targets must be classes." diff --git a/lib/sqlalchemy/ext/mutable.py b/lib/sqlalchemy/ext/mutable.py index 36d60d6d5..e290a93e2 100644 --- a/lib/sqlalchemy/ext/mutable.py +++ b/lib/sqlalchemy/ext/mutable.py @@ -302,6 +302,31 @@ will flag the attribute as "dirty" on the parent object:: >>> assert v1 in sess.dirty True +Coercing Mutable Composites +--------------------------- + +The :meth:`.MutableBase.coerce` method is also supported on composite types. +In the case of :class:`.MutableComposite`, the :meth:`.MutableBase.coerce` +method is only called for attribute set operations, not load operations. +Overriding the :meth:`.MutableBase.coerce` method is essentially equivalent +to using a :func:`.validates` validation routine for all attributes which +make use of the custom composite type:: + + class Point(MutableComposite): + # other Point methods + # ... + + def coerce(cls, key, value): + if isinstance(value, tuple): + value = Point(*value) + elif not isinstance(value, Point): + raise ValueError("tuple or Point expected") + return value + +.. versionadded:: 0.7.10,0.8.0b2 + Support for the :meth:`.MutableBase.coerce` method in conjunction with + objects of type :class:`.MutableComposite`. + Supporting Pickling -------------------- @@ -329,7 +354,7 @@ pickling process of the parent's object-relational state so that the """ from ..orm.attributes import flag_modified from .. import event, types -from ..orm import mapper, object_mapper +from ..orm import mapper, object_mapper, Mapper from ..util import memoized_property import weakref @@ -354,9 +379,27 @@ class MutableBase(object): @classmethod def coerce(cls, key, value): - """Given a value, coerce it into this type. + """Given a value, coerce it into the target type. + + Can be overridden by custom subclasses to coerce incoming + data into a particular type. + + By default, raises ``ValueError``. + + This method is called in different scenarios depending on if + the parent class is of type :class:`.Mutable` or of type + :class:`.MutableComposite`. In the case of the former, it is called + for both attribute-set operations as well as during ORM loading + operations. For the latter, it is only called during attribute-set + operations; the mechanics of the :func:`.composite` construct + handle coercion during load operations. + + + :param key: string name of the ORM-mapped attribute being set. + :param value: the incoming value. + :return: the method should return the coerced value, or raise + ``ValueError`` if the coercion cannot be completed. - By default raises ValueError. """ if value is None: return None @@ -523,11 +566,6 @@ class Mutable(MutableBase): return sqltype -class _MutableCompositeMeta(type): - def __init__(cls, classname, bases, dict_): - cls._setup_listeners() - return type.__init__(cls, classname, bases, dict_) - class MutableComposite(MutableBase): """Mixin that defines transparent propagation of change @@ -536,16 +574,7 @@ class MutableComposite(MutableBase): See the example in :ref:`mutable_composites` for usage information. - .. warning:: - - The listeners established by the :class:`.MutableComposite` - class are *global* to all mappers, and are *not* garbage - collected. Only use :class:`.MutableComposite` for types that are - permanent to an application, not with ad-hoc types else this will - cause unbounded growth in memory usage. - """ - __metaclass__ = _MutableCompositeMeta def changed(self): """Subclasses should call this method whenever change events occur.""" @@ -558,24 +587,16 @@ class MutableComposite(MutableBase): prop._attribute_keys): setattr(parent, attr_name, value) - @classmethod - def _setup_listeners(cls): - """Associate this wrapper with all future mapped composites - of the given type. - - This is a convenience method that calls ``associate_with_attribute`` - automatically. - - """ - - def listen_for_type(mapper, class_): - for prop in mapper.iterate_properties: - if (hasattr(prop, 'composite_class') and - issubclass(prop.composite_class, cls)): - cls._listen_on_attribute( - getattr(class_, prop.key), False, class_) - - event.listen(mapper, 'mapper_configured', listen_for_type) +def _setup_composite_listener(): + def _listen_for_type(mapper, class_): + for prop in mapper.iterate_properties: + if (hasattr(prop, 'composite_class') and + issubclass(prop.composite_class, MutableComposite)): + prop.composite_class._listen_on_attribute( + getattr(class_, prop.key), False, class_) + if not Mapper.dispatch.mapper_configured._contains(Mapper, _listen_for_type): + event.listen(Mapper, 'mapper_configured', _listen_for_type) +_setup_composite_listener() class MutableDict(Mutable, dict): -- cgit v1.2.1 From 73f25332b96988c34c17e39ffbd4f96e0c4c1967 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Wed, 5 Dec 2012 23:19:24 -0500 Subject: fixing broken links (see #2625) --- lib/sqlalchemy/engine/base.py | 2 +- lib/sqlalchemy/engine/reflection.py | 16 ++++++++-------- lib/sqlalchemy/schema.py | 23 +++++++++++------------ lib/sqlalchemy/sql/expression.py | 4 ++-- 4 files changed, 22 insertions(+), 23 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 626fee8c6..fdeeac516 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -1295,7 +1295,7 @@ class TwoPhaseTransaction(Transaction): class Engine(Connectable, log.Identified): """ Connects a :class:`~sqlalchemy.pool.Pool` and - :class:`~sqlalchemy.engine.base.Dialect` together to provide a source + :class:`~sqlalchemy.engine.Dialect` together to provide a source of database connectivity and behavior. An :class:`.Engine` object is instantiated publicly using the diff --git a/lib/sqlalchemy/engine/reflection.py b/lib/sqlalchemy/engine/reflection.py index 8367d8761..f1681d616 100644 --- a/lib/sqlalchemy/engine/reflection.py +++ b/lib/sqlalchemy/engine/reflection.py @@ -55,7 +55,7 @@ class Inspector(object): """Performs database schema inspection. The Inspector acts as a proxy to the reflection methods of the - :class:`~sqlalchemy.engine.base.Dialect`, providing a + :class:`~sqlalchemy.engine.interfaces.Dialect`, providing a consistent interface as well as caching support for previously fetched metadata. @@ -72,7 +72,7 @@ class Inspector(object): engine = create_engine('...') insp = Inspector.from_engine(engine) - Where above, the :class:`~sqlalchemy.engine.base.Dialect` may opt + Where above, the :class:`~sqlalchemy.engine.interfaces.Dialect` may opt to return an :class:`.Inspector` subclass that provides additional methods specific to the dialect's target database. @@ -81,13 +81,13 @@ class Inspector(object): def __init__(self, bind): """Initialize a new :class:`.Inspector`. - :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, + :param bind: a :class:`~sqlalchemy.engine.Connectable`, which is typically an instance of :class:`~sqlalchemy.engine.Engine` or :class:`~sqlalchemy.engine.Connection`. For a dialect-specific instance of :class:`.Inspector`, see - :meth:`Inspector.from_engine` + :meth:`.Inspector.from_engine` """ # this might not be a connection, it could be an engine. @@ -111,16 +111,16 @@ class Inspector(object): """Construct a new dialect-specific Inspector object from the given engine or connection. - :param bind: a :class:`~sqlalchemy.engine.base.Connectable`, + :param bind: a :class:`~sqlalchemy.engine.Connectable`, which is typically an instance of :class:`~sqlalchemy.engine.Engine` or :class:`~sqlalchemy.engine.Connection`. This method differs from direct a direct constructor call of :class:`.Inspector` in that the - :class:`~sqlalchemy.engine.base.Dialect` is given a chance to provide - a dialect-specific :class:`.Inspector` instance, which may provide - additional methods. + :class:`~sqlalchemy.engine.interfaces.Dialect` is given a chance to + provide a dialect-specific :class:`.Inspector` instance, which may + provide additional methods. See the example at :class:`.Inspector`. diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py index f6a6b83b4..2fb542a43 100644 --- a/lib/sqlalchemy/schema.py +++ b/lib/sqlalchemy/schema.py @@ -146,7 +146,7 @@ class Table(SchemaItem, expression.TableClause): table. The metadata is used as a point of association of this table with other tables which are referenced via foreign key. It also may be used to associate this table with a particular - :class:`~sqlalchemy.engine.base.Connectable`. + :class:`.Connectable`. :param \*args: Additional positional arguments are used primarily to add the list of :class:`.Column` objects contained within this @@ -240,7 +240,7 @@ class Table(SchemaItem, expression.TableClause): This alternate hook to :func:`.event.listen` allows the establishment of a listener function specific to this :class:`.Table` before the "autoload" process begins. Particularly useful for - the :meth:`.events.column_reflect` event:: + the :meth:`.DDLEvents.column_reflect` event:: def listen_for_reflect(table, column_info): "handle the column reflection event" @@ -254,7 +254,7 @@ class Table(SchemaItem, expression.TableClause): ]) :param mustexist: When ``True``, indicates that this Table must already - be present in the given :class:`.MetaData`` collection, else + be present in the given :class:`.MetaData` collection, else an exception is raised. :param prefixes: @@ -2600,9 +2600,8 @@ class MetaData(SchemaItem): in this ``MetaData`` no longer exists in the database. :param bind: - A :class:`~sqlalchemy.engine.base.Connectable` used to access the - database; if None, uses the existing bind on this ``MetaData``, if - any. + A :class:`.Connectable` used to access the database; if None, uses + the existing bind on this ``MetaData``, if any. :param schema: Optional, query and reflect tables from an alterate schema. @@ -2689,7 +2688,7 @@ class MetaData(SchemaItem): present in the target database. :param bind: - A :class:`~sqlalchemy.engine.base.Connectable` used to access the + A :class:`.Connectable` used to access the database; if None, uses the existing bind on this ``MetaData``, if any. @@ -2716,7 +2715,7 @@ class MetaData(SchemaItem): the target database. :param bind: - A :class:`~sqlalchemy.engine.base.Connectable` used to access the + A :class:`.Connectable` used to access the database; if None, uses the existing bind on this ``MetaData``, if any. @@ -2858,14 +2857,14 @@ class DDLElement(expression.Executable, _DDLCompiles): """Execute this DDL immediately. Executes the DDL statement in isolation using the supplied - :class:`~sqlalchemy.engine.base.Connectable` or - :class:`~sqlalchemy.engine.base.Connectable` assigned to the ``.bind`` + :class:`.Connectable` or + :class:`.Connectable` assigned to the ``.bind`` property, if not supplied. If the DDL has a conditional ``on`` criteria, it will be invoked with None as the event. :param bind: Optional, an ``Engine`` or ``Connection``. If not supplied, a valid - :class:`~sqlalchemy.engine.base.Connectable` must be present in the + :class:`.Connectable` must be present in the ``.bind`` property. :param target: @@ -3146,7 +3145,7 @@ class DDL(DDLElement): available for use in string substitutions on the DDL statement. :param bind: - Optional. A :class:`~sqlalchemy.engine.base.Connectable`, used by + Optional. A :class:`.Connectable`, used by default when ``execute()`` is invoked without a bind argument. diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index aa912a0f6..fa59683f2 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -1866,10 +1866,10 @@ class ClauseElement(Visitable): def compile(self, bind=None, dialect=None, **kw): """Compile this SQL expression. - The return value is a :class:`~sqlalchemy.engine.Compiled` object. + The return value is a :class:`~.Compiled` object. Calling ``str()`` or ``unicode()`` on the returned value will yield a string representation of the result. The - :class:`~sqlalchemy.engine.Compiled` object also can return a + :class:`~.Compiled` object also can return a dictionary of bind parameter names and values using the ``params`` accessor. -- cgit v1.2.1 From 54f45deb9744863da7dbb276e5204d37a7993927 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Thu, 6 Dec 2012 00:00:17 -0500 Subject: fixing broken links (see #2625) --- lib/sqlalchemy/engine/base.py | 14 +++++++------- lib/sqlalchemy/engine/result.py | 10 +++++----- lib/sqlalchemy/ext/compiler.py | 11 ++++++----- 3 files changed, 18 insertions(+), 17 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index fdeeac516..e3b09e63a 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -121,7 +121,7 @@ class Connection(Connectable): Note that any key/value can be passed to :meth:`.Connection.execution_options`, and it will be stored in the - ``_execution_options`` dictionary of the :class:`.Connnection`. It + ``_execution_options`` dictionary of the :class:`.Connection`. It is suitable for usage by end-user schemes to communicate with event listeners, for example. @@ -1295,8 +1295,8 @@ class TwoPhaseTransaction(Transaction): class Engine(Connectable, log.Identified): """ Connects a :class:`~sqlalchemy.pool.Pool` and - :class:`~sqlalchemy.engine.Dialect` together to provide a source - of database connectivity and behavior. + :class:`~sqlalchemy.engine.interfaces.Dialect` together to provide a + source of database connectivity and behavior. An :class:`.Engine` object is instantiated publicly using the :func:`~sqlalchemy.create_engine` function. @@ -1426,15 +1426,15 @@ class Engine(Connectable, log.Identified): @property def name(self): - """String name of the :class:`~sqlalchemy.engine.Dialect` in use by - this ``Engine``.""" + """String name of the :class:`~sqlalchemy.engine.interfaces.Dialect` + in use by this :class:`Engine`.""" return self.dialect.name @property def driver(self): - """Driver name of the :class:`~sqlalchemy.engine.Dialect` in use by - this ``Engine``.""" + """Driver name of the :class:`~sqlalchemy.engine.interfaces.Dialect` + in use by this :class:`Engine`.""" return self.dialect.driver diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 98b0ea4b2..dcfd5ac31 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -557,7 +557,7 @@ class ResultProxy(object): supports "returning" and the insert statement executed with the "implicit returning" enabled. - Raises :class:`.InvalidRequestError` if the executed + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed statement is not a compiled expression construct or is not an insert() construct. @@ -583,7 +583,7 @@ class ResultProxy(object): """Return the collection of updated parameters from this execution. - Raises :class:`.InvalidRequestError` if the executed + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed statement is not a compiled expression construct or is not an update() construct. @@ -605,7 +605,7 @@ class ResultProxy(object): """Return the collection of inserted parameters from this execution. - Raises :class:`.InvalidRequestError` if the executed + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed statement is not a compiled expression construct or is not an insert() construct. @@ -639,7 +639,7 @@ class ResultProxy(object): See :class:`.ExecutionContext` for details. - Raises :class:`.InvalidRequestError` if the executed + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed statement is not a compiled expression construct or is not an insert() or update() construct. @@ -661,7 +661,7 @@ class ResultProxy(object): See :class:`.ExecutionContext` for details. - Raises :class:`.InvalidRequestError` if the executed + Raises :class:`~sqlalchemy.exc.InvalidRequestError` if the executed statement is not a compiled expression construct or is not an insert() or update() construct. diff --git a/lib/sqlalchemy/ext/compiler.py b/lib/sqlalchemy/ext/compiler.py index 93984d0d1..25de2c0b6 100644 --- a/lib/sqlalchemy/ext/compiler.py +++ b/lib/sqlalchemy/ext/compiler.py @@ -65,11 +65,12 @@ dialect is used. Compiling sub-elements of a custom expression construct ======================================================= -The ``compiler`` argument is the :class:`~sqlalchemy.engine.base.Compiled` -object in use. This object can be inspected for any information about the -in-progress compilation, including ``compiler.dialect``, -``compiler.statement`` etc. The :class:`~sqlalchemy.sql.compiler.SQLCompiler` -and :class:`~sqlalchemy.sql.compiler.DDLCompiler` both include a ``process()`` +The ``compiler`` argument is the +:class:`~sqlalchemy.engine.interfaces.Compiled` object in use. This object +can be inspected for any information about the in-progress compilation, +including ``compiler.dialect``, ``compiler.statement`` etc. The +:class:`~sqlalchemy.sql.compiler.SQLCompiler` and +:class:`~sqlalchemy.sql.compiler.DDLCompiler` both include a ``process()`` method which can be used for compilation of embedded attributes:: from sqlalchemy.sql.expression import Executable, ClauseElement -- cgit v1.2.1 From af1a545bdd9d5210981a07f74e62f5d343b14537 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 6 Dec 2012 19:10:06 -0500 Subject: Repaired the usage of ``.prepare()`` in conjunction with cx_Oracle so that a return value of ``False`` will result in no call to ``connection.commit()``, hence avoiding "no transaction" errors. Two-phase transactions have now been shown to work in a rudimental fashion with SQLAlchemy and cx_oracle, however are subject to caveats observed with the driver; check the documentation for details. Also in 0.7.10. [ticket:2611] --- lib/sqlalchemy/dialects/oracle/cx_oracle.py | 59 ++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index bee730800..8b60d9af8 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -80,8 +80,40 @@ To disable this processing, pass ``auto_convert_lobs=False`` to :func:`create_en 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. +Two Phase transactions are implemented using XA transactions, and are known +to work in a rudimental fashion with recent versions of cx_Oracle +as of SQLAlchemy 0.8.0b2, 0.7.10. However, the mechanism is not yet +considered to be robust and should still be regarded as experimental. + +In particular, the cx_Oracle DBAPI as recently as 5.1.2 has a bug regarding +two phase which prevents +a particular DBAPI connection from being consistently usable in both +prepared transactions as well as traditional DBAPI usage patterns; therefore +once a particular connection is used via :meth:`.Connection.begin_prepared`, +all subsequent usages of the underlying DBAPI connection must be within +the context of prepared transactions. + +The default behavior of :class:`.Engine` is to maintain a pool of DBAPI +connections. Therefore, due to the above glitch, a DBAPI connection that has +been used in a two-phase operation, and is then returned to the pool, will +not be usable in a non-two-phase context. To avoid this situation, +the application can make one of several choices: + +* Disable connection pooling using :class:`.NullPool` + +* Ensure that the particular :class:`.Engine` in use is only used + for two-phase operations. A :class:`.Engine` bound to an ORM + :class:`.Session` which includes ``twophase=True`` will consistently + use the two-phase transaction style. + +* For ad-hoc two-phase operations without disabling pooling, the DBAPI + connection in use can be evicted from the connection pool using the + :class:`.Connection.detach` method. + +.. versionchanged:: 0.8.0b2,0.7.10 + Support for cx_oracle prepared transactions has been implemented + and tested. + Precision Numerics ------------------ @@ -150,8 +182,9 @@ a period "." as the decimal character. """ -from .base import OracleCompiler, OracleDialect, \ - RESERVED_WORDS, OracleExecutionContext +from __future__ import absolute_import + +from .base import OracleCompiler, OracleDialect, OracleExecutionContext from . import base as oracle from ...engine import result as _result from sqlalchemy import types as sqltypes, util, exc, processors @@ -779,15 +812,23 @@ class OracleDialect_cx_oracle(OracleDialect): connection.connection.begin(*xid) def do_prepare_twophase(self, connection, xid): - connection.connection.prepare() + result = connection.connection.prepare() + connection.info['cx_oracle_prepared'] = result - def do_rollback_twophase(self, connection, xid, is_prepared=True, recover=False): + def do_rollback_twophase(self, connection, xid, is_prepared=True, + recover=False): self.do_rollback(connection.connection) - def do_commit_twophase(self, connection, xid, is_prepared=True, recover=False): - self.do_commit(connection.connection) + def do_commit_twophase(self, connection, xid, is_prepared=True, + recover=False): + if not is_prepared: + self.do_commit(connection.connection) + else: + oci_prepared = connection.info['cx_oracle_prepared'] + if oci_prepared: + self.do_commit(connection.connection) def do_recover_twophase(self, connection): - pass + connection.info.pop('cx_oracle_prepared', None) dialect = OracleDialect_cx_oracle -- cgit v1.2.1 From 9d817406e2da0bda91c1e9a1411165f229e3d5c2 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Thu, 6 Dec 2012 19:30:49 -0500 Subject: The Oracle LONG type, while an unbounded text type, does not appear to use the cx_Oracle.LOB type when result rows are returned, so the dialect has been repaired to exclude LONG from having cx_Oracle.LOB filtering applied. Also in 0.7.10. [ticket:2620] --- lib/sqlalchemy/dialects/oracle/cx_oracle.py | 11 +++++++++++ 1 file changed, 11 insertions(+) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/dialects/oracle/cx_oracle.py b/lib/sqlalchemy/dialects/oracle/cx_oracle.py index 8b60d9af8..c5d9e8a89 100644 --- a/lib/sqlalchemy/dialects/oracle/cx_oracle.py +++ b/lib/sqlalchemy/dialects/oracle/cx_oracle.py @@ -303,6 +303,13 @@ class _OracleText(_LOBMixin, sqltypes.Text): return dbapi.CLOB +class _OracleLong(oracle.LONG): + # a raw LONG is a text type, but does *not* + # get the LobMixin with cx_oracle. + + def get_dbapi_type(self, dbapi): + return dbapi.LONG_STRING + class _OracleString(_NativeUnicodeMixin, sqltypes.String): pass @@ -532,6 +539,10 @@ class OracleDialect_cx_oracle(OracleDialect): sqltypes.UnicodeText: _OracleUnicodeText, sqltypes.CHAR: _OracleChar, + # a raw LONG is a text type, but does *not* + # get the LobMixin with cx_oracle. + oracle.LONG: _OracleLong, + # this is only needed for OUT parameters. # it would be nice if we could not use it otherwise. sqltypes.Integer: _OracleInteger, -- cgit v1.2.1 From 2b991ac9b55726b7c9e2bd696b7f2dcabcf27488 Mon Sep 17 00:00:00 2001 From: Diana Clarke Date: Fri, 7 Dec 2012 22:23:25 -0500 Subject: fixing sessionmaker link --- lib/sqlalchemy/orm/deprecated_interfaces.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib/sqlalchemy') diff --git a/lib/sqlalchemy/orm/deprecated_interfaces.py b/lib/sqlalchemy/orm/deprecated_interfaces.py index bc9b352d4..d5b3d848e 100644 --- a/lib/sqlalchemy/orm/deprecated_interfaces.py +++ b/lib/sqlalchemy/orm/deprecated_interfaces.py @@ -385,7 +385,7 @@ class SessionExtension(object): :class:`.SessionEvents`. Subclasses may be installed into a :class:`.Session` (or - :func:`.sessionmaker`) using the ``extension`` keyword + :class:`.sessionmaker`) using the ``extension`` keyword argument:: from sqlalchemy.orm.interfaces import SessionExtension -- cgit v1.2.1