summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES38
-rw-r--r--doc/build/core/types.rst4
-rw-r--r--doc/build/orm/extensions/sqlsoup.rst7
-rw-r--r--lib/sqlalchemy/cextension/processors.c21
-rw-r--r--lib/sqlalchemy/cextension/resultproxy.c12
-rw-r--r--lib/sqlalchemy/dialects/postgresql/base.py5
-rw-r--r--lib/sqlalchemy/ext/sqlsoup.py494
-rw-r--r--lib/sqlalchemy/orm/query.py12
-rw-r--r--lib/sqlalchemy/sql/expression.py42
-rw-r--r--lib/sqlalchemy/sql/operators.py11
-rw-r--r--lib/sqlalchemy/util/langhelpers.py10
-rw-r--r--test/aaa_profiling/test_memusage.py27
-rw-r--r--test/base/test_utils.py2
-rw-r--r--test/dialect/test_postgresql.py49
-rw-r--r--test/ext/test_sqlsoup.py77
-rw-r--r--test/orm/inheritance/test_basic.py29
-rw-r--r--test/orm/test_eager_relations.py3
-rw-r--r--test/orm/test_expire.py13
-rw-r--r--test/orm/test_mapper.py6
-rw-r--r--test/orm/test_query.py80
-rw-r--r--test/perf/insertspeed.py5
-rw-r--r--test/sql/test_compiler.py42
22 files changed, 774 insertions, 215 deletions
diff --git a/CHANGES b/CHANGES
index a551cfc28..597dfb08f 100644
--- a/CHANGES
+++ b/CHANGES
@@ -43,8 +43,23 @@ CHANGES
a placeholder flag for forwards compatibility,
as it will be needed in 0.7 for composites.
[ticket:1976]
-
+
+ - Query.get() will raise if the number of params
+ in a composite key is too large, as well as too
+ small. [ticket:1977]
+
+ - Backport of "optimized get" fix from 0.7,
+ improves the generation of joined-inheritance
+ "load expired row" behavior. [ticket:1992]
+
- sql
+ - Fixed operator precedence rules for multiple
+ chains of a single non-associative operator.
+ I.e. "x - (y - z)" will compile as "x - (y - z)"
+ and not "x - y - z". Also works with labels,
+ i.e. "x - (y - z).label('foo')"
+ [ticket:1984]
+
- The 'info' attribute of Column is copied during
Column.copy(), i.e. as occurs when using columns
in declarative mixins. [ticket:1967]
@@ -60,6 +75,9 @@ CHANGES
[ticket:1871]
- postgresql
+ - Single element tuple expressions inside an IN clause
+ parenthesize correctly, also from [ticket:1984]
+
- Ensured every numeric, float, int code, scalar + array,
are recognized by psycopg2 and pg8000's "numeric"
base type. [ticket:1955]
@@ -68,7 +86,11 @@ CHANGES
and return values as Python UUID() objects rather than
strings. Currently, the UUID type is only known to
work with psycopg2. [ticket:1956]
-
+
+ - Fixed bug whereby KeyError would occur with non-ENUM
+ supported PG versions after a pool dispose+recreate
+ would occur, [ticket:1989]
+
- mysql
- Fixed error handling for Jython + zxjdbc, such that
has_table() property works again. Regression from
@@ -98,6 +120,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
=====
diff --git a/doc/build/core/types.rst b/doc/build/core/types.rst
index ddb40d045..1910d9b7d 100644
--- a/doc/build/core/types.rst
+++ b/doc/build/core/types.rst
@@ -325,10 +325,10 @@ binary in CHAR(16) if desired::
return str(value)
else:
if not isinstance(value, uuid.UUID):
- return "%x" % uuid.UUID(value)
+ return "%.32x" % uuid.UUID(value)
else:
# hexstring
- return "%x" % value
+ return "%.32x" % value
def process_result_value(self, value, dialect):
if value is None:
diff --git a/doc/build/orm/extensions/sqlsoup.rst b/doc/build/orm/extensions/sqlsoup.rst
index fcc937166..7b5ab1e4b 100644
--- a/doc/build/orm/extensions/sqlsoup.rst
+++ b/doc/build/orm/extensions/sqlsoup.rst
@@ -2,5 +2,10 @@ SqlSoup
=======
.. automodule:: sqlalchemy.ext.sqlsoup
+
+
+SqlSoup API
+------------
+
+.. autoclass:: sqlalchemy.ext.sqlsoup.SqlSoup
:members:
-
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):
diff --git a/test/aaa_profiling/test_memusage.py b/test/aaa_profiling/test_memusage.py
index f6de1fa2f..53d7ff2e4 100644
--- a/test/aaa_profiling/test_memusage.py
+++ b/test/aaa_profiling/test_memusage.py
@@ -11,7 +11,10 @@ from sqlalchemy import MetaData, Integer, String, ForeignKey, \
from test.lib.schema import Table, Column
import sqlalchemy as sa
from sqlalchemy.sql import column
+from sqlalchemy.processors import to_decimal_processor_factory, \
+ to_unicode_processor_factory
from test.lib.util import gc_collect
+from decimal import Decimal as _python_Decimal
import gc
import weakref
from test.orm import _base
@@ -565,4 +568,26 @@ class MemUsageTest(EnsureZeroed):
dialect = SQLiteDialect()
cast.compile(dialect=dialect)
go()
-
+
+ @testing.requires.cextensions
+ def test_DecimalResultProcessor_init(self):
+ @profile_memory
+ def go():
+ to_decimal_processor_factory({}, 10)
+ go()
+
+ @testing.requires.cextensions
+ def test_DecimalResultProcessor_process(self):
+ @profile_memory
+ def go():
+ to_decimal_processor_factory(_python_Decimal, 10)(1.2)
+ go()
+
+ @testing.requires.cextensions
+ def test_UnicodeResultProcessor_init(self):
+ @profile_memory
+ def go():
+ to_unicode_processor_factory('utf8')
+ go()
+
+
diff --git a/test/base/test_utils.py b/test/base/test_utils.py
index 4f02d3811..d11644dbe 100644
--- a/test/base/test_utils.py
+++ b/test/base/test_utils.py
@@ -149,8 +149,6 @@ class ColumnCollectionTest(TestBase):
assert (cc1==cc2).compare(c1 == c2)
assert not (cc1==cc3).compare(c2 == c3)
-
-
class LRUTest(TestBase):
def test_lru(self):
diff --git a/test/dialect/test_postgresql.py b/test/dialect/test_postgresql.py
index 10dedab46..bff1fba68 100644
--- a/test/dialect/test_postgresql.py
+++ b/test/dialect/test_postgresql.py
@@ -411,6 +411,24 @@ class EnumTest(TestBase, AssertsExecutionResults, AssertsCompiledSQL):
metadata.drop_all()
assert not testing.db.dialect.has_type(testing.db,
'fourfivesixtype')
+
+ def test_no_support(self):
+ def server_version_info(self):
+ return (8, 2)
+
+ e = engines.testing_engine()
+ dialect = e.dialect
+ dialect._get_server_version_info = server_version_info
+
+ assert dialect.supports_native_enum
+ e.connect()
+ assert not dialect.supports_native_enum
+
+ # initialize is called again on new pool
+ e.dispose()
+ e.connect()
+ assert not dialect.supports_native_enum
+
def test_reflection(self):
metadata = MetaData(testing.db)
@@ -2041,3 +2059,34 @@ class MatchTest(TestBase, AssertsCompiledSQL):
matchtable.c.title.match('nutshells'
)))).order_by(matchtable.c.id).execute().fetchall()
eq_([1, 3, 5], [r.id for r in results])
+
+
+class TupleTest(TestBase):
+ __only_on__ = 'postgresql'
+
+ def test_tuple_containment(self):
+
+ for test, exp in [
+ ([('a', 'b')], True),
+ ([('a', 'c')], False),
+ ([('f', 'q'), ('a', 'b')], True),
+ ([('f', 'q'), ('a', 'c')], False)
+ ]:
+ eq_(
+ testing.db.execute(
+ select([
+ tuple_(
+ literal_column("'a'"),
+ literal_column("'b'")
+ ).\
+ in_([
+ tuple_(*[
+ literal_column("'%s'" % letter)
+ for letter in elem
+ ]) for elem in test
+ ])
+ ])
+ ).scalar(),
+ exp
+ )
+
diff --git a/test/ext/test_sqlsoup.py b/test/ext/test_sqlsoup.py
index f0ac6cbed..66f703068 100644
--- a/test/ext/test_sqlsoup.py
+++ b/test/ext/test_sqlsoup.py
@@ -1,7 +1,8 @@
from sqlalchemy.ext import sqlsoup
-from test.lib.testing import TestBase, eq_, assert_raises
+from test.lib.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')
diff --git a/test/orm/inheritance/test_basic.py b/test/orm/inheritance/test_basic.py
index d6b9d89f4..11fca582a 100644
--- a/test/orm/inheritance/test_basic.py
+++ b/test/orm/inheritance/test_basic.py
@@ -840,7 +840,7 @@ class DistinctPKTest(_base.MappedTest):
mapper(Employee, employee_table, inherits=person_mapper,
properties={'pid':person_table.c.id,
'eid':employee_table.c.id})
- self._do_test(True)
+ self._do_test(False)
def test_explicit_composite_pk(self):
person_mapper = mapper(Person, person_table)
@@ -1337,21 +1337,25 @@ class OptimizedLoadTest(_base.MappedTest):
pass
class Sub(Base):
pass
- mapper(Base, base, polymorphic_on=base.c.type, polymorphic_identity='base')
+ mapper(Base, base, polymorphic_on=base.c.type,
+ polymorphic_identity='base')
m = mapper(Sub, sub, inherits=Base, polymorphic_identity='sub')
s1 = Sub()
- assert m._optimized_get_statement(attributes.instance_state(s1), ['counter2']) is None
+ assert m._optimized_get_statement(attributes.instance_state(s1),
+ ['counter2']) is None
# loads s1.id as None
eq_(s1.id, None)
# this now will come up with a value of None for id - should reject
- assert m._optimized_get_statement(attributes.instance_state(s1), ['counter2']) is None
+ assert m._optimized_get_statement(attributes.instance_state(s1),
+ ['counter2']) is None
s1.id = 1
attributes.instance_state(s1).commit_all(s1.__dict__, None)
- assert m._optimized_get_statement(attributes.instance_state(s1), ['counter2']) is not None
+ assert m._optimized_get_statement(attributes.instance_state(s1),
+ ['counter2']) is not None
@testing.resolve_artifact_names
def test_load_expired_on_pending_twolevel(self):
@@ -1362,7 +1366,8 @@ class OptimizedLoadTest(_base.MappedTest):
class SubSub(Sub):
pass
- mapper(Base, base, polymorphic_on=base.c.type, polymorphic_identity='base')
+ mapper(Base, base, polymorphic_on=base.c.type,
+ polymorphic_identity='base')
mapper(Sub, sub, inherits=Base, polymorphic_identity='sub')
mapper(SubSub, subsub, inherits=Sub, polymorphic_identity='subsub')
sess = Session()
@@ -1372,11 +1377,13 @@ class OptimizedLoadTest(_base.MappedTest):
testing.db,
sess.flush,
CompiledSQL(
- "INSERT INTO base (data, type, counter) VALUES (:data, :type, :counter)",
+ "INSERT INTO base (data, type, counter) VALUES "
+ "(:data, :type, :counter)",
[{'data':'s1','type':'subsub','counter':1}]
),
CompiledSQL(
- "INSERT INTO sub (id, sub, counter) VALUES (:id, :sub, :counter)",
+ "INSERT INTO sub (id, sub, counter) VALUES "
+ "(:id, :sub, :counter)",
lambda ctx:[{'counter': 1, 'sub': None, 'id': s1.id}]
),
CompiledSQL(
@@ -1397,11 +1404,13 @@ class PKDiscriminatorTest(_base.MappedTest):
@classmethod
def define_tables(cls, metadata):
parents = Table('parents', metadata,
- Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+ Column('id', Integer, primary_key=True,
+ test_needs_autoincrement=True),
Column('name', String(60)))
children = Table('children', metadata,
- Column('id', Integer, ForeignKey('parents.id'), primary_key=True),
+ Column('id', Integer, ForeignKey('parents.id'),
+ primary_key=True),
Column('type', Integer,primary_key=True),
Column('name', String(60)))
diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py
index 6304ae45a..e3d14fede 100644
--- a/test/orm/test_eager_relations.py
+++ b/test/orm/test_eager_relations.py
@@ -610,7 +610,8 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
sel = sa.select([users, addresses.c.email_address],
users.c.id==addresses.c.user_id).alias('useralias')
mapper(User, sel, properties={
- 'orders':relationship(Order, primaryjoin=sel.c.id==orders.c.user_id, lazy='joined')
+ 'orders':relationship(Order, primaryjoin=sel.c.id==orders.c.user_id,
+ lazy='joined', order_by=orders.c.id)
})
mapper(Order, orders)
diff --git a/test/orm/test_expire.py b/test/orm/test_expire.py
index bbfdaa172..d691e7270 100644
--- a/test/orm/test_expire.py
+++ b/test/orm/test_expire.py
@@ -148,7 +148,8 @@ class ExpireTest(_fixtures.FixtureTest):
@testing.resolve_artifact_names
def test_refresh_collection_exception(self):
- """test graceful failure for currently unsupported immediate refresh of a collection"""
+ """test graceful failure for currently unsupported
+ immediate refresh of a collection"""
mapper(User, users, properties={
'addresses':relationship(Address, order_by=addresses.c.email_address)
@@ -156,10 +157,13 @@ class ExpireTest(_fixtures.FixtureTest):
mapper(Address, addresses)
s = create_session(autoflush=True, autocommit=False)
u = s.query(User).get(8)
- assert_raises_message(sa_exc.InvalidRequestError, "properties specified for refresh", s.refresh, u, ['addresses'])
+ assert_raises_message(sa_exc.InvalidRequestError,
+ "properties specified for refresh",
+ s.refresh, u, ['addresses'])
# in contrast to a regular query with no columns
- assert_raises_message(sa_exc.InvalidRequestError, "no columns with which to SELECT", s.query().all)
+ assert_raises_message(sa_exc.InvalidRequestError,
+ "no columns with which to SELECT", s.query().all)
@testing.resolve_artifact_names
def test_refresh_cancels_expire(self):
@@ -732,7 +736,8 @@ class ExpireTest(_fixtures.FixtureTest):
@testing.resolve_artifact_names
def test_expire_all(self):
mapper(User, users, properties={
- 'addresses':relationship(Address, backref='user', lazy='joined'),
+ 'addresses':relationship(Address, backref='user', lazy='joined',
+ order_by=addresses.c.id),
})
mapper(Address, addresses)
diff --git a/test/orm/test_mapper.py b/test/orm/test_mapper.py
index 09f9e9a86..10c3b3abe 100644
--- a/test/orm/test_mapper.py
+++ b/test/orm/test_mapper.py
@@ -1288,9 +1288,11 @@ class OptionsTest(_fixtures.FixtureTest):
@testing.resolve_artifact_names
def test_eager_degrade(self):
- """An eager relationship automatically degrades to a lazy relationship if eager columns are not available"""
+ """An eager relationship automatically degrades to a lazy relationship
+ if eager columns are not available"""
mapper(User, users, properties=dict(
- addresses = relationship(mapper(Address, addresses), lazy='joined')))
+ addresses = relationship(mapper(Address, addresses),
+ lazy='joined', order_by=addresses.c.id)))
sess = create_session()
# first test straight eager load, 1 statement
diff --git a/test/orm/test_query.py b/test/orm/test_query.py
index db7e53a0e..a7818fa57 100644
--- a/test/orm/test_query.py
+++ b/test/orm/test_query.py
@@ -138,15 +138,31 @@ class GetTest(QueryTest):
u2 = s.query(User).get(7)
assert u is not u2
- def test_get_composite_pk(self):
- s = create_session()
+ def test_get_composite_pk_no_result(self):
+ s = Session()
assert s.query(CompositePk).get((100,100)) is None
+
+ def test_get_composite_pk_result(self):
+ s = Session()
one_two = s.query(CompositePk).get((1,2))
assert one_two.i == 1
assert one_two.j == 2
assert one_two.k == 3
+
+ def test_get_too_few_params(self):
+ s = Session()
+ q = s.query(CompositePk)
+ assert_raises(sa_exc.InvalidRequestError, q.get, 7)
+
+ def test_get_too_few_params_tuple(self):
+ s = Session()
q = s.query(CompositePk)
- assert_raises(sa_exc.InvalidRequestError, q.get, 7)
+ assert_raises(sa_exc.InvalidRequestError, q.get, (7,))
+
+ def test_get_too_many_params(self):
+ s = Session()
+ q = s.query(CompositePk)
+ assert_raises(sa_exc.InvalidRequestError, q.get, (7, 10, 100))
def test_get_null_pk(self):
"""test that a mapping which can have None in a
@@ -214,8 +230,9 @@ class GetTest(QueryTest):
@testing.requires.unicode_connections
def test_unicode(self):
- """test that Query.get properly sets up the type for the bind parameter. using unicode would normally fail
- on postgresql, mysql and oracle unless it is converted to an encoded string"""
+ """test that Query.get properly sets up the type for the bind
+ parameter. using unicode would normally fail on postgresql, mysql and
+ oracle unless it is converted to an encoded string"""
metadata = MetaData(engines.utf8_engine())
table = Table('unicode_data', metadata,
@@ -2825,34 +2842,51 @@ class InstancesTest(QueryTest, AssertsCompiledSQL):
def test_from_alias(self):
- query = users.select(users.c.id==7).union(users.select(users.c.id>7)).alias('ulist').outerjoin(addresses).select(use_labels=True,order_by=['ulist.id', addresses.c.id])
+ query = users.select(users.c.id==7).\
+ union(users.select(users.c.id>7)).\
+ alias('ulist').\
+ outerjoin(addresses).\
+ select(use_labels=True,
+ order_by=['ulist.id', addresses.c.id])
sess =create_session()
q = sess.query(User)
def go():
- l = list(q.options(contains_alias('ulist'), contains_eager('addresses')).instances(query.execute()))
+ l = list(q.options(contains_alias('ulist'),
+ contains_eager('addresses')).\
+ instances(query.execute()))
assert self.static.user_address_result == l
self.assert_sql_count(testing.db, go, 1)
sess.expunge_all()
def go():
- l = q.options(contains_alias('ulist'), contains_eager('addresses')).from_statement(query).all()
+ l = q.options(contains_alias('ulist'),
+ contains_eager('addresses')).\
+ from_statement(query).all()
assert self.static.user_address_result == l
self.assert_sql_count(testing.db, go, 1)
# better way. use select_from()
def go():
- l = sess.query(User).select_from(query).options(contains_eager('addresses')).all()
+ l = sess.query(User).select_from(query).\
+ options(contains_eager('addresses')).all()
assert self.static.user_address_result == l
self.assert_sql_count(testing.db, go, 1)
- # same thing, but alias addresses, so that the adapter generated by select_from() is wrapped within
+ # same thing, but alias addresses, so that the adapter
+ # generated by select_from() is wrapped within
# the adapter created by contains_eager()
adalias = addresses.alias()
- query = users.select(users.c.id==7).union(users.select(users.c.id>7)).alias('ulist').outerjoin(adalias).select(use_labels=True,order_by=['ulist.id', adalias.c.id])
+ query = users.select(users.c.id==7).\
+ union(users.select(users.c.id>7)).\
+ alias('ulist').\
+ outerjoin(adalias).\
+ select(use_labels=True,
+ order_by=['ulist.id', adalias.c.id])
def go():
- l = sess.query(User).select_from(query).options(contains_eager('addresses', alias=adalias)).all()
+ l = sess.query(User).select_from(query).\
+ options(contains_eager('addresses', alias=adalias)).all()
assert self.static.user_address_result == l
self.assert_sql_count(testing.db, go, 1)
@@ -2860,13 +2894,19 @@ class InstancesTest(QueryTest, AssertsCompiledSQL):
sess = create_session()
# test that contains_eager suppresses the normal outer join rendering
- q = sess.query(User).outerjoin(User.addresses).options(contains_eager(User.addresses)).order_by(User.id)
- self.assert_compile(q.with_labels().statement,
- "SELECT addresses.id AS addresses_id, addresses.user_id AS addresses_user_id, "\
- "addresses.email_address AS addresses_email_address, users.id AS users_id, "\
- "users.name AS users_name FROM users LEFT OUTER JOIN addresses "\
- "ON users.id = addresses.user_id ORDER BY users.id"
- , dialect=default.DefaultDialect())
+ q = sess.query(User).outerjoin(User.addresses).\
+ options(contains_eager(User.addresses)).\
+ order_by(User.id, addresses.c.id)
+ self.assert_compile(q.with_labels().statement,
+ 'SELECT addresses.id AS addresses_id, '
+ 'addresses.user_id AS addresses_user_id, '
+ 'addresses.email_address AS '
+ 'addresses_email_address, users.id AS '
+ 'users_id, users.name AS users_name FROM '
+ 'users LEFT OUTER JOIN addresses ON '
+ 'users.id = addresses.user_id ORDER BY '
+ 'users.id, addresses.id',
+ dialect=default.DefaultDialect())
def go():
assert self.static.user_address_result == q.all()
@@ -2874,7 +2914,7 @@ class InstancesTest(QueryTest, AssertsCompiledSQL):
sess.expunge_all()
adalias = addresses.alias()
- q = sess.query(User).select_from(users.outerjoin(adalias)).options(contains_eager(User.addresses, alias=adalias))
+ q = sess.query(User).select_from(users.outerjoin(adalias)).options(contains_eager(User.addresses, alias=adalias)).order_by(User.id, adalias.c.id)
def go():
eq_(self.static.user_address_result, q.order_by(User.id).all())
self.assert_sql_count(testing.db, go, 1)
diff --git a/test/perf/insertspeed.py b/test/perf/insertspeed.py
index 3ae1ccbde..9b397771d 100644
--- a/test/perf/insertspeed.py
+++ b/test/perf/insertspeed.py
@@ -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()
diff --git a/test/sql/test_compiler.py b/test/sql/test_compiler.py
index 01fe648a4..bc0a2e9c3 100644
--- a/test/sql/test_compiler.py
+++ b/test/sql/test_compiler.py
@@ -678,7 +678,7 @@ class SelectTest(TestBase, AssertsCompiledSQL):
select([func.count(distinct(table1.c.myid))]),
"SELECT count(DISTINCT mytable.myid) AS count_1 FROM mytable"
)
-
+
def test_operators(self):
for (py_op, sql_op) in ((operator.add, '+'), (operator.mul, '*'),
(operator.sub, '-'),
@@ -1293,7 +1293,7 @@ class SelectTest(TestBase, AssertsCompiledSQL):
self.assert_compile(
select([value_tbl.c.id], value_tbl.c.val1 / (value_tbl.c.val2 - value_tbl.c.val1) /value_tbl.c.val1 > 2.0),
- "SELECT values.id FROM values WHERE values.val1 / (values.val2 - values.val1) / values.val1 > :param_1"
+ "SELECT values.id FROM values WHERE (values.val1 / (values.val2 - values.val1)) / values.val1 > :param_1"
)
def test_collate(self):
@@ -1936,7 +1936,7 @@ class SelectTest(TestBase, AssertsCompiledSQL):
tuple_(table1.c.myid, table1.c.name).in_(
[tuple_(table2.c.otherid, table2.c.othername)]
),
- "(mytable.myid, mytable.name) IN (myothertable.otherid, myothertable.othername)"
+ "(mytable.myid, mytable.name) IN ((myothertable.otherid, myothertable.othername))"
)
self.assert_compile(
@@ -2055,6 +2055,42 @@ class SelectTest(TestBase, AssertsCompiledSQL):
self.assert_compile(table.select(between((table.c.field == table.c.field), False, True)),
"SELECT op.field FROM op WHERE (op.field = op.field) BETWEEN :param_1 AND :param_2")
+ def test_associativity(self):
+ f = column('f')
+ self.assert_compile( f - f, "f - f" )
+ self.assert_compile( f - f - f, "(f - f) - f" )
+
+ self.assert_compile( (f - f) - f, "(f - f) - f" )
+ self.assert_compile( (f - f).label('foo') - f, "(f - f) - f" )
+
+ self.assert_compile( f - (f - f), "f - (f - f)" )
+ self.assert_compile( f - (f - f).label('foo'), "f - (f - f)" )
+
+ # because - less precedent than /
+ self.assert_compile( f / (f - f), "f / (f - f)" )
+ self.assert_compile( f / (f - f).label('foo'), "f / (f - f)" )
+
+ self.assert_compile( f / f - f, "f / f - f" )
+ self.assert_compile( (f / f) - f, "f / f - f" )
+ self.assert_compile( (f / f).label('foo') - f, "f / f - f" )
+
+ # because / more precedent than -
+ self.assert_compile( f - (f / f), "f - f / f" )
+ self.assert_compile( f - (f / f).label('foo'), "f - f / f" )
+ self.assert_compile( f - f / f, "f - f / f" )
+ self.assert_compile( (f - f) / f, "(f - f) / f" )
+
+ self.assert_compile( ((f - f) / f) - f, "(f - f) / f - f")
+ self.assert_compile( (f - f) / (f - f), "(f - f) / (f - f)")
+
+ # higher precedence
+ self.assert_compile( (f / f) - (f / f), "f / f - f / f")
+
+ self.assert_compile( (f / f) - (f - f), "f / f - (f - f)")
+ self.assert_compile( (f / f) / (f - f), "(f / f) / (f - f)")
+ self.assert_compile( f / (f / (f - f)), "f / (f / (f - f))")
+
+
def test_delayed_col_naming(self):
my_str = Column(String)