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

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

- Made EntityMeta public, so that people can actually define their own base

class (I thought I already made this possible but it seems like not).

- Removed all all declarations from subpackages as they are IMHO useless

since we can't get to them from outside of the lib, consequently replaced
all the "from xyz import *" lines in init.py by explicit imports.

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