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

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

- cleaned up "match_type_of" is relationship: now each relationship do it

itself, instead of having one generic method. I wonder how I didn't think
of this earlier...

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