]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- SqlSoup overhaul
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 24 Nov 2010 17:21:59 +0000 (12:21 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 24 Nov 2010 17:21:59 +0000 (12:21 -0500)
- Added "map_to()" method to SqlSoup, which is a "master"
method which accepts explicit arguments for each aspect of
the selectable and mapping, including a base class per
mapping. [ticket:1975]

- Mapped selectables used with the map(), with_labels(),
join() methods no longer put the given argument into the
internal "cache" dictionary.  Particularly since the
join() and select() objects are created in the method
itself this was pretty much a pure memory leaking behavior.

CHANGES
doc/build/orm/extensions/sqlsoup.rst
lib/sqlalchemy/ext/sqlsoup.py
test/ext/test_sqlsoup.py
test/perf/insertspeed.py

diff --git a/CHANGES b/CHANGES
index a551cfc28bbca0eea0c12f818bc1cfee16d8b21d..5bf836ac02534f7a9443823a5b43a12145ba3cae 100644 (file)
--- a/CHANGES
+++ b/CHANGES
@@ -98,6 +98,18 @@ CHANGES
 - declarative
   - An error is raised if __table_args__ is not in tuple
     or dict format, and is not None.  [ticket:1972]
+
+- sqlsoup
+  - Added "map_to()" method to SqlSoup, which is a "master"
+    method which accepts explicit arguments for each aspect of
+    the selectable and mapping, including a base class per
+    mapping. [ticket:1975]
+
+  - Mapped selectables used with the map(), with_labels(),
+    join() methods no longer put the given argument into the
+    internal "cache" dictionary.  Particularly since the
+    join() and select() objects are created in the method
+    itself this was pretty much a pure memory leaking behavior.
     
 0.6.5
 =====
index fcc937166af0227526882ada43633ed84cf42436..7b5ab1e4b6328c5dee0faf8e7024b0c59792ede1 100644 (file)
@@ -2,5 +2,10 @@ SqlSoup
 =======
 
 .. automodule:: sqlalchemy.ext.sqlsoup
+
+
+SqlSoup API
+------------
+
+.. autoclass:: sqlalchemy.ext.sqlsoup.SqlSoup
     :members:
-    
index e8234e7c77b476d133f9f9a12fb6ade3f6ee5809..3cca2c93fb68fcceec50ba07e5465a90e869d36f 100644 (file)
@@ -2,19 +2,20 @@
 Introduction
 ============
 
-SqlSoup provides a convenient way to access existing database tables without
-having to declare table or mapper classes ahead of time. It is built on top of
-the SQLAlchemy ORM and provides a super-minimalistic interface to an existing
-database.
-
-SqlSoup effectively provides a coarse grained, alternative interface to
-working with the SQLAlchemy ORM, providing a "self configuring" interface
-for extremely rudimental operations.  It's somewhat akin to a 
-"super novice mode" version of the ORM.  While SqlSoup can be very handy,
-users are strongly encouraged to use the full ORM for non-trivial applications.
+SqlSoup provides a convenient way to access existing database
+tables without having to declare table or mapper classes ahead
+of time. It is built on top of the SQLAlchemy ORM and provides a
+super-minimalistic interface to an existing database.
+
+SqlSoup effectively provides a coarse grained, alternative
+interface to working with the SQLAlchemy ORM, providing a "self
+configuring" interface for extremely rudimental operations. It's
+somewhat akin to a "super novice mode" version of the ORM. While
+SqlSoup can be very handy, users are strongly encouraged to use
+the full ORM for non-trivial applications.
 
 Suppose we have a database with users, books, and loans tables
-(corresponding to the PyWebOff dataset, if you're curious).  
+(corresponding to the PyWebOff dataset, if you're curious).
 
 Creating a SqlSoup gateway is just like creating an SQLAlchemy
 engine::
@@ -39,53 +40,73 @@ Loading objects is as easy as this::
     >>> users = db.users.all()
     >>> users.sort()
     >>> users
-    [MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0), MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)]
+    [
+        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+                password=u'student',classname=None,admin=0), 
+        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+                password=u'basepair',classname=None,admin=1)
+    ]
 
 Of course, letting the database do the sort is better::
 
     >>> db.users.order_by(db.users.name).all()
