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

Revision 93, 30.0 kB (checked in by ged, 6 years ago)

- (Hopefully) fixed reflecting has_and_belongs_to_many relationships.

Now, you have to specify at least one of either the local_side or remote_side
argument.

- Changed the approach to reflecting/autoloading belongs_to relationships.

This shouldn't change anything to how it's used but allowed me to factor
some code with has_and_belongs_to_many relationships.

- Fixed wrong field length is autoload test (it is not noticeable with sqlite).
- Use an explicit metaclass for entities, so that people can define their own

base class.

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