Changeset 475

Show
Ignore:
Timestamp:
09/29/09 14:14:37 (4 years ago)
Author:
ged
Message:

- changed the M2M column format again (only differ from the 0.6.x naming for

selfref M2M) and use the inverse relationship name to produce more sensible
column names.

- added a test simulating a migration from the old column format to the new

one.

- added lots of comments to explain what the different variables you can use in

the M2M format strings actually mean.

- changed the migration_aid_m2m_column_formatter so that the method can be

reused to migrate from any format to any format.

Location:
elixir/trunk
Files:
3 modified

Legend:

Unmodified
Added
Removed
  • elixir/trunk/elixir/options.py

    r466 r475  
    183183__doc_all__ = ['options_defaults'] 
    184184 
     185OLD_M2MCOL_NAMEFORMAT = "%(tablename)s_%(key)s%(numifself)s" 
     186ALTERNATE_M2MCOL_NAMEFORMAT = "%(inversename)s_%(key)s" 
     187 
     188def default_m2m_column_formatter(data): 
     189    if data['selfref']: 
     190        return ALTERNATE_M2MCOL_NAMEFORMAT % data 
     191    else: 
     192        return OLD_M2MCOL_NAMEFORMAT % data 
     193 
     194NEW_M2MCOL_NAMEFORMAT = default_m2m_column_formatter 
     195 
    185196# format constants 
    186197FKCOL_NAMEFORMAT = "%(relname)s_%(key)s" 
    187 OLD_M2MCOL_NAMEFORMAT = "%(tablename)s_%(key)s%(numifself)s" 
    188 NEW_M2MCOL_NAMEFORMAT = "%(relname)s_%(key)s" 
    189 M2MCOL_NAMEFORMAT = OLD_M2MCOL_NAMEFORMAT 
     198M2MCOL_NAMEFORMAT = NEW_M2MCOL_NAMEFORMAT 
    190199CONSTRAINT_NAMEFORMAT = "%(tablename)s_%(colnames)s_fk" 
    191200MULTIINHERITANCECOL_NAMEFORMAT = "%(entity)s_%(key)s" 
  • elixir/trunk/elixir/relationships.py

    r465 r475  
    870870        if options.MIGRATION_TO_07_AID: 
    871871            self.column_format = \ 
    872                 migration_aid_m2m_column_formatter(self.column_format) 
     872                migration_aid_m2m_column_formatter( 
     873                    lambda data: options.OLD_M2MCOL_NAMEFORMAT % data, 
     874                    self.column_format) 
    873875 
    874876        self.filter = filter 
     
    995997 
    996998            joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses) 
    997             for num, desc, fk_name, rel, colnames in ( 
    998               (0, e1_desc, source_fk_name, self, self.local_colname), 
    999               (1, e2_desc, target_fk_name, self.inverse, self.remote_colname)): 
     999            for num, desc, fk_name, rel, inverse, colnames in ( 
     1000              (0, e1_desc, source_fk_name, self, self.inverse, self.local_colname), 
     1001              (1, e2_desc, target_fk_name, self.inverse, self, self.remote_colname)): 
    10001002 
    10011003                fk_colnames = [] 
     
    10051007                    assert len(colnames) == len(desc.primary_keys) 
    10061008                else: 
    1007                     #FIXME: desc is not the target desc. Do I need to fix to 
    1008                     # code or the doc? in fact the relname corresponds to the 
    1009                     # relationship going from the entity to the M2M, so the new 
    1010                     # naming scheme might not really make sense 
    1011                     data = {# relationship info 
     1009                    # The data generated here will be fed to the M2M column 
     1010                    # formatter to generate the name of the columns of the 
     1011                    # intermediate table for *one* side of the relationship, 
     1012                    # that is, from the intermediate table to the current 
     1013                    # entity, as stored in the "desc" variable. 
     1014                    data = {# A) relationships info 
     1015 
     1016                            # the name of the rel going *from* the entity 
     1017                            # we are currently generating a column pointing 
     1018                            # *to*. This is generally *not* what you want to 
     1019                            # use. eg in a "Post" and "Tag" example, with 
     1020                            # relationships named 'tags' and 'posts', when 
     1021                            # creating the columns from the intermediate 
     1022                            # table to the "Post" entity, 'relname' will 
     1023                            # contain 'tags'. 
    10121024                            'relname': rel and rel.name or 'inverse', 
     1025 
     1026                            # the name of the inverse relationship. In the 
     1027                            # above example, 'inversename' will contain 
     1028                            # 'posts'. 
     1029                            'inversename': inverse and inverse.name 
     1030                                                   or 'inverse', 
     1031                            # is A == B? 
    10131032                            'selfref': e1_desc is e2_desc, 
     1033                            # provided for backward compatibility, DO NOT USE! 
    10141034                            'num': num, 
     1035                            # provided for backward compatibility, DO NOT USE! 
    10151036                            'numifself': e1_desc is e2_desc and str(num + 1) 
    10161037                                                            or '', 
    1017                             # source (not target!) info 
    1018                             'source': desc.entity, 
     1038                            # B) target information (from the perspective of 
     1039                            #    the intermediate table) 
     1040                            'target': desc.entity, 
    10191041                            'entity': desc.entity.__name__.lower(), 
    1020                             'tablename': desc.tablename 
     1042                            'tablename': desc.tablename, 
     1043 
     1044                            # C) current (intermediate) table name 
     1045                            'current_table': tablename 
    10211046                           } 
    10221047                    colnames = [] 
     
    10451070                ondelete = rel and rel.ondelete 
    10461071 
     1072                #FIXME: fk_name is misleading 
    10471073                constraints.append( 
    10481074                    ForeignKeyConstraint(fk_colnames, fk_refcols, 
     
    11041130 
    11051131 
    1106 def alternate_m2m_column_formatter(data): 
    1107     if data['selfref']: 
    1108         return options.NEW_M2MCOL_NAMEFORMAT % data 
    1109     else: 
    1110         return options.OLD_M2MCOL_NAMEFORMAT % data 
    1111  
    1112  
    1113 def migration_aid_m2m_column_formatter(formatter): 
     1132def migration_aid_m2m_column_formatter(oldformatter, newformatter): 
    11141133    def debug_formatter(data): 
    1115         new_name = formatter(data) 
    1116         old_name = options.OLD_M2MCOL_NAMEFORMAT % data 
     1134        old_name = oldformatter(data) 
     1135        new_name = newformatter(data) 
    11171136        if new_name != old_name: 
    11181137            complete_data = data.copy() 
    11191138            complete_data.update(old_name=old_name, 
    11201139                                 new_name=new_name, 
    1121                                  dir=data['num'] is 0 and 'local' or 'remote') 
     1140                                 targetname=data['target'].__name__) 
    11221141            # Specifying a stacklevel is useless in this case as the name 
    11231142            # generation is triggered by setup_all(), not by the declaration 
    11241143            # of the offending relationship. 
    1125             #FIXME: entity is probably wrong here since it refers to the target 
    1126             #entity. 
    1127             warnings.warn("The generated column name for the '%(relname)s' " 
    1128                           "relationship on the '%(entity)s' entity changed " 
    1129                           "from '%(old_name)s' to '%(new_name)s'. " 
     1144            warnings.warn("The '%(old_name)s' column in the " 
     1145                          "'%(current_table)s' table, used as the " 
     1146                          "intermediate table for the '%(relname)s' " 
     1147                          "relationship on the '%(targetname)s' entity " 
     1148                          "was renamed to '%(new_name)s'." 
    11301149                          % complete_data) 
    11311150        return new_name 
  • elixir/trunk/tests/test_m2m.py

    r464 r475  
    1717    def test_simple(self): 
    1818        class A(Entity): 
    19             name = Field(String(60)) 
    20             bs_ = ManyToMany('B') 
    21  
    22         class B(Entity): 
     19            using_options(shortnames=True) 
     20            name = Field(String(60)) 
     21            as_ = ManyToMany('A') 
     22            bs_ = ManyToMany('B') 
     23 
     24        class B(Entity): 
     25            using_options(shortnames=True) 
    2326            name = Field(String(60)) 
    2427            as_ = ManyToMany('A') 
     
    3336        # check column names 
    3437        m2m_cols = m2m_table.columns 
    35         assert 'bs__id' in m2m_cols 
     38        assert 'a_id' in m2m_cols 
     39        assert 'b_id' in m2m_cols 
     40 
     41        # check selfref m2m table column names were generated correctly 
     42        m2m_cols = A.as_.property.secondary.columns 
    3643        assert 'as__id' in m2m_cols 
     44        assert 'inverse_id' in m2m_cols 
    3745 
    3846        # check the relationships work as expected 
     
    7684        # this needs to be done before declaring the classes 
    7785        elixir.options.M2MCOL_NAMEFORMAT = \ 
    78             elixir.relationships.alternate_m2m_column_formatter 
     86            elixir.options.ALTERNATE_M2MCOL_NAMEFORMAT 
    7987 
    8088        class A(Entity): 
     
    93101        # check m2m table column names were generated correctly 
    94102        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 
     103        assert 'as__id' in m2m_cols 
     104        assert 'bs__id' in m2m_cols 
    97105 
    98106        # check selfref m2m table column names were generated correctly 
     
    101109        assert 'inverse_id' in m2m_cols 
    102110 
    103     #TODO: add an upgrade test 
    104     #def test_upgrade(self): 
    105 #        elixir.options.M2MCOL_NAMEFORMAT = elixir.options.OLD_M2MCOL_NAMEFORMAT 
     111    def test_upgrade(self): 
     112        elixir.options.M2MCOL_NAMEFORMAT = elixir.options.OLD_M2MCOL_NAMEFORMAT 
     113 
     114        class A(Entity): 
     115            using_options(shortnames=True) 
     116            name = Field(String(20)) 
     117            links_to = ManyToMany('A') 
     118            is_linked_from = ManyToMany('A') 
     119            bs_ = ManyToMany('B') 
     120 
     121        class B(Entity): 
     122            using_options(shortnames=True) 
     123            name = Field(String(20)) 
     124            as_ = ManyToMany('A') 
     125 
     126        setup_all(True) 
     127 
     128        a = A(name='a1', links_to=[A(name='a2')]) 
     129 
     130        session.commit() 
     131        session.clear() 
     132 
     133        del A 
     134        del B 
     135 
     136        # do not drop the tables, that's the whole point! 
     137        cleanup_all() 
     138 
     139        # simulate a renaming of columns (as given by the migration aid) 
     140        # 'a_id1' to 'is_linked_from_id'. 
     141        # 'a_id2' to 'links_to_id'. 
     142        conn = metadata.bind.connect() 
     143        conn.execute("ALTER TABLE a_links_to__a_is_linked_from RENAME TO temp") 
     144        conn.execute("CREATE TABLE a_links_to__a_is_linked_from (" 
     145                        "is_linked_from_id INTEGER NOT NULL, " 
     146                        "links_to_id INTEGER NOT NULL, " 
     147                     "PRIMARY KEY (is_linked_from_id, links_to_id), " 
     148                     "CONSTRAINT a_fk1 FOREIGN KEY(is_linked_from_id) " 
     149                                      "REFERENCES a (id), " 
     150                     "CONSTRAINT a_fk2 FOREIGN KEY(links_to_id) " 
     151                                      "REFERENCES a (id))") 
     152        conn.execute("INSERT INTO a_links_to__a_is_linked_from " 
     153                     "(is_linked_from_id, links_to_id) " 
     154                     "SELECT a_id1, a_id2 FROM temp") 
     155        conn.close() 
     156 
     157        # ... 
     158        elixir.options.M2MCOL_NAMEFORMAT = elixir.options.NEW_M2MCOL_NAMEFORMAT 
    106159#        elixir.options.MIGRATION_TO_07_AID = True 
    107 #        metadata.bind = 'sqlite://' 
    108 #        elixir.options.M2MCOL_NAMEFORMAT = elixir.options.NEW_M2MCOL_NAMEFORMAT    #    assert False 
     160 
     161        class A(Entity): 
     162            using_options(shortnames=True) 
     163            name = Field(String(20)) 
     164            links_to = ManyToMany('A') 
     165            is_linked_from = ManyToMany('A') 
     166            bs_ = ManyToMany('B') 
     167 
     168        class B(Entity): 
     169            using_options(shortnames=True) 
     170            name = Field(String(20)) 
     171            as_ = ManyToMany('A') 
     172 
     173        setup_all() 
     174 
     175        a1 = A.get_by(name='a1') 
     176        assert len(a1.links_to) == 1 
     177        assert not a1.is_linked_from 
     178 
     179        a2 = a1.links_to[0] 
     180        assert a2.name == 'a2' 
     181        assert not a2.links_to 
     182        assert a2.is_linked_from == [a1] 
    109183 
    110184    def test_manual_column_format(self):