# orm/properties.py
# Copyright (C) 2005-2011 the SQLAlchemy authors and contributors <see AUTHORS file>
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php

"""MapperProperty implementations.

This is a private module which defines the behavior of invidual ORM-
mapped attributes.

"""

from sqlalchemy import sql, util, log, exc as sa_exc
from sqlalchemy.sql.util import ClauseAdapter, criterion_as_pairs, \
    join_condition
from sqlalchemy.sql import operators, expression
from sqlalchemy.orm import attributes, dependency, mapper, \
    object_mapper, strategies, configure_mappers
from sqlalchemy.orm.util import CascadeOptions, _class_to_mapper, \
    _orm_annotate, _orm_deannotate
from sqlalchemy.orm.interfaces import MANYTOMANY, MANYTOONE, \
    MapperProperty, ONETOMANY, PropComparator, StrategizedProperty
mapperlib = util.importlater("sqlalchemy.orm", "mapperlib")
NoneType = type(None)

__all__ = ('ColumnProperty', 'CompositeProperty', 'SynonymProperty',
           'ComparableProperty', 'RelationshipProperty', 'RelationProperty')

from descriptor_props import CompositeProperty, SynonymProperty, \
            ComparableProperty,ConcreteInheritedProperty

class ColumnProperty(StrategizedProperty):
    """Describes an object attribute that corresponds to a table column."""

    def __init__(self, *columns, **kwargs):
        """Construct a ColumnProperty.

        :param \*columns: The list of `columns` describes a single
          object property. If there are multiple tables joined
          together for the mapper, this list represents the equivalent
          column as it appears across each table.

        :param group:

        :param deferred:

        :param comparator_factory:

        :param descriptor:

        :param extension:

        """
        self.columns = [expression._labeled(c) for c in columns]
        self.group = kwargs.pop('group', None)
        self.deferred = kwargs.pop('deferred', False)
        self.instrument = kwargs.pop('_instrument', True)
        self.comparator_factory = kwargs.pop('comparator_factory',
                                            self.__class__.Comparator)
        self.descriptor = kwargs.pop('descriptor', None)
        self.extension = kwargs.pop('extension', None)
        self.active_history = kwargs.pop('active_history', False)

        if 'doc' in kwargs:
            self.doc = kwargs.pop('doc')
        else:
            for col in reversed(self.columns):
                doc = getattr(col, 'doc', None)
                if doc is not None:
                    self.doc = doc
                    break
            else:
                self.doc = None

        if kwargs:
            raise TypeError(
                "%s received unexpected keyword argument(s): %s" % (
                    self.__class__.__name__, 
                    ', '.join(sorted(kwargs.keys()))))

        util.set_creation_order(self)
        if not self.instrument:
            self.strategy_class = strategies.UninstrumentedColumnLoader
        elif self.deferred:
            self.strategy_class = strategies.DeferredColumnLoader
        else:
            self.strategy_class = strategies.ColumnLoader

    def instrument_class(self, mapper):
        if not self.instrument:
            return

        attributes.register_descriptor(
            mapper.class_, 
            self.key, 
            comparator=self.comparator_factory(self, mapper), 
            parententity=mapper,
            property_=self,
            doc=self.doc
            )

    def do_init(self):
        super(ColumnProperty, self).do_init()
        if len(self.columns) > 1 and \
                set(self.parent.primary_key).issuperset(self.columns):
            util.warn(
                ("On mapper %s, primary key column '%s' is being combined "
                 "with distinct primary key column '%s' in attribute '%s'.  "
                 "Use explicit properties to give each column its own mapped "
                 "attribute name.") % (self.parent, self.columns[1],
                                       self.columns[0], self.key))

    def copy(self):
        return ColumnProperty(
                        deferred=self.deferred, 
                        group=self.group, 
                        active_history=self.active_history,
                        *self.columns)

    def _getcommitted(self, state, dict_, column, 
                    passive=attributes.PASSIVE_OFF):
        return state.get_impl(self.key).\
                    get_committed_value(state, dict_, passive=passive)

    def merge(self, session, source_state, source_dict, dest_state, 
                                dest_dict, load, _recursive):
        if self.key in source_dict:
            value = source_dict[self.key]

            if not load:
                dest_dict[self.key] = value
            else:
                impl = dest_state.get_impl(self.key)
                impl.set(dest_state, dest_dict, value, None)
        else:
            if dest_state.has_identity and self.key not in dest_dict:
                dest_state.expire_attributes(dest_dict, [self.key])

    class Comparator(PropComparator):
        @util.memoized_instancemethod
        def __clause_element__(self):
            if self.adapter:
                return self.adapter(self.prop.columns[0])
            else:
                return self.prop.columns[0]._annotate({
                                                "parententity": self.mapper,
                                                "parentmapper":self.mapper})

        def operate(self, op, *other, **kwargs):
            return op(self.__clause_element__(), *other, **kwargs)

        def reverse_operate(self, op, other, **kwargs):
            col = self.__clause_element__()
            return op(col._bind_param(op, other), col, **kwargs)

    # TODO: legacy..do we need this ? (0.5)
    ColumnComparator = Comparator

    def __str__(self):
        return str(self.parent.class_.__name__) + "." + self.key

log.class_logger(ColumnProperty)