-    [MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1), MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0)]
+    [
+        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+            password=u'basepair',classname=None,admin=1), 
+        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+            password=u'student',classname=None,admin=0)
+    ]
 
 Field access is intuitive::
 
     >>> users[0].email
     u'student@example.edu'
 
-Of course, you don't want to load all users very often.  Let's add a
-WHERE clause.  Let's also switch the order_by to DESC while we're at
-it::
+Of course, you don't want to load all users very often. Let's
+add a WHERE clause. Let's also switch the order_by to DESC while
+we're at it::
 
     >>> from sqlalchemy import or_, and_, desc
     >>> where = or_(db.users.name=='Bhargan Basepair', db.users.email=='student@example.edu')
     >>> db.users.filter(where).order_by(desc(db.users.name)).all()
-    [MappedUsers(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0), MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)]
+    [
+        MappedUsers(name=u'Joe Student',email=u'student@example.edu',
+            password=u'student',classname=None,admin=0), 
+        MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+            password=u'basepair',classname=None,admin=1)
+    ]
 
-You can also use .first() (to retrieve only the first object from a query) or
-.one() (like .first when you expect exactly one user -- it will raise an
-exception if more were returned)::
+You can also use .first() (to retrieve only the first object
+from a query) or .one() (like .first when you expect exactly one
+user -- it will raise an exception if more were returned)::
 
     >>> db.users.filter(db.users.name=='Bhargan Basepair').one()
-    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
+    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+            password=u'basepair',classname=None,admin=1)
 
 Since name is the primary key, this is equivalent to
 
     >>> db.users.get('Bhargan Basepair')
-    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
+    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+        password=u'basepair',classname=None,admin=1)
 
 This is also equivalent to
 
     >>> db.users.filter_by(name='Bhargan Basepair').one()
-    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',password=u'basepair',classname=None,admin=1)
+    MappedUsers(name=u'Bhargan Basepair',email=u'basepair@example.edu',
+        password=u'basepair',classname=None,admin=1)
 
-filter_by is like filter, but takes kwargs instead of full clause expressions.
-This makes it more concise for simple queries like this, but you can't do
-complex queries like the or\_ above or non-equality based comparisons this way.
+filter_by is like filter, but takes kwargs instead of full
+clause expressions. This makes it more concise for simple
+queries like this, but you can't do complex queries like the
+or\_ above or non-equality based comparisons this way.
 
 Full query documentation
 ------------------------
 
 Get, filter, filter_by, order_by, limit, and the rest of the
-query methods are explained in detail in :ref:`ormtutorial_querying`.
+query methods are explained in detail in
+:ref:`ormtutorial_querying`.
 
 Modifying objects
 =================
@@ -96,12 +117,12 @@ Modifying objects is intuitive::
     >>> user.email = 'basepair+nospam@example.edu'
     >>> db.commit()
 
-(SqlSoup leverages the sophisticated SQLAlchemy unit-of-work code, so
-multiple updates to a single object will be turned into a single
-``UPDATE`` statement when you commit.)
+(SqlSoup leverages the sophisticated SQLAlchemy unit-of-work
+code, so multiple updates to a single object will be turned into
+a single ``UPDATE`` statement when you commit.)
 
-To finish covering the basics, let's insert a new loan, then delete
-it::
+To finish covering the basics, let's insert a new loan, then
+delete it::
 
     >>> book_id = db.books.filter_by(title='Regional Variation in Moss').first().id
     >>> db.loans.insert(book_id=book_id, user_name=user.name)
@@ -111,14 +132,12 @@ it::
     >>> db.delete(loan)
     >>> db.commit()
 
-You can also delete rows that have not been loaded as objects. Let's
-do our insert/delete cycle once more, this time using the loans
-table's delete method. (For SQLAlchemy experts: note that no flush()
-call is required since this delete acts at the SQL level, not at the
-Mapper level.) The same where-clause construction rules apply here as
-to the select methods.
-
-::
+You can also delete rows that have not been loaded as objects.
+Let's do our insert/delete cycle once more, this time using the
+loans table's delete method. (For SQLAlchemy experts: note that
+no flush() call is required since this delete acts at the SQL
+level, not at the Mapper level.) The same where-clause
+construction rules apply here as to the select methods::
 
     >>> db.loans.insert(book_id=book_id, user_name=user.name)
     MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
