[Sqlalchemy-commits] [1318] sqlalchemy/branches/schema/test: added "hasparent" awareness to attribut
Brought to you by:
zzzeek
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head><style type="text/css"><!-- #msg dl { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; } #msg dt { float: left; width: 6em; font-weight: bold; } #msg dt:after { content:':';} #msg dl, #msg dt, #msg ul, #msg li { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; } #msg dl a { font-weight: bold} #msg dl a:link { color:#fc3; } #msg dl a:active { color:#ff0; } #msg dl a:visited { color:#cc6; } h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; } #msg pre { overflow: auto; background: #ffc; border: 1px #fc0 solid; padding: 6px; } #msg ul, pre { overflow: auto; } #patch { width: 100%; } #patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;} #patch .propset h4, #patch .binary h4 {margin:0;} #patch pre {padding:0;line-height:1.2em;margin:0;} #patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;} #patch .propset .diff, #patch .binary .diff {padding:10px 0;} #patch span {display:block;padding:0 10px;} #patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;} #patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;} #patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;} #patch .lines, .info {color:#888;background:#fff;} --></style> <title>[1318] sqlalchemy/branches/schema/test: added "hasparent" awareness to attributes package, allows delete-orphan to be cascaded at flush-time instead of operation time</title> </head> <body> <div id="msg"> <dl> <dt>Revision</dt> <dd>1318</dd> <dt>Author</dt> <dd>zzzeek</dd> <dt>Date</dt> <dd>2006-04-22 15:21:50 -0500 (Sat, 22 Apr 2006)</dd> </dl> <h3>Log Message</h3> <pre>added "hasparent" awareness to attributes package, allows delete-orphan to be cascaded at flush-time instead of operation time factored out all dependency processing from properties.py into dependency.py</pre> <h3>Modified Paths</h3> <ul> <li><a href="#sqlalchemybranchesschemalibsqlalchemyattributespy">sqlalchemy/branches/schema/lib/sqlalchemy/attributes.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemymappingobjectstorepy">sqlalchemy/branches/schema/lib/sqlalchemy/mapping/objectstore.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemymappingpropertiespy">sqlalchemy/branches/schema/lib/sqlalchemy/mapping/properties.py</a></li> <li><a href="#sqlalchemybranchesschemalibsqlalchemymappingunitofworkpy">sqlalchemy/branches/schema/lib/sqlalchemy/mapping/unitofwork.py</a></li> <li><a href="#sqlalchemybranchesschematestmasscreatepy">sqlalchemy/branches/schema/test/masscreate.py</a></li> <li><a href="#sqlalchemybranchesschematestobjectstorepy">sqlalchemy/branches/schema/test/objectstore.py</a></li> </ul> <h3>Added Paths</h3> <ul> <li><a href="#sqlalchemybranchesschemalibsqlalchemymappingdependencypy">sqlalchemy/branches/schema/lib/sqlalchemy/mapping/dependency.py</a></li> </ul> </div> <div id="patch"> <h3>Diff</h3> <a id="sqlalchemybranchesschemalibsqlalchemyattributespy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/attributes.py (1317 => 1318)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/attributes.py 2006-04-22 19:01:37 UTC (rev 1317) +++ sqlalchemy/branches/schema/lib/sqlalchemy/attributes.py 2006-04-22 20:21:50 UTC (rev 1318) </span><span class="lines">@@ -89,7 +89,11 @@ </span><span class="cx"> return self </span><span class="cx"> def plain_init(self, *args, **kwargs): </span><span class="cx"> pass </span><del>- </del><ins>+ def hasparent(self, item): + return item.__class__._attribute_manager.attribute_history(item).get('_hasparent_' + self.key) + def sethasparent(self, item, value): + item.__class__._attribute_manager.attribute_history(item)['_hasparent_' + self.key] = value + </ins><span class="cx"> class ScalarAttribute(ManagedAttribute): </span><span class="cx"> """Used by AttributeManager to track the history of a scalar attribute </span><span class="cx"> on an object instance. This is the "scalar history container" object. </span><span class="lines">@@ -97,10 +101,11 @@ </span><span class="cx"> so that the two objects can be called upon largely interchangeably.""" </span><span class="cx"> # make our own NONE to distinguish from "None" </span><span class="cx"> NONE = object() </span><del>- def __init__(self, obj, key, extension=None, **kwargs): </del><ins>+ def __init__(self, obj, key, extension=None, trackparent=False, **kwargs): </ins><span class="cx"> ManagedAttribute.__init__(self, obj, key) </span><span class="cx"> self.orig = ScalarAttribute.NONE </span><span class="cx"> self.extension = extension </span><ins>+ self.trackparent = trackparent </ins><span class="cx"> def clear(self): </span><span class="cx"> del self.obj.__dict__[self.key] </span><span class="cx"> def history_contains(self, obj): </span><span class="lines">@@ -120,6 +125,8 @@ </span><span class="cx"> if self.orig is ScalarAttribute.NONE: </span><span class="cx"> self.orig = orig </span><span class="cx"> self.obj.__dict__[self.key] = value </span><ins>+ if value is not None and self.trackparent: + self.sethasparent(value, True) </ins><span class="cx"> if self.extension is not None: </span><span class="cx"> self.extension.set(self.obj, value, orig) </span><span class="cx"> self.value_changed(orig, value) </span><span class="lines">@@ -128,6 +135,8 @@ </span><span class="cx"> if self.orig is ScalarAttribute.NONE: </span><span class="cx"> self.orig = orig </span><span class="cx"> self.obj.__dict__[self.key] = None </span><ins>+ if self.trackparent: + self.sethasparent(orig, False) </ins><span class="cx"> if self.extension is not None: </span><span class="cx"> self.extension.set(self.obj, None, orig) </span><span class="cx"> self.value_changed(orig, None) </span><span class="lines">@@ -164,9 +173,10 @@ </span><span class="cx"> This is the "list history container" object. </span><span class="cx"> Subclasses util.HistoryArraySet to provide "onchange" event handling as well </span><span class="cx"> as a plugin point for BackrefExtension objects.""" </span><del>- def __init__(self, obj, key, data=None, extension=None, **kwargs): </del><ins>+ def __init__(self, obj, key, data=None, extension=None, trackparent=False, **kwargs): </ins><span class="cx"> ManagedAttribute.__init__(self, obj, key) </span><span class="cx"> self.extension = extension </span><ins>+ self.trackparent = trackparent </ins><span class="cx"> # if we are given a list, try to behave nicely with an existing </span><span class="cx"> # list that might be set on the object already </span><span class="cx"> try: </span><span class="lines">@@ -194,6 +204,8 @@ </span><span class="cx"> def _setrecord(self, item): </span><span class="cx"> res = util.HistoryArraySet._setrecord(self, item) </span><span class="cx"> if res: </span><ins>+ if self.trackparent: + self.sethasparent(item, True) </ins><span class="cx"> self.value_changed(self.obj, self.key, item, self, False) </span><span class="cx"> if self.extension is not None: </span><span class="cx"> self.extension.append(self.obj, item) </span><span class="lines">@@ -201,6 +213,8 @@ </span><span class="cx"> def _delrecord(self, item): </span><span class="cx"> res = util.HistoryArraySet._delrecord(self, item) </span><span class="cx"> if res: </span><ins>+ if self.trackparent: + self.sethasparent(item, False) </ins><span class="cx"> self.value_changed(self.obj, self.key, item, self, True) </span><span class="cx"> if self.extension is not None: </span><span class="cx"> self.extension.delete(self.obj, item) </span><span class="lines">@@ -365,7 +379,8 @@ </span><span class="cx"> try: </span><span class="cx"> attributes = self.attribute_history(o) </span><span class="cx"> for hist in attributes.values(): </span><del>- hist.rollback() </del><ins>+ if isinstance(hist, ManagedAttribute): + hist.rollback() </ins><span class="cx"> except KeyError: </span><span class="cx"> pass </span><span class="cx"> o._managed_value_changed = False </span><span class="lines">@@ -377,7 +392,8 @@ </span><span class="cx"> try: </span><span class="cx"> attributes = self.attribute_history(o) </span><span class="cx"> for hist in attributes.values(): </span><del>- hist.commit() </del><ins>+ if isinstance(hist, ManagedAttribute): + hist.commit() </ins><span class="cx"> except KeyError: </span><span class="cx"> pass </span><span class="cx"> o._managed_value_changed = False </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemymappingdependencypy"></a> <div class="addfile"><h4>Added: sqlalchemy/branches/schema/lib/sqlalchemy/mapping/dependency.py (1317 => 1318)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/mapping/dependency.py 2006-04-22 19:01:37 UTC (rev 1317) +++ sqlalchemy/branches/schema/lib/sqlalchemy/mapping/dependency.py 2006-04-22 20:21:50 UTC (rev 1318) </span><span class="lines">@@ -0,0 +1,268 @@ </span><ins>+"""bridges the PropertyLoader (i.e. a relation()) and the UOWTransaction +together to allow processing of scalar- and list-based dependencies at flush time.""" + +from sync import ONETOMANY,MANYTOONE,MANYTOMANY +from sqlalchemy import sql + +class DependencyProcessor(object): + def __init__(self, key, syncrules, cascade, secondary=None, association=None, is_backref=False, post_update=False): + # TODO: update instance variable names to be more meaningful + self.syncrules = syncrules + self.cascade = cascade + self.mapper = syncrules.child_mapper + self.parent = syncrules.parent_mapper + self.association = association + self.secondary = secondary + self.direction = syncrules.direction + self.is_backref = is_backref + self.post_update = post_update + self.key = key + + class MapperStub(object): + """poses as a Mapper representing the association table in a many-to-many + join, when performing a commit(). + + The Task objects in the objectstore module treat it just like + any other Mapper, but in fact it only serves as a "dependency" placeholder + for the many-to-many update task.""" + def __init__(self, mapper): + self.mapper = mapper + def save_obj(self, *args, **kwargs): + pass + def delete_obj(self, *args, **kwargs): + pass + def _primary_mapper(self): + return self + + def register_dependencies(self, uowcommit): + """tells a UOWTransaction what mappers are dependent on which, with regards + to the two or three mappers handled by this PropertyLoader. + + Also registers itself as a "processor" for one of its mappers, which + will be executed after that mapper's objects have been saved or before + they've been deleted. The process operation manages attributes and dependent + operations upon the objects of one of the involved mappers.""" + if self.association is not None: + # association object. our mapper should be dependent on both + # the parent mapper and the association object mapper. + # this is where we put the "stub" as a marker, so we get + # association/parent->stub->self, then we process the child + # elments after the 'stub' save, which is before our own + # mapper's save. + stub = DependencyProcessor.MapperStub(self.association) + uowcommit.register_dependency(self.parent, stub) + uowcommit.register_dependency(self.association, stub) + uowcommit.register_dependency(stub, self.mapper) + uowcommit.register_processor(stub, self, self.parent, False) + uowcommit.register_processor(stub, self, self.parent, True) + + elif self.direction == MANYTOMANY: + # many-to-many. create a "Stub" mapper to represent the + # "middle table" in the relationship. This stub mapper doesnt save + # or delete any objects, but just marks a dependency on the two + # related mappers. its dependency processor then populates the + # association table. + + if self.is_backref: + # if we are the "backref" half of a two-way backref + # relationship, let the other mapper handle inserting the rows + return + stub = DependencyProcessor.MapperStub(self.mapper) + uowcommit.register_dependency(self.parent, stub) + uowcommit.register_dependency(self.mapper, stub) + uowcommit.register_processor(stub, self, self.parent, False) + uowcommit.register_processor(stub, self, self.parent, True) + elif self.direction == ONETOMANY: + if self.post_update: + stub = DependencyProcessor.MapperStub(self.mapper) + uowcommit.register_dependency(self.mapper, stub) + uowcommit.register_dependency(self.parent, stub) + uowcommit.register_processor(stub, self, self.parent, False) + uowcommit.register_processor(stub, self, self.parent, True) + else: + uowcommit.register_dependency(self.parent, self.mapper) + uowcommit.register_processor(self.parent, self, self.parent, False) + uowcommit.register_processor(self.parent, self, self.parent, True) + elif self.direction == MANYTOONE: + if self.post_update: + stub = DependencyProcessor.MapperStub(self.mapper) + uowcommit.register_dependency(self.mapper, stub) + uowcommit.register_dependency(self.parent, stub) + uowcommit.register_processor(stub, self, self.parent, False) + uowcommit.register_processor(stub, self, self.parent, True) + else: + uowcommit.register_dependency(self.mapper, self.parent) + uowcommit.register_processor(self.mapper, self, self.parent, False) + uowcommit.register_processor(self.mapper, self, self.parent, True) + else: + raise AssertionError(" no foreign key ?") + + # TODO: this method should be moved to an external object + def get_object_dependencies(self, obj, uowcommit, passive = True): + return uowcommit.uow.attributes.get_history(obj, self.key, passive = passive) + + # TODO: this method should be moved to an external object + def whose_dependent_on_who(self, obj1, obj2): + """given an object pair assuming obj2 is a child of obj1, returns a tuple + with the dependent object second, or None if they are equal. + used by objectstore's object-level topological sort (i.e. cyclical + table dependency).""" + if obj1 is obj2: + return None + elif self.direction == ONETOMANY: + return (obj1, obj2) + else: + return (obj2, obj1) + + # TODO: this method should be moved to an external object + def process_dependencies(self, task, deplist, uowcommit, delete = False): + """this method is called during a commit operation to synchronize data between a parent and child object. + it also can establish child or parent objects within the unit of work as "to be saved" or "deleted" + in some cases.""" + #print self.mapper.table.name + " " + self.key + " " + repr(len(deplist)) + " process_dep isdelete " + repr(delete) + " direction " + repr(self.direction) + + def getlist(obj, passive=True): + return self.get_object_dependencies(obj, uowcommit, passive) + + connection = uowcommit.transaction.connection(self.mapper) + + # plugin point + + if self.direction == MANYTOMANY: + secondary_delete = [] + secondary_insert = [] + if delete: + for obj in deplist: + childlist = getlist(obj, False) + for child in childlist.deleted_items() + childlist.unchanged_items(): + associationrow = {} + self._synchronize(obj, child, associationrow, False) + secondary_delete.append(associationrow) + else: + for obj in deplist: + childlist = getlist(obj) + if childlist is None: continue + for child in childlist.added_items(): + associationrow = {} + self._synchronize(obj, child, associationrow, False) + secondary_insert.append(associationrow) + for child in childlist.deleted_items(): + associationrow = {} + self._synchronize(obj, child, associationrow, False) + secondary_delete.append(associationrow) + if len(secondary_delete): + # TODO: precompile the delete/insert queries and store them as instance variables + # on the PropertyLoader + statement = self.secondary.delete(sql.and_(*[c == sql.bindparam(c.key) for c in self.secondary.c])) + connection.execute(statement, secondary_delete) + if len(secondary_insert): + statement = self.secondary.insert() + connection.execute(statement, secondary_insert) + elif self.direction == MANYTOONE and delete: + if self.cascade.delete_orphan: + for obj in deplist: + childlist = getlist(obj, False) + for child in childlist.deleted_items() + childlist.unchanged_items(): + if child is not None and childlist.hasparent(child) is False: + uowcommit.register_object(child, isdelete=True) + elif self.post_update: + # post_update means we have to update our row to not reference the child object + # before we can DELETE the row + for obj in deplist: + self._synchronize(obj, None, None, True) + uowcommit.register_object(obj, postupdate=True) + elif self.direction == ONETOMANY and delete: + # head object is being deleted, and we manage its list of child objects + # the child objects have to have their foreign key to the parent set to NULL + if self.cascade.delete_orphan and not self.post_update: + for obj in deplist: + childlist = getlist(obj, False) + for child in childlist.deleted_items(): + if child is not None and childlist.hasparent(child) is False: + uowcommit.register_object(child, isdelete=True) + for child in childlist.unchanged_items(): + if child is not None: + uowcommit.register_object(child, isdelete=True) + else: + for obj in deplist: + childlist = getlist(obj, False) + for child in childlist.deleted_items(): + if child is not None and childlist.hasparent(child) is False: + self._synchronize(obj, child, None, True) + uowcommit.register_object(child, postupdate=self.post_update) + for child in childlist.unchanged_items(): + if child is not None: + self._synchronize(obj, child, None, True) + uowcommit.register_object(child, postupdate=self.post_update) + elif self.association is not None: + # manage association objects. + for obj in deplist: + childlist = getlist(obj, passive=True) + if childlist is None: continue + + #print "DIRECTION", self.direction + d = {} + for child in childlist: + self._synchronize(obj, child, None, False) + key = self.mapper.instance_key(child) + #print "SYNCHRONIZED", child, "INSTANCE KEY", key + d[key] = child + uowcommit.unregister_object(child) + + for child in childlist.added_items(): + uowcommit.register_object(child) + key = self.mapper.instance_key(child) + #print "ADDED, INSTANCE KEY", key + d[key] = child + + for child in childlist.unchanged_items(): + key = self.mapper.instance_key(child) + o = d[key] + o._instance_key= key + + for child in childlist.deleted_items(): + key = self.mapper.instance_key(child) + #print "DELETED, INSTANCE KEY", key + if d.has_key(key): + o = d[key] + o._instance_key = key + uowcommit.unregister_object(child) + else: + #print "DELETE ASSOC OBJ", repr(child) + uowcommit.register_object(child, isdelete=True) + else: + for obj in deplist: + childlist = getlist(obj, passive=True) + if childlist is not None: + for child in childlist.added_items(): + self._synchronize(obj, child, None, False) + if self.direction == ONETOMANY and child is not None: + uowcommit.register_object(child, postupdate=self.post_update) + if self.direction == MANYTOONE: + uowcommit.register_object(obj, postupdate=self.post_update) + else: + for child in childlist.deleted_items(): + if not self.cascade.delete_orphan: + self._synchronize(obj, child, None, True) + uowcommit.register_object(child, isdelete=False) + elif childlist.hasparent(child) is False: + uowcommit.register_object(child, isdelete=True) + + # TODO: this method should be moved to an external object + def _synchronize(self, obj, child, associationrow, clearkeys): + """called during a commit to execute the full list of syncrules on the + given object/child/optional association row""" + if self.direction == ONETOMANY: + source = obj + dest = child + elif self.direction == MANYTOONE: + source = child + dest = obj + elif self.direction == MANYTOMANY: + dest = associationrow + source = None + + if dest is None: + return + + self.syncrules.execute(source, dest, obj, child, clearkeys) </ins></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemymappingobjectstorepy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/mapping/objectstore.py (1317 => 1318)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/mapping/objectstore.py 2006-04-22 19:01:37 UTC (rev 1317) +++ sqlalchemy/branches/schema/lib/sqlalchemy/mapping/objectstore.py 2006-04-22 20:21:50 UTC (rev 1318) </span><span class="lines">@@ -338,7 +338,6 @@ </span><span class="cx"> the keyword argument 'entity_name' can also be provided which will be used by the import.""" </span><span class="cx"> for o in obj: </span><span class="cx"> for c in object_mapper(o, **kwargs).cascade_iterator('delete', o): </span><del>- print "CASCADING DELETE TO", c </del><span class="cx"> if not self._is_bound(c): </span><span class="cx"> c = self.import_(c, **kwargs) </span><span class="cx"> self.uow.register_deleted(c) </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemymappingpropertiespy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/mapping/properties.py (1317 => 1318)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/mapping/properties.py 2006-04-22 19:01:37 UTC (rev 1317) +++ sqlalchemy/branches/schema/lib/sqlalchemy/mapping/properties.py 2006-04-22 20:21:50 UTC (rev 1318) </span><span class="lines">@@ -17,6 +17,7 @@ </span><span class="cx"> import sync </span><span class="cx"> import mapper </span><span class="cx"> import objectstore </span><ins>+import dependency </ins><span class="cx"> import util as mapperutil </span><span class="cx"> from sqlalchemy.exceptions import * </span><span class="cx"> import sets </span><span class="lines">@@ -95,7 +96,7 @@ </span><span class="cx"> if prop is self: </span><span class="cx"> continue </span><span class="cx"> instance.__dict__[prop.key] = row[prop.columns[0]] </span><del>- objectstore.global_attributes.create_history(instance, prop.key, uselist=False, cascade=self.cascade) </del><ins>+ objectstore.global_attributes.create_history(instance, prop.key, uselist=False, cascade=self.cascade, trackparent=True) </ins><span class="cx"> return row[self.columns[0]] </span><span class="cx"> else: </span><span class="cx"> return connection.scalar(sql.select([self.columns[0]], clause, use_labels=True),None) </span><span class="lines">@@ -107,7 +108,7 @@ </span><span class="cx"> def execute(self, session, instance, row, identitykey, imap, isnew): </span><span class="cx"> if isnew: </span><span class="cx"> if not self.is_primary(): </span><del>- objectstore.global_attributes.create_history(instance, self.key, False, callable_=self.setup_loader(instance), cascade=self.cascade) </del><ins>+ objectstore.global_attributes.create_history(instance, self.key, False, callable_=self.setup_loader(instance), cascade=self.cascade, trackparent=True) </ins><span class="cx"> else: </span><span class="cx"> objectstore.global_attributes.reset_history(instance, self.key) </span><span class="cx"> </span><span class="lines">@@ -120,7 +121,7 @@ </span><span class="cx"> </span><span class="cx"> """describes an object property that holds a single item or list of items that correspond </span><span class="cx"> to a related database table.""" </span><del>- def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, association=None, use_alias=None, selectalias=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False, cascade=None): </del><ins>+ def __init__(self, argument, secondary, primaryjoin, secondaryjoin, foreignkey=None, uselist=None, private=False, association=None, order_by=False, attributeext=None, backref=None, is_backref=False, post_update=False, cascade=None): </ins><span class="cx"> self.uselist = uselist </span><span class="cx"> self.argument = argument </span><span class="cx"> self.secondary = secondary </span><span class="lines">@@ -131,7 +132,7 @@ </span><span class="cx"> </span><span class="cx"> # would like to have foreignkey be a list. </span><span class="cx"> # however, have to figure out how to do </span><del>- # <column> in <list>, since column overrides the == operator or somethign </del><ins>+ # <column> in <list>, since column overrides the == operator </ins><span class="cx"> # and it doesnt work </span><span class="cx"> self.foreignkey = foreignkey #util.to_set(foreignkey) </span><span class="cx"> if foreignkey: </span><span class="lines">@@ -148,10 +149,6 @@ </span><span class="cx"> self.cascade = mapperutil.CascadeOptions() </span><span class="cx"> </span><span class="cx"> self.association = association </span><del>- if selectalias is not None: - print "'selectalias' argument to relation() is deprecated. eager loads automatically alias-ize tables now." - if use_alias is not None: - print "'use_alias' argument to relation() is deprecated. eager loads automatically alias-ize tables now." </del><span class="cx"> self.order_by = order_by </span><span class="cx"> self.attributeext=attributeext </span><span class="cx"> if isinstance(backref, str): </span><span class="lines">@@ -163,10 +160,8 @@ </span><span class="cx"> private = property(lambda s:s.cascade.delete_orphan) </span><span class="cx"> </span><span class="cx"> def cascade_iterator(self, type, object, recursive=None): </span><del>- </del><span class="cx"> if not type in self.cascade: </span><span class="cx"> return </span><del>- </del><span class="cx"> if recursive is None: </span><span class="cx"> recursive = sets.Set() </span><span class="cx"> </span><span class="lines">@@ -231,13 +226,14 @@ </span><span class="cx"> if self.direction is None: </span><span class="cx"> self.direction = self._get_direction() </span><span class="cx"> </span><del>- if self.uselist is None and self.direction == PropertyLoader.MANYTOONE: </del><ins>+ if self.uselist is None and self.direction == sync.MANYTOONE: </ins><span class="cx"> self.uselist = False </span><span class="cx"> </span><span class="cx"> if self.uselist is None: </span><span class="cx"> self.uselist = True </span><span class="cx"> </span><span class="cx"> self._compile_synchronizers() </span><ins>+ self._dependency_processor = dependency.DependencyProcessor(self.key, self.syncrules, self.cascade, secondary=self.secondary, association=self.association, is_backref=self.is_backref, post_update=self.post_update) </ins><span class="cx"> </span><span class="cx"> # primary property handler, set up class attributes </span><span class="cx"> if self.is_primary(): </span><span class="lines">@@ -258,23 +254,23 @@ </span><span class="cx"> </span><span class="cx"> def _set_class_attribute(self, class_, key): </span><span class="cx"> """sets attribute behavior on our target class.""" </span><del>- objectstore.global_attributes.register_attribute(class_, key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade) </del><ins>+ objectstore.global_attributes.register_attribute(class_, key, uselist = self.uselist, extension=self.attributeext, cascade=self.cascade, trackparent=True) </ins><span class="cx"> </span><span class="cx"> def _get_direction(self): </span><span class="cx"> """determines our 'direction', i.e. do we represent one to many, many to many, etc.""" </span><span class="cx"> #print self.key, repr(self.parent.table.name), repr(self.parent.primarytable.name), repr(self.foreignkey.table.name), repr(self.target), repr(self.foreigntable.name) </span><span class="cx"> </span><span class="cx"> if self.secondaryjoin is not None: </span><del>- return PropertyLoader.MANYTOMANY </del><ins>+ return sync.MANYTOMANY </ins><span class="cx"> elif self.parent.table is self.target: </span><span class="cx"> if self.foreignkey.primary_key: </span><del>- return PropertyLoader.MANYTOONE </del><ins>+ return sync.MANYTOONE </ins><span class="cx"> else: </span><del>- return PropertyLoader.ONETOMANY </del><ins>+ return sync.ONETOMANY </ins><span class="cx"> elif self.foreigntable == self.mapper.noninherited_table: </span><del>- return PropertyLoader.ONETOMANY </del><ins>+ return sync.ONETOMANY </ins><span class="cx"> elif self.foreigntable == self.parent.noninherited_table: </span><del>- return PropertyLoader.MANYTOONE </del><ins>+ return sync.MANYTOONE </ins><span class="cx"> else: </span><span class="cx"> raise ArgumentError("Cant determine relation direction") </span><span class="cx"> </span><span class="lines">@@ -334,251 +330,15 @@ </span><span class="cx"> return c.copy_container() </span><span class="cx"> return None </span><span class="cx"> </span><del>- - # TODO: this should be moved to an external object - class MapperStub(object): - """poses as a Mapper representing the association table in a many-to-many - join, when performing a commit(). - - The Task objects in the objectstore module treat it just like - any other Mapper, but in fact it only serves as a "dependency" placeholder - for the many-to-many update task.""" - def __init__(self, mapper): - self.mapper = mapper - def save_obj(self, *args, **kwargs): - pass - def delete_obj(self, *args, **kwargs): - pass - def _primary_mapper(self): - return self - - # TODO: this method should be moved to an external object - def register_dependencies(self, uowcommit): - """tells a UOWTransaction what mappers are dependent on which, with regards - to the two or three mappers handled by this PropertyLoader. - - Also registers itself as a "processor" for one of its mappers, which - will be executed after that mapper's objects have been saved or before - they've been deleted. The process operation manages attributes and dependent - operations upon the objects of one of the involved mappers.""" - if self.association is not None: - # association object. our mapper should be dependent on both - # the parent mapper and the association object mapper. - # this is where we put the "stub" as a marker, so we get - # association/parent->stub->self, then we process the child - # elments after the 'stub' save, which is before our own - # mapper's save. - stub = PropertyLoader.MapperStub(self.association) - uowcommit.register_dependency(self.parent, stub) - uowcommit.register_dependency(self.association, stub) - uowcommit.register_dependency(stub, self.mapper) - uowcommit.register_processor(stub, self, self.parent, False) - uowcommit.register_processor(stub, self, self.parent, True) - - elif self.direction == PropertyLoader.MANYTOMANY: - # many-to-many. create a "Stub" mapper to represent the - # "middle table" in the relationship. This stub mapper doesnt save - # or delete any objects, but just marks a dependency on the two - # related mappers. its dependency processor then populates the - # association table. - - if self.is_backref: - # if we are the "backref" half of a two-way backref - # relationship, let the other mapper handle inserting the rows - return - stub = PropertyLoader.MapperStub(self.mapper) - uowcommit.register_dependency(self.parent, stub) - uowcommit.register_dependency(self.mapper, stub) - uowcommit.register_processor(stub, self, self.parent, False) - uowcommit.register_processor(stub, self, self.parent, True) - elif self.direction == PropertyLoader.ONETOMANY: - if self.post_update: - stub = PropertyLoader.MapperStub(self.mapper) - uowcommit.register_dependency(self.mapper, stub) - uowcommit.register_dependency(self.parent, stub) - uowcommit.register_processor(stub, self, self.parent, False) - uowcommit.register_processor(stub, self, self.parent, True) - else: - uowcommit.register_dependency(self.parent, self.mapper) - uowcommit.register_processor(self.parent, self, self.parent, False) - uowcommit.register_processor(self.parent, self, self.parent, True) - elif self.direction == PropertyLoader.MANYTOONE: - if self.post_update: - stub = PropertyLoader.MapperStub(self.mapper) - uowcommit.register_dependency(self.mapper, stub) - uowcommit.register_dependency(self.parent, stub) - uowcommit.register_processor(stub, self, self.parent, False) - uowcommit.register_processor(stub, self, self.parent, True) - else: - uowcommit.register_dependency(self.mapper, self.parent) - uowcommit.register_processor(self.mapper, self, self.parent, False) - uowcommit.register_processor(self.mapper, self, self.parent, True) - else: - raise AssertionError(" no foreign key ?") - - # TODO: this method should be moved to an external object - def get_object_dependencies(self, obj, uowcommit, passive = True): - return uowcommit.uow.attributes.get_history(obj, self.key, passive = passive) - - # TODO: this method should be moved to an external object - def whose_dependent_on_who(self, obj1, obj2): - """given an object pair assuming obj2 is a child of obj1, returns a tuple - with the dependent object second, or None if they are equal. - used by objectstore's object-level topological sort (i.e. cyclical - table dependency).""" - if obj1 is obj2: - return None - elif self.direction == PropertyLoader.ONETOMANY: - return (obj1, obj2) - else: - return (obj2, obj1) - - # TODO: this method should be moved to an external object - def process_dependencies(self, task, deplist, uowcommit, delete = False): - """this method is called during a commit operation to synchronize data between a parent and child object. - it also can establish child or parent objects within the unit of work as "to be saved" or "deleted" - in some cases.""" - #print self.mapper.table.name + " " + self.key + " " + repr(len(deplist)) + " process_dep isdelete " + repr(delete) + " direction " + repr(self.direction) - - def getlist(obj, passive=True): - return self.get_object_dependencies(obj, uowcommit, passive) - - connection = uowcommit.transaction.connection(self.mapper) - - # plugin point - - if self.direction == PropertyLoader.MANYTOMANY: - secondary_delete = [] - secondary_insert = [] - if delete: - for obj in deplist: - childlist = getlist(obj, False) - for child in childlist.deleted_items() + childlist.unchanged_items(): - associationrow = {} - self._synchronize(obj, child, associationrow, False) - secondary_delete.append(associationrow) - else: - for obj in deplist: - childlist = getlist(obj) - if childlist is None: continue - for child in childlist.added_items(): - associationrow = {} - self._synchronize(obj, child, associationrow, False) - secondary_insert.append(associationrow) - for child in childlist.deleted_items(): - associationrow = {} - self._synchronize(obj, child, associationrow, False) - secondary_delete.append(associationrow) - if len(secondary_delete): - # TODO: precompile the delete/insert queries and store them as instance variables - # on the PropertyLoader - statement = self.secondary.delete(sql.and_(*[c == sql.bindparam(c.key) for c in self.secondary.c])) - connection.execute(statement, secondary_delete) - if len(secondary_insert): - statement = self.secondary.insert() - connection.execute(statement, secondary_insert) - elif self.direction == PropertyLoader.MANYTOONE and delete: - if self.private: - for obj in deplist: - childlist = getlist(obj, False) - for child in childlist.deleted_items() + childlist.unchanged_items(): - if child is None: - continue - # if private child object, and is in the uow's "deleted" list, - # insure its in the list of items to be deleted - if child in uowcommit.uow.deleted: - uowcommit.register_object(child, isdelete=True) - elif self.post_update: - # post_update means we have to update our row to not reference the child object - # before we can DELETE the row - for obj in deplist: - self._synchronize(obj, None, None, True) - uowcommit.register_object(obj, postupdate=True) - elif self.direction == PropertyLoader.ONETOMANY and delete: - # head object is being deleted, and we manage its list of child objects - # the child objects have to have their foreign key to the parent set to NULL - if self.private and not self.post_update: - for obj in deplist: - print "HI ON", obj - childlist = getlist(obj, False) - for child in childlist.deleted_items() + childlist.unchanged_items(): - if child is None: - continue - # if private child object, and is in the uow's "deleted" list, - # insure its in the list of items to be deleted - print "DEL CHILD", child - if child in uowcommit.uow.deleted: - print "REGISTER", child - uowcommit.register_object(child, isdelete=True) - else: - for obj in deplist: - print "HI 2 ON", obj - childlist = getlist(obj, False) - for child in childlist.deleted_items() + childlist.unchanged_items(): - print "DELCHILD", child - if child is not None: - self._synchronize(obj, child, None, True) - uowcommit.register_object(child, postupdate=self.post_update) - elif self.association is not None: - # manage association objects. - for obj in deplist: - childlist = getlist(obj, passive=True) - if childlist is None: continue - - #print "DIRECTION", self.direction - d = {} - for child in childlist: - self._synchronize(obj, child, None, False) - key = self.mapper.instance_key(child) - #print "SYNCHRONIZED", child, "INSTANCE KEY", key - d[key] = child - uowcommit.unregister_object(child) - - for child in childlist.added_items(): - uowcommit.register_object(child) - key = self.mapper.instance_key(child) - #print "ADDED, INSTANCE KEY", key - d[key] = child - - for child in childlist.unchanged_items(): - key = self.mapper.instance_key(child) - o = d[key] - o._instance_key= key - - for child in childlist.deleted_items(): - key = self.mapper.instance_key(child) - #print "DELETED, INSTANCE KEY", key - if d.has_key(key): - o = d[key] - o._instance_key = key - uowcommit.unregister_object(child) - else: - #print "DELETE ASSOC OBJ", repr(child) - uowcommit.register_object(child, isdelete=True) - else: - for obj in deplist: - childlist = getlist(obj, passive=True) - if childlist is not None: - for child in childlist.added_items(): - print "ADDCHILD", child - self._synchronize(obj, child, None, False) - if self.direction == PropertyLoader.ONETOMANY and child is not None: - uowcommit.register_object(child, postupdate=self.post_update) - if self.direction == PropertyLoader.MANYTOONE: - uowcommit.register_object(obj, postupdate=self.post_update) - if self.direction != PropertyLoader.MANYTOONE: - for child in childlist.deleted_items(): - if not self.private: - self._synchronize(obj, child, None, True) - uowcommit.register_object(child, isdelete=self.private) - </del><span class="cx"> def execute(self, session, instance, row, identitykey, imap, isnew): </span><span class="cx"> if self.is_primary(): </span><span class="cx"> return </span><span class="cx"> #print "PLAIN PROPLOADER EXEC NON-PRIAMRY", repr(id(self)), repr(self.mapper.class_), self.key </span><del>- objectstore.global_attributes.create_history(instance, self.key, self.uselist, cascade=self.cascade) </del><ins>+ objectstore.global_attributes.create_history(instance, self.key, self.uselist, cascade=self.cascade, trackparent=True) </ins><span class="cx"> </span><del>- # TODO: this method should be moved to an external object </del><ins>+ def register_dependencies(self, uowcommit): + self._dependency_processor.register_dependencies(uowcommit) + </ins><span class="cx"> def _compile_synchronizers(self): </span><span class="cx"> """assembles a list of 'synchronization rules', which are instructions on how to populate </span><span class="cx"> the objects on each side of a relationship. This is done when a PropertyLoader is </span><span class="lines">@@ -586,38 +346,17 @@ </span><span class="cx"> </span><span class="cx"> The list of rules is used within commits by the _synchronize() method when dependent </span><span class="cx"> objects are processed.""" </span><del>- - </del><span class="cx"> parent_tables = util.HashSet(self.parent.tables + [self.parent.primarytable]) </span><span class="cx"> target_tables = util.HashSet(self.mapper.tables + [self.mapper.primarytable]) </span><span class="cx"> </span><span class="cx"> self.syncrules = sync.ClauseSynchronizer(self.parent, self.mapper, self.direction) </span><del>- if self.direction == PropertyLoader.MANYTOMANY: </del><ins>+ if self.direction == sync.MANYTOMANY: </ins><span class="cx"> #print "COMPILING p/c", self.parent, self.mapper </span><span class="cx"> self.syncrules.compile(self.primaryjoin, parent_tables, [self.secondary], False) </span><span class="cx"> self.syncrules.compile(self.secondaryjoin, target_tables, [self.secondary], True) </span><span class="cx"> else: </span><span class="cx"> self.syncrules.compile(self.primaryjoin, parent_tables, target_tables) </span><span class="cx"> </span><del>- # TODO: this method should be moved to an external object - def _synchronize(self, obj, child, associationrow, clearkeys): - """called during a commit to execute the full list of syncrules on the - given object/child/optional association row""" - if self.direction == PropertyLoader.ONETOMANY: - source = obj - dest = child - elif self.direction == PropertyLoader.MANYTOONE: - source = child - dest = obj - elif self.direction == PropertyLoader.MANYTOMANY: - dest = associationrow - source = None - - if dest is None: - return - - self.syncrules.execute(source, dest, obj, child, clearkeys) - </del><span class="cx"> class LazyLoader(PropertyLoader): </span><span class="cx"> def do_init_subclass(self, key, parent): </span><span class="cx"> (self.lazywhere, self.lazybinds) = create_lazy_clause(self.parent.noninherited_table, self.primaryjoin, self.secondaryjoin, self.foreignkey) </span><span class="lines">@@ -628,7 +367,7 @@ </span><span class="cx"> def _set_class_attribute(self, class_, key): </span><span class="cx"> # establish a class-level lazy loader on our class </span><span class="cx"> #print "SETCLASSATTR LAZY", repr(class_), key </span><del>- objectstore.global_attributes.register_attribute(class_, key, uselist = self.uselist, callable_=lambda i: self.setup_loader(i), extension=self.attributeext, cascade=self.cascade) </del><ins>+ objectstore.global_attributes.register_attribute(class_, key, uselist = self.uselist, callable_=lambda i: self.setup_loader(i), extension=self.attributeext, cascade=self.cascade, trackparent=True) </ins><span class="cx"> </span><span class="cx"> def setup_loader(self, instance): </span><span class="cx"> if not self.parent.is_assigned(instance): </span><span class="lines">@@ -676,7 +415,7 @@ </span><span class="cx"> #print "EXEC NON-PRIAMRY", repr(self.mapper.class_), self.key </span><span class="cx"> # we are not the primary manager for this attribute on this class - set up a per-instance lazyloader, </span><span class="cx"> # which will override the class-level behavior </span><del>- objectstore.global_attributes.create_history(instance, self.key, self.uselist, callable_=self.setup_loader(instance), cascade=self.cascade) </del><ins>+ objectstore.global_attributes.create_history(instance, self.key, self.uselist, callable_=self.setup_loader(instance), cascade=self.cascade, trackparent=True) </ins><span class="cx"> else: </span><span class="cx"> #print "EXEC PRIMARY", repr(self.mapper.class_), self.key </span><span class="cx"> # we are the primary manager for this attribute on this class - reset its per-instance attribute state, </span><span class="lines">@@ -827,7 +566,7 @@ </span><span class="cx"> if isnew: </span><span class="cx"> # new row loaded from the database. initialize a blank container on the instance. </span><span class="cx"> # this will override any per-class lazyloading type of stuff. </span><del>- h = objectstore.global_attributes.create_history(instance, self.key, self.uselist, cascade=self.cascade) </del><ins>+ h = objectstore.global_attributes.create_history(instance, self.key, self.uselist, cascade=self.cascade, trackparent=True) </ins><span class="cx"> </span><span class="cx"> if not self.uselist: </span><span class="cx"> if isnew: </span><span class="lines">@@ -928,6 +667,7 @@ </span><span class="cx"> # else set one of us as the "backreference" </span><span class="cx"> if not prop.mapper.props[self.key].is_backref: </span><span class="cx"> prop.is_backref=True </span><ins>+ prop._dependency_processor.is_backref=True </ins><span class="cx"> def get_extension(self): </span><span class="cx"> """returns an attribute extension to use with this backreference.""" </span><span class="cx"> return attributes.GenericBackrefExtension(self.key) </span></span></pre></div> <a id="sqlalchemybranchesschemalibsqlalchemymappingunitofworkpy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/lib/sqlalchemy/mapping/unitofwork.py (1317 => 1318)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/lib/sqlalchemy/mapping/unitofwork.py 2006-04-22 19:01:37 UTC (rev 1317) +++ sqlalchemy/branches/schema/lib/sqlalchemy/mapping/unitofwork.py 2006-04-22 20:21:50 UTC (rev 1318) </span><span class="lines">@@ -44,16 +44,12 @@ </span><span class="cx"> self.cascade = cascade </span><span class="cx"> def do_value_changed(self, obj, key, item, listval, isdelete): </span><span class="cx"> sess = get_session(obj, raiseerror=False) </span><del>- if sess is None: - return - sess._register_dirty(obj) - if self.cascade is not None: - if isdelete: - if self.cascade.delete_orphan: - sess.delete(item) - else: - if self.cascade.save_update: - sess.save_or_update(item) </del><ins>+ if sess is not None: + sess._register_dirty(obj) + if self.cascade is not None: + if not isdelete: + if self.cascade.save_update: + sess.save_or_update(item) </ins><span class="cx"> def append(self, item, _mapper_nohistory = False): </span><span class="cx"> if _mapper_nohistory: </span><span class="cx"> self.append_nohistory(item) </span><span class="lines">@@ -70,9 +66,7 @@ </span><span class="cx"> if sess is not None: </span><span class="cx"> sess._register_dirty(obj) </span><span class="cx"> if self.cascade is not None: </span><del>- if oldvalue is not None and self.cascade.delete_orphan: - sess.delete(oldvalue) - if newvalue is not None and self.cascade.save_update: </del><ins>+ if self.cascade.save_update: </ins><span class="cx"> sess.save_or_update(newvalue) </span><span class="cx"> </span><span class="cx"> class UOWAttributeManager(attributes.AttributeManager): </span><span class="lines">@@ -427,7 +421,7 @@ </span><span class="cx"> class UOWDependencyProcessor(object): </span><span class="cx"> """in between the saving and deleting of objects, process "dependent" data, such as filling in </span><span class="cx"> a foreign key on a child item from a new primary key, or deleting association rows before a </span><del>- delete.""" </del><ins>+ delete. This object acts as a proxy to a DependencyProcessor.""" </ins><span class="cx"> def __init__(self, processor, targettask, isdeletefrom): </span><span class="cx"> self.processor = processor </span><span class="cx"> self.targettask = targettask </span></span></pre></div> <a id="sqlalchemybranchesschematestmasscreatepy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/test/masscreate.py (1317 => 1318)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/test/masscreate.py 2006-04-22 19:01:37 UTC (rev 1317) +++ sqlalchemy/branches/schema/test/masscreate.py 2006-04-22 20:21:50 UTC (rev 1318) </span><span class="lines">@@ -16,7 +16,7 @@ </span><span class="cx"> if manage_attributes: </span><span class="cx"> attr_manager.register_attribute(User, 'id', uselist=False) </span><span class="cx"> attr_manager.register_attribute(User, 'name', uselist=False) </span><del>- attr_manager.register_attribute(User, 'addresses', uselist=True) </del><ins>+ attr_manager.register_attribute(User, 'addresses', uselist=True, trackparent=True) </ins><span class="cx"> attr_manager.register_attribute(Address, 'email', uselist=False) </span><span class="cx"> </span><span class="cx"> now = time.time() </span><span class="lines">@@ -35,7 +35,7 @@ </span><span class="cx"> a.email = 'fo...@ba...' </span><span class="cx"> u.addresses.append(a) </span><span class="cx"> # gc.collect() </span><del>- print len(managed_attributes) </del><ins>+# print len(managed_attributes) </ins><span class="cx"> # managed_attributes.clear() </span><span class="cx"> total = time.time() - now </span><span class="cx"> print "Total time", total </span></span></pre></div> <a id="sqlalchemybranchesschematestobjectstorepy"></a> <div class="modfile"><h4>Modified: sqlalchemy/branches/schema/test/objectstore.py (1317 => 1318)</h4> <pre class="diff"><span> <span class="info">--- sqlalchemy/branches/schema/test/objectstore.py 2006-04-22 19:01:37 UTC (rev 1317) +++ sqlalchemy/branches/schema/test/objectstore.py 2006-04-22 20:21:50 UTC (rev 1318) </span><span class="lines">@@ -253,8 +253,7 @@ </span><span class="cx"> class B(object):pass </span><span class="cx"> </span><span class="cx"> assign_mapper(B,b_table) </span><del>- assign_mapper(A,a_table,properties= {'bs' : relation - (B.mapper,private=True)}) </del><ins>+ assign_mapper(A,a_table,properties= {'bs' : relation(B.mapper,private=True)}) </ins><span class="cx"> </span><span class="cx"> # create some objects </span><span class="cx"> a = A(data='a1') </span><span class="lines">@@ -539,6 +538,8 @@ </span><span class="cx"> objectstore.get_session().flush() </span><span class="cx"> </span><span class="cx"> def testchildmove(self): </span><ins>+ """tests moving a child from one parent to the other, then deleting the first parent, properly + updates the child with the new parent. this tests the 'trackparent' option in the attributes module.""" </ins><span class="cx"> m = mapper(User, users, properties = dict( </span><span class="cx"> addresses = relation(mapper(Address, addresses), lazy = True, private = False) </span><span class="cx"> )) </span><span class="lines">@@ -557,8 +558,7 @@ </span><span class="cx"> objectstore.clear() </span><s... [truncated message content] |