1from sqlalchemy.testing import eq_, assert_raises, \
2    assert_raises_message, is_
3from sqlalchemy.ext import declarative as decl
4import sqlalchemy as sa
5from sqlalchemy import testing
6from sqlalchemy import Integer, String, ForeignKey, select, func
7from sqlalchemy.testing.schema import Table, Column
8from sqlalchemy.orm import relationship, create_session, class_mapper, \
9    configure_mappers, clear_mappers, \
10    deferred, column_property, Session, base as orm_base
11from sqlalchemy.util import classproperty
12from sqlalchemy.ext.declarative import declared_attr, declarative_base
13from sqlalchemy.orm import events as orm_events
14from sqlalchemy.testing import fixtures, mock
15from sqlalchemy.testing.util import gc_collect
16
17Base = None
18
19
20class DeclarativeTestBase(fixtures.TestBase, testing.AssertsExecutionResults):
21
22    def setup(self):
23        global Base
24        Base = decl.declarative_base(testing.db)
25
26    def teardown(self):
27        Session.close_all()
28        clear_mappers()
29        Base.metadata.drop_all()
30
31
32class DeclarativeMixinTest(DeclarativeTestBase):
33
34    def test_simple(self):
35
36        class MyMixin(object):
37
38            id = Column(Integer, primary_key=True,
39                        test_needs_autoincrement=True)
40
41            def foo(self):
42                return 'bar' + str(self.id)
43
44        class MyModel(Base, MyMixin):
45
46            __tablename__ = 'test'
47            name = Column(String(100), nullable=False, index=True)
48
49        Base.metadata.create_all()
50        session = create_session()
51        session.add(MyModel(name='testing'))
52        session.flush()
53        session.expunge_all()
54        obj = session.query(MyModel).one()
55        eq_(obj.id, 1)
56        eq_(obj.name, 'testing')
57        eq_(obj.foo(), 'bar1')
58
59    def test_unique_column(self):
60
61        class MyMixin(object):
62
63            id = Column(Integer, primary_key=True)
64            value = Column(String, unique=True)
65
66        class MyModel(Base, MyMixin):
67
68            __tablename__ = 'test'
69
70        assert MyModel.__table__.c.value.unique
71
72    def test_hierarchical_bases(self):
73
74        class MyMixinParent:
75
76            id = Column(Integer, primary_key=True,
77                        test_needs_autoincrement=True)
78
79            def foo(self):
80                return 'bar' + str(self.id)
81
82        class MyMixin(MyMixinParent):
83
84            baz = Column(String(100), nullable=False, index=True)
85
86        class MyModel(Base, MyMixin):
87
88            __tablename__ = 'test'
89            name = Column(String(100), nullable=False, index=True)
90
91        Base.metadata.create_all()
92        session = create_session()
93        session.add(MyModel(name='testing', baz='fu'))
94        session.flush()
95        session.expunge_all()
96        obj = session.query(MyModel).one()
97        eq_(obj.id, 1)
98        eq_(obj.name, 'testing')
99        eq_(obj.foo(), 'bar1')
100        eq_(obj.baz, 'fu')
101
102    def test_mixin_overrides(self):
103        """test a mixin that overrides a column on a superclass."""
104
105        class MixinA(object):
106            foo = Column(String(50))
107
108        class MixinB(MixinA):
109            foo = Column(Integer)
110
111        class MyModelA(Base, MixinA):
112            __tablename__ = 'testa'
113            id = Column(Integer, primary_key=True)
114
115        class MyModelB(Base, MixinB):
116            __tablename__ = 'testb'
117            id = Column(Integer, primary_key=True)
118
119        eq_(MyModelA.__table__.c.foo.type.__class__, String)
120        eq_(MyModelB.__table__.c.foo.type.__class__, Integer)
121
122    def test_not_allowed(self):
123
124        class MyMixin:
125            foo = Column(Integer, ForeignKey('bar.id'))
126
127        def go():
128            class MyModel(Base, MyMixin):
129                __tablename__ = 'foo'
130
131        assert_raises(sa.exc.InvalidRequestError, go)
132
133        class MyRelMixin:
134            foo = relationship('Bar')
135
136        def go():
137            class MyModel(Base, MyRelMixin):
138
139                __tablename__ = 'foo'
140
141        assert_raises(sa.exc.InvalidRequestError, go)
142
143        class MyDefMixin:
144            foo = deferred(Column('foo', String))
145
146        def go():
147            class MyModel(Base, MyDefMixin):
148                __tablename__ = 'foo'
149
150        assert_raises(sa.exc.InvalidRequestError, go)
151
152        class MyCPropMixin:
153            foo = column_property(Column('foo', String))
154
155        def go():
156            class MyModel(Base, MyCPropMixin):
157                __tablename__ = 'foo'
158
159        assert_raises(sa.exc.InvalidRequestError, go)
160
161    def test_table_name_inherited(self):
162
163        class MyMixin:
164
165            @declared_attr
166            def __tablename__(cls):
167                return cls.__name__.lower()
168            id = Column(Integer, primary_key=True)
169
170        class MyModel(Base, MyMixin):
171            pass
172
173        eq_(MyModel.__table__.name, 'mymodel')
174
175    def test_classproperty_still_works(self):
176        class MyMixin(object):
177
178            @classproperty
179            def __tablename__(cls):
180                return cls.__name__.lower()
181            id = Column(Integer, primary_key=True)
182
183        class MyModel(Base, MyMixin):
184            __tablename__ = 'overridden'
185
186        eq_(MyModel.__table__.name, 'overridden')
187
188    def test_table_name_not_inherited(self):
189
190        class MyMixin:
191
192            @declared_attr
193            def __tablename__(cls):
194                return cls.__name__.lower()
195            id = Column(Integer, primary_key=True)
196
197        class MyModel(Base, MyMixin):
198            __tablename__ = 'overridden'
199
200        eq_(MyModel.__table__.name, 'overridden')
201
202    def test_table_name_inheritance_order(self):
203
204        class MyMixin1:
205
206            @declared_attr
207            def __tablename__(cls):
208                return cls.__name__.lower() + '1'
209
210        class MyMixin2:
211
212            @declared_attr
213            def __tablename__(cls):
214                return cls.__name__.lower() + '2'
215
216        class MyModel(Base, MyMixin1, MyMixin2):
217            id = Column(Integer, primary_key=True)
218
219        eq_(MyModel.__table__.name, 'mymodel1')
220
221    def test_table_name_dependent_on_subclass(self):
222
223        class MyHistoryMixin:
224
225            @declared_attr
226            def __tablename__(cls):
227                return cls.parent_name + '_changelog'
228
229        class MyModel(Base, MyHistoryMixin):
230            parent_name = 'foo'
231            id = Column(Integer, primary_key=True)
232
233        eq_(MyModel.__table__.name, 'foo_changelog')
234
235    def test_table_args_inherited(self):
236
237        class MyMixin:
238            __table_args__ = {'mysql_engine': 'InnoDB'}
239
240        class MyModel(Base, MyMixin):
241            __tablename__ = 'test'
242            id = Column(Integer, primary_key=True)
243
244        eq_(MyModel.__table__.kwargs, {'mysql_engine': 'InnoDB'})
245
246    def test_table_args_inherited_descriptor(self):
247
248        class MyMixin:
249
250            @declared_attr
251            def __table_args__(cls):
252                return {'info': cls.__name__}
253
254        class MyModel(Base, MyMixin):
255            __tablename__ = 'test'
256            id = Column(Integer, primary_key=True)
257
258        eq_(MyModel.__table__.info, 'MyModel')
259
260    def test_table_args_inherited_single_table_inheritance(self):
261
262        class MyMixin:
263            __table_args__ = {'mysql_engine': 'InnoDB'}
264
265        class General(Base, MyMixin):
266            __tablename__ = 'test'
267            id = Column(Integer, primary_key=True)
268            type_ = Column(String(50))
269            __mapper__args = {'polymorphic_on': type_}
270
271        class Specific(General):
272            __mapper_args__ = {'polymorphic_identity': 'specific'}
273
274        assert Specific.__table__ is General.__table__
275        eq_(General.__table__.kwargs, {'mysql_engine': 'InnoDB'})
276
277    def test_columns_single_table_inheritance(self):
278        """Test a column on a mixin with an alternate attribute name,
279        mapped to a superclass and single-table inheritance subclass.
280        The superclass table gets the column, the subclass shares
281        the MapperProperty.
282
283        """
284
285        class MyMixin(object):
286            foo = Column('foo', Integer)
287            bar = Column('bar_newname', Integer)
288
289        class General(Base, MyMixin):
290            __tablename__ = 'test'
291            id = Column(Integer, primary_key=True)
292            type_ = Column(String(50))
293            __mapper__args = {'polymorphic_on': type_}
294
295        class Specific(General):
296            __mapper_args__ = {'polymorphic_identity': 'specific'}
297
298        assert General.bar.prop.columns[0] is General.__table__.c.bar_newname
299        assert len(General.bar.prop.columns) == 1
300        assert Specific.bar.prop is General.bar.prop
301
302    @testing.skip_if(lambda: testing.against('oracle'),
303                     "Test has an empty insert in it at the moment")
304    def test_columns_single_inheritance_conflict_resolution(self):
305        """Test that a declared_attr can return the existing column and it will
306        be ignored.  this allows conditional columns to be added.
307
308        See [ticket:2472].
309
310        """
311        class Person(Base):
312            __tablename__ = 'person'
313            id = Column(Integer, primary_key=True)
314
315        class Mixin(object):
316
317            @declared_attr
318            def target_id(cls):
319                return cls.__table__.c.get(
320                    'target_id',
321                    Column(Integer, ForeignKey('other.id'))
322                )
323
324            @declared_attr
325            def target(cls):
326                return relationship("Other")
327
328        class Engineer(Mixin, Person):
329
330            """single table inheritance"""
331
332        class Manager(Mixin, Person):
333
334            """single table inheritance"""
335
336        class Other(Base):
337            __tablename__ = 'other'
338            id = Column(Integer, primary_key=True)
339
340        is_(
341            Engineer.target_id.property.columns[0],
342            Person.__table__.c.target_id
343        )
344        is_(
345            Manager.target_id.property.columns[0],
346            Person.__table__.c.target_id
347        )
348        # do a brief round trip on this
349        Base.metadata.create_all()
350        session = Session()
351        o1, o2 = Other(), Other()
352        session.add_all([
353            Engineer(target=o1),
354            Manager(target=o2),
355            Manager(target=o1)
356        ])
357        session.commit()
358        eq_(session.query(Engineer).first().target, o1)
359
360    def test_columns_joined_table_inheritance(self):
361        """Test a column on a mixin with an alternate attribute name,
362        mapped to a superclass and joined-table inheritance subclass.
363        Both tables get the column, in the case of the subclass the two
364        columns are joined under one MapperProperty.
365
366        """
367
368        class MyMixin(object):
369            foo = Column('foo', Integer)
370            bar = Column('bar_newname', Integer)
371
372        class General(Base, MyMixin):
373            __tablename__ = 'test'
374            id = Column(Integer, primary_key=True)
375            type_ = Column(String(50))
376            __mapper__args = {'polymorphic_on': type_}
377
378        class Specific(General):
379            __tablename__ = 'sub'
380            id = Column(Integer, ForeignKey('test.id'), primary_key=True)
381            __mapper_args__ = {'polymorphic_identity': 'specific'}
382
383        assert General.bar.prop.columns[0] is General.__table__.c.bar_newname
384        assert len(General.bar.prop.columns) == 1
385        assert Specific.bar.prop is General.bar.prop
386        eq_(len(Specific.bar.prop.columns), 1)
387        assert Specific.bar.prop.columns[0] is General.__table__.c.bar_newname
388
389    def test_column_join_checks_superclass_type(self):
390        """Test that the logic which joins subclass props to those
391        of the superclass checks that the superclass property is a column.
392
393        """
394
395        class General(Base):
396            __tablename__ = 'test'
397            id = Column(Integer, primary_key=True)
398            general_id = Column(Integer, ForeignKey('test.id'))
399            type_ = relationship("General")
400
401        class Specific(General):
402            __tablename__ = 'sub'
403            id = Column(Integer, ForeignKey('test.id'), primary_key=True)
404            type_ = Column('foob', String(50))
405
406        assert isinstance(General.type_.property, sa.orm.RelationshipProperty)
407        assert Specific.type_.property.columns[0] is Specific.__table__.c.foob
408
409    def test_column_join_checks_subclass_type(self):
410        """Test that the logic which joins subclass props to those
411        of the superclass checks that the subclass property is a column.
412
413        """
414
415        def go():
416            class General(Base):
417                __tablename__ = 'test'
418                id = Column(Integer, primary_key=True)
419                type_ = Column('foob', Integer)
420
421            class Specific(General):
422                __tablename__ = 'sub'
423                id = Column(Integer, ForeignKey('test.id'), primary_key=True)
424                specific_id = Column(Integer, ForeignKey('sub.id'))
425                type_ = relationship("Specific")
426        assert_raises_message(
427            sa.exc.ArgumentError, "column 'foob' conflicts with property", go
428        )
429
430    def test_table_args_overridden(self):
431
432        class MyMixin:
433            __table_args__ = {'mysql_engine': 'Foo'}
434
435        class MyModel(Base, MyMixin):
436            __tablename__ = 'test'
437            __table_args__ = {'mysql_engine': 'InnoDB'}
438            id = Column(Integer, primary_key=True)
439
440        eq_(MyModel.__table__.kwargs, {'mysql_engine': 'InnoDB'})
441
442    @testing.teardown_events(orm_events.MapperEvents)
443    def test_declare_first_mixin(self):
444        canary = mock.Mock()
445
446        class MyMixin(object):
447            @classmethod
448            def __declare_first__(cls):
449                canary.declare_first__(cls)
450
451            @classmethod
452            def __declare_last__(cls):
453                canary.declare_last__(cls)
454
455        class MyModel(Base, MyMixin):
456            __tablename__ = 'test'
457            id = Column(Integer, primary_key=True)
458
459        configure_mappers()
460
461        eq_(
462            canary.mock_calls,
463            [
464                mock.call.declare_first__(MyModel),
465                mock.call.declare_last__(MyModel),
466            ]
467        )
468
469    @testing.teardown_events(orm_events.MapperEvents)
470    def test_declare_first_base(self):
471        canary = mock.Mock()
472
473        class MyMixin(object):
474            @classmethod
475            def __declare_first__(cls):
476                canary.declare_first__(cls)
477
478            @classmethod
479            def __declare_last__(cls):
480                canary.declare_last__(cls)
481
482        class Base(MyMixin):
483            pass
484        Base = declarative_base(cls=Base)
485
486        class MyModel(Base):
487            __tablename__ = 'test'
488            id = Column(Integer, primary_key=True)
489
490        configure_mappers()
491
492        eq_(
493            canary.mock_calls,
494            [
495                mock.call.declare_first__(MyModel),
496                mock.call.declare_last__(MyModel),
497            ]
498        )
499
500    @testing.teardown_events(orm_events.MapperEvents)
501    def test_declare_first_direct(self):
502        canary = mock.Mock()
503
504        class MyOtherModel(Base):
505            __tablename__ = 'test2'
506            id = Column(Integer, primary_key=True)
507
508            @classmethod
509            def __declare_first__(cls):
510                canary.declare_first__(cls)
511
512            @classmethod
513            def __declare_last__(cls):
514                canary.declare_last__(cls)
515
516        configure_mappers()
517
518        eq_(
519            canary.mock_calls,
520            [
521                mock.call.declare_first__(MyOtherModel),
522                mock.call.declare_last__(MyOtherModel)
523            ]
524        )
525
526    def test_mapper_args_declared_attr(self):
527
528        class ComputedMapperArgs:
529
530            @declared_attr
531            def __mapper_args__(cls):
532                if cls.__name__ == 'Person':
533                    return {'polymorphic_on': cls.discriminator}
534                else:
535                    return {'polymorphic_identity': cls.__name__}
536
537        class Person(Base, ComputedMapperArgs):
538            __tablename__ = 'people'
539            id = Column(Integer, primary_key=True)
540            discriminator = Column('type', String(50))
541
542        class Engineer(Person):
543            pass
544
545        configure_mappers()
546        assert class_mapper(Person).polymorphic_on \
547            is Person.__table__.c.type
548        eq_(class_mapper(Engineer).polymorphic_identity, 'Engineer')
549
550    def test_mapper_args_declared_attr_two(self):
551
552        # same as test_mapper_args_declared_attr, but we repeat
553        # ComputedMapperArgs on both classes for no apparent reason.
554
555        class ComputedMapperArgs:
556
557            @declared_attr
558            def __mapper_args__(cls):
559                if cls.__name__ == 'Person':
560                    return {'polymorphic_on': cls.discriminator}
561                else:
562                    return {'polymorphic_identity': cls.__name__}
563
564        class Person(Base, ComputedMapperArgs):
565
566            __tablename__ = 'people'
567            id = Column(Integer, primary_key=True)
568            discriminator = Column('type', String(50))
569
570        class Engineer(Person, ComputedMapperArgs):
571            pass
572
573        configure_mappers()
574        assert class_mapper(Person).polymorphic_on \
575            is Person.__table__.c.type
576        eq_(class_mapper(Engineer).polymorphic_identity, 'Engineer')
577
578    def test_table_args_composite(self):
579
580        class MyMixin1:
581
582            __table_args__ = {'info': {'baz': 'bob'}}
583
584        class MyMixin2:
585
586            __table_args__ = {'info': {'foo': 'bar'}}
587
588        class MyModel(Base, MyMixin1, MyMixin2):
589
590            __tablename__ = 'test'
591
592            @declared_attr
593            def __table_args__(self):
594                info = {}
595                args = dict(info=info)
596                info.update(MyMixin1.__table_args__['info'])
597                info.update(MyMixin2.__table_args__['info'])
598                return args
599            id = Column(Integer, primary_key=True)
600
601        eq_(MyModel.__table__.info, {'foo': 'bar', 'baz': 'bob'})
602
603    def test_mapper_args_inherited(self):
604
605        class MyMixin:
606
607            __mapper_args__ = {'always_refresh': True}
608
609        class MyModel(Base, MyMixin):
610
611            __tablename__ = 'test'
612            id = Column(Integer, primary_key=True)
613
614        eq_(MyModel.__mapper__.always_refresh, True)
615
616    def test_mapper_args_inherited_descriptor(self):
617
618        class MyMixin:
619
620            @declared_attr
621            def __mapper_args__(cls):
622
623                # tenuous, but illustrates the problem!
624
625                if cls.__name__ == 'MyModel':
626                    return dict(always_refresh=True)
627                else:
628                    return dict(always_refresh=False)
629
630        class MyModel(Base, MyMixin):
631
632            __tablename__ = 'test'
633            id = Column(Integer, primary_key=True)
634
635        eq_(MyModel.__mapper__.always_refresh, True)
636
637    def test_mapper_args_polymorphic_on_inherited(self):
638
639        class MyMixin:
640
641            type_ = Column(String(50))
642            __mapper_args__ = {'polymorphic_on': type_}
643
644        class MyModel(Base, MyMixin):
645
646            __tablename__ = 'test'
647            id = Column(Integer, primary_key=True)
648
649        col = MyModel.__mapper__.polymorphic_on
650        eq_(col.name, 'type_')
651        assert col.table is not None
652
653    def test_mapper_args_overridden(self):
654
655        class MyMixin:
656
657            __mapper_args__ = dict(always_refresh=True)
658
659        class MyModel(Base, MyMixin):
660
661            __tablename__ = 'test'
662            __mapper_args__ = dict(always_refresh=False)
663            id = Column(Integer, primary_key=True)
664
665        eq_(MyModel.__mapper__.always_refresh, False)
666
667    def test_mapper_args_composite(self):
668
669        class MyMixin1:
670
671            type_ = Column(String(50))
672            __mapper_args__ = {'polymorphic_on': type_}
673
674        class MyMixin2:
675
676            __mapper_args__ = {'always_refresh': True}
677
678        class MyModel(Base, MyMixin1, MyMixin2):
679
680            __tablename__ = 'test'
681
682            @declared_attr
683            def __mapper_args__(cls):
684                args = {}
685                args.update(MyMixin1.__mapper_args__)
686                args.update(MyMixin2.__mapper_args__)
687                if cls.__name__ != 'MyModel':
688                    args.pop('polymorphic_on')
689                    args['polymorphic_identity'] = cls.__name__
690
691                return args
692            id = Column(Integer, primary_key=True)
693
694        class MySubModel(MyModel):
695            pass
696
697        eq_(
698            MyModel.__mapper__.polymorphic_on.name,
699            'type_'
700        )
701        assert MyModel.__mapper__.polymorphic_on.table is not None
702        eq_(MyModel.__mapper__.always_refresh, True)
703        eq_(MySubModel.__mapper__.always_refresh, True)
704        eq_(MySubModel.__mapper__.polymorphic_identity, 'MySubModel')
705
706    def test_mapper_args_property(self):
707        class MyModel(Base):
708
709            @declared_attr
710            def __tablename__(cls):
711                return cls.__name__.lower()
712
713            @declared_attr
714            def __table_args__(cls):
715                return {'mysql_engine': 'InnoDB'}
716
717            @declared_attr
718            def __mapper_args__(cls):
719                args = {}
720                args['polymorphic_identity'] = cls.__name__
721                return args
722            id = Column(Integer, primary_key=True)
723
724        class MySubModel(MyModel):
725            id = Column(Integer, ForeignKey('mymodel.id'), primary_key=True)
726
727        class MySubModel2(MyModel):
728            __tablename__ = 'sometable'
729            id = Column(Integer, ForeignKey('mymodel.id'), primary_key=True)
730
731        eq_(MyModel.__mapper__.polymorphic_identity, 'MyModel')
732        eq_(MySubModel.__mapper__.polymorphic_identity, 'MySubModel')
733        eq_(MyModel.__table__.kwargs['mysql_engine'], 'InnoDB')
734        eq_(MySubModel.__table__.kwargs['mysql_engine'], 'InnoDB')
735        eq_(MySubModel2.__table__.kwargs['mysql_engine'], 'InnoDB')
736        eq_(MyModel.__table__.name, 'mymodel')
737        eq_(MySubModel.__table__.name, 'mysubmodel')
738
739    def test_mapper_args_custom_base(self):
740        """test the @declared_attr approach from a custom base."""
741
742        class Base(object):
743
744            @declared_attr
745            def __tablename__(cls):
746                return cls.__name__.lower()
747
748            @declared_attr
749            def __table_args__(cls):
750                return {'mysql_engine': 'InnoDB'}
751
752            @declared_attr
753            def id(self):
754                return Column(Integer, primary_key=True)
755
756        Base = decl.declarative_base(cls=Base)
757
758        class MyClass(Base):
759            pass
760
761        class MyOtherClass(Base):
762            pass
763
764        eq_(MyClass.__table__.kwargs['mysql_engine'], 'InnoDB')
765        eq_(MyClass.__table__.name, 'myclass')
766        eq_(MyOtherClass.__table__.name, 'myotherclass')
767        assert MyClass.__table__.c.id.table is MyClass.__table__
768        assert MyOtherClass.__table__.c.id.table is MyOtherClass.__table__
769
770    def test_single_table_no_propagation(self):
771
772        class IdColumn:
773
774            id = Column(Integer, primary_key=True)
775
776        class Generic(Base, IdColumn):
777
778            __tablename__ = 'base'
779            discriminator = Column('type', String(50))
780            __mapper_args__ = dict(polymorphic_on=discriminator)
781            value = Column(Integer())
782
783        class Specific(Generic):
784
785            __mapper_args__ = dict(polymorphic_identity='specific')
786
787        assert Specific.__table__ is Generic.__table__
788        eq_(list(Generic.__table__.c.keys()), ['id', 'type', 'value'])
789        assert class_mapper(Specific).polymorphic_on \
790            is Generic.__table__.c.type
791        eq_(class_mapper(Specific).polymorphic_identity, 'specific')
792
793    def test_joined_table_propagation(self):
794
795        class CommonMixin:
796
797            @declared_attr
798            def __tablename__(cls):
799                return cls.__name__.lower()
800            __table_args__ = {'mysql_engine': 'InnoDB'}
801            timestamp = Column(Integer)
802            id = Column(Integer, primary_key=True)
803
804        class Generic(Base, CommonMixin):
805
806            discriminator = Column('python_type', String(50))
807            __mapper_args__ = dict(polymorphic_on=discriminator)
808
809        class Specific(Generic):
810
811            __mapper_args__ = dict(polymorphic_identity='specific')
812            id = Column(Integer, ForeignKey('generic.id'),
813                        primary_key=True)
814
815        eq_(Generic.__table__.name, 'generic')
816        eq_(Specific.__table__.name, 'specific')
817        eq_(list(Generic.__table__.c.keys()), ['timestamp', 'id',
818                                               'python_type'])
819        eq_(list(Specific.__table__.c.keys()), ['id'])
820        eq_(Generic.__table__.kwargs, {'mysql_engine': 'InnoDB'})
821        eq_(Specific.__table__.kwargs, {'mysql_engine': 'InnoDB'})
822
823    def test_some_propagation(self):
824
825        class CommonMixin:
826
827            @declared_attr
828            def __tablename__(cls):
829                return cls.__name__.lower()
830            __table_args__ = {'mysql_engine': 'InnoDB'}
831            timestamp = Column(Integer)
832
833        class BaseType(Base, CommonMixin):
834
835            discriminator = Column('type', String(50))
836            __mapper_args__ = dict(polymorphic_on=discriminator)
837            id = Column(Integer, primary_key=True)
838            value = Column(Integer())
839
840        class Single(BaseType):
841
842            __tablename__ = None
843            __mapper_args__ = dict(polymorphic_identity='type1')
844
845        class Joined(BaseType):
846
847            __mapper_args__ = dict(polymorphic_identity='type2')
848            id = Column(Integer, ForeignKey('basetype.id'),
849                        primary_key=True)
850
851        eq_(BaseType.__table__.name, 'basetype')
852        eq_(list(BaseType.__table__.c.keys()), ['timestamp', 'type', 'id',
853                                                'value'])
854        eq_(BaseType.__table__.kwargs, {'mysql_engine': 'InnoDB'})
855        assert Single.__table__ is BaseType.__table__
856        eq_(Joined.__table__.name, 'joined')
857        eq_(list(Joined.__table__.c.keys()), ['id'])
858        eq_(Joined.__table__.kwargs, {'mysql_engine': 'InnoDB'})
859
860    def test_col_copy_vs_declared_attr_joined_propagation(self):
861        class Mixin(object):
862            a = Column(Integer)
863
864            @declared_attr
865            def b(cls):
866                return Column(Integer)
867
868        class A(Mixin, Base):
869            __tablename__ = 'a'
870            id = Column(Integer, primary_key=True)
871
872        class B(A):
873            __tablename__ = 'b'
874            id = Column(Integer, ForeignKey('a.id'), primary_key=True)
875
876        assert 'a' in A.__table__.c
877        assert 'b' in A.__table__.c
878        assert 'a' not in B.__table__.c
879        assert 'b' not in B.__table__.c
880
881    def test_col_copy_vs_declared_attr_joined_propagation_newname(self):
882        class Mixin(object):
883            a = Column('a1', Integer)
884
885            @declared_attr
886            def b(cls):
887                return Column('b1', Integer)
888
889        class A(Mixin, Base):
890            __tablename__ = 'a'
891            id = Column(Integer, primary_key=True)
892
893        class B(A):
894            __tablename__ = 'b'
895            id = Column(Integer, ForeignKey('a.id'), primary_key=True)
896
897        assert 'a1' in A.__table__.c
898        assert 'b1' in A.__table__.c
899        assert 'a1' not in B.__table__.c
900        assert 'b1' not in B.__table__.c
901
902    def test_col_copy_vs_declared_attr_single_propagation(self):
903        class Mixin(object):
904            a = Column(Integer)
905
906            @declared_attr
907            def b(cls):
908                return Column(Integer)
909
910        class A(Mixin, Base):
911            __tablename__ = 'a'
912            id = Column(Integer, primary_key=True)
913
914        class B(A):
915            pass
916
917        assert 'a' in A.__table__.c
918        assert 'b' in A.__table__.c
919
920    def test_non_propagating_mixin(self):
921
922        class NoJoinedTableNameMixin:
923
924            @declared_attr
925            def __tablename__(cls):
926                if decl.has_inherited_table(cls):
927                    return None
928                return cls.__name__.lower()
929
930        class BaseType(Base, NoJoinedTableNameMixin):
931
932            discriminator = Column('type', String(50))
933            __mapper_args__ = dict(polymorphic_on=discriminator)
934            id = Column(Integer, primary_key=True)
935            value = Column(Integer())
936
937        class Specific(BaseType):
938
939            __mapper_args__ = dict(polymorphic_identity='specific')
940
941        eq_(BaseType.__table__.name, 'basetype')
942        eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'value'])
943        assert Specific.__table__ is BaseType.__table__
944        assert class_mapper(Specific).polymorphic_on \
945            is BaseType.__table__.c.type
946        eq_(class_mapper(Specific).polymorphic_identity, 'specific')
947
948    def test_non_propagating_mixin_used_for_joined(self):
949
950        class TableNameMixin:
951
952            @declared_attr
953            def __tablename__(cls):
954                if decl.has_inherited_table(cls) and TableNameMixin \
955                        not in cls.__bases__:
956                    return None
957                return cls.__name__.lower()
958
959        class BaseType(Base, TableNameMixin):
960
961            discriminator = Column('type', String(50))
962            __mapper_args__ = dict(polymorphic_on=discriminator)
963            id = Column(Integer, primary_key=True)
964            value = Column(Integer())
965
966        class Specific(BaseType, TableNameMixin):
967
968            __mapper_args__ = dict(polymorphic_identity='specific')
969            id = Column(Integer, ForeignKey('basetype.id'),
970                        primary_key=True)
971
972        eq_(BaseType.__table__.name, 'basetype')
973        eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'value'])
974        eq_(Specific.__table__.name, 'specific')
975        eq_(list(Specific.__table__.c.keys()), ['id'])
976
977    def test_single_back_propagate(self):
978
979        class ColumnMixin:
980
981            timestamp = Column(Integer)
982
983        class BaseType(Base):
984
985            __tablename__ = 'foo'
986            discriminator = Column('type', String(50))
987            __mapper_args__ = dict(polymorphic_on=discriminator)
988            id = Column(Integer, primary_key=True)
989
990        class Specific(BaseType, ColumnMixin):
991
992            __mapper_args__ = dict(polymorphic_identity='specific')
993
994        eq_(list(BaseType.__table__.c.keys()), ['type', 'id', 'timestamp'])
995
996    def test_table_in_model_and_same_column_in_mixin(self):
997
998        class ColumnMixin:
999
1000            data = Column(Integer)
1001
1002        class Model(Base, ColumnMixin):
1003
1004            __table__ = Table('foo', Base.metadata,
1005                              Column('data', Integer),
1006                              Column('id', Integer, primary_key=True))
1007
1008        model_col = Model.__table__.c.data
1009        mixin_col = ColumnMixin.data
1010        assert model_col is not mixin_col
1011        eq_(model_col.name, 'data')
1012        assert model_col.type.__class__ is mixin_col.type.__class__
1013
1014    def test_table_in_model_and_different_named_column_in_mixin(self):
1015
1016        class ColumnMixin:
1017            tada = Column(Integer)
1018
1019        def go():
1020
1021            class Model(Base, ColumnMixin):
1022
1023                __table__ = Table('foo', Base.metadata,
1024                                  Column('data', Integer),
1025                                  Column('id', Integer, primary_key=True))
1026                foo = relationship("Dest")
1027
1028        assert_raises_message(sa.exc.ArgumentError,
1029                              "Can't add additional column 'tada' when "
1030                              "specifying __table__", go)
1031
1032    def test_table_in_model_and_different_named_alt_key_column_in_mixin(self):
1033
1034        # here, the __table__ has a column 'tada'.  We disallow
1035        # the add of the 'foobar' column, even though it's
1036        # keyed to 'tada'.
1037
1038        class ColumnMixin:
1039            tada = Column('foobar', Integer)
1040
1041        def go():
1042
1043            class Model(Base, ColumnMixin):
1044
1045                __table__ = Table('foo', Base.metadata,
1046                                  Column('data', Integer),
1047                                  Column('tada', Integer),
1048                                  Column('id', Integer, primary_key=True))
1049                foo = relationship("Dest")
1050
1051        assert_raises_message(sa.exc.ArgumentError,
1052                              "Can't add additional column 'foobar' when "
1053                              "specifying __table__", go)
1054
1055    def test_table_in_model_overrides_different_typed_column_in_mixin(self):
1056
1057        class ColumnMixin:
1058
1059            data = Column(String)
1060
1061        class Model(Base, ColumnMixin):
1062
1063            __table__ = Table('foo', Base.metadata,
1064                              Column('data', Integer),
1065                              Column('id', Integer, primary_key=True))
1066
1067        model_col = Model.__table__.c.data
1068        mixin_col = ColumnMixin.data
1069        assert model_col is not mixin_col
1070        eq_(model_col.name, 'data')
1071        assert model_col.type.__class__ is Integer
1072
1073    def test_mixin_column_ordering(self):
1074
1075        class Foo(object):
1076
1077            col1 = Column(Integer)
1078            col3 = Column(Integer)
1079
1080        class Bar(object):
1081
1082            col2 = Column(Integer)
1083            col4 = Column(Integer)
1084
1085        class Model(Base, Foo, Bar):
1086
1087            id = Column(Integer, primary_key=True)
1088            __tablename__ = 'model'
1089
1090        eq_(list(Model.__table__.c.keys()), ['col1', 'col3', 'col2', 'col4',
1091                                             'id'])
1092
1093    def test_honor_class_mro_one(self):
1094        class HasXMixin(object):
1095
1096            @declared_attr
1097            def x(self):
1098                return Column(Integer)
1099
1100        class Parent(HasXMixin, Base):
1101            __tablename__ = 'parent'
1102            id = Column(Integer, primary_key=True)
1103
1104        class Child(Parent):
1105            __tablename__ = 'child'
1106            id = Column(Integer, ForeignKey('parent.id'), primary_key=True)
1107
1108        assert "x" not in Child.__table__.c
1109
1110    def test_honor_class_mro_two(self):
1111        class HasXMixin(object):
1112
1113            @declared_attr
1114            def x(self):
1115                return Column(Integer)
1116
1117        class Parent(HasXMixin, Base):
1118            __tablename__ = 'parent'
1119            id = Column(Integer, primary_key=True)
1120
1121            def x(self):
1122                return "hi"
1123
1124        class C(Parent):
1125            __tablename__ = 'c'
1126            id = Column(Integer, ForeignKey('parent.id'), primary_key=True)
1127
1128        assert C().x() == 'hi'
1129
1130    def test_arbitrary_attrs_one(self):
1131        class HasMixin(object):
1132
1133            @declared_attr
1134            def some_attr(cls):
1135                return cls.__name__ + "SOME ATTR"
1136
1137        class Mapped(HasMixin, Base):
1138            __tablename__ = 't'
1139            id = Column(Integer, primary_key=True)
1140
1141        eq_(Mapped.some_attr, "MappedSOME ATTR")
1142        eq_(Mapped.__dict__['some_attr'], "MappedSOME ATTR")
1143
1144    def test_arbitrary_attrs_two(self):
1145        from sqlalchemy.ext.associationproxy import association_proxy
1146
1147        class FilterA(Base):
1148            __tablename__ = 'filter_a'
1149            id = Column(Integer(), primary_key=True)
1150            parent_id = Column(Integer(),
1151                               ForeignKey('type_a.id'))
1152            filter = Column(String())
1153
1154            def __init__(self, filter_, **kw):
1155                self.filter = filter_
1156
1157        class FilterB(Base):
1158            __tablename__ = 'filter_b'
1159            id = Column(Integer(), primary_key=True)
1160            parent_id = Column(Integer(),
1161                               ForeignKey('type_b.id'))
1162            filter = Column(String())
1163
1164            def __init__(self, filter_, **kw):
1165                self.filter = filter_
1166
1167        class FilterMixin(object):
1168
1169            @declared_attr
1170            def _filters(cls):
1171                return relationship(cls.filter_class,
1172                                    cascade='all,delete,delete-orphan')
1173
1174            @declared_attr
1175            def filters(cls):
1176                return association_proxy('_filters', 'filter')
1177
1178        class TypeA(Base, FilterMixin):
1179            __tablename__ = 'type_a'
1180            filter_class = FilterA
1181            id = Column(Integer(), primary_key=True)
1182
1183        class TypeB(Base, FilterMixin):
1184            __tablename__ = 'type_b'
1185            filter_class = FilterB
1186            id = Column(Integer(), primary_key=True)
1187
1188        TypeA(filters=['foo'])
1189        TypeB(filters=['foo'])
1190
1191
1192class DeclarativeMixinPropertyTest(DeclarativeTestBase):
1193
1194    def test_column_property(self):
1195
1196        class MyMixin(object):
1197
1198            @declared_attr
1199            def prop_hoho(cls):
1200                return column_property(Column('prop', String(50)))
1201
1202        class MyModel(Base, MyMixin):
1203
1204            __tablename__ = 'test'
1205            id = Column(Integer, primary_key=True,
1206                        test_needs_autoincrement=True)
1207
1208        class MyOtherModel(Base, MyMixin):
1209
1210            __tablename__ = 'othertest'
1211            id = Column(Integer, primary_key=True,
1212                        test_needs_autoincrement=True)
1213
1214        assert MyModel.__table__.c.prop is not None
1215        assert MyOtherModel.__table__.c.prop is not None
1216        assert MyModel.__table__.c.prop \
1217            is not MyOtherModel.__table__.c.prop
1218        assert MyModel.prop_hoho.property.columns \
1219            == [MyModel.__table__.c.prop]
1220        assert MyOtherModel.prop_hoho.property.columns \
1221            == [MyOtherModel.__table__.c.prop]
1222        assert MyModel.prop_hoho.property \
1223            is not MyOtherModel.prop_hoho.property
1224        Base.metadata.create_all()
1225        sess = create_session()
1226        m1, m2 = MyModel(prop_hoho='foo'), MyOtherModel(prop_hoho='bar')
1227        sess.add_all([m1, m2])
1228        sess.flush()
1229        eq_(sess.query(MyModel).filter(MyModel.prop_hoho == 'foo'
1230                                       ).one(), m1)
1231        eq_(sess.query(MyOtherModel).filter(MyOtherModel.prop_hoho
1232                                            == 'bar').one(), m2)
1233
1234    def test_doc(self):
1235        """test documentation transfer.
1236
1237        the documentation situation with @declared_attr is problematic.
1238        at least see if mapped subclasses get the doc.
1239
1240        """
1241
1242        class MyMixin(object):
1243
1244            @declared_attr
1245            def type_(cls):
1246                """this is a document."""
1247
1248                return Column(String(50))
1249
1250            @declared_attr
1251            def t2(cls):
1252                """this is another document."""
1253
1254                return column_property(Column(String(50)))
1255
1256        class MyModel(Base, MyMixin):
1257
1258            __tablename__ = 'test'
1259            id = Column(Integer, primary_key=True)
1260
1261        configure_mappers()
1262        eq_(MyModel.type_.__doc__, """this is a document.""")
1263        eq_(MyModel.t2.__doc__, """this is another document.""")
1264
1265    def test_column_in_mapper_args(self):
1266
1267        class MyMixin(object):
1268
1269            @declared_attr
1270            def type_(cls):
1271                return Column(String(50))
1272            __mapper_args__ = {'polymorphic_on': type_}
1273
1274        class MyModel(Base, MyMixin):
1275
1276            __tablename__ = 'test'
1277            id = Column(Integer, primary_key=True)
1278
1279        configure_mappers()
1280        col = MyModel.__mapper__.polymorphic_on
1281        eq_(col.name, 'type_')
1282        assert col.table is not None
1283
1284    def test_column_in_mapper_args_used_multiple_times(self):
1285
1286        class MyMixin(object):
1287
1288            version_id = Column(Integer)
1289            __mapper_args__ = {'version_id_col': version_id}
1290
1291        class ModelOne(Base, MyMixin):
1292
1293            __tablename__ = 'm1'
1294            id = Column(Integer, primary_key=True)
1295
1296        class ModelTwo(Base, MyMixin):
1297
1298            __tablename__ = 'm2'
1299            id = Column(Integer, primary_key=True)
1300
1301        is_(
1302            ModelOne.__mapper__.version_id_col,
1303            ModelOne.__table__.c.version_id
1304        )
1305        is_(
1306            ModelTwo.__mapper__.version_id_col,
1307            ModelTwo.__table__.c.version_id
1308        )
1309
1310    def test_deferred(self):
1311
1312        class MyMixin(object):
1313
1314            @declared_attr
1315            def data(cls):
1316                return deferred(Column('data', String(50)))
1317
1318        class MyModel(Base, MyMixin):
1319
1320            __tablename__ = 'test'
1321            id = Column(Integer, primary_key=True,
1322                        test_needs_autoincrement=True)
1323
1324        Base.metadata.create_all()
1325        sess = create_session()
1326        sess.add_all([MyModel(data='d1'), MyModel(data='d2')])
1327        sess.flush()
1328        sess.expunge_all()
1329        d1, d2 = sess.query(MyModel).order_by(MyModel.data)
1330        assert 'data' not in d1.__dict__
1331        assert d1.data == 'd1'
1332        assert 'data' in d1.__dict__
1333
1334    def _test_relationship(self, usestring):
1335
1336        class RefTargetMixin(object):
1337
1338            @declared_attr
1339            def target_id(cls):
1340                return Column('target_id', ForeignKey('target.id'))
1341            if usestring:
1342
1343                @declared_attr
1344                def target(cls):
1345                    return relationship('Target',
1346                                        primaryjoin='Target.id==%s.target_id'
1347                                        % cls.__name__)
1348            else:
1349
1350                @declared_attr
1351                def target(cls):
1352                    return relationship('Target')
1353
1354        class Foo(Base, RefTargetMixin):
1355
1356            __tablename__ = 'foo'
1357            id = Column(Integer, primary_key=True,
1358                        test_needs_autoincrement=True)
1359
1360        class Bar(Base, RefTargetMixin):
1361
1362            __tablename__ = 'bar'
1363            id = Column(Integer, primary_key=True,
1364                        test_needs_autoincrement=True)
1365
1366        class Target(Base):
1367
1368            __tablename__ = 'target'
1369            id = Column(Integer, primary_key=True,
1370                        test_needs_autoincrement=True)
1371
1372        Base.metadata.create_all()
1373        sess = create_session()
1374        t1, t2 = Target(), Target()
1375        f1, f2, b1 = Foo(target=t1), Foo(target=t2), Bar(target=t1)
1376        sess.add_all([f1, f2, b1])
1377        sess.flush()
1378        eq_(sess.query(Foo).filter(Foo.target == t2).one(), f2)
1379        eq_(sess.query(Bar).filter(Bar.target == t2).first(), None)
1380        sess.expire_all()
1381        eq_(f1.target, t1)
1382
1383    def test_relationship(self):
1384        self._test_relationship(False)
1385
1386    def test_relationship_primryjoin(self):
1387        self._test_relationship(True)
1388
1389
1390class DeclaredAttrTest(DeclarativeTestBase, testing.AssertsCompiledSQL):
1391    __dialect__ = 'default'
1392
1393    def test_singleton_behavior_within_decl(self):
1394        counter = mock.Mock()
1395
1396        class Mixin(object):
1397            @declared_attr
1398            def my_prop(cls):
1399                counter(cls)
1400                return Column('x', Integer)
1401
1402        class A(Base, Mixin):
1403            __tablename__ = 'a'
1404            id = Column(Integer, primary_key=True)
1405
1406            @declared_attr
1407            def my_other_prop(cls):
1408                return column_property(cls.my_prop + 5)
1409
1410        eq_(counter.mock_calls, [mock.call(A)])
1411
1412        class B(Base, Mixin):
1413            __tablename__ = 'b'
1414            id = Column(Integer, primary_key=True)
1415
1416            @declared_attr
1417            def my_other_prop(cls):
1418                return column_property(cls.my_prop + 5)
1419
1420        eq_(
1421            counter.mock_calls,
1422            [mock.call(A), mock.call(B)])
1423
1424        # this is why we need singleton-per-class behavior.   We get
1425        # an un-bound "x" column otherwise here, because my_prop() generates
1426        # multiple columns.
1427        a_col = A.my_other_prop.__clause_element__().element.left
1428        b_col = B.my_other_prop.__clause_element__().element.left
1429        is_(a_col.table, A.__table__)
1430        is_(b_col.table, B.__table__)
1431        is_(a_col, A.__table__.c.x)
1432        is_(b_col, B.__table__.c.x)
1433
1434        s = Session()
1435        self.assert_compile(
1436            s.query(A),
1437            "SELECT a.x AS a_x, a.x + :x_1 AS anon_1, a.id AS a_id FROM a"
1438        )
1439        self.assert_compile(
1440            s.query(B),
1441            "SELECT b.x AS b_x, b.x + :x_1 AS anon_1, b.id AS b_id FROM b"
1442        )
1443
1444    @testing.requires.predictable_gc
1445    def test_singleton_gc(self):
1446        counter = mock.Mock()
1447
1448        class Mixin(object):
1449            @declared_attr
1450            def my_prop(cls):
1451                counter(cls.__name__)
1452                return Column('x', Integer)
1453
1454        class A(Base, Mixin):
1455            __tablename__ = 'b'
1456            id = Column(Integer, primary_key=True)
1457
1458            @declared_attr
1459            def my_other_prop(cls):
1460                return column_property(cls.my_prop + 5)
1461
1462        eq_(counter.mock_calls, [mock.call("A")])
1463        del A
1464        gc_collect()
1465        assert "A" not in Base._decl_class_registry
1466
1467    def test_can_we_access_the_mixin_straight(self):
1468        class Mixin(object):
1469            @declared_attr
1470            def my_prop(cls):
1471                return Column('x', Integer)
1472
1473        assert_raises_message(
1474            sa.exc.SAWarning,
1475            "Unmanaged access of declarative attribute my_prop "
1476            "from non-mapped class Mixin",
1477            getattr, Mixin, "my_prop"
1478        )
1479
1480    def test_non_decl_access(self):
1481        counter = mock.Mock()
1482
1483        class Mixin(object):
1484            @declared_attr
1485            def __tablename__(cls):
1486                counter(cls)
1487                return "foo"
1488
1489        class Foo(Mixin, Base):
1490            id = Column(Integer, primary_key=True)
1491
1492            @declared_attr
1493            def x(cls):
1494                cls.__tablename__
1495
1496            @declared_attr
1497            def y(cls):
1498                cls.__tablename__
1499
1500        eq_(
1501            counter.mock_calls,
1502            [mock.call(Foo)]
1503        )
1504
1505        eq_(Foo.__tablename__, 'foo')
1506        eq_(Foo.__tablename__, 'foo')
1507
1508        eq_(
1509            counter.mock_calls,
1510            [mock.call(Foo), mock.call(Foo), mock.call(Foo)]
1511        )
1512
1513    def test_property_noncascade(self):
1514        counter = mock.Mock()
1515
1516        class Mixin(object):
1517            @declared_attr
1518            def my_prop(cls):
1519                counter(cls)
1520                return column_property(cls.x + 2)
1521
1522        class A(Base, Mixin):
1523            __tablename__ = 'a'
1524
1525            id = Column(Integer, primary_key=True)
1526            x = Column(Integer)
1527
1528        class B(A):
1529            pass
1530
1531        eq_(counter.mock_calls, [mock.call(A)])
1532
1533    def test_property_cascade(self):
1534        counter = mock.Mock()
1535
1536        class Mixin(object):
1537            @declared_attr.cascading
1538            def my_prop(cls):
1539                counter(cls)
1540                return column_property(cls.x + 2)
1541
1542        class A(Base, Mixin):
1543            __tablename__ = 'a'
1544
1545            id = Column(Integer, primary_key=True)
1546            x = Column(Integer)
1547
1548        class B(A):
1549            pass
1550
1551        eq_(counter.mock_calls, [mock.call(A), mock.call(B)])
1552
1553    def test_col_prop_attrs_associated_w_class_for_mapper_args(self):
1554        from sqlalchemy import Column
1555        import collections
1556
1557        asserted = collections.defaultdict(set)
1558
1559        class Mixin(object):
1560            @declared_attr.cascading
1561            def my_attr(cls):
1562                if decl.has_inherited_table(cls):
1563                    id = Column(ForeignKey('a.my_attr'), primary_key=True)
1564                    asserted['b'].add(id)
1565                else:
1566                    id = Column(Integer, primary_key=True)
1567                    asserted['a'].add(id)
1568                return id
1569
1570        class A(Base, Mixin):
1571            __tablename__ = 'a'
1572
1573            @declared_attr
1574            def __mapper_args__(cls):
1575                asserted['a'].add(cls.my_attr)
1576                return {}
1577
1578        # here:
1579        # 1. A is mapped.  so A.my_attr is now the InstrumentedAttribute.
1580        # 2. B wants to call my_attr also.  Due to .cascading, it has been
1581        # invoked specific to B, and is present in the dict_ that will
1582        # be used when we map the class.  But except for the
1583        # special setattr() we do in _scan_attributes() in this case, would
1584        # otherwise not been set on the class as anything from this call;
1585        # the usual mechanics of calling it from the descriptor also do not
1586        # work because A is fully mapped and because A set it up, is currently
1587        # that non-expected InstrumentedAttribute and replaces the
1588        # descriptor from being invoked.
1589
1590        class B(A):
1591            __tablename__ = 'b'
1592
1593            @declared_attr
1594            def __mapper_args__(cls):
1595                asserted['b'].add(cls.my_attr)
1596                return {}
1597
1598        eq_(
1599            asserted,
1600            {
1601                'a': set([A.my_attr.property.columns[0]]),
1602                'b': set([B.my_attr.property.columns[0]])
1603            }
1604        )
1605
1606    def test_column_pre_map(self):
1607        counter = mock.Mock()
1608
1609        class Mixin(object):
1610            @declared_attr
1611            def my_col(cls):
1612                counter(cls)
1613                assert not orm_base._mapper_or_none(cls)
1614                return Column('x', Integer)
1615
1616        class A(Base, Mixin):
1617            __tablename__ = 'a'
1618
1619            id = Column(Integer, primary_key=True)
1620
1621        eq_(counter.mock_calls, [mock.call(A)])
1622
1623    def test_mixin_attr_refers_to_column_copies(self):
1624        # this @declared_attr can refer to User.id
1625        # freely because we now do the "copy column" operation
1626        # before the declared_attr is invoked.
1627
1628        counter = mock.Mock()
1629
1630        class HasAddressCount(object):
1631            id = Column(Integer, primary_key=True)
1632
1633            @declared_attr
1634            def address_count(cls):
1635                counter(cls.id)
1636                return column_property(
1637                    select([func.count(Address.id)]).
1638                    where(Address.user_id == cls.id).
1639                    as_scalar()
1640                )
1641
1642        class Address(Base):
1643            __tablename__ = 'address'
1644            id = Column(Integer, primary_key=True)
1645            user_id = Column(ForeignKey('user.id'))
1646
1647        class User(Base, HasAddressCount):
1648            __tablename__ = 'user'
1649
1650        eq_(
1651            counter.mock_calls,
1652            [mock.call(User.id)]
1653        )
1654
1655        sess = Session()
1656        self.assert_compile(
1657            sess.query(User).having(User.address_count > 5),
1658            'SELECT (SELECT count(address.id) AS '
1659            'count_1 FROM address WHERE address.user_id = "user".id) '
1660            'AS anon_1, "user".id AS user_id FROM "user" '
1661            'HAVING (SELECT count(address.id) AS '
1662            'count_1 FROM address WHERE address.user_id = "user".id) '
1663            '> :param_1'
1664        )
1665
1666
1667class AbstractTest(DeclarativeTestBase):
1668
1669    def test_abstract_boolean(self):
1670
1671        class A(Base):
1672            __abstract__ = True
1673            __tablename__ = 'x'
1674            id = Column(Integer, primary_key=True)
1675
1676        class B(Base):
1677            __abstract__ = False
1678            __tablename__ = 'y'
1679            id = Column(Integer, primary_key=True)
1680
1681        class C(Base):
1682            __abstract__ = False
1683            __tablename__ = 'z'
1684            id = Column(Integer, primary_key=True)
1685
1686        class D(Base):
1687            __tablename__ = 'q'
1688            id = Column(Integer, primary_key=True)
1689
1690        eq_(set(Base.metadata.tables), set(['y', 'z', 'q']))
1691
1692    def test_middle_abstract_attributes(self):
1693        # test for [ticket:3219]
1694        class A(Base):
1695            __tablename__ = 'a'
1696
1697            id = Column(Integer, primary_key=True)
1698            name = Column(String)
1699
1700        class B(A):
1701            __abstract__ = True
1702            data = Column(String)
1703
1704        class C(B):
1705            c_value = Column(String)
1706
1707        eq_(
1708            sa.inspect(C).attrs.keys(), ['id', 'name', 'data', 'c_value']
1709        )
1710
1711    def test_middle_abstract_inherits(self):
1712        # test for [ticket:3240]
1713
1714        class A(Base):
1715            __tablename__ = 'a'
1716            id = Column(Integer, primary_key=True)
1717
1718        class AAbs(A):
1719            __abstract__ = True
1720
1721        class B1(A):
1722            __tablename__ = 'b1'
1723            id = Column(ForeignKey('a.id'), primary_key=True)
1724
1725        class B2(AAbs):
1726            __tablename__ = 'b2'
1727            id = Column(ForeignKey('a.id'), primary_key=True)
1728
1729        assert B1.__mapper__.inherits is A.__mapper__
1730
1731        assert B2.__mapper__.inherits is A.__mapper__
1732