]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- a new super-small "declarative" extension has been added,
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 10 Mar 2008 17:14:08 +0000 (17:14 +0000)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 10 Mar 2008 17:14:08 +0000 (17:14 +0000)
which allows Table and mapper() configuration to take place
inline underneath a class declaration.  This extension differs
from ActiveMapper and Elixir in that it does not redefine
any SQLAlchemy semantics at all; literal Column, Table
and relation() constructs are used to define the class
behavior and table definition.

CHANGES
doc/build/content/ormtutorial.txt
doc/build/content/plugins.txt
doc/build/gen_docstrings.py
lib/sqlalchemy/ext/declarative.py [new file with mode: 0644]
test/ext/alltests.py
test/ext/declarative.py [new file with mode: 0644]

diff --git a/CHANGES b/CHANGES
index d905bbc6dea54dff30cb0776d19109a8456663f1..938e720e0a253d4970cabf610985669113ef83bf 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -124,6 +124,15 @@ CHANGES
       when used with the ORM, mutable-style equality/
       copy-on-write techniques are used to test for changes.
       
+- extensions
+    - a new super-small "declarative" extension has been added,
+      which allows Table and mapper() configuration to take place
+      inline underneath a class declaration.  This extension differs
+      from ActiveMapper and Elixir in that it does not redefine
+      any SQLAlchemy semantics at all; literal Column, Table
+      and relation() constructs are used to define the class 
+      behavior and table definition.
+      
 0.4.3
 ------
 - sql
index b20570d53c68a83f454928f4afe7e2b2735be6f8..260b805fbac1ba3ae93b5246612fc363db2b02fb 100644 (file)
@@ -95,6 +95,13 @@ The `mapper()` function creates a new `Mapper` object and stores it away for fut
     
 What was that last `id` attribute?  That was placed there by the `Mapper`, to track the value of the `id` column in the `users_table`.  Since our `User` doesn't exist in the database, its id is `None`.  When we save the object, it will get populated automatically with its new id.
 