@@ -129,7 +148,8 @@ book_id to 1 in all loans whose book_id is 2::
 
     >>> db.loans.update(db.loans.book_id==2, book_id=1)
     >>> db.loans.filter_by(book_id=1).all()
-    [MappedLoans(book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
+    [MappedLoans(book_id=1,user_name=u'Joe Student',
+        loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
 
 
 Joins
@@ -140,13 +160,15 @@ tables all at once.  In this situation, it is far more efficient to
 have the database perform the necessary join.  (Here we do not have *a
 lot of data* but hopefully the concept is still clear.)  SQLAlchemy is
 smart enough to recognize that loans has a foreign key to users, and
-uses that as the join condition automatically.
-
-::
+uses that as the join condition automatically::
 
     >>> join1 = db.join(db.users, db.loans, isouter=True)
     >>> join1.filter_by(name='Joe Student').all()
-    [MappedJoin(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0,book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
+    [
+        MappedJoin(name=u'Joe Student',email=u'student@example.edu',
+            password=u'student',classname=None,admin=0,book_id=1,
+            user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))
+    ]
 
 If you're unfortunate enough to be using MySQL with the default MyISAM
 storage engine, you'll have to specify the join condition manually,
@@ -162,20 +184,29 @@ books table::
 
     >>> join2 = db.join(join1, db.books)
     >>> join2.all()
-    [MappedJoin(name=u'Joe Student',email=u'student@example.edu',password=u'student',classname=None,admin=0,book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0),id=1,title=u'Mustards I Have Known',published_year=u'1989',authors=u'Jones')]
+    [
+        MappedJoin(name=u'Joe Student',email=u'student@example.edu',
+            password=u'student',classname=None,admin=0,book_id=1,
+            user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0),
+            id=1,title=u'Mustards I Have Known',published_year=u'1989',
+            authors=u'Jones')
+    ]
 
 If you join tables that have an identical column name, wrap your join
 with `with_labels`, to disambiguate columns with their table name
 (.c is short for .columns)::
 
     >>> db.with_labels(join1).c.keys()
-    [u'users_name', u'users_email', u'users_password', u'users_classname', u'users_admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
+    [u'users_name', u'users_email', u'users_password', 
+        u'users_classname', u'users_admin', u'loans_book_id', 
+        u'loans_user_name', u'loans_loan_date']
 
 You can also join directly to a labeled object::
 
     >>> labeled_loans = db.with_labels(db.loans)
     >>> db.join(db.users, labeled_loans, isouter=True).c.keys()
-    [u'name', u'email', u'password', u'classname', u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
+    [u'name', u'email', u'password', u'classname', 
+        u'admin', u'loans_book_id', u'loans_user_name', u'loans_loan_date']
 
 
 Relationships
@@ -188,13 +219,16 @@ You can define relationships on SqlSoup classes:
 These can then be used like a normal SA property:
 
     >>> db.users.get('Joe Student').loans
-    [MappedLoans(book_id=1,user_name=u'Joe Student',loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
+    [MappedLoans(book_id=1,user_name=u'Joe Student',
+                    loan_date=datetime.datetime(2006, 7, 12, 0, 0))]
 
     >>> db.users.filter(~db.users.loans.any()).all()
-    [MappedUsers(name=u'Bhargan Basepair',email='basepair+nospam@example.edu',password=u'basepair',classname=None,admin=1)]
-
+    [MappedUsers(name=u'Bhargan Basepair',
+            email='basepair+nospam@example.edu',
+            password=u'basepair',classname=None,admin=1)]
 
-relate can take any options that the relationship function accepts in normal mapper definition:
+relate can take any options that the relationship function
+accepts in normal mapper definition:
 
     >>> del db._cache['users']
     >>> db.users.relate('loans', db.loans, order_by=db.loans.loan_date, cascade='all, delete-orphan')
@@ -205,38 +239,47 @@ Advanced Use
 Sessions, Transations and Application Integration
 -------------------------------------------------
 
-**Note:** please read and understand this section thoroughly before using SqlSoup in any web application.
+**Note:** please read and understand this section thoroughly
+before using SqlSoup in any web application.
 
-SqlSoup uses a ScopedSession to provide thread-local sessions.  You
-can get a reference to the current one like this::
+SqlSoup uses a ScopedSession to provide thread-local sessions.
+You can get a reference to the current one like this::
 
     >>> session = db.session
 
-The default session is available at the module level in SQLSoup, via::
+The default session is available at the module level in SQLSoup,
+via::
 
     >>> from sqlalchemy.ext.sqlsoup import Session
     
-The configuration of this session is ``autoflush=True``, ``autocommit=False``.
-This means when you work with the SqlSoup object, you need to call ``db.commit()``
-in order to have changes persisted.   You may also call ``db.rollback()`` to
-roll things back.
-
-Since the SqlSoup object's Session automatically enters into a transaction as soon 
-as it's used, it is *essential* that you call ``commit()`` or ``rollback()``
-on it when the work within a thread completes.  This means all the guidelines
-for web application integration at :ref:`session_lifespan` must be followed.
-
-The SqlSoup object can have any session or scoped session configured onto it.
-This is of key importance when integrating with existing code or frameworks
-such as Pylons.   If your application already has a ``Session`` configured,
-pass it to your SqlSoup object::
+The configuration of this session is ``autoflush=True``,
+``autocommit=False``. This means when you work with the SqlSoup
+object, you need to call ``db.commit()`` in order to have
+changes persisted. You may also call ``db.rollback()`` to roll
+things back.
+
+Since the SqlSoup object's Session automatically enters into a
+transaction as soon as it's used, it is *essential* that you
+call ``commit()`` or ``rollback()`` on it when the work within a
+thread completes. This means all the guidelines for web
+application integration at :ref:`session_lifespan` must be
+followed.
+
+The SqlSoup object can have any session or scoped session
+configured onto it. This is of key importance when integrating
+with existing code or frameworks such as Pylons. If your
+application already has a ``Session`` configured, pass it to
+your SqlSoup object::
 
     >>> from myapplication import Session
     >>> db = SqlSoup(session=Session)
 
-If the ``Session`` is configured with ``autocommit=True``, use ``flush()`` 
-instead of ``commit()`` to persist changes - in this case, the ``Session``
-closes out its transaction immediately and no external management is needed.  ``rollback()`` is also not available.  Configuring a new SQLSoup object in "autocommit" mode looks like::
+If the ``Session`` is configured with ``autocommit=True``, use
+``flush()`` instead of ``commit()`` to persist changes - in this
+case, the ``Session`` closes out its transaction immediately and
+no external management is needed. ``rollback()`` is also not
+available. Configuring a new SQLSoup object in "autocommit" mode
+looks like::
 
     >>> from sqlalchemy.orm import scoped_session, sessionmaker
     >>> db = SqlSoup('sqlite://', session=scoped_session(sessionmaker(autoflush=False, expire_on_commit=False, autocommit=True)))
@@ -245,15 +288,13 @@ closes out its transaction immediately and no external management is needed.  ``
 Mapping arbitrary Selectables
 -----------------------------
 
-SqlSoup can map any SQLAlchemy ``Selectable`` with the map
-method. Let's map a ``Select`` object that uses an aggregate function;
-we'll use the SQLAlchemy ``Table`` that SqlSoup introspected as the
-basis. (Since we're not mapping to a simple table or join, we need to
-tell SQLAlchemy how to find the *primary key* which just needs to be
-unique within the select, and not necessarily correspond to a *real*
-PK in the database.)
-
-::
+SqlSoup can map any SQLAlchemy :class:`.Selectable` with the map
+method. Let's map an :func:`.expression.select` object that uses an aggregate
+function; we'll use the SQLAlchemy :class:`.Table` that SqlSoup
+introspected as the basis. (Since we're not mapping to a simple
+table or join, we need to tell SQLAlchemy how to find the
+*primary key* which just needs to be unique within the select,
+and not necessarily correspond to a *real* PK in the database.)::
 
     >>> from sqlalchemy import select, func
     >>> b = db.books._table
@@ -276,44 +317,45 @@ your db object::
 
 Python is flexible like that!
 
-
 Raw SQL
 -------
 
-SqlSoup works fine with SQLAlchemy's text construct, described in :ref:`sqlexpression_text`. 
-You can also execute textual SQL directly using the `execute()` method,
-which corresponds to the `execute()` method on the underlying `Session`.
-Expressions here are expressed like ``text()`` constructs, using named parameters
+SqlSoup works fine with SQLAlchemy's text construct, described
+in :ref:`sqlexpression_text`. You can also execute textual SQL
+directly using the `execute()` method, which corresponds to the
+`execute()` method on the underlying `Session`. Expressions here
+are expressed like ``text()`` constructs, using named parameters
 with colons::
 
     >>> rp = db.execute('select name, email from users where name like :name order by name', name='%Bhargan%')
     >>> for name, email in rp.fetchall(): print name, email
     Bhargan Basepair basepair+nospam@example.edu
 
-Or you can get at the current transaction's connection using `connection()`.  This is the 
-raw connection object which can accept any sort of SQL expression or raw SQL string passed to the database::
+Or you can get at the current transaction's connection using
+`connection()`. This is the raw connection object which can
+accept any sort of SQL expression or raw SQL string passed to
+the database::
 
     >>> conn = db.connection()
     >>> conn.execute("'select name, email from users where name like ? order by name'", '%Bhargan%')
 
-
 Dynamic table names
 -------------------
 
-You can load a table whose name is specified at runtime with the entity() method:
+You can load a table whose name is specified at runtime with the
+entity() method:
 
     >>> tablename = 'loans'
     >>> db.entity(tablename) == db.loans
     True
 
-entity() also takes an optional schema argument.  If none is specified, the
-default schema is used.
-
+entity() also takes an optional schema argument. If none is
+specified, the default schema is used.
 
 """
 
 from sqlalchemy import Table, MetaData, join
-from sqlalchemy import schema, sql
+from sqlalchemy import schema, sql, util
 from sqlalchemy.engine.base import Engine
 from sqlalchemy.orm import scoped_session, sessionmaker, mapper, \
                             class_mapper, relationship, session,\
@@ -403,7 +445,7 @@ def _selectable_name(selectable):
             x = x[1:]
         return x
 
-def _class_for_table(session, engine, selectable, base_cls=object, **mapper_kwargs):
+def _class_for_table(session, engine, selectable, base_clsmapper_kwargs):
     selectable = expression._clause_element_as_expr(selectable)
     mapname = 'Mapped' + _selectable_name(selectable)
     # Py2K
@@ -459,16 +501,25 @@ def _class_for_table(session, engine, selectable, base_cls=object, **mapper_kwar
     return klass
 
 class SqlSoup(object):
-    def __init__(self, engine_or_metadata, base=object, **kw):
-        """Initialize a new ``SqlSoup``.
-
-        `base` is the class that all created entity classes should subclass.
+    """Represent an ORM-wrapped database resource."""
+    
+    def __init__(self, engine_or_metadata, base=object, session=None):
+        """Initialize a new :class:`.SqlSoup`.
+
+        :param engine_or_metadata: a string database URL, :class:`.Engine` 
+          or :class:`.MetaData` object to associate with. If the
+          argument is a :class:`.MetaData`, it should be *bound*
+          to an :class:`.Engine`.
+        :param base: a class which will serve as the default class for 
+          returned mapped classes.  Defaults to ``object``.
+        :param session: a :class:`.ScopedSession` or :class:`.Session` with
+          which to associate ORM operations for this :class:`.SqlSoup` instance.
+          If ``None``, a :class:`.ScopedSession` that's local to this 
+          module is used.
 
-        `args` may either be an ``SQLEngine`` or a set of arguments
-        suitable for passing to ``create_engine``.
         """
         
-        self.session = kw.pop('session', Session)
+        self.session = session or Session
         self.base=base
         
         if isinstance(engine_or_metadata, MetaData):
@@ -476,21 +527,32 @@ class SqlSoup(object):
         elif isinstance(engine_or_metadata, (basestring, Engine)):
             self._metadata = MetaData(engine_or_metadata)
         else:
-            raise ArgumentError("invalid engine or metadata argument %r" % engine_or_metadata)
+            raise ArgumentError("invalid engine or metadata argument %r" % 
+                                engine_or_metadata)
             
         self._cache = {}
         self.schema = None
     
     @property
-    def engine(self):
+    def bind(self):
+        """The :class:`.Engine` associated with this :class:`.SqlSoup`."""
         return self._metadata.bind
 
-    bind = engine
+    engine = bind
 
-    def delete(self, *args, **kwargs):
-        self.session.delete(*args, **kwargs)
+    def delete(self, instance):
+        """Mark an instance as deleted."""
+
+        self.session.delete(instance)
     
     def execute(self, stmt, **params):
+        """Execute a SQL statement.
+        
+        The statement may be a string SQL string,
+        an :func:`.expression.select` construct, or an :func:`.expression.text` 
+        construct.
+        
+        """
         return self.session.execute(sql.text(stmt, bind=self.bind), **params)
     
     @property
@@ -501,58 +563,222 @@ class SqlSoup(object):
             return self.session()
             
     def connection(self):
+        """Return the current :class:`.Connection` in use by the current transaction."""
+        
         return self._underlying_session._connection_for_bind(self.bind)
         
     def flush(self):
+        """Flush pending changes to the database.
+        
+        See :meth:`.Session.flush`.
+        
+        """
         self.session.flush()
     
     def rollback(self):
+        """Rollback the current transction.
+        
+        See :meth:`.Session.rollback`.
+        
+        """
         self.session.rollback()
         
     def commit(self):
+        """Commit the current transaction.
+        
+        See :meth:`.Session.commit`.
+        
+        """
         self.session.commit()
         
     def clear(self):
+        """Synonym for :meth:`.SqlSoup.expunge_all`."""
+        
         self.session.expunge_all()
     
-    def expunge(self, *args, **kw):
-        self.session.expunge(*args, **kw)
+    def expunge(self, instance):
+        """Remove an instance from the :class:`.Session`.
+        
+        See :meth:`.Session.expunge`.
+        
+        """
+        self.session.expunge(instance)
         
     def expunge_all(self):
+        """Clear all objects from the current :class:`.Session`.
+        
+        See :meth:`.Session.expunge_all`.
+        
+        """
         self.session.expunge_all()
 
-    def map(self, selectable, **kwargs):
-        try:
-            t = self._cache[selectable]
-        except KeyError:
-            t = _class_for_table(self.session, self.engine, selectable, **kwargs)
-            self._cache[selectable] = t
-        return t
+    def map_to(self, attrname, tablename=None, selectable=None, 
+                    schema=None, base=None, mapper_args=util.frozendict()):
+        """Configure a mapping to the given attrname.
+        
+        This is the "master" method that can be used to create any 
+        configuration.
+        
+        :param attrname: String attribute name which will be
+          established as an attribute on this :class:.`.SqlSoup`
+          instance.
+        :param base: a Python class which will be used as the
+          base for the mapped class. If ``None``, the "base"
+          argument specified by this :class:`.SqlSoup`
+          instance's constructor will be used, which defaults to
+          ``object``.
+        :param mapper_args: Dictionary of arguments which will
+          be passed directly to :func:`.orm.mapper`.
+        :param tablename: String name of a :class:`.Table` to be
+          reflected. If a :class:`.Table` is already available,
+          use the ``selectable`` argument. This argument is
+          mutually exclusive versus the ``selectable`` argument.
+        :param selectable: a :class:`.Table`, :class:`.Join`, or
+          :class:`.Select` object which will be mapped. This
+          argument is mutually exclusive versus the ``tablename``
+          argument.
+        :param schema: String schema name to use if the
+          ``tablename`` argument is present.
+          
+          
+        """
+        if attrname in self._cache:
+            raise InvalidRequestError(
+                "Attribute '%s' is already mapped to '%s'" % (
+                attrname,
+                class_mapper(self._cache[attrname]).mapped_table
+            ))
+            
+        if tablename is not None:
+            if not isinstance(tablename, basestring):
+                raise ArgumentError("'tablename' argument must be a string."
+                                    )
+            if selectable is not None:
+                raise ArgumentError("'tablename' and 'selectable' "
+                                    "arguments are mutually exclusive")
+
+            selectable = Table(tablename, 
+                                        self._metadata, 
+                                        autoload=True, 
+                                        autoload_with=self.bind, 
+                                        schema=schema or self.schema)
+        elif schema:
+            raise ArgumentError("'tablename' argument is required when "
+                                "using 'schema'.")
+        elif selectable is not None:
+            if not isinstance(selectable, expression.FromClause):
+                raise ArgumentError("'selectable' argument must be a "
+                                    "table, select, join, or other "
+                                    "selectable construct.")
+        else:
+            raise ArgumentError("'tablename' or 'selectable' argument is "
+                                    "required.")
+
+        if not selectable.primary_key.columns:
+            if tablename:
+                raise PKNotFoundError(
+                            "table '%s' does not have a primary "
+                            "key defined" % tablename)
+            else:
+                raise PKNotFoundError(
+                            "selectable '%s' does not have a primary "
+                            "key defined" % selectable)
+                
+        mapped_cls = _class_for_table(
+            self.session,
+            self.engine,
+            selectable,
+            base or self.base,
+            mapper_args
+        )
+        self._cache[attrname] = mapped_cls
+        return mapped_cls
+        
+
+    def map(self, selectable, base=None, **mapper_args):
+        """Map a selectable directly.
+        
+        The class and its mapping are not cached and will
+        be discarded once dereferenced (as of 0.6.6).
+        
+        :param selectable: an :func:`.expression.select` construct.
+        :param base: a Python class which will be used as the
+          base for the mapped class. If ``None``, the "base"
+          argument specified by this :class:`.SqlSoup`
+          instance's constructor will be used, which defaults to
+          ``object``.
+        :param mapper_args: Dictionary of arguments which will
+          be passed directly to :func:`.orm.mapper`.
+        
+        """
 
-    def with_labels(self, item):
+        return _class_for_table(
+            self.session,
+            self.engine,
+            selectable,
+            base or self.base,
+            mapper_args
+        )
+
+    def with_labels(self, selectable, base=None, **mapper_args):
+        """Map a selectable directly, wrapping the 
+        selectable in a subquery with labels.
+
+        The class and its mapping are not cached and will
+        be discarded once dereferenced (as of 0.6.6).
+        
+        :param selectable: an :func:`.expression.select` construct.
+        :param base: a Python class which will be used as the
+          base for the mapped class. If ``None``, the "base"
+          argument specified by this :class:`.SqlSoup`
+          instance's constructor will be used, which defaults to
+          ``object``.
+        :param mapper_args: Dictionary of arguments which will
+          be passed directly to :func:`.orm.mapper`.
+        
+        """
+        
         # TODO give meaningful aliases
         return self.map(
-                    expression._clause_element_as_expr(item).
+                    expression._clause_element_as_expr(selectable).
                             select(use_labels=True).
-                            alias('foo'))
+                            alias('foo'), base=base, **mapper_args)
 
-    def join(self, *args, **kwargs):
-        j = join(*args, **kwargs)
-        return self.map(j)
+    def join(self, left, right, onclause=None, isouter=False, 
+                base=None, **mapper_args):
+        """Create an :func:`.expression.join` and map to it.
+
+        The class and its mapping are not cached and will
+        be discarded once dereferenced (as of 0.6.6).
+        
+        :param left: a mapped class or table object.
+        :param right: a mapped class or table object.
+        :param onclause: optional "ON" clause construct..
+        :param isouter: if True, the join will be an OUTER join.
+        :param base: a Python class which will be used as the
+          base for the mapped class. If ``None``, the "base"
+          argument specified by this :class:`.SqlSoup`
+          instance's constructor will be used, which defaults to
+          ``object``.
+        :param mapper_args: Dictionary of arguments which will
+          be passed directly to :func:`.orm.mapper`.
+        
+        """
+        
+        j = join(left, right, onclause=onclause, isouter=isouter)
+        return self.map(j, base=base, **mapper_args)
 
     def entity(self, attr, schema=None):
+        """Return the named entity from this :class:`.SqlSoup`, or 
+        create if not present.
+        
+        For more generalized mapping, see :meth:`.map_to`.
+        
+        """
         try:
-            t = self._cache[attr]
+            return self._cache[attr]
         except KeyError, ke:
-            table = Table(attr, self._metadata, autoload=True, autoload_with=self.bind, schema=schema or self.schema)
-            if not table.primary_key.columns:
-                raise PKNotFoundError('table %r does not have a primary key defined [columns: %s]' % (attr, ','.join(table.c.keys())))
-            if table.columns:
-                t = _class_for_table(self.session, self.engine, table, self.base)
-            else:
-                t = None
-            self._cache[attr] = t
-        return t
+            return self.map_to(attr, tablename=attr, schema=schema)
     
     def __getattr__(self, attr):
         return self.entity(attr)
index 7fe8ab1782fac12031c1ea449181f7c3e183c22b..0767d6c7aa4ad67e29f0128abb4504b9e5dc70db 100644 (file)
@@ -1,7 +1,8 @@
 from sqlalchemy.ext import sqlsoup
-from sqlalchemy.test.testing import TestBase, eq_, assert_raises
+from sqlalchemy.test.testing import TestBase, eq_, assert_raises, \
+    assert_raises_message
 from sqlalchemy import create_engine, or_, desc, select, func, exc, \
-    Table, util
+    Table, util, Column, Integer
 from sqlalchemy.orm import scoped_session, sessionmaker
 import datetime
 
@@ -30,6 +31,76 @@ class SQLSoupTest(TestBase):
         for sql in _teardown:
             engine.execute(sql)
 
+    def test_map_to_attr_present(self):
+        db = sqlsoup.SqlSoup(engine)
+
+        users = db.users
+        assert_raises_message(
+            exc.InvalidRequestError,
+            "Attribute 'users' is already mapped",
+            db.map_to, 'users', tablename='users'
+        )
+
+    def test_map_to_table_not_string(self):
+        db = sqlsoup.SqlSoup(engine)
+        
+        table = Table('users', db._metadata, Column('id', Integer, primary_key=True))
+        assert_raises_message(
+            exc.ArgumentError,
+            "'tablename' argument must be a string.",
+            db.map_to, 'users', tablename=table
+        )
+
+    def test_map_to_table_or_selectable(self):
+        db = sqlsoup.SqlSoup(engine)
+
+        table = Table('users', db._metadata, Column('id', Integer, primary_key=True))
+        assert_raises_message(
+            exc.ArgumentError,
+            "'tablename' and 'selectable' arguments are mutually exclusive",
+            db.map_to, 'users', tablename='users', selectable=table
+        )
+
+    def test_map_to_no_pk_selectable(self):
+        db = sqlsoup.SqlSoup(engine)
+
+        table = Table('users', db._metadata, Column('id', Integer))
+        assert_raises_message(
+            sqlsoup.PKNotFoundError,
+            "table 'users' does not have a primary ",
+            db.map_to, 'users', selectable=table
+        )
+    def test_map_to_invalid_schema(self):
+        db = sqlsoup.SqlSoup(engine)
+
+        table = Table('users', db._metadata, Column('id', Integer))
+        assert_raises_message(
+            exc.ArgumentError,
+            "'tablename' argument is required when "
+                                "using 'schema'.",
+            db.map_to, 'users', selectable=table, schema='hoho'
+        )
+    def test_map_to_nothing(self):
+        db = sqlsoup.SqlSoup(engine)
+
+        assert_raises_message(
+            exc.ArgumentError,
+            "'tablename' or 'selectable' argument is "
+                                    "required.",
+            db.map_to, 'users', 
+        )
+
+    def test_map_to_string_not_selectable(self):
+        db = sqlsoup.SqlSoup(engine)
+
+        assert_raises_message(
+            exc.ArgumentError,
+            "'selectable' argument must be a "
+                                    "table, select, join, or other "
+                                    "selectable construct.",
+            db.map_to, 'users', selectable='users'
+        )
+
     def test_bad_names(self):
         db = sqlsoup.SqlSoup(engine)
 
@@ -278,7 +349,7 @@ class SQLSoupTest(TestBase):
             email=u'student@example.edu', password=u'student',
             classname=None, admin=0)])
 
-    def test_no_pk(self):
+    def test_no_pk_reflected(self):
         db = sqlsoup.SqlSoup(engine)
         assert_raises(sqlsoup.PKNotFoundError, getattr, db, 'nopk')
 
index 0491e9f95950ac81bfc69500c521ce11395c73d7..7d2712837ab7a7c53ca2db745b1f63bd7a96b16f 100644 (file)
@@ -1,4 +1,3 @@
-import testenv; testenv.simple_setup()
 import sys, time
 from sqlalchemy import *
 from sqlalchemy.orm import *
@@ -87,7 +86,7 @@ def all():
 
         run_profiled(sa_profiled_insert_many,
                      'SQLAlchemy bulk insert/select, profiled',
-                     1000)
+                     50000)
 
         print "\nIndividual INSERTS via execute():\n"
 
@@ -101,7 +100,7 @@ def all():
 
         run_profiled(sa_profiled_insert,
                      'SQLAlchemy individual insert/select, profiled',
-                     1000)
+                     50000)
 
     finally:
         metadata.drop_all()