Changeset 397
- Timestamp:
- 08/21/08 17:32:31 (5 years ago)
- Location:
- elixir/trunk
- Files:
-
- 8 modified
-
CHANGES (modified) (1 diff)
-
elixir/__init__.py (modified) (1 diff)
-
elixir/options.py (modified) (1 diff)
-
elixir/relationships.py (modified) (18 diffs)
-
setup.cfg (modified) (1 diff)
-
setup.py (modified) (1 diff)
-
tests/test_autoload.py (modified) (1 diff)
-
tests/test_m2m.py (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
elixir/trunk/CHANGES
r392 r397 1 0.7.0 2 3 Please see http://elixir.ematia.de/trac/wiki/Migrate06to07 for detailed 4 upgrade notes. 5 6 New features: 7 - The local_colname and remote_colname arguments on ManyToMany relationships 8 can now also be used to set custom names for the ManyToMany table columns. 9 This effectively replace the column_format on ManyToMany relationships which 10 is now deprecated. Change based on a patch from Diez B. Roggisch. 11 - Added (or rather fixed and documented) a "table" argument on ManyToMany 12 relationships to allow using a manually-defined Table (closes #44). 13 - Added a "schema" argument on ManyToMany relationship to be able to create the 14 ManyToMany table in a custom schema and not necessarily the same schema as 15 the table of the "source" entity (patch from Diez B. Roggisch). 16 17 Changes: 18 - Renamed remote_side and local_side ManyToMany arguments to remote_colname and 19 local_colname respectively to not collide with the remote_side argument 20 provided by SA (it doesn't make much sense on ManyToMany relationships but 21 still). 22 23 Bug fixes: 24 - Changed slightly the algorithm to generate the name of the table for 25 bidirectional self-referential ManyToMany relationships so that it doesn't 26 depend on the order of declaration of each side (closes #19). If you are 27 upgrading an application with existing data from an earlier version of 28 Elixir, you are STRONGLY ADVISED to read the upgrade notes! 29 ================= 30 TEMPORARY NOTICE: 31 ================= 32 For now it is even worse: the table works but the relationship's meaning is 33 reversed. Will be fixed before 0.7 ships (see ticket #69). 34 ================= 35 - Added missing documentation for the "filter" argument on OneToMany 36 relationships 37 1 38 0.6.1 - 2008-08-18 2 39 -
elixir/trunk/elixir/__init__.py
r385 r397 42 42 43 43 44 __version__ = '0. 6.1'44 __version__ = '0.7.0' 45 45 46 46 __all__ = ['Entity', 'EntityMeta', 'EntityCollection', 'entities', -
elixir/trunk/elixir/options.py
r349 r397 185 185 POLYMORPHIC_COL_TYPE = String(POLYMORPHIC_COL_SIZE) 186 186 187 # debugging/migration constants 188 CHECK_TABLENAME_CHANGES = False 189 187 190 # 188 191 options_defaults = dict( -
elixir/trunk/elixir/relationships.py
r389 r397 165 165 | | by a minus (for descending order). | 166 166 +--------------------+--------------------------------------------------------+ 167 | ``filter`` | Specify a filter criterion (as a clause element) for | 168 | | this relationship. This criterion will be and_'ed with | 169 | | the normal join criterion (primaryjoin) generated by | 170 | | Elixir for the relationship. For example: | 171 | | boston_addresses = \ | 172 | | OneToMany('Address', filter=Address.city == 'Boston') | 173 +--------------------+--------------------------------------------------------+ 167 174 168 175 Additionally, Elixir supports an alternate, DSL-based, syntax to define 169 176 OneToMany_ relationships, with the has_many_ statement. 170 171 Also, as for standard SQLAlchemy relations, the ``order_by`` keyword argument172 177 173 178 … … 220 225 221 226 If the entity containing your ``ManyToMany`` relationship is 222 autoloaded, you **must** specify at least one of either the ``remote_ side`` or223 ``local_side`` argument.227 autoloaded, you **must** specify at least one of either the ``remote_colname`` 228 or ``local_colname`` argument. 224 229 225 230 In addition to keyword arguments inherited from SQLAlchemy, ``ManyToMany`` … … 234 239 | | database. | 235 240 +--------------------+--------------------------------------------------------+ 236 | ``remote_side`` | A column name or list of column names specifying | 237 | | which column(s) in the intermediary table are used | 238 | | for the "remote" part of a self-referential | 239 | | relationship. This argument has an effect only when | 240 | | your entities are autoloaded. | 241 +--------------------+--------------------------------------------------------+ 242 | ``local_side`` | A column name or list of column names specifying | 243 | | which column(s) in the intermediary table are used | 244 | | for the "local" part of a self-referential | 245 | | relationship. This argument has an effect only when | 246 | | your entities are autoloaded. | 241 | ``schema`` | Specify a custom schema for the intermediate table. | 242 | | This can be used both when the tables needs to | 243 | | be created and when the table is autoloaded/reflected | 244 | | from the database. | 245 +--------------------+--------------------------------------------------------+ 246 | ``remote_colname`` | A string or list of strings specifying the names of | 247 | | the column(s) in the intermediary table which | 248 | | reference the "remote"/target entity's table. | 249 +--------------------+--------------------------------------------------------+ 250 | ``local_colname`` | A string or list of strings specifying the names of | 251 | | the column(s) in the intermediary table which | 252 | | reference the "local"/current entity's table. | 253 +--------------------+--------------------------------------------------------+ 254 | ``table`` | Use a manually created table. If this argument is | 255 | | used, Elixir won't generate a table for this | 256 | | relationship, and use the one given instead. | 247 257 +--------------------+--------------------------------------------------------+ 248 258 | ``order_by`` | Specify which field(s) should be used to sort the | … … 252 262 | | entity. These field names can optionally be prefixed | 253 263 | | by a minus (for descending order). | 254 +--------------------+--------------------------------------------------------+ 255 | ``column_format`` | Specify an alternate format string for naming the | 264 +----------------------+------------------------------------------------------+ 265 | ``ondelete`` | Value for the foreign key constraint ondelete clause. | 266 | | May be one of: ``cascade``, ``restrict``, | 267 | | ``set null``, or ``set default``. | 268 +--------------------+--------------------------------------------------------+ 269 | ``onupdate`` | Value for the foreign key constraint onupdate clause. | 270 | | May be one of: ``cascade``, ``restrict``, | 271 | | ``set null``, or ``set default``. | 272 +--------------------+--------------------------------------------------------+ 273 | ``column_format`` | DEPRECATED. Specify an alternate format string for | 274 | | naming the | 256 275 | | columns in the mapping table. The default value is | 257 276 | | defined in ``elixir.options.M2MCOL_NAMEFORMAT``. You | … … 356 375 357 376 import sys 358 359 from sqlalchemy import ForeignKeyConstraint, Column, \ 360 Table, and_361 from sqlalchemy.orm import relation, backref377 import warnings 378 379 from sqlalchemy import ForeignKeyConstraint, Column, Table, and_ 380 from sqlalchemy.orm import relation, backref 362 381 from sqlalchemy.ext.associationproxy import association_proxy 363 382 364 383 import options 365 from elixir.statements import ClassMutator366 from elixir.fields import Field367 from elixir.properties import Property368 from elixir.entity import EntityDescriptor, EntityMeta384 from elixir.statements import ClassMutator 385 from elixir.fields import Field 386 from elixir.properties import Property 387 from elixir.entity import EntityDescriptor, EntityMeta 369 388 370 389 … … 418 437 419 438 # transform callable arguments 420 for arg in ('primaryjoin', 'secondaryjoin', 'remote_side' ):439 for arg in ('primaryjoin', 'secondaryjoin', 'remote_side', 'filter'): 421 440 kwarg = kwargs.get(arg, None) 422 441 if callable(kwarg): … … 669 688 670 689 if self.entity.table is self.target.table: 690 # this is needed because otherwise SA has no way to know what is 691 # the direction of the relationship since both columns present in 692 # the primaryjoin belong to the same table. In other words, it is 693 # necessary to know if this particular relation 694 # is the many-to-one side, or the one-to-xxx side. The foreignkey 695 # doesn't help in this case. 671 696 kwargs['remote_side'] = \ 672 697 [col for col in self.target.table.primary_key.columns] … … 686 711 self.filter = kwargs.pop('filter', None) 687 712 if self.filter is not None: 688 kwargs['viewonly'] = True 713 # We set viewonly to True by default for filtered relationship, 714 # unless manually overridden. 715 # This is not strictly necessary, as SQLAlchemy allows non viewonly 716 # relationships with a custom join/filter. The example at: 717 # SADOCS/05/mappers.html#advdatamapping_relation_customjoin 718 # is not viewonly. Those relationships can be used as if the extra 719 # filter wasn't present when inserting. This can lead to a 720 # confusing behavior (if you insert data which doesn't match the 721 # extra criterion it'll get inserted anyway but you won't see it 722 # when you query back the attribute after a round-trip to the 723 # database). 724 if 'viewonly' not in kwargs: 725 kwargs['viewonly'] = True 689 726 super(OneToOne, self).__init__(*args, **kwargs) 690 727 … … 701 738 "inheritance you " 702 739 "might need to specify inverse relationships " 703 "manually by using the inverse keyword."740 "manually by using the 'inverse' argument." 704 741 % (self.target.__name__, self.name, 705 742 self.entity.__name__)) … … 711 748 # So, we should either complete the selfref test to prove that they 712 749 # are indeed useful, or remove them. It might be they are indeed 713 # useless because of the primaryjoin, and that the remote_side is714 # already setup in the other way (belongs_to).750 # useless because the remote_side is already setup in the other way 751 # (ManyToOne). 715 752 if self.entity.table is self.target.table: 716 753 #FIXME: IF this code is of any use, it will probably break for … … 738 775 def __init__(self, *args, **kwargs): 739 776 self.user_tablename = kwargs.pop('tablename', None) 740 self.local_ side = kwargs.pop('local_side', [])741 if self.local_ side and not isinstance(self.local_side, list):742 self.local_ side = [self.local_side]743 self.remote_ side = kwargs.pop('remote_side', [])744 if self.remote_ side and not isinstance(self.remote_side, list):745 self.remote_ side = [self.remote_side]777 self.local_colname = kwargs.pop('local_colname', []) 778 if self.local_colname and not isinstance(self.local_colname, list): 779 self.local_colname = [self.local_colname] 780 self.remote_colname = kwargs.pop('remote_colname', []) 781 if self.remote_colname and not isinstance(self.remote_colname, list): 782 self.remote_colname = [self.remote_colname] 746 783 self.ondelete = kwargs.pop('ondelete', None) 747 784 self.onupdate = kwargs.pop('onupdate', None) 748 self.column_format = kwargs.pop('column_format', options.M2MCOL_NAMEFORMAT) 785 if 'column_format' in kwargs: 786 warnings.warn("The 'column_format' argument on ManyToMany " 787 "relationships is deprecated. Please use the 'local_colname' " 788 "and/or 'remote_colname' arguments if you want custom " 789 "column names for this table only, or modify " 790 "options.M2MCOL_NAMEFORMAT if you want a custom format for " 791 "all ManyToMany tables", DeprecationWarning, stacklevel=3) 792 self.column_format = kwargs.pop('column_format', 793 options.M2MCOL_NAMEFORMAT) 749 794 750 795 self.secondary_table = kwargs.pop('table', None) 796 self.schema = kwargs.pop('schema', None) 797 751 798 self.primaryjoin_clauses = list() 752 799 self.secondaryjoin_clauses = list() … … 762 809 # autoloaded entities 763 810 if self.secondary_table: 811 self._reflect_table() 764 812 return 765 813 … … 776 824 e1_schema = e1_desc.table_options.get('schema', None) 777 825 e2_schema = e2_desc.table_options.get('schema', None) 778 assert e1_schema == e2_schema, \ 779 "Schema %r for entity %s differs from schema %r of entity %s" \ 826 schema = (self.schema is not None) and schema or e1_schema 827 828 assert e1_schema == e2_schema or self.schema, \ 829 "Schema %r for entity %s differs from schema %r of entity %s." \ 830 " Consider using the schema-parameter. "\ 780 831 % (e1_schema, self.entity.__name__, 781 832 e2_schema, self.target.__name__) … … 804 855 # We need to keep the table name consistent (independant of 805 856 # whether this relation or its inverse is setup first). 806 if self.inverse and e1_desc.tablename < e2_desc.tablename: 857 if self.inverse and source_part < target_part: 858 #TODO: if self-relf, only include source_part 807 859 tablename = "%s__%s" % (target_part, source_part) 860 if options.CHECK_TABLENAME_CHANGES and \ 861 e1_desc.tablename >= e2_desc.tablename: 862 oldname = "%s__%s" % (source_part, target_part) 863 864 raise Exception( 865 "The generated table name for the '%s' relationship " 866 "on the '%s' entity changed from '%s' (the name " 867 "generated by Elixir 0.6.1 and earlier) to '%s'. " 868 "You should either rename the table in the database " 869 "to the new name or use the tablename argument on the " 870 "relationship to force the old name: tablename='%s'!" 871 % (self.name, self.entity.__name__, oldname, 872 tablename, oldname)) 808 873 else: 809 874 tablename = "%s__%s" % (source_part, target_part) 810 875 811 876 if e1_desc.autoload: 812 self._reflect_table(tablename) 877 if not e2_desc.autoload: 878 raise Exception( 879 "Entity '%s' is autoloaded and its '%s' " 880 "ManyToMany relationship points to " 881 "the '%s' entity which is not autoloaded" 882 % (self.entity.__name__, self.name, 883 self.target.__name__)) 884 885 self.secondary_table = Table(tablename, e1_desc.metadata, 886 autoload=True) 887 self._reflect_table() 813 888 else: 814 889 # We pre-compute the names of the foreign key constraints … … 828 903 829 904 joins = (self.primaryjoin_clauses, self.secondaryjoin_clauses) 830 for num, desc, fk_name, rel in ( 831 (0, e1_desc, source_fk_name, self), 832 (1, e2_desc, target_fk_name, self.inverse)): 905 for num, desc, fk_name, rel, colnames in ( 906 (0, e1_desc, source_fk_name, self, self.local_colname), 907 (1, e2_desc, target_fk_name, self.inverse, self.remote_colname)): 908 833 909 fk_colnames = list() 834 910 fk_refcols = list() 835 911 836 for pk_col in desc.primary_keys: 837 colname = self.column_format % \ 838 {'tablename': desc.tablename, 839 'key': pk_col.key, 840 'entity': desc.entity.__name__.lower()} 841 842 # In case we have a many-to-many self-reference, we 843 # need to tweak the names of the columns so that we 844 # don't end up with twice the same column name. 845 if self.entity is self.target: 846 colname += str(num + 1) 847 912 if colnames: 913 assert len(colnames) == len(desc.primary_keys) 914 else: 915 for pk_col in desc.primary_keys: 916 colname = self.column_format % \ 917 {'tablename': desc.tablename, 918 'key': pk_col.key, 919 'entity': desc.entity.__name__.lower()} 920 # In case we have a many-to-many self-reference, we 921 # need to tweak the names of the columns so that we 922 # don't end up with twice the same column name. 923 if self.entity is self.target: 924 colname += str(num + 1) 925 colnames.append(colname) 926 927 for pk_col, colname in zip(desc.primary_keys, colnames): 848 928 col = Column(colname, pk_col.type, primary_key=True) 849 929 columns.append(col) … … 873 953 874 954 self.secondary_table = Table(tablename, e1_desc.metadata, 875 schema=e1_schema, *args) 876 877 def _reflect_table(self, tablename): 878 if not self.target._descriptor.autoload: 879 raise Exception( 880 "Entity '%s' is autoloaded and its '%s' " 881 "ManyToMany relationship points to " 882 "the '%s' entity which is not autoloaded" 883 % (self.entity.__name__, self.name, 884 self.target.__name__)) 885 886 self.secondary_table = Table(tablename, 887 self.entity._descriptor.metadata, 888 autoload=True) 889 955 schema=schema, *args) 956 957 def _reflect_table(self): 890 958 # In the case we have a self-reference, we need to build join clauses 891 959 if self.entity is self.target: 892 960 #CHECKME: maybe we should try even harder by checking if that 893 961 # information was defined on the inverse relationship) 894 if not self.local_ side and not self.remote_side:962 if not self.local_colname and not self.remote_colname: 895 963 raise Exception( 896 964 "Self-referential ManyToMany " 897 965 "relationships in autoloaded entities need to have at " 898 "least one of either 'local_ side' or 'remote_side' "966 "least one of either 'local_colname' or 'remote_colname' " 899 967 "argument specified. The '%s' relationship in the '%s' " 900 968 "entity doesn't have either." … … 903 971 self.primaryjoin_clauses, self.secondaryjoin_clauses = \ 904 972 _get_join_clauses(self.secondary_table, 905 self.local_ side, self.remote_side,973 self.local_colname, self.remote_colname, 906 974 self.entity.table) 907 975 … … 946 1014 947 1015 # if all columns point to the correct table, we use the constraint 1016 #TODO: check that it contains as many columns as the pk of the 1017 #target entity, or even that it points to the actual pk columns 948 1018 for fk in constraint.elements: 949 1019 if fk.references(target_table): -
elixir/trunk/setup.cfg
r377 r397 9 9 modules = elixir, elixir.ext.associable, elixir.ext.versioned, 10 10 elixir.ext.encrypted, elixir.ext.list 11 trac_browser_url = http://elixir.ematia.de/trac/browser/elixir/tags/0. 6.111 trac_browser_url = http://elixir.ematia.de/trac/browser/elixir/tags/0.7.0 12 12 -
elixir/trunk/setup.py
r377 r397 2 2 3 3 setup(name="Elixir", 4 version="0. 6.1",4 version="0.7.0", 5 5 description="Declarative Mapper for SQLAlchemy", 6 6 long_description=""" -
elixir/trunk/tests/test_autoload.py
r388 r397 60 60 appreciate = ManyToMany('Person', 61 61 tablename='person_person', 62 local_ side='person_id1')62 local_colname='person_id1') 63 63 isappreciatedby = ManyToMany('Person', 64 64 tablename='person_person', 65 local_ side='person_id2')65 local_colname='person_id2') 66 66 67 67 class Animal(Entity): -
elixir/trunk/tests/test_m2m.py
r349 r397 121 121 def test_selfref(self): 122 122 class Person(Entity): 123 using_options(shortnames=True) 123 124 name = Field(String(30)) 124 125 125 126 friends = ManyToMany('Person') 127 128 setup_all(True) 129 130 barney = Person(name="Barney") 131 homer = Person(name="Homer", friends=[barney]) 132 barney.friends.append(homer) 133 134 session.commit() 135 session.clear() 136 137 homer = Person.get_by(name="Homer") 138 barney = Person.get_by(name="Barney") 139 140 assert homer in barney.friends 141 assert barney in homer.friends 142 143 def test_bidirectional_selfref(self): 144 class Person(Entity): 145 using_options(shortnames=True) 146 name = Field(String(30)) 147 148 friends = ManyToMany('Person') 149 is_friend_of = ManyToMany('Person') 126 150 127 151 setup_all(True) … … 170 194 assert not a3.bs 171 195 196 def test_local_and_remote_colnames(self): 197 class A(Entity): 198 using_options(shortnames=True) 199 key1 = Field(Integer, primary_key=True, autoincrement=False) 200 key2 = Field(String(40), primary_key=True) 201 202 bs_ = ManyToMany('B', local_colname=['foo', 'bar'], 203 remote_colname="baz") 204 205 class B(Entity): 206 using_options(shortnames=True) 207 name = Field(String(60)) 208 as_ = ManyToMany('A', remote_colname=['foo', 'bar'], 209 local_colname="baz") 210 211 setup_all(True) 212 213 b1 = B(name='b1', as_=[A(key1=10, key2='a1')]) 214 215 session.commit() 216 session.clear() 217 218 a = A.query.one() 219 b = B.query.one() 220 221 assert a in b.as_ 222 assert b in a.bs_ 223 224 def test_manual_table_auto_joins(self): 225 from sqlalchemy import Table, Column, ForeignKey, ForeignKeyConstraint 226 227 a_b = Table('a_b', metadata, 228 Column('a_key1', None), 229 Column('a_key2', None), 230 Column('b_id', None, ForeignKey('b.id')), 231 ForeignKeyConstraint(['a_key1', 'a_key2'], 232 ['a.key1', 'a.key2'])) 233 234 class A(Entity): 235 using_options(shortnames=True) 236 key1 = Field(Integer, primary_key=True, autoincrement=False) 237 key2 = Field(String(40), primary_key=True) 238 239 bs_ = ManyToMany('B', table=a_b) 240 241 class B(Entity): 242 using_options(shortnames=True) 243 name = Field(String(60)) 244 as_ = ManyToMany('A', table=a_b) 245 246 setup_all(True) 247 248 b1 = B(name='b1', as_=[A(key1=10, key2='a1')]) 249 250 session.commit() 251 session.clear() 252 253 a = A.query.one() 254 b = B.query.one() 255 256 assert a in b.as_ 257 assert b in a.bs_ 258 259 def test_manual_table_manual_joins(self): 260 from sqlalchemy import Table, Column, ForeignKey, \ 261 ForeignKeyConstraint, and_ 262 263 a_b = Table('a_b', metadata, 264 Column('a_key1', Integer), 265 Column('a_key2', String(40)), 266 Column('b_id', String(60))) 267 268 class A(Entity): 269 using_options(shortnames=True) 270 key1 = Field(Integer, primary_key=True, autoincrement=False) 271 key2 = Field(String(40), primary_key=True) 272 273 bs_ = ManyToMany('B', table=a_b, 274 primaryjoin=lambda: and_(A.key1 == a_b.c.a_key1, 275 A.key2 == a_b.c.a_key2), 276 secondaryjoin=lambda: B.id == a_b.c.b_id, 277 foreign_keys=[a_b.c.a_key1, a_b.c.a_key2, 278 a_b.c.b_id]) 279 280 class B(Entity): 281 using_options(shortnames=True) 282 name = Field(String(60)) 283 284 setup_all(True) 285 286 a1 = A(key1=10, key2='a1', bs_=[B(name='b1')]) 287 288 session.commit() 289 session.clear() 290 291 a = A.query.one() 292 b = B.query.one() 293 294 assert b in a.bs_
