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

Revision 139, 30.2 kB (checked in by ged, 7 years ago)

- Applied patch from Ants Aasma to make Elixir compatible with the 0.4 branch

of SQLAlchemy.

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 ForeignKeyConstraint, Column, \
193                               Table, and_
194from sqlalchemy.orm     import relation
195from elixir.statements  import Statement
196from elixir.fields      import Field
197from elixir.entity      import EntityDescriptor
198
199import sys
200
201__pudge_all__ = []
202
203class Relationship(object):
204    '''
205    Base class for relationships.
206    '''
207   
208    def __init__(self, entity, name, *args, **kwargs):
209        self.entity = entity
210        self.name = name
211        self.of_kind = kwargs.pop('of_kind')
212        self.inverse_name = kwargs.pop('inverse', None)
213       
214        self._target = None
215        self._inverse = None
216       
217        self.property = None # sqlalchemy property
218       
219        #TODO: unused for now
220        self.args = args
221        self.kwargs = kwargs
222       
223        self.entity._descriptor.relationships[self.name] = self
224   
225    def create_keys(self):
226        '''
227        Subclasses (ie. concrete relationships) may override this method to
228        create foreign keys.
229        '''
230   
231    def create_tables(self):
232        '''
233        Subclasses (ie. concrete relationships) may override this method to
234        create secondary tables.
235        '''
236   
237    def create_properties(self):
238        '''
239        Subclasses (ie. concrete relationships) may override this method to add
240        properties to the involved entities.
241        '''
242   
243    def setup(self):
244        '''
245        Sets up the relationship, creates foreign keys and secondary tables.
246        '''
247
248        if not self.target:
249            return False
250
251        if self.property:
252            return True
253
254        self.create_keys()
255        self.create_tables()
256        self.create_properties()
257       
258        return True
259   
260    def target(self):
261        if not self._target:
262            path = self.of_kind.rsplit('.', 1)
263            classname = path.pop()
264
265            if path:
266                # do we have a fully qualified entity name?
267                module = sys.modules[path.pop()]
268            else: 
269                # if not, try the same module as the source
270                module = self.entity._descriptor.module
271
272            self._target = getattr(module, classname, None)
273            if not self._target:
274                # This is ugly but we need it because the class which is
275                # currently being defined (we have to keep in mind we are in
276                # its metaclass code) is not yet available in the module
277                # namespace, so the getattr above fails. And unfortunately,
278                # this doesn't only happen for the owning entity of this
279                # relation since we might be setting up a deferred relation.
280                e = EntityDescriptor.current.entity
281                if classname == e.__name__ or \
282                        self.of_kind == e.__module__ +'.'+ e.__name__:
283                    self._target = e
284                else:
285                    return None
286       
287        return self._target
288    target = property(target)
289   
290    def inverse(self):
291        if not self._inverse:
292            if self.inverse_name:
293                desc = self.target._descriptor
294                # we use all_relationships so that relationships from parent
295                # entities are included too
296                inverse = desc.all_relationships.get(self.inverse_name, None)
297                if inverse is None:
298                    raise Exception(
299                              "Couldn't find a relationship named '%s' in "
300                              "entity '%s' or its parent entities." 
301                              % (self.inverse_name, self.target.__name__))
302                assert self.match_type_of(inverse)
303            else:
304                inverse = self.target._descriptor.get_inverse_relation(self)
305
306            if inverse:
307                self._inverse = inverse
308                inverse._inverse = self
309       
310        return self._inverse
311    inverse = property(inverse)
312   
313    def match_type_of(self, other):
314        t1, t2 = type(self), type(other)
315   
316        if t1 is HasAndBelongsToMany:
317            return t1 is t2
318        elif t1 in (HasOne, HasMany):
319            return t2 is BelongsTo
320        elif t1 is BelongsTo:
321            return t2 in (HasMany, HasOne)
322        else:
323            return False
324
325    def is_inverse(self, other):
326        return other is not self and \
327               self.match_type_of(other) and \
328               self.entity == other.target and \
329               other.entity == self.target and \
330               (self.inverse_name == other.name or not self.inverse_name) and \
331               (other.inverse_name == self.name or not other.inverse_name)
332
333
334class BelongsTo(Relationship):
335    '''
336   
337    '''
338   
339    def __init__(self, entity, name, *args, **kwargs):
340        self.colname = kwargs.pop('colname', [])
341        if self.colname and not isinstance(self.colname, list):
342            self.colname = [self.colname]
343
344        self.column_kwargs = kwargs.pop('column_kwargs', {})
345        if 'required' in kwargs:
346            self.column_kwargs['nullable'] = not kwargs.pop('required')
347
348        self.constraint_kwargs = kwargs.pop('constraint_kwargs', {})
349        if 'use_alter' in kwargs:
350            self.constraint_kwargs['use_alter'] = kwargs.pop('use_alter')
351       
352        if 'ondelete' in kwargs:
353            self.constraint_kwargs['ondelete'] = kwargs.pop('ondelete')
354       
355        self.foreign_key = list()
356        self.primaryjoin_clauses = list()
357        super(BelongsTo, self).__init__(entity, name, *args, **kwargs)
358   
359    def create_keys(self):
360        '''
361        Find all primary keys on the target and create foreign keys on the
362        source accordingly.
363        '''
364
365        source_desc = self.entity._descriptor
366        target_desc = self.target._descriptor
367
368        if source_desc.autoload:
369            #TODO: test if this works when colname is a list
370            if self.colname:
371                self.primaryjoin_clauses = \
372                    _build_join_clauses(self.entity.table, 
373                                        self.colname, None, 
374                                        self.target.table)[0]
375                if not self.primaryjoin_clauses:
376                    raise Exception(
377                        "Couldn't find a foreign key constraint in table "
378                        "'%s' using the following columns: %s."
379                        % (self.entity.table.name, ', '.join(self.colname)))
380        else:
381            fk_refcols = list()
382            fk_colnames = list()
383
384            if self.colname and \
385               len(self.colname) != len(target_desc.primary_keys):
386                raise Exception(
387                        "The number of column names provided in the colname "
388                        "keyword argument of the '%s' relationship of the "
389                        "'%s' entity is not the same as the number of columns "
390                        "of the primary key of '%s'."
391                        % (self.name, self.entity.__name__, 
392                           self.target.__name__))
393
394            for key_num, key in enumerate(target_desc.primary_keys):
395                pk_col = key.column
396
397                if self.colname:
398                    colname = self.colname[key_num]
399                else:
400                    colname = '%s_%s' % (self.name, pk_col.name)
401
402                # we use a Field here instead of using a Column directly
403                # because of add_field
404                field = Field(pk_col.type, colname=colname, index=True, 
405                              **self.column_kwargs)
406                source_desc.add_field(field)
407
408                # build the list of local columns which will be part of
409                # the foreign key
410                self.foreign_key.append(field.column)
411
412                # store the names of those columns
413                fk_colnames.append(colname)
414
415                # build the list of columns the foreign key will point to
416                if target_desc.entity.table.schema:
417                    fk_refcols.append("%s.%s.%s" % (
418                        target_desc.entity.table.schema,
419                        target_desc.entity.table.name,
420                        pk_col.name))
421                else:
422                    fk_refcols.append("%s.%s" % (target_desc.entity.table.name,
423                                                 pk_col.name))
424
425                # build up the primary join. This is needed when you have
426                # several belongs_to relations between two objects
427                self.primaryjoin_clauses.append(field.column == pk_col)
428           
429            # In some databases (at lease MySQL) the constraint name needs to
430            # be unique for the whole database, instead of per table.
431            fk_name = "%s_%s_fk" % (self.entity.table.name, 
432                                    '_'.join(fk_colnames))
433            source_desc.add_constraint(ForeignKeyConstraint(
434                                            fk_colnames, fk_refcols,
435                                            name=fk_name,
436                                            **self.constraint_kwargs))
437   
438    def create_properties(self):
439        kwargs = self.kwargs
440       
441        if self.entity.table is self.target.table:
442            if self.entity._descriptor.autoload:
443                cols = [col for col in self.target.table.primary_key.columns]
444            else:
445                cols = [k.column for k in self.target._descriptor.primary_keys]
446            kwargs['remote_side'] = cols
447
448        if self.primaryjoin_clauses:
449            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
450        kwargs['uselist'] = False
451       
452        self.property = relation(self.target, **kwargs)
453        self.entity.mapper.add_property(self.name, self.property)
454
455
456class HasOne(Relationship):
457    uselist = False
458
459    def create_keys(self):
460        # make sure an inverse relationship exists
461        if self.inverse is None:
462            raise Exception(
463                      "Couldn't find any relationship in '%s' which "
464                      "match as inverse of the '%s' relationship "
465                      "defined in the '%s' entity. If you are using "
466                      "inheritance you "
467                      "might need to specify inverse relationships "
468                      "manually by using the inverse keyword."
469                      % (self.target.__name__, self.name,
470                         self.entity.__name__))
471        # make sure it is set up because it creates the foreign key we'll need
472        self.inverse.setup()
473   
474    def create_properties(self):
475        kwargs = self.kwargs
476       
477        #TODO: for now, we don't break any test if we remove those 2 lines.
478        # So, we should either complete the selfref test to prove that they
479        # are indeed useful, or remove them. It might be they are indeed
480        # useless because of the primaryjoin, and that the remote_side is
481        # already setup in the other way (belongs_to).
482        if self.entity.table is self.target.table:
483            #FIXME: IF this code is of any use, it will probably break for
484            # autoloaded tables
485            kwargs['remote_side'] = self.inverse.foreign_key
486       
487        if self.inverse.primaryjoin_clauses:
488            kwargs['primaryjoin'] = and_(*self.inverse.primaryjoin_clauses)
489
490        kwargs['uselist'] = self.uselist
491       
492        self.property = relation(self.target, **kwargs)
493        self.entity.mapper.add_property(self.name, self.property)
494
495
496class HasMany(HasOne):
497    uselist = True
498
499    def create_properties(self):
500        if 'order_by' in self.kwargs:
501            self.kwargs['order_by'] = \
502                self.target._descriptor.translate_order_by(
503                    self.kwargs['order_by'])
504
505        super(HasMany, self).create_properties()
506
507
508class HasAndBelongsToMany(Relationship):
509    uselist = True
510
511    def __init__(self, entity, name, *args, **kwargs):
512        self.user_tablename = kwargs.pop('tablename', None)
513        self.local_side = kwargs.pop('local_side', [])
514        if self.local_side and not isinstance(self.local_side, list):
515            self.local_side = [self.local_side]
516        self.remote_side = kwargs.pop('remote_side', [])
517        if self.remote_side and not isinstance(self.remote_side, list):
518            self.remote_side = [self.remote_side]
519        self.secondary_table = None
520        self.primaryjoin_clauses = list()
521        self.secondaryjoin_clauses = list()
522        super(HasAndBelongsToMany, self).__init__(entity, name, 
523                                                  *args, **kwargs)
524
525    def create_tables(self):
526        if self.inverse:
527            if self.inverse.secondary_table:
528                self.secondary_table = self.inverse.secondary_table
529                self.primaryjoin_clauses = self.inverse.secondaryjoin_clauses
530                self.secondaryjoin_clauses = self.inverse.primaryjoin_clauses
531
532        if not self.secondary_table:
533            e1_desc = self.entity._descriptor
534            e2_desc = self.target._descriptor
535           
536            # First, we compute the name of the table. Note that some of the
537            # intermediary variables are reused later for the constraint
538            # names.
539           
540            # We use the name of the relation for the first entity
541            # (instead of the name of its primary key), so that we can
542            # have two many-to-many relations between the same objects
543            # without having a table name collision.
544            source_part = "%s_%s" % (e1_desc.tablename, self.name)
545
546            # And we use only the name of the table of the second entity
547            # when there is no inverse, so that a many-to-many relation
548            # can be defined without an inverse.
549            if self.inverse:
550                target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name)
551            else:
552                target_part = e2_desc.tablename
553           
554            if self.user_tablename:
555                tablename = self.user_tablename
556            else:
557                # We need to keep the table name consistent (independant of
558                # whether this relation or its inverse is setup first).
559                if self.inverse and e1_desc.tablename < e2_desc.tablename:
560                    tablename = "%s__%s" % (target_part, source_part)
561                else:
562                    tablename = "%s__%s" % (source_part, target_part)
563
564            if e1_desc.autoload:
565                self._reflect_table(tablename)
566            else:
567                # We pre-compute the names of the foreign key constraints
568                # pointing to the source (local) entity's table and to the
569                # target's table
570
571                # In some databases (at lease MySQL) the constraint names need
572                # to be unique for the whole database, instead of per table.
573                source_fk_name = "%s_fk" % source_part
574                if self.inverse:
575                    target_fk_name = "%s_fk" % target_part
576                else:
577                    target_fk_name = "%s_inverse_fk" % source_part
578
579                columns = list()
580                constraints = list()
581
582                joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses)
583                for num, desc, fk_name in ((0, e1_desc, source_fk_name), 
584                                           (1, e2_desc, target_fk_name)):
585                    fk_colnames = list()
586                    fk_refcols = list()
587               
588                    for key in desc.primary_keys:
589                        pk_col = key.column
590                       
591                        colname = '%s_%s' % (desc.tablename, pk_col.name)
592
593                        # In case we have a many-to-many self-reference, we
594                        # need to tweak the names of the columns so that we
595                        # don't end up with twice the same column name.
596                        if self.entity is self.target:
597                            colname += str(num + 1)
598
599                        col = Column(colname, pk_col.type)
600                        columns.append(col)
601
602                        # Build the list of local columns which will be part
603                        # of the foreign key.
604                        fk_colnames.append(colname)
605
606                        # Build the list of columns the foreign key will point
607                        # to.
608                        fk_refcols.append(desc.tablename + '.' + pk_col.name)
609
610                        # Build join clauses (in case we have a self-ref)
611                        if self.entity is self.target:
612                            joins[num].append(col == pk_col)
613                   
614                    constraints.append(
615                        ForeignKeyConstraint(fk_colnames, fk_refcols,
616                                             name=fk_name))
617
618                args = columns + constraints
619               
620                self.secondary_table = Table(tablename, e1_desc.metadata, 
621                                             *args)
622
623    def _reflect_table(self, tablename):
624        if not self.target._descriptor.autoload:
625            raise Exception(
626                "Entity '%s' is autoloaded and its '%s' "
627                "has_and_belongs_to_many relationship points to "
628                "the '%s' entity which is not autoloaded"
629                % (self.entity.__name__, self.name,
630                   self.target.__name__))
631               
632        self.secondary_table = Table(tablename, 
633                                     self.entity._descriptor.metadata,
634                                     autoload=True)
635
636        # In the case we have a self-reference, we need to build join clauses
637        if self.entity is self.target:
638            #CHECKME: maybe we should try even harder by checking if that
639            # information was defined on the inverse relationship)
640            if not self.local_side and not self.remote_side:
641                raise Exception(
642                    "Self-referential has_and_belongs_to_many "
643                    "relationships in autoloaded entities need to have at "
644                    "least one of either 'local_side' or 'remote_side' "
645                    "argument specified. The '%s' relationship in the '%s' "
646                    "entity doesn't have either."
647                    % (self.name, self.entity.__name__))
648
649            self.primaryjoin_clauses, self.secondaryjoin_clauses = \
650                _build_join_clauses(self.secondary_table, 
651                                    self.local_side, self.remote_side, 
652                                    self.entity.table)
653
654    def create_properties(self):
655        kwargs = self.kwargs
656
657        if self.target is self.entity:
658            kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
659            kwargs['secondaryjoin'] = and_(*self.secondaryjoin_clauses)
660
661        if 'order_by' in kwargs:
662            kwargs['order_by'] = \
663                self.target._descriptor.translate_order_by(kwargs['order_by'])
664
665        self.property = relation(self.target, secondary=self.secondary_table,
666                                 uselist=self.uselist, **kwargs)
667        self.entity.mapper.add_property(self.name, self.property)
668
669    def is_inverse(self, other):
670        return super(HasAndBelongsToMany, self).is_inverse(other) and \
671               (self.user_tablename == other.user_tablename or 
672                (not self.user_tablename and not other.user_tablename))
673
674
675def _build_join_clauses(local_table, local_cols1, local_cols2, target_table):
676    primary_join, secondary_join = [], []
677    cols1 = local_cols1[:]
678    cols1.sort()
679    cols1 = tuple(cols1)
680
681    if local_cols2 is not None:
682        cols2 = local_cols2[:]
683        cols2.sort()
684        cols2 = tuple(cols2)
685    else:
686        cols2 = None
687    constraint_map = {}
688    for constraint in local_table.constraints:
689        if isinstance(constraint, ForeignKeyConstraint):
690            use_constraint = False
691            fk_colnames = []
692            for fk in constraint.elements:
693                fk_colnames.append(fk.parent.name)
694                if fk.references(target_table):
695                    use_constraint = True
696            if use_constraint:
697                fk_colnames.sort()
698                constraint_map[tuple(fk_colnames)] = constraint
699
700    # Either the fk column names match explicitely with the columns given for
701    # one of the joins (primary or secondary), or we assume the current
702    # columns match because the columns for this join were not given and we
703    # know the other join is either not used (is None) or has an explicit
704    # match.
705    for cols, constraint in constraint_map.iteritems():
706        if cols == cols1 or (cols != cols2 and 
707                             not cols1 and (cols2 in constraint_map or
708                                            cols2 is None)):
709            join = primary_join
710        elif cols == cols2 or (cols2 == () and cols1 in constraint_map):
711            join = secondary_join
712        else:
713            continue
714        for fk in constraint.elements:
715            join.append(fk.parent == fk.column)
716    return primary_join, secondary_join
717
718
719belongs_to = Statement(BelongsTo)
720has_one = Statement(HasOne)
721has_many = Statement(HasMany)
722has_and_belongs_to_many = Statement(HasAndBelongsToMany)
Note: See TracBrowser for help on using the browser.