summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/ext/sqlsoup.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/sqlalchemy/ext/sqlsoup.py')
-rw-r--r--lib/sqlalchemy/ext/sqlsoup.py267
1 files changed, 103 insertions, 164 deletions
diff --git a/lib/sqlalchemy/ext/sqlsoup.py b/lib/sqlalchemy/ext/sqlsoup.py
index 6eef4657c..592878acd 100644
--- a/lib/sqlalchemy/ext/sqlsoup.py
+++ b/lib/sqlalchemy/ext/sqlsoup.py
@@ -2,17 +2,11 @@
Introduction
============
-SqlSoup provides a convenient way to access database tables without
-having to declare table or mapper classes ahead of time.
+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.
Suppose we have a database with users, books, and loans tables
-(corresponding to the PyWebOff dataset, if you're curious). For
-testing purposes, we'll create this db as follows::
-
- >>> from sqlalchemy import create_engine
- >>> e = create_engine('sqlite:///:memory:')
- >>> for sql in _testsql: e.execute(sql) #doctest: +ELLIPSIS
- <...
+(corresponding to the PyWebOff dataset, if you're curious).
Creating a SqlSoup gateway is just like creating an SQLAlchemy
engine::
@@ -20,15 +14,14 @@ engine::
>>> from sqlalchemy.ext.sqlsoup import SqlSoup
>>> db = SqlSoup('sqlite:///:memory:')
-or, you can re-use an existing metadata or engine::
+or, you can re-use an existing engine::
- >>> db = SqlSoup(MetaData(e))
+ >>> db = SqlSoup(engine)
You can optionally specify a schema within the database for your
SqlSoup::
- # >>> db.schema = myschemaname
-
+ >>> db.schema = myschemaname
Loading objects
===============
@@ -84,10 +77,7 @@ Full query documentation
------------------------
Get, filter, filter_by, order_by, limit, and the rest of the
-query methods are explained in detail in the `SQLAlchemy documentation`__.
-
-__ http://www.sqlalchemy.org/docs/05/ormtutorial.html#datamapping_querying
-
+query methods are explained in detail in :ref:`ormtutorial_querying`.
Modifying objects
=================
@@ -96,11 +86,11 @@ Modifying objects is intuitive::
>>> user = _
>>> user.email = 'basepair+nospam@example.edu'
- >>> db.flush()
+ >>> 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 flush.)
+``UPDATE`` statement when you commit.)
To finish covering the basics, let's insert a new loan, then delete
it::
@@ -108,11 +98,10 @@ 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)
MappedLoans(book_id=2,user_name=u'Bhargan Basepair',loan_date=None)
- >>> db.flush()
>>> loan = db.loans.filter_by(book_id=2, user_name='Bhargan Basepair').one()
>>> db.delete(loan)
- >>> db.flush()
+ >>> 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
@@ -125,7 +114,6 @@ 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)
- >>> db.flush()
>>> db.loans.delete(db.loans.book_id==2)
You can similarly update multiple rows at once. This will change the
@@ -203,23 +191,47 @@ relate can take any options that the relation function accepts in normal mapper
>>> del db._cache['users']
>>> db.users.relate('loans', db.loans, order_by=db.loans.loan_date, cascade='all, delete-orphan')
-
Advanced Use
============
-Accessing the Session
----------------------
+Sessions, Transations and Application Integration
+-------------------------------------------------
+
+**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::
+ >>> session = db.session
+
+The default session is available at the module level in SQLSoup, via::
+
>>> from sqlalchemy.ext.sqlsoup import Session
- >>> session = 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.
-Now you have access to all the standard session-based SA features,
-such as transactions. (SqlSoup's ``flush()`` is normally
-transactionalized, but you can perform manual transaction management
-if you need a transaction to span multiple flushes.)
+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::
+
+ >>> from sqlalchemy.orm import scoped_session, sessionmaker
+ >>> db = SqlSoup('sqlite://', session=scoped_session(sessionmaker(autoflush=False, expire_on_commit=False, autocommit=True)))
Mapping arbitrary Selectables
@@ -260,21 +272,21 @@ Python is flexible like that!
Raw SQL
-------
-SqlSoup works fine with SQLAlchemy's `text block support`__.
-
-__ http://www.sqlalchemy.org/docs/04/sqlexpression.html#sql_text
+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::
-You can also access the SqlSoup's `engine` attribute to compose SQL
-directly. The engine's ``execute`` method corresponds to the one of a
-DBAPI cursor, and returns a ``ResultProxy`` that has ``fetch`` methods
-you would also see on a cursor::
-
- >>> rp = db.bind.execute('select name, email from users order by name')
+ >>> 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
- Joe Student student@example.edu
-You can also pass this engine object to other SQLAlchemy constructs.
+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
@@ -290,95 +302,19 @@ entity() also takes an optional schema argument. If none is specified, the
default schema is used.
-Extra tests
-===========
-
-Boring tests here. Nothing of real expository value.
-
-::
-
- >>> db.users.filter_by(classname=None).order_by(db.users.name).all()
- [MappedUsers(name=u'Bhargan Basepair',email=u'basepair+nospam@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)]
-
- >>> db.nopk
- Traceback (most recent call last):
- ...
- PKNotFoundError: table 'nopk' does not have a primary key defined [columns: i]
-
- >>> db.nosuchtable
- Traceback (most recent call last):
- ...
- NoSuchTableError: nosuchtable
-
- >>> years_with_count.insert(published_year='2007', n=1)
- Traceback (most recent call last):
- ...
- InvalidRequestError: SQLSoup can only modify mapped Tables (found: Alias)
-
- [tests clear()]
- >>> db.loans.count()
- 1
- >>> _ = db.loans.insert(book_id=1, user_name='Bhargan Basepair')
- >>> db.expunge_all()
- >>> db.flush()
- >>> db.loans.count()
- 1
"""
from sqlalchemy import Table, MetaData, join
from sqlalchemy import schema, sql
-from sqlalchemy.orm import scoped_session, sessionmaker, mapper, class_mapper, relation
-from sqlalchemy.exceptions import SQLAlchemyError, InvalidRequestError
+from sqlalchemy.engine.base import Engine
+from sqlalchemy.orm import scoped_session, sessionmaker, mapper, class_mapper, relation, session
+from sqlalchemy.exceptions import SQLAlchemyError, InvalidRequestError, ArgumentError
from sqlalchemy.sql import expression
-_testsql = """
-CREATE TABLE books (
- id integer PRIMARY KEY, -- auto-increments in sqlite
- title text NOT NULL,
- published_year char(4) NOT NULL,
- authors text NOT NULL
-);
-
-CREATE TABLE users (
- name varchar(32) PRIMARY KEY,
- email varchar(128) NOT NULL,
- password varchar(128) NOT NULL,
- classname text,
- admin int NOT NULL -- 0 = false
-);
-
-CREATE TABLE loans (
- book_id int PRIMARY KEY REFERENCES books(id),
- user_name varchar(32) references users(name)
- ON DELETE SET NULL ON UPDATE CASCADE,
- loan_date datetime DEFAULT current_timestamp
-);
-
-insert into users(name, email, password, admin)
-values('Bhargan Basepair', 'basepair@example.edu', 'basepair', 1);
-insert into users(name, email, password, admin)
-values('Joe Student', 'student@example.edu', 'student', 0);
-
-insert into books(title, published_year, authors)
-values('Mustards I Have Known', '1989', 'Jones');
-insert into books(title, published_year, authors)
-values('Regional Variation in Moss', '1971', 'Flim and Flam');
-
-insert into loans(book_id, user_name, loan_date)
-values (
- (select min(id) from books),
- (select name from users where name like 'Joe%'),
- '2006-07-12 0:0:0')
-;
-
-CREATE TABLE nopk (
- i int
-);
-""".split(';')
__all__ = ['PKNotFoundError', 'SqlSoup']
-Session = scoped_session(sessionmaker(autoflush=True))
+Session = scoped_session(sessionmaker(autoflush=True, autocommit=False))
class PKNotFoundError(SQLAlchemyError):
pass
@@ -394,12 +330,6 @@ class SelectableClassType(type):
def insert(cls, **kwargs):
_ddl_error(cls)
- def delete(cls, *args, **kwargs):
- _ddl_error(cls)
-
- def update(cls, whereclause=None, values=None, **kwargs):
- _ddl_error(cls)
-
def __clause_element__(cls):
return cls._table
@@ -415,12 +345,6 @@ class TableClassType(SelectableClassType):
o.__dict__.update(kwargs)
return o
- def delete(cls, *args, **kwargs):
- cls._table.delete(*args, **kwargs).execute()
-
- def update(cls, whereclause=None, values=None, **kwargs):
- cls._table.update(whereclause, values).execute(**kwargs)
-
def relate(cls, propname, *args, **kwargs):
class_mapper(cls)._configure_property(propname, relation(*args, **kwargs))
@@ -479,7 +403,6 @@ def class_for_table(selectable, **mapper_kwargs):
mappr = mapper(klass,
selectable,
extension=Session.extension,
- allow_null_pks=_is_outer_join(selectable),
**mapper_kwargs)
for k in mappr.iterate_properties:
@@ -488,26 +411,26 @@ def class_for_table(selectable, **mapper_kwargs):
klass._query = Session.query_property()
return klass
-class SqlSoup:
- def __init__(self, *args, **kwargs):
+class SqlSoup(object):
+ def __init__(self, engine_or_metadata, **kw):
"""Initialize a new ``SqlSoup``.
`args` may either be an ``SQLEngine`` or a set of arguments
suitable for passing to ``create_engine``.
"""
-
- # meh, sometimes having method overloading instead of kwargs would be easier
- if isinstance(args[0], MetaData):
- args = list(args)
- metadata = args.pop(0)
- if args or kwargs:
- raise ArgumentError('Extra arguments not allowed when metadata is given')
+
+ self.session = kw.pop('session', Session)
+
+ if isinstance(engine_or_metadata, MetaData):
+ self._metadata = engine_or_metadata
+ elif isinstance(engine_or_metadata, (basestring, Engine)):
+ self._metadata = MetaData(engine_or_metadata)
else:
- metadata = MetaData(*args, **kwargs)
- self._metadata = metadata
+ raise ArgumentError("invalid engine or metadata argument %r" % engine_or_metadata)
+
self._cache = {}
self.schema = None
-
+
def engine(self):
return self._metadata.bind
@@ -515,16 +438,38 @@ class SqlSoup:
bind = engine
def delete(self, *args, **kwargs):
- Session.delete(*args, **kwargs)
-
+ self.session.delete(*args, **kwargs)
+
+ def execute(self, stmt, **params):
+ return self.session.execute(sql.text(stmt, bind=self.bind), **params)
+
+ @property
+ def _underlying_session(self):
+ if isinstance(self.session, session.Session):
+ return self.session
+ else:
+ return self.session()
+
+ def connection(self):
+ return self._underlying_session._connection_for_bind(self.bind)
+
def flush(self):
- Session.flush()
-
+ self.session.flush()
+
+ def rollback(self):
+ self.session.rollback()
+
+ def commit(self):
+ self.session.commit()
+
def clear(self):
- Session.expunge_all()
-
+ self.session.expunge_all()
+
+ def expunge(self, *args, **kw):
+ self.session.expunge(*args, **kw)
+
def expunge_all(self):
- Session.expunge_all()
+ self.session.expunge_all()
def map(self, selectable, **kwargs):
try:
@@ -536,7 +481,10 @@ class SqlSoup:
def with_labels(self, item):
# TODO give meaningful aliases
- return self.map(expression._clause_element_as_expr(item).select(use_labels=True).alias('foo'))
+ return self.map(
+ expression._clause_element_as_expr(item).
+ select(use_labels=True).
+ alias('foo'))
def join(self, *args, **kwargs):
j = join(*args, **kwargs)
@@ -546,13 +494,9 @@ class SqlSoup:
try:
t = self._cache[attr]
except KeyError, ke:
- table = Table(attr, self._metadata, autoload=True, schema=schema or self.schema)
+ table = Table(attr, self._metadata, autoload=True, autoload_with=self.bind, schema=schema or self.schema)
if not table.primary_key.columns:
- # Py3K
- #raise PKNotFoundError('table %r does not have a primary key defined [columns: %s]' % (attr, ','.join(table.c.keys()))) from ke
- # Py2K
raise PKNotFoundError('table %r does not have a primary key defined [columns: %s]' % (attr, ','.join(table.c.keys())))
- # end Py2K
if table.columns:
t = class_for_table(table)
else:
@@ -566,8 +510,3 @@ class SqlSoup:
def __repr__(self):
return 'SqlSoup(%r)' % self._metadata
-if __name__ == '__main__':
- import logging
- logging.basicConfig()
- import doctest
- doctest.testmod()