diff options
| author | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-12-05 14:56:26 -0500 |
|---|---|---|
| committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2010-12-05 14:56:26 -0500 |
| commit | f1e54a69fb9b07a73bd8488110b1ceace168a50a (patch) | |
| tree | 9ffef9bc95b2015877f5dcfa63fb6574a87ca15e /lib | |
| parent | 77763969ff06b12edcbaedab62890afa27e105b4 (diff) | |
| parent | 926b0d7fa860abee8b34236148fa34a6666700a9 (diff) | |
| download | sqlalchemy-f1e54a69fb9b07a73bd8488110b1ceace168a50a.tar.gz | |
- merge default tip
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/cextension/processors.c | 21 | ||||
| -rw-r--r-- | lib/sqlalchemy/cextension/resultproxy.c | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 5 | ||||
| -rw-r--r-- | lib/sqlalchemy/ext/sqlsoup.py | 494 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/query.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/expression.py | 42 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/operators.py | 11 | ||||
| -rw-r--r-- | lib/sqlalchemy/util/langhelpers.py | 10 |
8 files changed, 446 insertions, 161 deletions
diff --git a/lib/sqlalchemy/cextension/processors.c b/lib/sqlalchemy/cextension/processors.c index 327462fa8..36745c817 100644 --- a/lib/sqlalchemy/cextension/processors.c +++ b/lib/sqlalchemy/cextension/processors.c @@ -204,6 +204,14 @@ UnicodeResultProcessor_process(UnicodeResultProcessor *self, PyObject *value) return PyUnicode_Decode(str, len, encoding, errors); } +static void +UnicodeResultProcessor_dealloc(UnicodeResultProcessor *self) +{ + Py_XDECREF(self->encoding); + Py_XDECREF(self->errors); + self->ob_type->tp_free((PyObject*)self); +} + static PyMethodDef UnicodeResultProcessor_methods[] = { {"process", (PyCFunction)UnicodeResultProcessor_process, METH_O, "The value processor itself."}, @@ -216,7 +224,7 @@ static PyTypeObject UnicodeResultProcessorType = { "sqlalchemy.cprocessors.UnicodeResultProcessor", /* tp_name */ sizeof(UnicodeResultProcessor), /* tp_basicsize */ 0, /* tp_itemsize */ - 0, /* tp_dealloc */ + (destructor)UnicodeResultProcessor_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ @@ -289,6 +297,7 @@ DecimalResultProcessor_process(DecimalResultProcessor *self, PyObject *value) return NULL; str = PyString_Format(self->format, args); + Py_DECREF(args); if (str == NULL) return NULL; @@ -300,6 +309,14 @@ DecimalResultProcessor_process(DecimalResultProcessor *self, PyObject *value) } } +static void +DecimalResultProcessor_dealloc(DecimalResultProcessor *self) +{ + Py_XDECREF(self->type); + Py_XDECREF(self->format); + self->ob_type->tp_free((PyObject*)self); +} + static PyMethodDef DecimalResultProcessor_methods[] = { {"process", (PyCFunction)DecimalResultProcessor_process, METH_O, "The value processor itself."}, @@ -312,7 +329,7 @@ static PyTypeObject DecimalResultProcessorType = { "sqlalchemy.DecimalResultProcessor", /* tp_name */ sizeof(DecimalResultProcessor), /* tp_basicsize */ 0, /* tp_itemsize */ - 0, /* tp_dealloc */ + (destructor)DecimalResultProcessor_dealloc, /* tp_dealloc */ 0, /* tp_print */ 0, /* tp_getattr */ 0, /* tp_setattr */ diff --git a/lib/sqlalchemy/cextension/resultproxy.c b/lib/sqlalchemy/cextension/resultproxy.c index 73e127345..93471073b 100644 --- a/lib/sqlalchemy/cextension/resultproxy.c +++ b/lib/sqlalchemy/cextension/resultproxy.c @@ -107,11 +107,11 @@ BaseRowProxy_init(BaseRowProxy *self, PyObject *args, PyObject *kwds) static PyObject * BaseRowProxy_reduce(PyObject *self) { - PyObject *method, *state; - PyObject *module, *reconstructor, *cls; + PyObject *method, *state; + PyObject *module, *reconstructor, *cls; - method = PyObject_GetAttrString(self, "__getstate__"); - if (method == NULL) + method = PyObject_GetAttrString(self, "__getstate__"); + if (method == NULL) return NULL; state = PyObject_CallObject(method, NULL); @@ -503,8 +503,8 @@ static PyGetSetDef BaseRowProxy_getseters[] = { static PyMethodDef BaseRowProxy_methods[] = { {"values", (PyCFunction)BaseRowProxy_values, METH_NOARGS, "Return the values represented by this BaseRowProxy as a list."}, - {"__reduce__", (PyCFunction)BaseRowProxy_reduce, METH_NOARGS, - "Pickle support method."}, + {"__reduce__", (PyCFunction)BaseRowProxy_reduce, METH_NOARGS, + "Pickle support method."}, {NULL} /* Sentinel */ }; diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index f000520ca..821ec5cfb 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -771,7 +771,10 @@ class PGDialect(default.DefaultDialect): self.supports_native_enum = self.server_version_info >= (8, 3) if not self.supports_native_enum: self.colspecs = self.colspecs.copy() - del self.colspecs[ENUM] + # pop base Enum type + self.colspecs.pop(sqltypes.Enum, None) + # psycopg2, others may have placed ENUM here as well + self.colspecs.pop(ENUM, None) def on_connect(self): if self.isolation_level is not None: diff --git a/lib/sqlalchemy/ext/sqlsoup.py b/lib/sqlalchemy/ext/sqlsoup.py index e8234e7c7..9ff503dfa 100644 --- a/lib/sqlalchemy/ext/sqlsoup.py +++ b/lib/sqlalchemy/ext/sqlsoup.py @@ -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_cls, mapper_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,224 @@ 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. + + (new in 0.6.6) + + :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) diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 4e709e447..609805373 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -1926,6 +1926,12 @@ class Query(object): q = self._clone() if ident is not None: + if len(ident) != len(mapper.primary_key): + raise sa_exc.InvalidRequestError( + "Incorrect number of values in identifier to formulate " + "primary key for query.get(); primary key columns are %s" % + ','.join("'%s'" % c for c in mapper.primary_key)) + (_get_clause, _get_params) = mapper._get_clause # None present in ident - turn those comparisons @@ -1946,12 +1952,6 @@ class Query(object): for id_val, primary_key in zip(ident, mapper.primary_key) ]) - if len(params) != len(mapper.primary_key): - raise sa_exc.InvalidRequestError( - "Incorrect number of values in identifier to formulate " - "primary key for query.get(); primary key columns are %s" % - ','.join("'%s'" % c for c in mapper.primary_key)) - q._params = params if lockmode is not None: diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py index 91bd72257..99663c9c6 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -1311,6 +1311,29 @@ class ClauseElement(Visitable): return [] def self_group(self, against=None): + """Apply a 'grouping' to this :class:`.ClauseElement`. + + This method is overridden by subclasses to return a + "grouping" construct, i.e. parenthesis. In particular + it's used by "binary" expressions to provide a grouping + around themselves when placed into a larger expression, + as well as by :func:`.select` constructs when placed into + the FROM clause of another :func:`.select`. (Note that + subqueries should be normally created using the + :func:`.Select.alias` method, as many platforms require + nested SELECT statements to be named). + + As expressions are composed together, the application of + :meth:`self_group` is automatic - end-user code should never + need to use this method directly. Note that SQLAlchemy's + clause constructs take operator precedence into account - + so parenthesis might not be needed, for example, in + an expression like ``x OR (y AND z)`` - AND takes precedence + over OR. + + The base :meth:`self_group` method of :class:`.ClauseElement` + just returns self. + """ return self # TODO: remove .bind as a method from the root ClauseElement. @@ -2705,8 +2728,7 @@ class ClauseList(ClauseElement): return list(itertools.chain(*[c._from_objects for c in self.clauses])) def self_group(self, against=None): - if self.group and self.operator is not against and \ - operators.is_precedent(self.operator, against): + if self.group and operators.is_precedent(self.operator, against): return _Grouping(self) else: return self @@ -3032,10 +3054,7 @@ class _BinaryExpression(ColumnElement): ) def self_group(self, against=None): - # use small/large defaults for comparison so that unknown - # operators are always parenthesized - if self.operator is not against and \ - operators.is_precedent(self.operator, against): + if operators.is_precedent(self.operator, against): return _Grouping(self) else: return self @@ -3405,7 +3424,16 @@ class _Label(ColumnElement): @util.memoized_property def element(self): return self._element.self_group(against=operators.as_) - + + def self_group(self, against=None): + sub_element = self._element.self_group(against=against) + if sub_element is not self._element: + return _Label(self.name, + sub_element, + type_=self._type) + else: + return self._element + @property def primary_key(self): return self.element.primary_key diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index 67830f7cf..0577e6668 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -83,10 +83,14 @@ def desc_op(a): def asc_op(a): return a.asc() -_commutative = set([eq, ne, add, mul, and_]) +_commutative = set([eq, ne, add, mul]) + def is_commutative(op): return op in _commutative +_associative = _commutative.union([concat_op, and_, or_]) + + _smallest = symbol('_smallest') _largest = symbol('_largest') @@ -131,5 +135,8 @@ _PRECEDENCE = { } def is_precedent(operator, against): - return (_PRECEDENCE.get(operator, _PRECEDENCE[_smallest]) <= + if operator is against and operator in _associative: + return False + else: + return (_PRECEDENCE.get(operator, _PRECEDENCE[_smallest]) <= _PRECEDENCE.get(against, _PRECEDENCE[_largest])) diff --git a/lib/sqlalchemy/util/langhelpers.py b/lib/sqlalchemy/util/langhelpers.py index aac22ba30..d85793ee0 100644 --- a/lib/sqlalchemy/util/langhelpers.py +++ b/lib/sqlalchemy/util/langhelpers.py @@ -453,18 +453,20 @@ class importlater(object): @memoized_property def module(self): - m = __import__(self._il_path) - for token in self._il_path.split(".")[1:]: - m = getattr(m, token) if self._il_addtl: + m = __import__(self._il_path, globals(), locals(), + [self._il_addtl]) try: return getattr(m, self._il_addtl) except AttributeError: - raise AttributeError( + raise ImportError( "Module %s has no attribute '%s'" % (self._il_path, self._il_addtl) ) else: + m = __import__(self._il_path) + for token in self._il_path.split(".")[1:]: + m = getattr(m, token) return m def __getattr__(self, key): |
