diff options
-rw-r--r-- | CHANGES | 38 | ||||
-rw-r--r-- | doc/build/core/types.rst | 4 | ||||
-rw-r--r-- | doc/build/orm/extensions/sqlsoup.rst | 7 | ||||
-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 | ||||
-rw-r--r-- | test/aaa_profiling/test_memusage.py | 27 | ||||
-rw-r--r-- | test/base/test_utils.py | 2 | ||||
-rw-r--r-- | test/dialect/test_postgresql.py | 49 | ||||
-rw-r--r-- | test/ext/test_sqlsoup.py | 77 | ||||
-rw-r--r-- | test/orm/inheritance/test_basic.py | 29 | ||||
-rw-r--r-- | test/orm/test_eager_relations.py | 3 | ||||
-rw-r--r-- | test/orm/test_expire.py | 13 | ||||
-rw-r--r-- | test/orm/test_mapper.py | 6 | ||||
-rw-r--r-- | test/orm/test_query.py | 80 | ||||
-rw-r--r-- | test/perf/insertspeed.py | 5 | ||||
-rw-r--r-- | test/sql/test_compiler.py | 42 |
22 files changed, 774 insertions, 215 deletions
@@ -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) |