Changeset 458

Show
Ignore:
Timestamp:
09/17/09 13:54:19 (4 years ago)
Author:
ged
Message:

- Changed the pattern used by default to generate column names for (all)

ManyToMany relationships so that the meaning of bidirectional
self-referential relationships does not depend on the order of declaration
of each side (closes #69). See upgrade notes for details.

- Made M2MCOL_NAMEFORMAT accept a callable, so that more complex naming

generation can be used if so desired.

Location:
elixir/trunk
Files:
4 modified

Legend:

Unmodified
Added
Removed
  • elixir/trunk/CHANGES

    r448 r458  
    22 
    33Please see http://elixir.ematia.de/trac/wiki/Migrate06to07 for detailed 
    4 upgrade notes. 
     4upgrade notes. If you are upgrading an application with existing data from an 
     5earlier version of Elixir, you are VERY STRONGLY ADVISED to read them! 
    56 
    67New features: 
     
    2021  ManyToMany table in a custom schema and not necessarily the same schema as 
    2122  the table of the "source" entity (patch from Diez B. Roggisch). 
    22 - Added a new "target_column" argument on ManyToOne relationships (ticket #26). 
    23   TODOC 
     23- Added a new "target_column" argument on ManyToOne relationships so that you 
     24  can target unique but non-primary key columns. At the moment, this only works 
     25  if the target column is declared before the ManyToOne (see ticket #26). 
    2426- Added new column_names argument to the acts_as_versioned extension, allowing 
    2527  to specify custom column names (inspired by a patch by Alex Bodnaru). 
     
    4951 
    5052Bug fixes: 
     53- Changed the pattern used to generate column names for all ManyToMany 
     54  relationships so that the meaning of bidirectional 
     55  self-referential relationships does not depend on the order of declaration 
     56  of each side (closes #69). See upgrade notes for details. 
     57- Changed slightly the pattern used to generate the name of the table for 
     58  bidirectional self-referential ManyToMany relationships so that it doesn't 
     59  depend on the order of declaration of each side (closes #19). 
     60  See upgrade notes for details. 
    5161- default EntityCollection raise an exception instead of returning None when 
    5262  trying to resolve an inexisting Entity from outside of another entity (for 
    5363  example through EntityCollection.__getattr__ 
    54 - Changed slightly the algorithm to generate the name of the table for 
    55   bidirectional self-referential ManyToMany relationships so that it doesn't 
    56   depend on the order of declaration of each side (closes #19). If you are 
    57   upgrading an application with existing data from an earlier version of 
    58   Elixir, you are STRONGLY ADVISED to read the upgrade notes! 
    59   ================= 
    60   TEMPORARY NOTICE: 
    61   ================= 
    62   For now it is even worse: the table works but the relationship's meaning is 
    63   reversed. Will be fixed before 0.7 ships (see ticket #69). 
    64   ================= 
    6564- Fixed the case where you specify both "primaryjoin" and "colname" arguments 
    6665  (useless in this case, but harmless) on a ManyToOne relationship of an 
     
    7271  secondaryjoin as not been specified manually. 
    7372- Added missing documentation for the "filter" argument on OneToMany 
    74   relationships 
     73  relationships. 
    7574- Fixed the act_as_list extension's move_to_bottom method to work on MySQL 
    7675  (closes #34). 
    7776- Fixed event methods not being called when they are defined on a parent class. 
    78   (introduced in r262). 
     77  (introduced in release 0.5.0). 
    7978- Added workaround for an odd mod_python behavior (class.__module__ returns a 
    8079  weird name which is not in sys.modules). 
  • elixir/trunk/elixir/options.py

    r437 r458  
    184184# format constants 
    185185FKCOL_NAMEFORMAT = "%(relname)s_%(key)s" 
    186 M2MCOL_NAMEFORMAT = "%(tablename)s_%(key)s" 
     186OLD_M2MCOL_NAMEFORMAT = "%(tablename)s_%(key)s%(numifself)s" 
     187NEW_M2MCOL_NAMEFORMAT = "%(relname)s_%(key)s" 
     188M2MCOL_NAMEFORMAT = NEW_M2MCOL_NAMEFORMAT 
    187189CONSTRAINT_NAMEFORMAT = "%(tablename)s_%(colnames)s_fk" 
    188190MULTIINHERITANCECOL_NAMEFORMAT = "%(entity)s_%(key)s" 
     
    197199 
    198200# debugging/migration help 
    199 CHECK_TABLENAME_CHANGES = False 
     201MIGRATION_TO_07_AID = False 
    200202 
    201203# 
  • elixir/trunk/elixir/relationships.py

    r455 r458  
    417417__doc_all__ = [] 
    418418 
     419 
    419420class Relationship(Property): 
    420421    ''' 
     
    862863                "all ManyToMany tables", DeprecationWarning, stacklevel=3) 
    863864        self.column_format = column_format or options.M2MCOL_NAMEFORMAT 
     865        if not callable(self.column_format): 
     866            # we need to store the format in a variable so that the 
     867            # closure of the lambda is correct 
     868            format = self.column_format 
     869            self.column_format = lambda data: format % data 
     870        if options.MIGRATION_TO_07_AID: 
     871            self.column_format = \ 
     872                migration_aid_column_formatter(self.column_format) 
    864873 
    865874        self.filter = filter 
     
    938947            # whether this relation or its inverse is setup first). 
    939948            if self.inverse and source_part < target_part: 
    940                 #TODO: if self-relf, only include source_part 
     949                #XXX: use a different scheme for selfref (to not include the 
     950                #     table name twice)? 
    941951                tablename = "%s__%s" % (target_part, source_part) 
    942                 if options.CHECK_TABLENAME_CHANGES and \ 
     952                if options.MIGRATION_TO_07_AID and \ 
    943953                   e1_desc.tablename >= e2_desc.tablename: 
    944954                    oldname = "%s__%s" % (source_part, target_part) 
    945  
    946                     raise Exception( 
     955                    warnings.warn( 
    947956                        "The generated table name for the '%s' relationship " 
    948957                        "on the '%s' entity changed from '%s' (the name " 
     
    9961005                    assert len(colnames) == len(desc.primary_keys) 
    9971006                else: 
     1007                    data = {# relationship info 
     1008                            'relname': rel and rel.name or 'inverse', 
     1009                            'selfref': e1_desc is e2_desc, 
     1010                            'num': num, 
     1011                            'numifself': e1_desc is e2_desc and str(num + 1) 
     1012                                                            or '', 
     1013                            # target info 
     1014                            'target': desc.entity, 
     1015                            'entity': desc.entity.__name__.lower(), 
     1016                            'tablename': desc.tablename 
     1017                           } 
     1018                    colnames = [] 
    9981019                    for pk_col in desc.primary_keys: 
    999                         colname = self.column_format % \ 
    1000                                   {'tablename': desc.tablename, 
    1001                                    'key': pk_col.key, 
    1002                                    'entity': desc.entity.__name__.lower()} 
    1003                         # In case we have a many-to-many self-reference, we 
    1004                         # need to tweak the names of the columns so that we 
    1005                         # don't end up with twice the same column name. 
    1006                         if self.entity is self.target: 
    1007                             colname += str(num + 1) 
    1008                         colnames.append(colname) 
     1020                        data.update(key=pk_col.key) 
     1021                        colnames.append(self.column_format(data)) 
    10091022 
    10101023                for pk_col, colname in zip(desc.primary_keys, colnames): 
     
    10871100 
    10881101 
     1102def alternate_m2m_column_formatter(data): 
     1103    if data['selfref']: 
     1104        return options.NEW_M2MCOL_NAMEFORMAT % data 
     1105    else: 
     1106        return options.OLD_M2MCOL_NAMEFORMAT % data 
     1107 
     1108 
     1109def migration_aid_m2m_column_formatter(formatter): 
     1110    def debug_formatter(data): 
     1111        new_name = formatter(data) 
     1112        old_name = options.OLD_M2MCOL_NAMEFORMAT % data 
     1113        if new_name != old_name: 
     1114            complete_data = data.copy() 
     1115            #TODO: use explicit num and selfref variables 
     1116            complete_data.update(old_name=old_name, 
     1117                                 new_name=new_name, 
     1118                                 dir=data['num'] is 0 and 'local' or 'remote') 
     1119            # Specifying a stacklevel is useless in this case as the name 
     1120            # generation is triggered by setup_all(), not by the declaration 
     1121            # of the offending relationship. 
     1122            warnings.warn("The generated column name for the '%(relname)s' " 
     1123                          "relationship on the '%(entity)s' entity changed " 
     1124                          "from '%(old_name)s' to '%(new_name)s'. " 
     1125                          % complete_data) 
     1126        return new_name 
     1127    return debug_formatter 
     1128 
     1129 
    10891130def _get_join_clauses(local_table, local_cols1, local_cols2, target_table): 
    10901131    primary_join, secondary_join = [], [] 
  • elixir/trunk/tests/test_m2m.py

    r451 r458  
    44 
    55from elixir import * 
     6import elixir 
    67 
    78#----------- 
     
    2425 
    2526        setup_all(True) 
    26  
     27        A.mapper.compile() 
     28 
     29        # check m2m table was generated correctly 
     30        m2m_table = A.bs_.property.secondary 
     31        assert m2m_table.name in metadata.tables 
     32 
     33        # check column names 
     34        m2m_cols = m2m_table.columns 
     35        assert 'bs__id' in m2m_cols 
     36        assert 'as__id' in m2m_cols 
     37 
     38        # check the relationships work as expected 
    2739        b1 = B(name='b1', as_=[A(name='a1')]) 
    2840 
     
    3648        assert b in a.bs_ 
    3749 
    38     def test_column_format(self): 
     50    def test_custom_global_column_nameformat(self): 
     51        # this needs to be done before declaring the classes 
     52        elixir.options.M2MCOL_NAMEFORMAT = elixir.options.OLD_M2MCOL_NAMEFORMAT 
     53 
     54        class A(Entity): 
     55            bs_ = ManyToMany('B') 
     56 
     57        class B(Entity): 
     58            as_ = ManyToMany('A') 
     59 
     60        setup_all(True) 
     61 
     62        # revert to original format 
     63        elixir.options.M2MCOL_NAMEFORMAT = elixir.options.NEW_M2MCOL_NAMEFORMAT 
     64 
     65        # check m2m table was generated correctly 
     66        A.mapper.compile() 
     67        m2m_table = A.bs_.property.secondary 
     68        assert m2m_table.name in metadata.tables 
     69 
     70        # check column names 
     71        m2m_cols = m2m_table.columns 
     72        assert '%s_id' % A.table.name in m2m_cols 
     73        assert '%s_id' % B.table.name in m2m_cols 
     74 
     75    def test_alternate_column_formatter(self): 
     76        # this needs to be done before declaring the classes 
     77        elixir.options.M2MCOL_NAMEFORMAT = \ 
     78            elixir.relationships.alternate_m2m_column_formatter 
     79 
     80        class A(Entity): 
     81            as_ = ManyToMany('A') 
     82            bs_ = ManyToMany('B') 
     83 
     84        class B(Entity): 
     85            as_ = ManyToMany('A') 
     86 
     87        setup_all(True) 
     88        A.mapper.compile() 
     89 
     90        # revert to original format 
     91        elixir.options.M2MCOL_NAMEFORMAT = elixir.options.NEW_M2MCOL_NAMEFORMAT 
     92 
     93        # check m2m table column names were generated correctly 
     94        m2m_cols = A.bs_.property.secondary.columns 
     95        assert '%s_id' % A.table.name in m2m_cols 
     96        assert '%s_id' % B.table.name in m2m_cols 
     97 
     98        # check selfref m2m table column names were generated correctly 
     99        m2m_cols = A.as_.property.secondary.columns 
     100        assert 'as__id' in m2m_cols 
     101        assert 'inverse_id' in m2m_cols 
     102 
     103    def test_manual_column_format(self): 
    39104        class A(Entity): 
    40105            using_options(tablename='aye') 
     
    49114        setup_all(True) 
    50115 
    51         b1 = B(name='b1', as_=[A(name='a1')]) 
    52  
    53         session.commit() 
    54         session.clear() 
    55  
    56         a = A.query.one() 
    57         b = B.query.one() 
    58  
    59         assert a in b.as_ 
    60         assert b in a.bs_ 
    61  
     116        # check column names were generated correctly 
     117        A.mapper.compile() 
    62118        m2m_cols = A.bs_.property.secondary.columns 
    63119        assert 'a_id' in m2m_cols 
    64120        assert 'b_id' in m2m_cols 
    65121 
     122        # check the relationships work as expected 
     123        b1 = B(name='b1', as_=[A(name='a1')]) 
     124 
     125        session.commit() 
     126        session.clear() 
     127 
     128        a = A.query.one() 
     129        b = B.query.one() 
     130 
     131        assert a in b.as_ 
     132        assert b in a.bs_ 
     133 
    66134    def test_multi_pk_in_target(self): 
    67135        class A(Entity): 
     
    137205        assert barney in homer.friends 
    138206 
     207        m2m_cols = Person.friends.property.secondary.columns 
     208        assert 'friends_id' in m2m_cols 
     209        assert 'inverse_id' in m2m_cols 
     210 
    139211    def test_bidirectional_selfref(self): 
    140212        class Person(Entity): 
     
    159231        assert homer in barney.friends 
    160232        assert barney in homer.friends 
     233 
     234        m2m_cols = Person.friends.property.secondary.columns 
     235        assert 'friends_id' in m2m_cols 
     236        assert 'is_friend_of_id' in m2m_cols 
    161237 
    162238    def test_has_and_belongs_to_many(self):