class RelationshipProperty(StrategizedProperty):
    """Describes an object property that holds a single item or list
    of items that correspond to a related database table.
    """

    def __init__(self, argument,
        secondary=None, primaryjoin=None,
        secondaryjoin=None, 
        foreign_keys=None,
        uselist=None,
        order_by=False,
        backref=None,
        back_populates=None,
        post_update=False,
        cascade=False, extension=None,
        viewonly=False, lazy=True,
        collection_class=None, passive_deletes=False,
        passive_updates=True, remote_side=None,
        enable_typechecks=True, join_depth=None,
        comparator_factory=None,
        single_parent=False, innerjoin=False,
        doc=None,
        active_history=False,
        cascade_backrefs=True,
        load_on_pending=False,
        strategy_class=None, _local_remote_pairs=None, 
        query_class=None):

        self.uselist = uselist
        self.argument = argument
        self.secondary = secondary
        self.primaryjoin = primaryjoin
        self.secondaryjoin = secondaryjoin
        self.post_update = post_update
        self.direction = None
        self.viewonly = viewonly
        self.lazy = lazy
        self.single_parent = single_parent
        self._user_defined_foreign_keys = foreign_keys
        self.collection_class = collection_class
        self.passive_deletes = passive_deletes
        self.cascade_backrefs = cascade_backrefs
        self.passive_updates = passive_updates
        self.remote_side = remote_side
        self.enable_typechecks = enable_typechecks
        self.query_class = query_class
        self.innerjoin = innerjoin
        self.doc = doc
        self.active_history = active_history
        self.join_depth = join_depth
        self.local_remote_pairs = _local_remote_pairs
        self.extension = extension
        self.load_on_pending = load_on_pending
        self.comparator_factory = comparator_factory or \
                                    RelationshipProperty.Comparator
        self.comparator = self.comparator_factory(self, None)
        util.set_creation_order(self)

        if strategy_class:
            self.strategy_class = strategy_class
        elif self.lazy== 'dynamic':
            from sqlalchemy.orm import dynamic
            self.strategy_class = dynamic.DynaLoader
        else:
            self.strategy_class = strategies.factory(self.lazy)

        self._reverse_property = set()

        if cascade is not False:
            self.cascade = CascadeOptions(cascade)
        else:
            self.cascade = CascadeOptions("save-update, merge")

        if self.passive_deletes == 'all' and \
                    ("delete" in self.cascade or 
                    "delete-orphan" in self.cascade):
            raise sa_exc.ArgumentError(
                            "Can't set passive_deletes='all' in conjunction "
                            "with 'delete' or 'delete-orphan' cascade")

        self.order_by = order_by

        self.back_populates = back_populates

        if self.back_populates:
            if backref:
                raise sa_exc.ArgumentError(
                            "backref and back_populates keyword arguments "
                            "are mutually exclusive")
            self.backref = None
        else:
            self.backref = backref

    def instrument_class(self, mapper):
        attributes.register_descriptor(
            mapper.class_, 
            self.key, 
            comparator=self.comparator_factory(self, mapper), 
            parententity=mapper,
            property_=self,
            doc=self.doc,
            )

    class Comparator(PropComparator):
        def __init__(self, prop, mapper, of_type=None, adapter=None):
            self.prop = prop
            self.mapper = mapper
            self.adapter = adapter
            if of_type:
                self._of_type = _class_to_mapper(of_type)

        def adapted(self, adapter):
            """Return a copy of this PropComparator which will use the
            given adaption function on the local side of generated
            expressions.

            """

            return self.__class__(self.property, self.mapper,
                                  getattr(self, '_of_type', None),
                                  adapter)

        @property
        def parententity(self):
            return self.property.parent

        def __clause_element__(self):
            elem = self.property.parent._with_polymorphic_selectable
            if self.adapter:
                return self.adapter(elem)
            else:
                return elem

        def operate(self, op, *other, **kwargs):
            return op(self, *other, **kwargs)

        def reverse_operate(self, op, other, **kwargs):
            return op(self, *other, **kwargs)

        def of_type(self, cls):
            return RelationshipProperty.Comparator(
                                        self.property, 
                                        self.mapper, 
                                        cls, adapter=self.adapter)

        def in_(self, other):
            raise NotImplementedError('in_() not yet supported for '
                    'relationships.  For a simple many-to-one, use '
                    'in_() against the set of foreign key values.')

        __hash__ = None

        def __eq__(self, other):
            if isinstance(other, (NoneType, expression._Null)):
                if self.property.direction in [ONETOMANY, MANYTOMANY]:
                    return ~self._criterion_exists()
                else:
                    return _orm_annotate(self.property._optimized_compare(
                            None, adapt_source=self.adapter))
            elif self.property.uselist:
                raise sa_exc.InvalidRequestError("Can't compare a colle"
                        "ction to an object or collection; use "
                        "contains() to test for membership.")
            else:
                return _orm_annotate(self.property._optimized_compare(other,
                        adapt_source=self.adapter))

        def _criterion_exists(self, criterion=None, **kwargs):
            if getattr(self, '_of_type', None):
                target_mapper = self._of_type
                to_selectable = target_mapper._with_polymorphic_selectable
                if self.property._is_self_referential():
                    to_selectable = to_selectable.alias()

                single_crit = target_mapper._single_table_criterion
                if single_crit is not None:
                    if criterion is not None:
                        criterion = single_crit & criterion
                    else:
                        criterion = single_crit
            else:
                to_selectable = None

            if self.adapter:
                source_selectable = self.__clause_element__()
            else:
                source_selectable = None

            pj, sj, source, dest, secondary, target_adapter = \
                self.property._create_joins(dest_polymorphic=True,
                        dest_selectable=to_selectable,
                        source_selectable=source_selectable)

            for k in kwargs:
                crit = self.property.mapper.class_manager[k] == kwargs[k]
                if criterion is None:
                    criterion = crit
                else:
                    criterion = criterion & crit

            # annotate the *local* side of the join condition, in the case
            # of pj + sj this is the full primaryjoin, in the case of just
            # pj its the local side of the primaryjoin.
            if sj is not None:
                j = _orm_annotate(pj) & sj
            else:
                j = _orm_annotate(pj, exclude=self.property.remote_side)

            if criterion is not None and target_adapter:
                # limit this adapter to annotated only?
                criterion = target_adapter.traverse(criterion)

            # only have the "joined left side" of what we 
            # return be subject to Query adaption.  The right
            # side of it is used for an exists() subquery and 
            # should not correlate or otherwise reach out
            # to anything in the enclosing query.
            if criterion is not None:
                criterion = criterion._annotate({'_halt_adapt': True})

            crit = j & criterion

            return sql.exists([1], crit, from_obj=dest).correlate(source)

        def any(self, criterion=None, **kwargs):
            if not self.property.uselist:
                raise sa_exc.InvalidRequestError(
                            "'any()' not implemented for scalar "
                            "attributes. Use has()."
                        )

            return self._criterion_exists(criterion, **kwargs)

        def has(self, criterion=None, **kwargs):
            if self.property.uselist:
                raise sa_exc.InvalidRequestError(
                            "'has()' not implemented for collections.  "
                            "Use any().")
            return self._criterion_exists(criterion, **kwargs)

        def contains(self, other, **kwargs):
            if not self.property.uselist:
                raise sa_exc.InvalidRequestError(
                            "'contains' not implemented for scalar "
                            "attributes.  Use ==")
            clause = self.property._optimized_compare(other,
                    adapt_source=self.adapter)

            if self.property.secondaryjoin is not None:
                clause.negation_clause = \
                    self.__negated_contains_or_equals(other)

            return clause

        def __negated_contains_or_equals(self, other):
            if self.property.direction == MANYTOONE:
                state = attributes.instance_state(other)

                def state_bindparam(x, state, col):
                    o = state.obj() # strong ref
                    return sql.bindparam(x, unique=True, callable_=lambda : \
                        self.property.mapper._get_committed_attr_by_column(o,
                            col))

                def adapt(col):
                    if self.adapter:
                        return self.adapter(col)
                    else:
                        return col

                if self.property._use_get:
                    return sql.and_(*[
                        sql.or_(
                        adapt(x) != state_bindparam(adapt(x), state, y),
                        adapt(x) == None)
                        for (x, y) in self.property.local_remote_pairs])

            criterion = sql.and_(*[x==y for (x, y) in 
                                zip(
                                    self.property.mapper.primary_key,
                                    self.property.\
                                            mapper.\
                                            primary_key_from_instance(other))
                                    ])
            return ~self._criterion_exists(criterion)

        def __ne__(self, other):
            if isinstance(other, (NoneType, expression._Null)):
                if self.property.direction == MANYTOONE:
                    return sql.or_(*[x != None for x in
                                   self.property._calculated_foreign_keys])
                else:
                    return self._criterion_exists()
            elif self.property.uselist:
                raise sa_exc.InvalidRequestError("Can't compare a collection"
                        " to an object or collection; use "
                        "contains() to test for membership.")
            else:
                return self.__negated_contains_or_equals(other)

        @util.memoized_property
        def property(self):
            if mapperlib.module._new_mappers:
                configure_mappers()
            return self.prop

    def compare(self, op, value, 
                            value_is_parent=False, 
                            alias_secondary=True):
        if op == operators.eq:
            if value is None:
                if self.uselist:
                    return ~sql.exists([1], self.primaryjoin)
                else:
                    return self._optimized_compare(None, 
                                    value_is_parent=value_is_parent,
                                    alias_secondary=alias_secondary)
            else:
                return self._optimized_compare(value, 
                                value_is_parent=value_is_parent,
                                alias_secondary=alias_secondary)
        else:
            return op(self.comparator, value)

    def _optimized_compare(self, value, value_is_parent=False, 
                                    adapt_source=None, 
                                    alias_secondary=True):
        if value is not None:
            value = attributes.instance_state(value)
        return self._get_strategy(strategies.LazyLoader).lazy_clause(value,
                reverse_direction=not value_is_parent,
                alias_secondary=alias_secondary,
                adapt_source=adapt_source)

    def __str__(self):
        return str(self.parent.class_.__name__) + "." + self.key

    def merge(self, 
                    session,
                    source_state,
                    source_dict,
                    dest_state,
                    dest_dict, 
                    load, _recursive):
        if load:
            # TODO: no test coverage for recursive check
            for r in self._reverse_property:
                if (source_state, r) in _recursive:
                    return

        if not "merge" in self.cascade:
            return

        if self.key not in source_dict:
            return

        if self.uselist:
            instances = source_state.get_impl(self.key).\
                            get(source_state, source_dict)
            if hasattr(instances, '_sa_adapter'):
                # convert collections to adapters to get a true iterator
                instances = instances._sa_adapter

            if load:
                # for a full merge, pre-load the destination collection,
                # so that individual _merge of each item pulls from identity
                # map for those already present.
                # also assumes CollectionAttrbiuteImpl behavior of loading
                # "old" list in any case
                dest_state.get_impl(self.key).get(dest_state, dest_dict)

            dest_list = []
            for current in instances:
                current_state = attributes.instance_state(current)
                current_dict = attributes.instance_dict(current)
                _recursive[(current_state, self)] = True
                obj = session._merge(current_state, current_dict,
                        load=load, _recursive=_recursive)
                if obj is not None:
                    dest_list.append(obj)

            if not load:
                coll = attributes.init_state_collection(dest_state,
                        dest_dict, self.key)
                for c in dest_list:
                    coll.append_without_event(c)
            else:
                dest_state.get_impl(self.key)._set_iterable(dest_state,
                        dest_dict, dest_list)
        else:
            current = source_dict[self.key]
            if current is not None:
                current_state = attributes.instance_state(current)
                current_dict = attributes.instance_dict(current)
                _recursive[(current_state, self)] = True
                obj = session._merge(current_state, current_dict,
                        load=load, _recursive=_recursive)
            else:
                obj = None
            if not load:
                dest_dict[self.key] = obj
            else:
                dest_state.get_impl(self.key).set(dest_state,
                        dest_dict, obj, None)

    def cascade_iterator(self, type_, state, dict_, visited_states, halt_on=None):
        #assert type_ in self.cascade

        # only actively lazy load on the 'delete' cascade
        if type_ != 'delete' or self.passive_deletes:
            passive = attributes.PASSIVE_NO_INITIALIZE
        else:
            passive = attributes.PASSIVE_OFF

        if type_ == 'save-update':
            tuples = state.manager[self.key].impl.\
                        get_all_pending(state, dict_)

        else:
            tuples = state.value_as_iterable(dict_, self.key,
                            passive=passive)

        skip_pending = type_ == 'refresh-expire' and 'delete-orphan' \
            not in self.cascade

        for instance_state, c in tuples:
            if instance_state in visited_states:
                continue

            instance_dict = attributes.instance_dict(c)

            if halt_on and halt_on(instance_state):
                continue

            if skip_pending and not instance_state.key:
                continue

            instance_mapper = instance_state.manager.mapper

            if not instance_mapper.isa(self.mapper.class_manager.mapper):
                raise AssertionError("Attribute '%s' on class '%s' "
                                    "doesn't handle objects "
                                    "of type '%s'" % (
                                        self.key, 
                                        self.parent.class_, 
                                        c.__class__
                                    ))

            visited_states.add(instance_state)

            yield c, instance_mapper, instance_state, instance_dict


    def _add_reverse_property(self, key):
        other = self.mapper.get_property(key, _compile_mappers=False)
        self._reverse_property.add(other)
        other._reverse_property.add(self)

        if not other._get_target().common_parent(self.parent):
            raise sa_exc.ArgumentError('reverse_property %r on '
                    'relationship %s references relationship %s, which '
                    'does not reference mapper %s' % (key, self, other,
                    self.parent))
        if self.direction in (ONETOMANY, MANYTOONE) and self.direction \
            == other.direction:
            raise sa_exc.ArgumentError('%s and back-reference %s are '
                    'both of the same direction %r.  Did you mean to '
                    'set remote_side on the many-to-one side ?'
                    % (other, self, self.direction))

    def do_init(self):
        self._get_target()
        self._assert_is_primary()
        self._process_dependent_arguments()
        self._determine_joins()
        self._determine_synchronize_pairs()
        self._determine_direction()
        self._determine_local_remote_pairs()
        self._post_init()
        self._generate_backref()
        super(RelationshipProperty, self).do_init()

    def _get_target(self):
        if not hasattr(self, 'mapper'):
            if isinstance(self.argument, type):
                self.mapper = mapper.class_mapper(self.argument,
                        compile=False)
            elif isinstance(self.argument, mapper.Mapper):
                self.mapper = self.argument
            elif util.callable(self.argument):

                # accept a callable to suit various deferred-
                # configurational schemes

                self.mapper = mapper.class_mapper(self.argument(),
                        compile=False)
            else:
                raise sa_exc.ArgumentError("relationship '%s' expects "
                        "a class or a mapper argument (received: %s)"
                        % (self.key, type(self.argument)))
            assert isinstance(self.mapper, mapper.Mapper), self.mapper
        return self.mapper

    def _process_dependent_arguments(self):

        # accept callables for other attributes which may require
        # deferred initialization

        for attr in (
            'order_by',
            'primaryjoin',
            'secondaryjoin',
            'secondary',
            '_user_defined_foreign_keys',
            'remote_side',
            ):
            if util.callable(getattr(self, attr)):
                setattr(self, attr, getattr(self, attr)())

        # in the case that InstrumentedAttributes were used to construct
        # primaryjoin or secondaryjoin, remove the "_orm_adapt"
        # annotation so these interact with Query in the same way as the
        # original Table-bound Column objects

        for attr in 'primaryjoin', 'secondaryjoin':
            val = getattr(self, attr)
            if val is not None:
                setattr(self, attr, _orm_deannotate(
                    expression._only_column_elements(val, attr))
                )
        if self.order_by is not False and self.order_by is not None:
            self.order_by = [expression._only_column_elements(x, "order_by") for x in
                             util.to_list(self.order_by)]
        self._user_defined_foreign_keys = \
            util.column_set(expression._only_column_elements(x, "foreign_keys") for x in
                            util.to_column_set(self._user_defined_foreign_keys))
        self.remote_side = \
            util.column_set(expression._only_column_elements(x, "remote_side") for x in
                            util.to_column_set(self.remote_side))
        if not self.parent.concrete:
            for inheriting in self.parent.iterate_to_root():
                if inheriting is not self.parent \
                    and inheriting.has_property(self.key):
                    util.warn("Warning: relationship '%s' on mapper "
                              "'%s' supersedes the same relationship "
                              "on inherited mapper '%s'; this can "
                              "cause dependency issues during flush"
                              % (self.key, self.parent, inheriting))

        # TODO: remove 'self.table'

        self.target = self.table = self.mapper.mapped_table
        if self.cascade.delete_orphan:
            if self.parent.class_ is self.mapper.class_:
                raise sa_exc.ArgumentError("In relationship '%s', "
                        "can't establish 'delete-orphan' cascade rule "
                        "on a self-referential relationship.  You "
                        "probably want cascade='all', which includes "
                        "delete cascading but not orphan detection."
                        % str(self))
            self.mapper.primary_mapper().delete_orphans.append((self.key,
                    self.parent.class_))

    def _determine_joins(self):
        if self.secondaryjoin is not None and self.secondary is None:
            raise sa_exc.ArgumentError("Property '" + self.key
                    + "' specified with secondary join condition but "
                    "no secondary argument")

        # if join conditions were not specified, figure them out based
        # on foreign keys

        def _search_for_join(mapper, table):

            # find a join between the given mapper's mapped table and
            # the given table. will try the mapper's local table first
            # for more specificity, then if not found will try the more
            # general mapped table, which in the case of inheritance is
            # a join.

            try:
                return join_condition(mapper.local_table, table)
            except sa_exc.ArgumentError, e:
                return join_condition(mapper.mapped_table, table)

        try:
            if self.secondary is not None:
                if self.secondaryjoin is None:
                    self.secondaryjoin = _search_for_join(self.mapper,
                            self.secondary)
                if self.primaryjoin is None:
                    self.primaryjoin = _search_for_join(self.parent,
                            self.secondary)
            else:
                if self.primaryjoin is None:
                    self.primaryjoin = _search_for_join(self.parent,
                            self.target)
        except sa_exc.ArgumentError, e:
            raise sa_exc.ArgumentError("Could not determine join "
                    "condition between parent/child tables on "
                    "relationship %s.  Specify a 'primaryjoin' "
                    "expression.  If 'secondary' is present, "
                    "'secondaryjoin' is needed as well."
                    % self)

    def _col_is_part_of_mappings(self, column):
        if self.secondary is None:
            return self.parent.mapped_table.c.contains_column(column) or \
                self.target.c.contains_column(column)
        else:
            return self.parent.mapped_table.c.contains_column(column) or \
                self.target.c.contains_column(column) or \
                self.secondary.c.contains_column(column) is not None

    def _sync_pairs_from_join(self, join_condition, primary):
        """Given a join condition, figure out what columns are foreign
        and are part of a binary "equated" condition to their referenced
        columns, and convert into a list of tuples of (primary col->foreign col).

        Make several attempts to determine if cols are compared using 
        "=" or other comparators (in which case suggest viewonly), 
        columns are present but not part of the expected mappings, columns
        don't have any :class:`.ForeignKey` information on them, or 
        the ``foreign_keys`` attribute is being used incorrectly.

        """
        eq_pairs = criterion_as_pairs(join_condition,
                consider_as_foreign_keys=self._user_defined_foreign_keys,
                any_operator=self.viewonly)

        eq_pairs = [(l, r) for (l, r) in eq_pairs
                    if self._col_is_part_of_mappings(l)
                    and self._col_is_part_of_mappings(r)
                    or self.viewonly and r in self._user_defined_foreign_keys]

        if not eq_pairs and \
                self.secondary is not None and \
                not self._user_defined_foreign_keys:
            fks = set(self.secondary.c)
            eq_pairs = criterion_as_pairs(join_condition,
                    consider_as_foreign_keys=fks,
                    any_operator=self.viewonly)

            eq_pairs = [(l, r) for (l, r) in eq_pairs
                        if self._col_is_part_of_mappings(l)
                        and self._col_is_part_of_mappings(r)
                        or self.viewonly and r in fks]
            if eq_pairs:
                util.warn("No ForeignKey objects were present "
                            "in secondary table '%s'.  Assumed referenced "
                            "foreign key columns %s for join condition '%s' "
                            "on relationship %s" % (
                                self.secondary.description,
                                ", ".join(sorted(["'%s'" % col for col in fks])),
                                join_condition,
                                self
                            ))

        if not eq_pairs:
            if not self.viewonly and criterion_as_pairs(join_condition,
                    consider_as_foreign_keys=self._user_defined_foreign_keys,
                    any_operator=True):

                err = "Could not locate any "\
                        "foreign-key-equated, locally mapped column "\
                        "pairs for %s "\
                        "condition '%s' on relationship %s." % (
                            primary and 'primaryjoin' or 'secondaryjoin', 
                            join_condition, 
                            self
                        )

                if not self._user_defined_foreign_keys:
                    err += "  Ensure that the "\
                            "referencing Column objects have a "\
                            "ForeignKey present, or are otherwise part "\
                            "of a ForeignKeyConstraint on their parent "\
                            "Table, or specify the foreign_keys parameter "\
                            "to this relationship."

                err += "  For more "\
                        "relaxed rules on join conditions, the "\
                        "relationship may be marked as viewonly=True."

                raise sa_exc.ArgumentError(err)
            else:
                if self._user_defined_foreign_keys:
                    raise sa_exc.ArgumentError("Could not determine "
                            "relationship direction for %s condition "
                            "'%s', on relationship %s, using manual "
                            "'foreign_keys' setting.  Do the columns "
                            "in 'foreign_keys' represent all, and "
                            "only, the 'foreign' columns in this join "
                            "condition?  Does the %s Table already "
                            "have adequate ForeignKey and/or "
                            "ForeignKeyConstraint objects established "
                            "(in which case 'foreign_keys' is usually "
                            "unnecessary)?" 
                            % (
                                primary and 'primaryjoin' or 'secondaryjoin',
                                join_condition, 
                                self,
                                primary and 'mapped' or 'secondary'
                            ))
                else:
                    raise sa_exc.ArgumentError("Could not determine "
                            "relationship direction for %s condition "
                            "'%s', on relationship %s. Ensure that the "
                            "referencing Column objects have a "
                            "ForeignKey present, or are otherwise part "
                            "of a ForeignKeyConstraint on their parent "
                            "Table, or specify the foreign_keys parameter " 
                            "to this relationship."
                            % (
                                primary and 'primaryjoin' or 'secondaryjoin', 
                                join_condition, 
                                self
                            ))
        return eq_pairs

    def _determine_synchronize_pairs(self):
        if self.local_remote_pairs:
            if not self._user_defined_foreign_keys:
                raise sa_exc.ArgumentError('foreign_keys argument is '
                        'required with _local_remote_pairs argument')
            self.synchronize_pairs = []
            for l, r in self.local_remote_pairs:
                if r in self._user_defined_foreign_keys:
                    self.synchronize_pairs.append((l, r))
                elif l in self._user_defined_foreign_keys:
                    self.synchronize_pairs.append((r, l))
        else:
            eq_pairs = self._sync_pairs_from_join(self.primaryjoin, True)
            self.synchronize_pairs = eq_pairs
        if self.secondaryjoin is not None:
            sq_pairs = self._sync_pairs_from_join(self.secondaryjoin, False)
            self.secondary_synchronize_pairs = sq_pairs
        else:
            self.secondary_synchronize_pairs = None
        self._calculated_foreign_keys = util.column_set(r for (l, r) in
                self.synchronize_pairs)
        if self.secondary_synchronize_pairs:
            self._calculated_foreign_keys.update(r for (l, r) in
                    self.secondary_synchronize_pairs)

    def _determine_direction(self):
        if self.secondaryjoin is not None:
            self.direction = MANYTOMANY
        elif self._refers_to_parent_table():

            # self referential defaults to ONETOMANY unless the "remote"
            # side is present and does not reference any foreign key
            # columns

            if self.local_remote_pairs:
                remote = [r for (l, r) in self.local_remote_pairs]
            elif self.remote_side:
                remote = self.remote_side
            else:
                remote = None
            if not remote or self._calculated_foreign_keys.difference(l for (l,
                    r) in self.synchronize_pairs).intersection(remote):
                self.direction = ONETOMANY
            else:
                self.direction = MANYTOONE
        else:
            foreign_keys = [f for (c, f) in self.synchronize_pairs]
            parentcols = util.column_set(self.parent.mapped_table.c)
            targetcols = util.column_set(self.mapper.mapped_table.c)

            # fk collection which suggests ONETOMANY.

            onetomany_fk = targetcols.intersection(foreign_keys)

            # fk collection which suggests MANYTOONE.

            manytoone_fk = parentcols.intersection(foreign_keys)
            if not onetomany_fk and not manytoone_fk:
                raise sa_exc.ArgumentError("Can't determine relationshi"
                        "p direction for relationship '%s' - foreign "
                        "key columns are present in neither the parent "
                        "nor the child's mapped tables" % self)
            elif onetomany_fk and manytoone_fk:

                # fks on both sides.  do the same test only based on the
                # local side.

                referents = [c for (c, f) in self.synchronize_pairs]
                onetomany_local = parentcols.intersection(referents)
                manytoone_local = targetcols.intersection(referents)
                if onetomany_local and not manytoone_local:
                    self.direction = ONETOMANY
                elif manytoone_local and not onetomany_local:
                    self.direction = MANYTOONE
            elif onetomany_fk:
                self.direction = ONETOMANY
            elif manytoone_fk:
                self.direction = MANYTOONE
            if not self.direction:
                raise sa_exc.ArgumentError("Can't determine relationship"
                        " direction for relationship '%s' - foreign "
                        "key columns are present in both the parent "
                        "and the child's mapped tables.  Specify "
                        "'foreign_keys' argument." % self)
        if self.cascade.delete_orphan and not self.single_parent \
            and (self.direction is MANYTOMANY or self.direction
                 is MANYTOONE):
            util.warn('On %s, delete-orphan cascade is not supported '
                      'on a many-to-many or many-to-one relationship '
                      'when single_parent is not set.   Set '
                      'single_parent=True on the relationship().'
                      % self)
        if self.direction is MANYTOONE and self.passive_deletes:
            util.warn("On %s, 'passive_deletes' is normally configured "
                      "on one-to-many, one-to-one, many-to-many "
                      "relationships only."
                       % self)

    def _determine_local_remote_pairs(self):
        if not self.local_remote_pairs:
            if self.remote_side:
                if self.direction is MANYTOONE:
                    self.local_remote_pairs = [(r, l) for (l, r) in
                            criterion_as_pairs(self.primaryjoin,
                            consider_as_referenced_keys=self.remote_side,
                            any_operator=True)]
                else:
                    self.local_remote_pairs = \
                        criterion_as_pairs(self.primaryjoin,
                            consider_as_foreign_keys=self.remote_side,
                            any_operator=True)
                if not self.local_remote_pairs:
                    raise sa_exc.ArgumentError('Relationship %s could '
                            'not determine any local/remote column '
                            'pairs from remote side argument %r'
                            % (self, self.remote_side))
            else:
                if self.viewonly:
                    eq_pairs = self.synchronize_pairs
                    if self.secondaryjoin is not None:
                        eq_pairs += self.secondary_synchronize_pairs
                else:
                    eq_pairs = criterion_as_pairs(self.primaryjoin,
                            consider_as_foreign_keys=self._calculated_foreign_keys,
                            any_operator=True)
                    if self.secondaryjoin is not None:
                        eq_pairs += \
                            criterion_as_pairs(self.secondaryjoin,
                                consider_as_foreign_keys=self._calculated_foreign_keys,
                                any_operator=True)
                    eq_pairs = [(l, r) for (l, r) in eq_pairs
                                if self._col_is_part_of_mappings(l)
                                and self._col_is_part_of_mappings(r)]
                if self.direction is MANYTOONE:
                    self.local_remote_pairs = [(r, l) for (l, r) in
                            eq_pairs]
                else:
                    self.local_remote_pairs = eq_pairs
        elif self.remote_side:
            raise sa_exc.ArgumentError('remote_side argument is '
                    'redundant against more detailed '
                    '_local_remote_side argument.')
        for l, r in self.local_remote_pairs:
            if self.direction is ONETOMANY \
                and not self._col_is_part_of_mappings(l):
                raise sa_exc.ArgumentError("Local column '%s' is not "
                        "part of mapping %s.  Specify remote_side "
                        "argument to indicate which column lazy join "
                        "condition should compare against." % (l,
                        self.parent))
            elif self.direction is MANYTOONE \
                and not self._col_is_part_of_mappings(r):
                raise sa_exc.ArgumentError("Remote column '%s' is not "
                        "part of mapping %s. Specify remote_side "
                        "argument to indicate which column lazy join "
                        "condition should bind." % (r, self.mapper))
        self.local_side, self.remote_side = [util.ordered_column_set(x)
                for x in zip(*list(self.local_remote_pairs))]

    def _assert_is_primary(self):
        if not self.is_primary() \
            and not mapper.class_mapper(self.parent.class_,
                compile=False).has_property(self.key):
            raise sa_exc.ArgumentError("Attempting to assign a new "
                    "relationship '%s' to a non-primary mapper on "
                    "class '%s'.  New relationships can only be added "
                    "to the primary mapper, i.e. the very first mapper "
                    "created for class '%s' " % (self.key,
                    self.parent.class_.__name__,
                    self.parent.class_.__name__))

    def _generate_backref(self):
        if not self.is_primary():
            return
        if self.backref is not None and not self.back_populates:
            if isinstance(self.backref, basestring):
                backref_key, kwargs = self.backref, {}
            else:
                backref_key, kwargs = self.backref
            mapper = self.mapper.primary_mapper()
            if mapper.has_property(backref_key):
                raise sa_exc.ArgumentError("Error creating backref "
                        "'%s' on relationship '%s': property of that "
                        "name exists on mapper '%s'" % (backref_key,
                        self, mapper))
            if self.secondary is not None:
                pj = kwargs.pop('primaryjoin', self.secondaryjoin)
                sj = kwargs.pop('secondaryjoin', self.primaryjoin)
            else:
                pj = kwargs.pop('primaryjoin', self.primaryjoin)
                sj = kwargs.pop('secondaryjoin', None)
                if sj:
                    raise sa_exc.InvalidRequestError(
                        "Can't assign 'secondaryjoin' on a backref against "
                        "a non-secondary relationship."
                            )
            foreign_keys = kwargs.pop('foreign_keys',
                    self._user_defined_foreign_keys)
            parent = self.parent.primary_mapper()
            kwargs.setdefault('viewonly', self.viewonly)
            kwargs.setdefault('post_update', self.post_update)
            kwargs.setdefault('passive_updates', self.passive_updates)
            self.back_populates = backref_key
            relationship = RelationshipProperty(
                parent,
                self.secondary,
                pj,
                sj,
                foreign_keys=foreign_keys,
                back_populates=self.key,
                **kwargs
                )
            mapper._configure_property(backref_key, relationship)
        if self.back_populates:
            self._add_reverse_property(self.back_populates)

    def _post_init(self):
        self.logger.info('%s setup primary join %s', self,
                         self.primaryjoin)
        self.logger.info('%s setup secondary join %s', self,
                         self.secondaryjoin)
        self.logger.info('%s synchronize pairs [%s]', self,
                         ','.join('(%s => %s)' % (l, r) for (l, r) in
                         self.synchronize_pairs))
        self.logger.info('%s secondary synchronize pairs [%s]', self,
                         ','.join('(%s => %s)' % (l, r) for (l, r) in
                         self.secondary_synchronize_pairs or []))
        self.logger.info('%s local/remote pairs [%s]', self,
                         ','.join('(%s / %s)' % (l, r) for (l, r) in
                         self.local_remote_pairs))
        self.logger.info('%s relationship direction %s', self,
                         self.direction)
        if self.uselist is None:
            self.uselist = self.direction is not MANYTOONE
        if not self.viewonly:
            self._dependency_processor = \
                dependency.DependencyProcessor.from_relationship(self)

    @util.memoized_property
    def _use_get(self):
        """memoize the 'use_get' attribute of this RelationshipLoader's
        lazyloader."""

        strategy = self._get_strategy(strategies.LazyLoader)
        return strategy.use_get

    def _refers_to_parent_table(self):
        pt = self.parent.mapped_table
        mt = self.mapper.mapped_table
        for c, f in self.synchronize_pairs:
            if (
                pt.is_derived_from(c.table) and \
                pt.is_derived_from(f.table) and \
                mt.is_derived_from(c.table) and \
                mt.is_derived_from(f.table)
            ):
                return True
        else:
            return False

    def _is_self_referential(self):
        return self.mapper.common_parent(self.parent)

    def per_property_preprocessors(self, uow):
        if not self.viewonly and self._dependency_processor:
            self._dependency_processor.per_property_preprocessors(uow)

    def _create_joins(self, source_polymorphic=False, 
                            source_selectable=None, dest_polymorphic=False, 
                            dest_selectable=None, of_type=None):
        if source_selectable is None:
            if source_polymorphic and self.parent.with_polymorphic:
                source_selectable = self.parent._with_polymorphic_selectable

        aliased = False
        if dest_selectable is None:
            if dest_polymorphic and self.mapper.with_polymorphic:
                dest_selectable = self.mapper._with_polymorphic_selectable
                aliased = True
            else:
                dest_selectable = self.mapper.mapped_table

            if self._is_self_referential() and source_selectable is None:
                dest_selectable = dest_selectable.alias()
                aliased = True
        else:
            aliased = True

        aliased = aliased or (source_selectable is not None)

        primaryjoin, secondaryjoin, secondary = self.primaryjoin, \
            self.secondaryjoin, self.secondary

        # adjust the join condition for single table inheritance,
        # in the case that the join is to a subclass
        # this is analogous to the "_adjust_for_single_table_inheritance()"
        # method in Query.

        dest_mapper = of_type or self.mapper

        single_crit = dest_mapper._single_table_criterion
        if single_crit is not None:
            if secondaryjoin is not None:
                secondaryjoin = secondaryjoin & single_crit
            else:
                primaryjoin = primaryjoin & single_crit

        if aliased:
            if secondary is not None:
                secondary = secondary.alias()
                primary_aliasizer = ClauseAdapter(secondary)
                if dest_selectable is not None:
                    secondary_aliasizer = \
                        ClauseAdapter(dest_selectable,
                            equivalents=self.mapper._equivalent_columns).\
                            chain(primary_aliasizer)
                else:
                    secondary_aliasizer = primary_aliasizer
                if source_selectable is not None:
                    primary_aliasizer = \
                        ClauseAdapter(secondary).\
                            chain(ClauseAdapter(source_selectable,
                            equivalents=self.parent._equivalent_columns))
                secondaryjoin = \
                    secondary_aliasizer.traverse(secondaryjoin)
            else:
                if dest_selectable is not None:
                    primary_aliasizer = ClauseAdapter(dest_selectable,
                            exclude=self.local_side,
                            equivalents=self.mapper._equivalent_columns)
                    if source_selectable is not None:
                        primary_aliasizer.chain(
                            ClauseAdapter(source_selectable,
                                exclude=self.remote_side,
                                equivalents=self.parent._equivalent_columns))
                elif source_selectable is not None:
                    primary_aliasizer = \
                        ClauseAdapter(source_selectable,
                            exclude=self.remote_side,
                            equivalents=self.parent._equivalent_columns)
                secondary_aliasizer = None
            primaryjoin = primary_aliasizer.traverse(primaryjoin)
            target_adapter = secondary_aliasizer or primary_aliasizer
            target_adapter.include = target_adapter.exclude = None
        else:
            target_adapter = None
        if source_selectable is None:
            source_selectable = self.parent.local_table
        if dest_selectable is None:
            dest_selectable = self.mapper.local_table
        return (
            primaryjoin,
            secondaryjoin,
            source_selectable,
            dest_selectable,
            secondary,
            target_adapter,
            )


PropertyLoader = RelationProperty = RelationshipProperty
log.class_logger(RelationshipProperty)