+## Too Verbose ?  There are alternatives
+
+Some users, upon seeing the full set of steps to map a class, which are to define a `Table`, define a class, and then define a `mapper()`, are too verbose and disjoint.  Most popular object relational products use the so-called "active record" approach, where the table definition and its class mapping are all defined at once.  With SQLAlchemy, there are two excellent alternatives to its usual configuration:
+
+  * [Elixir](http://elixir.ematia.de/) is a "sister" product to SQLAlchemy, which is a full "declarative" layer built on top of SQLAlchemy.  It has existed almost as long as SA itself and defines a rich featureset on top of SA's normal configuration, adding many new capabilities such as plugins, automatic generation of table and column names based on configurations, and an intuitive system of defining relations.
+  * [declarative](rel:plugins_declarative) is a "micro-declarative" plugin included with SQLAlchemy 0.4.4 and above.  In contrast to Elixir, it maintains virtually the identical configuration outlined in this tutorial, except it allows the `Column`, `relation()`, and other constructs to be defined "inline" with the mapped class itself.
+
 ## Creating a Session
 
 We're now ready to start talking to the database.  The ORM's "handle" to the database is the `Session`.  When we first set up the application, at the same level as our `create_engine()` statement, we define a second object called `Session` (or whatever you want to call it, `create_session`, etc.) which is configured by the `sessionmaker()` function.  This function is configurational and need only be called once.  
index fee1852b300ce0bf0c23a169d1b2a6c9e79c7add..ed3869c248241bf29cce566af4826bf69a730ddb 100644 (file)
@@ -3,6 +3,156 @@ Plugins  {@name=plugins}
 
 SQLAlchemy has a variety of extensions available which provide extra functionality to SA, either via explicit usage or by augmenting the core behavior.  Several of these extensions are designed to work together.
 
+### declarative
+
+**Author:** Mike Bayer<br/>
+**Version:** 0.4.4 or greater
+
+`declarative` intends to be a fully featured replacement for the very old `activemapper` extension.  Its goal is to redefine the organization of class, `Table`, and `mapper()` constructs such that they can all be defined "at once" underneath a class declaration.   Unlike `activemapper`, it does not redefine normal SQLAlchemy configurational semantics - regular `Column`, `relation()` and other schema or ORM constructs are used in almost all cases.
+
+`declarative` is a so-called "micro declarative layer"; it does not generate table or column names and requires almost as fully verbose a configuration as that of straight tables and mappers.  As an alternative, the [Elixir](http://elixir.ematia.de/) project is a full community-supported declarative layer for SQLAlchemy, and is recommended for its active-record-like semantics, its convention-based configuration, and plugin capabilities.
+
+SQLAlchemy object-relational configuration involves the usage of Table, mapper(), and class objects to define the three areas of configuration.
+declarative moves these three types of configuration underneath the individual mapped class. Regular SQLAlchemy schema and ORM constructs are used
+in most cases:
+
+    {python}
+    from sqlalchemy.ext.declarative import declarative_base, declared_synonym
+    
+    engine = create_engine('sqlite://')
+    Base = declarative_base(engine)
+    
+    class SomeClass(Base):
+        __tablename__ = 'some_table'
+        id = Column('id', Integer, primary_key=True)
+        name =  Column('name', String(50))
+
+Above, the `declarative_base` callable produces a new base class from which all mapped classes inherit from. When the class definition is
+completed, a new `Table` and `mapper()` have been generated, accessible via the `__table__` and `__mapper__` attributes on the
+`SomeClass` class.
+
+Attributes may be added to the class after its construction, and they will be added to the underlying `Table` and `mapper()` definitions as
+appropriate:
+
+    {python}
+    SomeClass.data = Column('data', Unicode)
+    SomeClass.related = relation(RelatedInfo)
+
+Classes which are mapped explicitly using `mapper()` can interact freely with declarative classes. The `declarative_base` base class contains a
+`MetaData` object as well as a dictionary of all classes created against the base. So to access the above metadata and create tables we can say:
+
+    {python}
+    Base.metadata.create_all()
+    
+The `declarative_base` can also receive a pre-created `MetaData` object:
+
+    {python}
+    mymetadata = MetaData()
+    Base = declarative_base(metadata=mymetadata)
+
+Relations to other classes are done in the usual way, with the added feature that the class specified to `relation()` may be a string name. The
+"class registry" associated with `Base` is used at mapper compilation time to resolve the name into the actual class object, which is expected to
+have been defined once the mapper configuration is used:
+
+    {python}
+    class User(Base):
+        __tablename__ = 'users'
+
+        id = Column('id', Integer, primary_key=True)
+        name = Column('name', String(50))
+        addresses = relation("Address", backref="user")
+    
+    class Address(Base):
+        __tablename__ = 'addresses'
+
+        id = Column('id', Integer, primary_key=True)
+        email = Column('email', String(50))
+        user_id = Column('user_id', Integer, ForeignKey('users.id'))
+
+Column constructs, since they are just that, are immediately usable, as below where we define a primary join condition on the `Address` class
+using them:
+
+    {python}
+    class Address(Base)
+        __tablename__ = 'addresses'
+
+        id = Column('id', Integer, primary_key=True)
+        email = Column('email', String(50))
+        user_id = Column('user_id', Integer, ForeignKey('users.id'))
+        user = relation(User, primaryjoin=user_id==User.id)
+
+Synonyms are one area where `declarative` needs to slightly change the usual SQLAlchemy configurational syntax. To define a getter/setter which
+proxies to an underlying attribute, use `declared_synonym`:
+
+    {python}
+    class MyClass(Base):
+        __tablename__ = 'sometable'
+        
+        _attr = Column('attr', String)
+        
+        def _get_attr(self):
+            return self._some_attr
+        def _set_attr(self, attr)
+            self._some_attr = attr
+        attr = declared_synonym(property(_get_attr, _set_attr), '_attr')
+        
+The above synonym is then usable as an instance attribute as well as a class-level expression construct:
+
+    {python}
+    x = MyClass()
+    x.attr = "some value"
+    session.query(MyClass).filter(MyClass.attr == 'some other value').all()
+        
+As an alternative to `__tablename__`, a direct `Table` construct may be used:
+
+    {python}
+    class MyClass(Base):
+        __table__ = Table('my_table', Base.metadata,
+            Column('id', Integer, primary_key=True),
+            Column('name', String(50))
+        )
+
+This is the preferred approach when using reflected tables, as below:
+
+    {python}
+    class MyClass(Base):
+        __table__ = Table('my_table', Base.metadata, autoload=True)
+
+Mapper arguments are specified using the `__mapper_args__` class variable. Note that the column objects declared on the class are immediately
+usable, as in this joined-table inheritance example:
+
+    {python}
+    class Person(Base):
+        __tablename__ = 'people'
+        id = Column('id', Integer, primary_key=True)
+        discriminator = Column('type', String(50))
+        __mapper_args__ = {'polymorphic_on':discriminator}
+    
+    class Engineer(Person):
+        __tablename__ = 'engineers'
+        __mapper_args__ = {'polymorphic_identity':'engineer'}
+        id = Column('id', Integer, ForeignKey('people.id'), primary_key=True)
+        primary_language = Column('primary_language', String(50))
+        
+For single-table inheritance, the `__tablename__` and `__table__` class variables are optional on a class when the class inherits from another
+mapped class.
+
+As a convenience feature, the `declarative_base()` sets a default constructor on classes which takes keyword arguments, and assigns them to the
+named attributes:
+
+    {python}
+    e = Engineer(primary_language='python')
+
+Note that `declarative` has no integration built in with sessions, and is only intended as an optional syntax for the regular usage of mappers
+and Table objects. A typical application setup using `scoped_session` might look like:
+
+    {python}
+    engine = create_engine('postgres://scott:tiger@localhost/test')
+    Session = scoped_session(sessionmaker(transactional=True, autoflush=False, bind=engine))
+    Base = declarative_base()
+    
+Mapped instances then make usage of `Session` in the usual way.
+
 
 ### associationproxy
 
@@ -361,7 +511,7 @@ Full SqlSoup documentation is on the [SQLAlchemy Wiki](http://www.sqlalchemy.org
 
 ### Deprecated Extensions
 
-A lot of our extensions are deprecated.  But this is a good thing.  Why ?  Because all of them have been refined and focused, and rolled into the core of SQLAlchemy (or in the case of `ActiveMapper`, it's become **Elixir**).  So they aren't removed, they've just graduated into fully integrated features.  Below we describe a set of extensions which are present in 0.4 but are deprecated.
+A lot of our extensions are deprecated.  But this is a good thing.  Why ?  Because all of them have been refined and focused, and rolled into the core of SQLAlchemy.  So they aren't removed, they've just graduated into fully integrated features.  Below we describe a set of extensions which are present in 0.4 but are deprecated.
 
 #### SelectResults
 
@@ -444,7 +594,7 @@ For docs on `assignmapper`, see the SQLAlchemy 0.3 documentation.
 
 **Author:** Jonathan LaCour
 
-Please note that ActiveMapper has been deprecated in favor of [Elixir](http://elixir.ematia.de/), a more comprehensive solution to declarative mapping, of which Jonathan is a co-author.
+Please note that ActiveMapper has been deprecated in favor of either [Elixir](http://elixir.ematia.de/), a comprehensive solution to declarative mapping, or [declarative](rel:plugins_declarative), a built in convenience tool which reorganizes `Table` and `mapper()` configuration.
 
 ActiveMapper is a so-called "declarative layer" which allows the construction of a class, a `Table`, and a `Mapper` all in one step:
 
index d7c6b210f1f9fa6472279b544876276f32245e6b..d9bad13841d88e9aabde56937e51ec41b94fbd8f 100644 (file)
@@ -12,6 +12,7 @@ import sqlalchemy.ext.orderinglist as orderinglist
 import sqlalchemy.ext.associationproxy as associationproxy
 import sqlalchemy.ext.assignmapper as assignmapper
 import sqlalchemy.ext.sqlsoup as sqlsoup
+import sqlalchemy.ext.declarative as declarative
 
 def make_doc(obj, classes=None, functions=None, **kwargs):
     """generate a docstring.ObjectDoc structure for an individual module, list of classes, and list of functions."""
@@ -45,6 +46,7 @@ def make_all_docs():
         make_doc(obj=orm.query, classes=[orm.query.Query]),
         make_doc(obj=orm.session, classes=[orm.session.Session, orm.session.SessionExtension]),
         make_doc(obj=orm.shard),
+        make_doc(obj=declarative),
         make_doc(obj=associationproxy, classes=[associationproxy.AssociationProxy]),
         make_doc(obj=orderinglist, classes=[orderinglist.OrderingList]),
         make_doc(obj=sqlsoup),
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py
new file mode 100644 (file)
index 0000000..ac315e7
--- /dev/null
@@ -0,0 +1,321 @@
+"""A simple declarative layer for SQLAlchemy ORM.
+
+SQLAlchemy object-relational configuration involves the usage of Table,
+mapper(), and class objects to define the three areas of configuration.
+declarative moves these three types of configuration underneath the 
+individual mapped class.   Regular SQLAlchemy schema and ORM 
+constructs are used in most cases::
+
+    from sqlalchemy.ext.declarative import declarative_base, declared_synonym
+    
+    engine = create_engine('sqlite://')
+    Base = declarative_base(engine)
+    
+    class SomeClass(Base):
+        __tablename__ = 'some_table'
+        id = Column('id', Integer, primary_key=True)
+        name =  Column('name', String(50))
+
+Above, the ``declarative_base`` callable produces a new base class from which all
+mapped classes inherit from.  When the class definition is completed, a new 
+``Table`` and ``mapper()`` have been generated, accessible via the ``__table__``
+and ``__mapper__`` attributes on the ``SomeClass`` class.
+
+Attributes may be added to the class after its construction, and they will
+be added to the underlying ``Table`` and ``mapper()`` definitions as appropriate::
+
+    SomeClass.data = Column('data', Unicode)
+    SomeClass.related = relation(RelatedInfo)
+
+Classes which are mapped explicitly using ``mapper()`` can interact freely with 
+declarative classes.  The ``declarative_base`` base class contains a ``MetaData`` 
+object as well as a dictionary of all classes created against the base.  
+So to access the above metadata and create tables we can say::
+
+    Base.metadata.create_all()
+    
+The ``declarative_base`` can also receive a pre-created ``MetaData`` object::
+
+    mymetadata = MetaData()
+    Base = declarative_base(metadata=mymetadata)
+
+Relations to other classes are done in the usual way, with the added feature
+that the class specified to ``relation()`` may be a string name.  The 
+"class registry" associated with ``Base`` is used at mapper compilation time
+to resolve the name into the actual class object, which is expected to have been
+defined once the mapper configuration is used::
+
+    class User(Base):
+        __tablename__ = 'users'
+
+        id = Column('id', Integer, primary_key=True)
+        name = Column('name', String(50))
+        addresses = relation("Address", backref="user")
+    
+    class Address(Base):
+        __tablename__ = 'addresses'
+
+        id = Column('id', Integer, primary_key=True)
+        email = Column('email', String(50))
+        user_id = Column('user_id', Integer, ForeignKey('users.id'))
+
+Column constructs, since they are just that, are immediately usable, as
+below where we define a primary join condition on the ``Address`` class
+using them::
+
+    class Address(Base)
+        __tablename__ = 'addresses'
+
+        id = Column('id', Integer, primary_key=True)
+        email = Column('email', String(50))
+        user_id = Column('user_id', Integer, ForeignKey('users.id'))
+        user = relation(User, primaryjoin=user_id==User.id)
+
+Synonyms are one area where ``declarative`` needs to slightly change the usual
+SQLAlchemy configurational syntax.  To define a getter/setter which proxies
+to an underlying attribute, use ``declared_synonym``::
+
+    class MyClass(Base):
+        __tablename__ = 'sometable'
+        
+        _attr = Column('attr', String)
+        
+        def _get_attr(self):
+            return self._some_attr
+        def _set_attr(self, attr)
+            self._some_attr = attr
+        attr = declared_synonym(property(_get_attr, _set_attr), '_attr')
+        
+The above synonym is then usable as an instance attribute as well as a class-level
+expression construct::
+
+    x = MyClass()
+    x.attr = "some value"
+    session.query(MyClass).filter(MyClass.attr == 'some other value').all()
+        
+As an alternative to ``__tablename__``, a direct ``Table`` construct may be used::
+
+    class MyClass(Base):
+        __table__ = Table('my_table', Base.metadata,
+            Column('id', Integer, primary_key=True),
+            Column('name', String(50))
+        )
+
+This is the preferred approach when using reflected tables, as below::
+
+    class MyClass(Base):
+        __table__ = Table('my_table', Base.metadata, autoload=True)
+
+Mapper arguments are specified using the ``__mapper_args__`` class variable.  
+Note that the column objects declared on the class are immediately usable, as 
+in this joined-table inheritance example::
+
+    class Person(Base):
+        __tablename__ = 'people'
+        id = Column('id', Integer, primary_key=True)
+        discriminator = Column('type', String(50))
+        __mapper_args__ = {'polymorphic_on':discriminator}
+    
+    class Engineer(Person):
+        __tablename__ = 'engineers'
+        __mapper_args__ = {'polymorphic_identity':'engineer'}
+        id = Column('id', Integer, ForeignKey('people.id'), primary_key=True)
+        primary_language = Column('primary_language', String(50))
+        
+For single-table inheritance, the ``__tablename__`` and ``__table__`` class
+variables are optional on a class when the class inherits from another mapped
+class.
+
+As a convenience feature, the ``declarative_base()`` sets a default constructor
+on classes which takes keyword arguments, and assigns them to the named attributes::
+
+    e = Engineer(primary_language='python')
+
+Note that ``declarative`` has no integration built in with sessions, and is only
+intended as an optional syntax for the regular usage of mappers and Table objects.
+A typical application setup using ``scoped_session`` might look like::
+
+    engine = create_engine('postgres://scott:tiger@localhost/test')
+    Session = scoped_session(sessionmaker(transactional=True, autoflush=False, bind=engine))
+    Base = declarative_base()
+    
+Mapped instances then make usage of ``Session`` in the usual way.
+
+"""
+from sqlalchemy.schema import Table, SchemaItem, Column, MetaData
+from sqlalchemy.orm import synonym as _orm_synonym, mapper
+from sqlalchemy.orm.interfaces import MapperProperty
+from sqlalchemy.orm.properties import PropertyLoader
+
+__all__ = ['declarative_base', 'declared_synonym']
+
+class DeclarativeMeta(type):
+    def __init__(cls, classname, bases, dict_):
+        if '_decl_class_registry' in cls.__dict__:
+            return type.__init__(cls, classname, bases, dict_)
+        
+        cls._decl_class_registry[classname] = cls
+        our_stuff = {}
+        for k in dict_:
+            value = dict_[k]
+            if not isinstance(value, (Column, MapperProperty, declared_synonym)):
+                continue
+            if isinstance(value, declared_synonym):
+                value._setup(cls, k, our_stuff)
+            else:
+                prop = _deferred_relation(cls, value)
+                our_stuff[k] = prop
+        
+        table = None
+        if '__table__' not in cls.__dict__:
+            if '__tablename__' in cls.__dict__:
+                tablename = cls.__tablename__
+                cls.__table__ = table = Table(tablename, cls.metadata, *[
+                    c for c in our_stuff.values() if isinstance(c, Column)
+                ])
+        else:
+            table = cls.__table__
+        
+        inherits = cls.__mro__[1]
+        inherits = cls._decl_class_registry.get(inherits.__name__, None)
+        mapper_args = getattr(cls, '__mapper_args__', {})
+        
+        cls.__mapper__ = mapper(cls, table, inherits=inherits, properties=our_stuff, **mapper_args)
+        return type.__init__(cls, classname, bases, dict_)
+    
+    def __setattr__(cls, key, value):
+        if '__mapper__' in cls.__dict__:
+            if isinstance(value, Column):
+                cls.__table__.append_column(value)
+                cls.__mapper__.add_property(key, value)
+            elif isinstance(value, MapperProperty):
+                cls.__mapper__.add_property(key, _deferred_relation(cls, value))
+            elif isinstance(value, declared_synonym):
+                value._setup(cls, key, None)
+            else:
+                type.__setattr__(cls, key, value)
+        else:
+            type.__setattr__(cls, key, value)
+
+def _deferred_relation(cls, prop):
+    if isinstance(prop, PropertyLoader) and isinstance(prop.argument, basestring):
+        arg = prop.argument
+        def return_cls():
+            return cls._decl_class_registry[arg]
+        prop.argument = return_cls
+
+    return prop
+
+class declared_synonym(object):
+    def __init__(self, prop, name, mapperprop=None):
+        self.prop = prop
+        self.name = name
+        self.mapperprop = mapperprop
+        
+    def _setup(self, cls, key, init_dict):
+        prop = self.mapperprop or getattr(cls, self.name)
+        prop = _deferred_relation(cls, prop)
+        setattr(cls, key, self.prop)
+        if init_dict is not None:
+            init_dict[self.name] = prop
+            init_dict[key] = _orm_synonym(self.name)
+        else:
+            setattr(cls, self.name, prop)
+            setattr(cls, key, _orm_synonym(self.name))
+        
+        
+def declarative_base(engine=None, metadata=None):
+    lcl_metadata = metadata or MetaData()
+    class Base(object):
+        __metaclass__ = DeclarativeMeta
+        metadata = lcl_metadata
+        if engine:
+            metadata.bind = engine
+        _decl_class_registry = {}
+        def __init__(self, **kwargs):
+            for k in kwargs:
+                setattr(self, k, kwargs[k])
+    return Base
+
+if __name__ == '__main__':
+    # sample usage:
+    
+    from sqlalchemy import *
+    from sqlalchemy.orm import *
+
+    # Base is created per-app (or per desired scope) 
+    # and houses a MetaData, and optionally an engine
+    Base = declarative_base(create_engine('sqlite://', echo=False))
+    
+    class User(Base):
+        __tablename__ = 'users'
+    
+        id = Column('id', Integer, primary_key=True)
+        name = Column('name', String(50))
+        addresses = relation("Address", backref="user")
+        type = Column('type', String(50))
+        
+        __mapper_args__ = dict(polymorphic_on=type, polymorphic_identity='user')
+
+    class AdminUser(User):
+        __tablename__ = 'admin_users'
+        __mapper_args__ = dict(polymorphic_identity='adminuser')
+
+        id = Column('id', Integer, ForeignKey('users.id'), primary_key=True)
+        supername = Column('supername', String(50))
+        
+    class Address(Base):
+        __tablename__ = 'addresses'
+
+        id = Column('id', Integer, primary_key=True)
+        user_id = Column('user_id', Integer, ForeignKey('users.id'))
+        _email = Column('email', String(50))
+        
+        # illustrate a synonym
+        def _set_email(self, email):
+            self._email = email
+        def _get_email(self):
+            return self._email
+        email = declared_synonym(property(_get_email, _set_email), '_email')
+        
+    class Keyword(Base):
+        __tablename__ = 'keywords'
+        
+        id = Column('id', Integer, primary_key=True)
+        name = Column('name', String(50))
+    
+    # m2m tables are just created as Table objects
+    user_keywords = Table('user_keywords', Base.metadata, 
+        Column('user_id', Integer, ForeignKey('users.id')), 
+        Column('keyword_id', Integer, ForeignKey('keywords.id'))
+    )
+    
+    # test adding relations after the fact
+    User.keywords = relation(Keyword, secondary=user_keywords, backref='users')
+        
+    Base.metadata.create_all()
+
+    sess = create_session()
+
+    u1 = User(name='ed', 
+        addresses = [Address(email='eds email')],
+        keywords = [Keyword(name='one'), Keyword(name='two')]
+        )
+
+    sess.save(u1)
+
+    a1 = AdminUser(name='some admin', supername='root', addresses=[
+        Address(email='admin email')
+    ], keywords=[])
+    sess.save(a1)
+
+    sess.flush()
+    
+    sess.clear()
+    
+    print sess.query(User).filter(User.name=='ed').all()
+    print sess.query(User).filter(User.addresses.any(Address.email.like('%ed%'))).all()
+    
+    for user in sess.query(User).with_polymorphic('*').all():
+        print user, user.addresses, user.keywords
+    
\ No newline at end of file
index 6f74e3dbced33736842af0d7d385bc9dcaff1bb2..d5db4d01ed1e496c57bedaa37e561219d25725ba 100644 (file)
@@ -4,6 +4,7 @@ import doctest, sys, unittest
 def suite():
     unittest_modules = ['ext.activemapper',
                         'ext.assignmapper',
+                        'ext.declarative',
                         'ext.orderinglist',
                         'ext.associationproxy']
 
diff --git a/test/ext/declarative.py b/test/ext/declarative.py
new file mode 100644 (file)
index 0000000..73d2578
--- /dev/null
@@ -0,0 +1,240 @@
+import testenv; testenv.configure_for_tests()
+
+from sqlalchemy import *
+from sqlalchemy.orm import *
+from sqlalchemy.ext.declarative import declarative_base, declared_synonym
+from testlib.fixtures import Base as Fixture
+from testlib import *
+
+
+class DeclarativeTest(TestBase):
+    def setUp(self):
+        global Base
+        Base = declarative_base(testing.db)
+        
+    def tearDown(self):
+        Base.metadata.drop_all()
+        
+    def test_basic(self):
+        class User(Base, Fixture):
+            __tablename__ = 'users'
+
+            id = Column('id', Integer, primary_key=True)
+            name = Column('name', String(50))
+            addresses = relation("Address", backref="user")
+
+        class Address(Base, Fixture):
+            __tablename__ = 'addresses'
+            
+            id = Column('id', Integer, primary_key=True)
+            email = Column('email', String(50))
+            user_id = Column('user_id', Integer, ForeignKey('users.id'))
+            
+        Base.metadata.create_all()
+        
+        u1 = User(name='u1', addresses=[
+            Address(email='one'),
+            Address(email='two'),
+        ])
+        sess = create_session()
+        sess.save(u1)
+        sess.flush()
+        sess.clear()
+        
+        self.assertEquals(sess.query(User).all(), [User(name='u1', addresses=[
+            Address(email='one'),
+            Address(email='two'),
+        ])])
+        
+        a1 = sess.query(Address).filter(Address.email=='two').one()
+        self.assertEquals(a1, Address(email='two'))
+        self.assertEquals(a1.user, User(name='u1'))
+
+    def test_expression(self):
+        class User(Base, Fixture):
+            __tablename__ = 'users'
+
+            id = Column('id', Integer, primary_key=True)
+            name = Column('name', String(50))
+            addresses = relation("Address", backref="user")
+            
+        class Address(Base, Fixture):
+            __tablename__ = 'addresses'
+
+            id = Column('id', Integer, primary_key=True)
+            email = Column('email', String(50))
+            user_id = Column('user_id', Integer, ForeignKey('users.id'))
+
+        User.address_count = column_property(select([func.count(Address.id)]).where(Address.user_id==User.id).as_scalar())
+
+        Base.metadata.create_all()
+
+        u1 = User(name='u1', addresses=[
+            Address(email='one'),
+            Address(email='two'),
+        ])
+        sess = create_session()
+        sess.save(u1)
+        sess.flush()
+        sess.clear()
+
+        self.assertEquals(sess.query(User).all(), [User(name='u1', address_count=2, addresses=[
+            Address(email='one'),
+            Address(email='two'),
+        ])])
+
+    def test_synonym_inline(self):
+        class User(Base, Fixture):
+            __tablename__ = 'users'
+
+            id = Column('id', Integer, primary_key=True)
+            _name = Column('name', String(50))
+            def _set_name(self, name):
+                self._name = "SOMENAME " + name
+            def _get_name(self):
+                return self._name
+            name = declared_synonym(property(_get_name, _set_name), '_name')
+            
+        Base.metadata.create_all()
+        
+        sess = create_session()
+        u1 = User(name='someuser')
+        assert u1.name == "SOMENAME someuser", u1.name
+        sess.save(u1)
+        sess.flush()
+        self.assertEquals(sess.query(User).filter(User.name=="SOMENAME someuser").one(), u1)
+
+    def test_synonym_added(self):
+        class User(Base, Fixture):
+            __tablename__ = 'users'
+
+            id = Column('id', Integer, primary_key=True)
+            _name = Column('name', String(50))
+            def _set_name(self, name):
+                self._name = "SOMENAME " + name
+            def _get_name(self):
+                return self._name
+            name = property(_get_name, _set_name)
+        User.name = declared_synonym(User.name, '_name')
+
+        Base.metadata.create_all()
+
+        sess = create_session()
+        u1 = User(name='someuser')
+        assert u1.name == "SOMENAME someuser", u1.name
+        sess.save(u1)
+        sess.flush()
+        self.assertEquals(sess.query(User).filter(User.name=="SOMENAME someuser").one(), u1)
+    
+    def test_joined_inheritance(self):
+        class Company(Base, Fixture):
+            __tablename__ = 'companies'
+            id = Column('id', Integer, primary_key=True)
+            name = Column('name', String(50))
+            employees = relation("Person")
+            
+        class Person(Base, Fixture):
+            __tablename__ = 'people'
+            id = Column('id', Integer, primary_key=True)
+            company_id = Column('company_id', Integer, ForeignKey('companies.id'))
+            name = Column('name', String(50))
+            discriminator = Column('type', String(50))
+            __mapper_args__ = {'polymorphic_on':discriminator}
+            
+        class Engineer(Person):
+            __tablename__ = 'engineers'
+            __mapper_args__ = {'polymorphic_identity':'engineer'}
+            id = Column('id', Integer, ForeignKey('people.id'), primary_key=True)
+            primary_language = Column('primary_language', String(50))
+
+        class Manager(Person):
+            __tablename__ = 'managers'
+            __mapper_args__ = {'polymorphic_identity':'manager'}
+            id = Column('id', Integer, ForeignKey('people.id'), primary_key=True)
+            golf_swing = Column('golf_swing', String(50))
+        
+        Base.metadata.create_all()
+
+        sess = create_session()
+        c1 = Company(name="MegaCorp, Inc.", employees=[
+            Engineer(name="dilbert", primary_language="java"),
+            Engineer(name="wally", primary_language="c++"),
+            Manager(name="dogbert", golf_swing="fore!")
+        ])
+
+        c2 = Company(name="Elbonia, Inc.", employees=[
+            Engineer(name="vlad", primary_language="cobol")
+        ])
+
+        sess.save(c1)
+        sess.save(c2)
+        sess.flush()
+        sess.clear()
+        
+        self.assertEquals(sess.query(Company).filter(Company.employees.of_type(Engineer).any(Engineer.primary_language=='cobol')).first(), c2)
+
+    def test_single_inheritance(self):
+        class Company(Base, Fixture):
+            __tablename__ = 'companies'
+            id = Column('id', Integer, primary_key=True)
+            name = Column('name', String(50))
+            employees = relation("Person")
+            
+        class Person(Base, Fixture):
+            __tablename__ = 'people'
+            id = Column('id', Integer, primary_key=True)
+            company_id = Column('company_id', Integer, ForeignKey('companies.id'))
+            name = Column('name', String(50))
+            discriminator = Column('type', String(50))
+            primary_language = Column('primary_language', String(50))
+            golf_swing = Column('golf_swing', String(50))
+            __mapper_args__ = {'polymorphic_on':discriminator}
+            
+        class Engineer(Person):
+            __mapper_args__ = {'polymorphic_identity':'engineer'}
+
+        class Manager(Person):
+            __mapper_args__ = {'polymorphic_identity':'manager'}
+        
+        Base.metadata.create_all()
+
+        sess = create_session()
+        c1 = Company(name="MegaCorp, Inc.", employees=[
+            Engineer(name="dilbert", primary_language="java"),
+            Engineer(name="wally", primary_language="c++"),
+            Manager(name="dogbert", golf_swing="fore!")
+        ])
+
+        c2 = Company(name="Elbonia, Inc.", employees=[
+            Engineer(name="vlad", primary_language="cobol")
+        ])
+
+        sess.save(c1)
+        sess.save(c2)
+        sess.flush()
+        sess.clear()
+        
+        self.assertEquals(sess.query(Person).filter(Engineer.primary_language=='cobol').first(), Engineer(name='vlad'))
+        self.assertEquals(sess.query(Company).filter(Company.employees.of_type(Engineer).any(Engineer.primary_language=='cobol')).first(), c2)
+    
+    def test_reflection(self):
+        meta = MetaData(testing.db)
+        t1 = Table('t1', meta, Column('id', String(50), primary_key=True), Column('data', String(50)))
+        meta.create_all()
+        try:
+            class MyObj(Base):
+                __table__ = Table('t1', Base.metadata, autoload=True)
+            
+            sess = create_session()
+            m = MyObj(id="someid", data="somedata")
+            sess.save(m)
+            sess.flush()
+            
+            assert t1.select().execute().fetchall() == [('someid', 'somedata')]
+            
+        finally:
+            meta.drop_all()
+        
+        
+if __name__ == '__main__':
+    testing.main()
\ No newline at end of file