root / elixir / trunk / elixir / relationships.py @ 136

Revision 136, 30.2 kB (checked in by ged, 6 years ago)

- made uselist=True a default for has_and_belongs_to_many relationships

instead of being an hardcoded value

Line 
1'''
2Relationship statements for Elixir entities
3
4=============
5Relationships
6=============
7
8This module provides support for defining relationships between your Elixir
9entities.  Elixir supports the following types of relationships: belongs_to_,
10has_one_, has_many_ and has_and_belongs_to_many_.
11
12The first argument to all those statements is the name of the relationship, the
13second is the 'kind' of object you are relating to (it is usually given using
14the ``of_kind`` keyword).
15
16Additionally, if you want a bidirectionnal relationship, you should define the
17inverse relationship on the other entity explicitly (as opposed to how
18SQLAlchemy's backrefs are defined). In non-ambiguous situations, Elixir will
19match relationships together automatically. If there are several relationships
20of the same type between two entities, Elixir is not able to determine which
21relationship is the inverse of which, so you have to disambiguate the
22situation by giving the name of the inverse relationship in the ``inverse``
23keyword argument.
24
25Following these "common" arguments, any number of additional keyword arguments
26can be specified for advanced behavior. The keyword arguments are passed on to
27the SQLAlchemy ``relation`` function. Please refer to the `SQLAlchemy relation
28function's documentation <http://www.sqlalchemy.org/docs/adv_datamapping.myt
29#advdatamapping_properties_relationoptions>`_ for further detail about which
30keyword arguments are supported, but you should keep in mind, the following
31keyword arguments are taken care of by Elixir and should not be used:
32``uselist``, ``remote_side``, ``secondary``, ``primaryjoin`` and
33``secondaryjoin``.
34
35.. _order_by:
36
37Also, as for standard SQLAlchemy relations, the ``order_by`` keyword argument
38can be used to sort the results given by accessing a relation field (this only
39makes sense for has_many and has_and_belongs_to_many relationships). The value
40of that argument is different though: you can either use a string or a list of
41strings, each corresponding to the name of a field in the target entity. These
42field names can optionally be prefixed by a minus (for descending order).
43
44Here is a detailed explanation of each relation type:
45
46`belongs_to`
47------------
48
49Describes the child's side of a parent-child relationship.  For example,
50a `Pet` object may belong to its owner, who is a `Person`.  This could be
51expressed like so:
52
53::
54
55    class Pet(Entity):
56        belongs_to('owner', of_kind='Person')
57
58Behind the scene, assuming the primary key of the `Person` entity is
59an integer column named `id`, the ``belongs_to`` relationship will
60automatically add an integer column named `owner_id` to the entity, with a
61foreign key referencing the `id` column of the `Person` entity.
62
63In addition to the keyword arguments inherited from SQLAlchemy's relation
64function, ``belongs_to`` relationships accept the following optional arguments
65which will be directed to the created column:
66
67+----------------------+------------------------------------------------------+
68| Option Name          | Description                                          |
69+======================+======================================================+
70| ``colname``          | Specify a custom column name.                        |
71+----------------------+------------------------------------------------------+
72| ``required``         | Specify whether or not this field can be set to None |
73|                      | (left without a value). Defaults to ``False``,       |
74|                      | unless the field is a primary key.                   |
75+----------------------+------------------------------------------------------+
76| ``column_kwargs``    | A dictionary holding any other keyword argument you  |
77|                      | might want to pass to the Column.                    |
78+----------------------+------------------------------------------------------+
79
80The following optional arguments are also supported to customize the
81ForeignKeyConstraint that is created:
82
83+----------------------+------------------------------------------------------+
84| Option Name          | Description                                          |
85+======================+======================================================+
86| ``use_alter``        | If True, SQLAlchemy will add the constraint in a     |
87|                      | second SQL statement (as opposed to within the       |
88|                      | create table statement). This permits to define      |
89|                      | tables with a circular foreign key dependency        |
90|                      | between them.                                        |
91+----------------------+------------------------------------------------------+
92| ``ondelete``         | Value for the foreign key constraint ondelete clause.|
93|                      | May be one of: ``cascade``, ``restrict``,            |
94|                      | ``set null``, or ``set default``.                    |
95+----------------------+------------------------------------------------------+
96| ``constraint_kwargs``| A dictionary holding any other keyword argument you  |
97|                      | might want to pass to the Constraint.                |
98+----------------------+------------------------------------------------------+
99
100`has_one`
101---------
102
103Describes the parent's side of a parent-child relationship when there is only
104one child.  For example, a `Car` object has one gear stick, which is
105represented as a `GearStick` object. This could be expressed like so:
106
107::
108
109    class Car(Entity):
110        has_one('gear_stick', of_kind='GearStick', inverse='car')
111
112    class GearStick(Entity):
113        belongs_to('car', of_kind='Car')
114
115Note that an ``has_one`` relationship **cannot exist** without a corresponding
116``belongs_to`` relationship in the other way. This is because the ``has_one``
117relationship needs the foreign_key created by the ``belongs_to`` relationship.
118
119`has_many`
120----------
121
122Describes the parent's side of a parent-child relationship when there can be
123several children.  For example, a `Person` object has many children, each of
124them being a `Person`. This could be expressed like so:
125
126::
127
128    class Person(Entity):
129        belongs_to('parent', of_kind='Person')
130        has_many('children', of_kind='Person')
131
132Note that an ``has_many`` relationship **cannot exist** without a
133corresponding ``belongs_to`` relationship in the other way. This is because the
134``has_many`` relationship needs the foreign key created by the ``belongs_to``
135relationship.
136
137`has_and_belongs_to_many`
138-------------------------
139
140Describes a relationship in which one kind of entity can be related to several
141objects of the other kind but the objects of that other kind can be related to
142several objects of the first kind.  For example, an `Article` can have several
143tags, but the same `Tag` can be used on several articles.
144
145::
146
147    class Article(Entity):
148        has_and_belongs_to_many('tags', of_kind='Tag')
149
150    class Tag(Entity):
151        has_and_belongs_to_many('articles', of_kind='Article')
152
153Behind the scene, the ``has_and_belongs_to_many`` relationship will
154automatically create an intermediate table to host its data.
155
156Note that you don't necessarily need to define the inverse relationship.  In
157our example, even though we want tags to be usable on several articles, we
158might not be interested in which articles correspond to a particular tag.  In
159that case, we could have omitted the `Tag` side of the relationship.
160
161If the entity containg your ``has_and_belongs_to_many`` relationship is
162autoloaded, you **must** specify at least one of either the ``remote_side`` or
163``local_side`` argument.
164
165In addition to the order_by_ keyword argument, and the other keyword arguments
166inherited from SQLAlchemy, ``has_and_belongs_to_many`` relationships accept
167the following optional (keyword) arguments:
168
169+--------------------+--------------------------------------------------------+
170| Option Name        | Description                                            |
171+====================+========================================================+
172| ``tablename``      | Specify a custom name for the intermediary table. This |
173|                    | can be used both when the tables needs to be created   |
174|                    | and when the table is autoloaded/reflected from the    |
175|                    | database.                                              |
176+--------------------+--------------------------------------------------------+
177| ``remote_side``    | A column name or list of column names specifying       |
178|                    | which column(s) in the intermediary table are used     |
179|                    | for the "remote" part of a self-referential            |
180|                    | relationship. This argument has an effect only when    |
181|                    | your entities are autoloaded.                          |
182+--------------------+--------------------------------------------------------+
183| ``local_side``     | A column name or list of column names specifying       |
184|                    | which column(s) in the intermediary table are used     |
185|                    | for the "local" part of a self-referential             |
186|                    | relationship. This argument has an effect only when    |
187|                    | your entities are autoloaded.                          |
188+--------------------+--------------------------------------------------------+
189
190'''
191
192from sqlalchemy         import relation, ForeignKeyConstraint, Column, \
193                               Table, and_
194from elixir.statements  import Statement
195from elixir.fields      import Field
196from elixir.entity      import EntityDescriptor
197
198import sys
199
200__pudge_all__ = []
201
202class Relationship(object):
203    '''
204    Base class for relationships.
205    '''
206   
207    def __init__(self, entity, name, *args, **kwargs):
208        self.entity = entity
209        self.name = name
210        self.of_kind = kwargs.pop('of_kind')
211        self.inverse_name = kwargs.pop('inverse', None)
212       
213        self._target = None
214        self._inverse = None
215       
216        self.property = None # sqlalchemy property
217       
218        #TODO: unused for now
219        self.args = args
220        self.kwargs = kwargs
221       
222        self.entity._descriptor.relationships[self.name] = self
223   
224    def create_keys(self):
225        '''
226        Subclasses (ie. concrete relationships) may override this method to
227        create foreign keys.
228        '''
229   
230    def create_tables(self):
231        '''
232        Subclasses (ie. concrete relationships) may override this method to
233        create secondary tables.
234        '''
235   
236    def create_properties(self):
237        '''
238        Subclasses (ie. concrete relationships) may override this method to add
239        properties to the involved entities.
240        '''
241   
242    def setup(self):
243        '''
244        Sets up the relationship, creates foreign keys and secondary tables.
245        '''
246
247        if not self.target:
248            return False
249
250        if self.property:
251            return True
252
253        self.create_keys()
254        self.create_tables()
255        self.create_properties()
256       
257        return True
258   
259    def target(self):
260        if not self._target:
261            path = self.of_kind.rsplit('.', 1)
262            classname = path.pop()
263
264            if path:
265                # do we have a fully qualified entity name?
266                module = sys.modules[path.pop()]
267            else: 
268                # if not, try the same module as the source
269                module = self.entity._descriptor.module
270
271            self._target = getattr(module, classname, None)
272            if not self._target:
273                # This is ugly but we need it because the class which is
274                # currently being defined (we have to keep in mind we are in
275                # its metaclass code) is not yet available in the module
276                # namespace, so the getattr above fails. And unfortunately,
277                # this doesn't only happen for the owning entity of this
278                # relation since we might be setting up a deferred relation.
279                e = EntityDescriptor.current.entity
280                if classname == e.__name__ or \
281                        self.of_kind == e.__module__ +'.'+ e.__name__:
282                    self._target = e
283                else:
284                    return None
285       
286        return self._target
287    target = property(target)
288   
289    def inverse(self):
290        if not self._inverse:
291            if self.inverse_name:
292                desc = self.target._descriptor
293                # we use all_relationships so that relationships from parent
294                # entities are included too
295                inverse = desc.all_relationships.get(self.inverse_name, None)
296                if inverse is None:
297                    raise Exception(
298                              "Couldn't find a relationship named '%s' in "
299                              "entity '%s' or its parent entities." 
300                              % (self.inverse_name, self.target.__name__))
301                assert self.match_type_of(inverse)
302            else:
303                inverse = self.target._descriptor.get_inverse_relation(self)
304
305            if inverse:
306                self._inverse = inverse
307                inverse._inverse = self
308       
309        return self._inverse
310    inverse = property(inverse)
311   
312    def match_type_of(self, other):
313        t1, t2 = type(self), type(other)
314   
315        if t1 is HasAndBelongsToMany:
316            return t1 is t2
317        elif t1 in (HasOne, HasMany):
318            return t2 is BelongsTo
319        elif t1 is BelongsTo:
320            return t2 in (HasMany, HasOne)
321        else:
322            return False
323
324    def is_inverse(self, other):
325        return other is not self and \
326               self.match_type_of(other) and \
327               self.entity == other.target and \
328               other.entity == self.target and \
329               (self.inverse_name == other.name or not self.inverse_name) and \
330               (other.inverse_name == self.name or not other.inverse_name)
331
332
333class BelongsTo(Relationship):
334    '''
335   
336    '''
337   
338    def __init__(self, entity, name, *args, **kwargs):
339        self.colname = kwargs.pop('colname', [])
340        if self.colname and not isinstance(self.colname, list):
341            self.colname = [self.colname]
342
343        self.column_kwargs = kwargs.pop('column_kwargs', {})
344        if 'required' in kwargs:
345            self.column_kwargs['nullable'] = not kwargs.pop('required')
346
347        self.constraint_kwargs = kwargs.pop('constraint_kwargs', {})
348        if 'use_alter' in kwargs:
349            self.constraint_kwargs['use_alter'] = kwargs.pop('use_alter')
350       
351        if 'ondelete' in kwargs:
352            self.constraint_kwargs['ondelete'] = kwargs.pop('ondelete')
353       
354        self.foreign_key = list()
355        self.primaryjoin_clauses = list()
356        super(BelongsTo, self).__init__(entity, name, *args, **kwargs)
357   
358    def create_keys(self):
359        '''
360        Find all primary keys on the target and create foreign keys on the
361        source accordingly.
362        '''
363
364        source_desc = self.entity._descriptor
365        target_desc = self.target._descriptor
366
367        if source_desc.autoload:
368            #TODO: test if this works when colname is a list
369            if self.colname:
370                self.primaryjoin_clauses = \
371                    _build_join_clauses(self.entity.table, 
372                                        self.colname, None, 
373                                        self.target.table)[0]
374                if not self.primaryjoin_clauses:
375                    raise Exception(
376                        "Couldn't find a foreign key constraint in table "
377                        "'%s' using the following columns: %s."
378                        % (self.entity.table.name, ', '.join(self.colname)))
379        else:
380            fk_refcols = list()
381            fk_colnames = list()
382
383            if self.colname and \
384               len(self.colname) != len(target_desc.primary_keys):
385                raise Exception(
386                        "The number of column names provided in the colname "
387                        "keyword argument of the '%s' relationship of the "
388                        "'%s' entity is not the same as the number of columns "
389                        "of the primary key of '%s'."
390                        % (self.name, self.entity.__name__, 
391                           self.target.__name__))
392
393            for key_num, key in enumerate(target_desc.primary_keys):
394                pk_col = key.column
395
396                if self.colname:
397                    colname = self.colname[key_num]
398                else:
399                    colname = '%s_%s' % (self.name, pk_col.name)
400
401                # we use a Field here instead of using a Column directly
402                # because of add_field
403                field = Field(pk_col.type, colname=colname, index=True, 
404                              **self.column_kwargs)
405                source_desc.add_field(field)
406
407                # build the list of local columns which will be part of
408                # the foreign key
409                self.foreign_key.append(field.column)
410
411                # store the names of those columns
412                fk_colnames.append(colname)
413
414                # build the list of columns the foreign key will point to
415                if target_desc.entity.table.schema:
416                    fk_refcols.append("%s.%s.%s" % (
417                        target_desc.entity.table.schema,
418                        target_desc.entity.table.name,
419                        pk_col.name))
420                else:
421                    fk_refcols.append("%s.%s" % (target_desc.entity.table.name,
422                                                 pk_col.name))
423
424                # build up the primary join. This is needed when you have
425                # several belongs_to relations between two objects
426                self.primaryjoin_clauses.append(field.column == pk_col)
427           
428            # In some databases (at lease MySQL) the constraint name needs to
429            # be unique for the whole database, instead of per table.
430            fk_name = "%s_%s_fk" % (self.entity.table.name, 
431                                    '_'.join(fk_colnames))
432            source_desc.add_constraint(ForeignKeyConstraint(
433                                            fk_colnames, fk_refcols,
434                                            name=fk_name,
435                                            **self.constraint_kwargs))
436   
437    def create_properties(self):
438        kwargs = self.kwargs
439       
440        if self.entity.table is self.target.table:
441            if self.entity._descriptor.autoload:
442                cols = [col for col in self.target.table.primary_key.columns]
443            else:
444                cols = [k.column for k in self.target._descriptor.primary_keys]
445            kwargs['remote_side'] = cols
446
447        if self.primaryjoin_clauses:
448            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
449        kwargs['uselist'] = False
450       
451        self.property = relation(self.target, **kwargs)
452        self.entity.mapper.add_property(self.name, self.property)
453
454
455class HasOne(Relationship):
456    uselist = False
457
458    def create_keys(self):
459        # make sure an inverse relationship exists
460        if self.inverse is None:
461            raise Exception(
462                      "Couldn't find any relationship in '%s' which "
463                      "match as inverse of the '%s' relationship "
464                      "defined in the '%s' entity. If you are using "
465                      "inheritance you "
466                      "might need to specify inverse relationships "
467                      "manually by using the inverse keyword."
468                      % (self.target.__name__, self.name,
469                         self.entity.__name__))
470        # make sure it is set up because it creates the foreign key we'll need
471        self.inverse.setup()
472   
473    def create_properties(self):
474        kwargs = self.kwargs
475       
476        #TODO: for now, we don't break any test if we remove those 2 lines.
477        # So, we should either complete the selfref test to prove that they
478        # are indeed useful, or remove them. It might be they are indeed
479        # useless because of the primaryjoin, and that the remote_side is
480        # already setup in the other way (belongs_to).
481        if self.entity.table is self.target.table:
482            #FIXME: IF this code is of any use, it will probably break for
483            # autoloaded tables
484            kwargs['remote_side'] = self.inverse.foreign_key
485       
486        if self.inverse.primaryjoin_clauses:
487            kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
488
489        kwargs['uselist'] = self.uselist
490       
491        self.property = relation(self.target, **kwargs)
492        self.entity.mapper.add_property(self.name, self.property)
493
494
495class HasMany(HasOne):
496    uselist = True
497
498    def create_properties(self):
499        if 'order_by' in self.kwargs:
500            self.kwargs['order_by'] = \
501                self.target._descriptor.translate_order_by(
502                    self.kwargs['order_by'])
503
504        super(HasMany, self).create_properties()
505
506
507class HasAndBelongsToMany(Relationship):
508    uselist = True
509
510    def __init__(self, entity, name, *args, **kwargs):
511        self.user_tablename = kwargs.pop('tablename', None)
512        self.local_side = kwargs.pop('local_side', [])
513        if self.local_side and not isinstance(self.local_side, list):
514            self.local_side = [self.local_side]
515        self.remote_side = kwargs.pop('remote_side', [])
516        if self.remote_side and not isinstance(self.remote_side, list):
517            self.remote_side = [self.remote_side]
518        self.secondary_table = None
519        self.primaryjoin_clauses = list()
520        self.secondaryjoin_clauses = list()
521        super(HasAndBelongsToMany, self).__init__(entity, name, 
522                                                  *args, **kwargs)
523
524    def create_tables(self):
525        if self.inverse:
526            if self.inverse.secondary_table:
527                self.secondary_table = self.inverse.secondary_table
528                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
529                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
530
531        if not self.secondary_table:
532            e1_desc = self.entity._descriptor
533            e2_desc = self.target._descriptor
534           
535            # First, we compute the name of the table. Note that some of the
536            # intermediary variables are reused later for the constraint
537            # names.
538           
539            # We use the name of the relation for the first entity
540            # (instead of the name of its primary key), so that we can
541            # have two many-to-many relations between the same objects
542            # without having a table name collision.
543            source_part = "%s_%s" % (e1_desc.tablename, self.name)
544
545            # And we use only the name of the table of the second entity
546            # when there is no inverse, so that a many-to-many relation
547            # can be defined without an inverse.
548            if self.inverse:
549                target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name)
550            else:
551                target_part = e2_desc.tablename
552           
553            if self.user_tablename:
554                tablename = self.user_tablename
555            else:
556                # We need to keep the table name consistent (independant of
557                # whether this relation or its inverse is setup first).
558                if self.inverse and e1_desc.tablename < e2_desc.tablename:
559                    tablename = "%s__%s" % (target_part, source_part)
560                else:
561                    tablename = "%s__%s" % (source_part, target_part)
562
563            if e1_desc.autoload:
564                self._reflect_table(tablename)
565            else:
566                # We pre-compute the names of the foreign key constraints
567                # pointing to the source (local) entity's table and to the
568                # target's table
569
570                # In some databases (at lease MySQL) the constraint names need
571                # to be unique for the whole database, instead of per table.
572                source_fk_name = "%s_fk" % source_part
573                if self.inverse:
574                    target_fk_name = "%s_fk" % target_part
575                else:
576                    target_fk_name = "%s_inverse_fk" % source_part
577
578                columns = list()
579                constraints = list()
580
581                joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses)
582                for num, desc, fk_name in ((0, e1_desc, source_fk_name), 
583                                           (1, e2_desc, target_fk_name)):
584                    fk_colnames = list()
585                    fk_refcols = list()
586               
587                    for key in desc.primary_keys:
588                        pk_col = key.column
589                       
590                        colname = '%s_%s' % (desc.tablename, pk_col.name)
591
592                        # In case we have a many-to-many self-reference, we
593                        # need to tweak the names of the columns so that we
594                        # don't end up with twice the same column name.
595                        if self.entity is self.target:
596                            colname += str(num + 1)
597
598                        col = Column(colname, pk_col.type)
599                        columns.append(col)
600
601                        # Build the list of local columns which will be part
602                        # of the foreign key.
603                        fk_colnames.append(colname)
604
605                        # Build the list of columns the foreign key will point
606                        # to.
607                        fk_refcols.append(desc.tablename + '.' + pk_col.name)
608
609                        # Build join clauses (in case we have a self-ref)
610                        if self.entity is self.target:
611                            joins[num].append(col == pk_col)
612                   
613                    constraints.append(
614                        ForeignKeyConstraint(fk_colnames, fk_refcols,
615                                             name=fk_name))
616
617                args = columns + constraints
618               
619                self.secondary_table = Table(tablename, e1_desc.metadata, 
620                                             *args)
621
622    def _reflect_table(self, tablename):
623        if not self.target._descriptor.autoload:
624            raise Exception(
625                "Entity '%s' is autoloaded and its '%s' "
626                "has_and_belongs_to_many relationship points to "
627                "the '%s' entity which is not autoloaded"
628                % (self.entity.__name__, self.name,
629                   self.target.__name__))
630               
631        self.secondary_table = Table(tablename, 
632                                     self.entity._descriptor.metadata,
633                                     autoload=True)
634
635        # In the case we have a self-reference, we need to build join clauses
636        if self.entity is self.target:
637            #CHECKME: maybe we should try even harder by checking if that
638            # information was defined on the inverse relationship)
639            if not self.local_side and not self.remote_side:
640                raise Exception(
641                    "Self-referential has_and_belongs_to_many "
642                    "relationships in autoloaded entities need to have at "
643                    "least one of either 'local_side' or 'remote_side' "
644                    "argument specified. The '%s' relationship in the '%s' "
645                    "entity doesn't have either."
646                    % (self.name, self.entity.__name__))
647
648            self.primaryjoin_clauses, self.secondaryjoin_clauses = \
649                _build_join_clauses(self.secondary_table, 
650                                    self.local_side, self.remote_side, 
651                                    self.entity.table)
652
653    def create_properties(self):
654        kwargs = self.kwargs
655
656        if self.target is self.entity:
657            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
658            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
659
660        if 'order_by' in kwargs:
661            kwargs['order_by'] = \
662                self.target._descriptor.translate_order_by(kwargs['order_by'])
663
664        self.property = relation(self.target, secondary=self.secondary_table,
665                                 uselist=self.uselist, **kwargs)
666        self.entity.mapper.add_property(self.name, self.property)
667
668    def is_inverse(self, other):
669        return super(HasAndBelongsToMany, self).is_inverse(other) and \
670               (self.user_tablename == other.user_tablename or 
671                (not self.user_tablename and not other.user_tablename))
672
673
674def _build_join_clauses(local_table, local_cols1, local_cols2, target_table):
675    primary_join, secondary_join = [], []
676    cols1 = local_cols1[:]
677    cols1.sort()
678    cols1 = tuple(cols1)
679
680    if local_cols2 is not None:
681        cols2 = local_cols2[:]
682        cols2.sort()
683        cols2 = tuple(cols2)
684    else:
685        cols2 = None
686    constraint_map = {}
687    for constraint in local_table.constraints:
688        if isinstance(constraint, ForeignKeyConstraint):
689            use_constraint = False
690            fk_colnames = []
691            for fk in constraint.elements:
692                fk_colnames.append(fk.parent.name)
693                if fk.references(target_table):
694                    use_constraint = True
695            if use_constraint:
696                fk_colnames.sort()
697                constraint_map[tuple(fk_colnames)] = constraint
698
699    # Either the fk column names match explicitely with the columns given for
700    # one of the joins (primary or secondary), or we assume the current
701    # columns match because the columns for this join were not given and we
702    # know the other join is either not used (is None) or has an explicit
703    # match.
704    for cols, constraint in constraint_map.iteritems():
705        if cols == cols1 or (cols != cols2 and 
706                             not cols1 and (cols2 in constraint_map or
707                                            cols2 is None)):
708            join = primary_join
709        elif cols == cols2 or (cols2 == () and cols1 in constraint_map):
710            join = secondary_join
711        else:
712            continue
713        for fk in constraint.elements:
714            join.append(fk.parent == fk.column)
715    return primary_join, secondary_join
716
717
718belongs_to = Statement(BelongsTo)
719has_one = Statement(HasOne)
720has_many = Statement(HasMany)
721has_and_belongs_to_many = Statement(HasAndBelongsToMany)
Note: See TracBrowser for help on using the browser.