# mapper/sync.py
# Copyright (C) 2005,2006 Michael Bayer mike_mp@zzzcomputing.com
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
from sqlalchemy import sql,schema,exceptions
from sqlalchemy import logging
from sqlalchemy.orm import util
"""contains the ClauseSynchronizer class, which is used to map attributes between two objects
in a manner corresponding to a SQL clause that compares column values."""
ONETOMANY = 0
MANYTOONE = 1
MANYTOMANY = 2
class ClauseSynchronizer(object):
"""Given a SQL clause, usually a series of one or more binary
expressions between columns, and a set of 'source' and 'destination' mappers, compiles a set of SyncRules
corresponding to that information. The ClauseSynchronizer can then be executed given a set of parent/child
objects or destination dictionary, which will iterate through each of its SyncRules and execute them.
Each SyncRule will copy the value of a single attribute from the parent
to the child, corresponding to the pair of columns in a particular binary expression, using the source and
destination mappers to map those two columns to object attributes within parent and child."""
def __init__(self, parent_mapper, child_mapper, direction):
self.parent_mapper = parent_mapper
self.child_mapper = child_mapper
self.direction = direction
self.syncrules = []
def compile(self, sqlclause, issecondary=None, foreignkey=None):
def compile_binary(binary):
"""assemble a SyncRule given a single binary condition"""
if binary.operator != '=' or not isinstance(binary.left, schema.Column) or not isinstance(binary.right, schema.Column):
return
source_column = None
dest_column = None
if foreignkey is not None:
# for self-referential relationships,
# the best we can do right now is figure out which side
# is the primary key
# TODO: need some better way for this
if binary.left.table == binary.right.table:
if binary.left.primary_key:
source_column = binary.left
dest_column = binary.right
elif binary.right.primary_key:
source_column = binary.right
dest_column = binary.left
else:
raise exceptions.ArgumentError("Can't locate a primary key column in self-referential equality clause '%s'" % str(binary))
# for other relationships we are more flexible
# and go off the 'foreignkey' property
elif binary.left in foreignkey:
dest_column = binary.left
source_column = binary.right
elif binary.right in foreignkey:
dest_column = binary.right
source_column = binary.left
else:
return
else:
if binary.left in [f.column for f in binary.right.foreign_keys]:
dest_column = binary.right
source_column = binary.left
elif binary.right in [f.column for f in binary.left.foreign_keys]:
dest_column = binary.left
source_column = binary.right
if source_column and dest_column:
if self.direction == ONETOMANY:
self.syncrules.append(SyncRule(self.parent_mapper, source_column, dest_column, dest_mapper=self.child_mapper))
elif self.direction == MANYTOONE:
self.syncrules.append(SyncRule(self.child_mapper, source_column, dest_column, dest_mapper=self.parent_mapper))
else:
if not issecondary:
self.syncrules.append(SyncRule(self.parent_mapper, source_column, dest_column, dest_mapper=self.child_mapper, issecondary=issecondary))
else:
self.syncrules.append(SyncRule(self.child_mapper, source_column, dest_column, dest_mapper=self.parent_mapper, issecondary=issecondary))
rules_added = len(self.syncrules)
processor = BinaryVisitor(compile_binary)
sqlclause.accept_visitor(processor)
if len(self.syncrules) == rules_added:
raise exceptions.ArgumentError("No syncrules generated for join criterion " + str(sqlclause))
def dest_columns(self):
return [r.dest_column for r in self.syncrules if r.dest_column is not None]
def execute(self, source, dest, obj=None, child=None, clearkeys=None):
for rule in self.syncrules:
rule.execute(source, dest, obj, child, clearkeys)
class SyncRule(object):
"""An instruction indicating how to populate the objects on each side of a relationship.
i.e. if table1 column A is joined against
table2 column B, and we are a one-to-many from table1 to table2, a syncrule would say
'take the A attribute from object1 and assign it to the B attribute on object2'.
A rule contains the source mapper, the source column, destination column,
destination mapper in the case of a one/many relationship, and
the integer direction of this mapper relative to the association in the case
of a many to many relationship.
"""
def __init__(self, source_mapper, source_column, dest_column, dest_mapper=None, issecondary=None):
self.source_mapper = source_mapper
self.source_column = source_column
self.issecondary = issecondary
self.dest_mapper = dest_mapper
self.dest_column = dest_column
#print "SyncRule", source_mapper, source_column, dest_column, dest_mapper
def dest_primary_key(self):
try:
return self._dest_primary_key
except AttributeError:
self._dest_primary_key = self.dest_mapper is not None and self.dest_column in self.dest_mapper.pks_by_table[self.dest_column.table]
return self._dest_primary_key
def execute(self, source, dest, obj, child, clearkeys):
if source is None:
if self.issecondary is False:
source = obj
elif self.issecondary is True:
source = child
if clearkeys or source is None:
value = None
else:
value = self.source_mapper.get_attr_by_column(source, self.source_column)
if isinstance(dest, dict):
dest[self.dest_column.key] = value
else:
if clearkeys and self.dest_primary_key():
raise exceptions.AssertionError("Dependency rule tried to blank-out primary key column '%s' on instance '%s'" % (str(self.dest_column), mapperutil.instance_str(dest)))
if logging.is_debug_enabled(self.logger):
self.logger.debug("execute() instances: %s(%s)->%s(%s) ('%s')" % (mapperutil.instance_str(source), str(self.source_column), mapperutil.instance_str(dest), str(self.dest_column), value))
self.dest_mapper.set_attr_by_column(dest, self.dest_column, value)
SyncRule.logger = logging.class_logger(SyncRule)
class BinaryVisitor(sql.ClauseVisitor):
def __init__(self, func):
self.func = func
def visit_binary(self, binary):
self.func(binary)
|