summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2010-08-08 17:50:44 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2010-08-08 17:50:44 -0400
commitbb5a85feca1a6ed1aa7d18a04c0dfff3afa306c1 (patch)
tree12d6f26d360310bfa8b718e0876eff8eedd42c11
parent0b9afe1412898ba31282e75d90ca4d728613ca2b (diff)
parentbb3be98d3bee4b2bcef791be022ddb2510b9cf9c (diff)
downloadsqlalchemy-bb5a85feca1a6ed1aa7d18a04c0dfff3afa306c1.tar.gz
merge tip
-rw-r--r--CHANGES75
-rw-r--r--doc/build/mappers.rst72
-rw-r--r--doc/build/ormtutorial.rst39
-rw-r--r--doc/build/sqlexpression.rst45
-rw-r--r--doc/build/static/docs.css3
-rw-r--r--lib/sqlalchemy/dialects/sqlite/base.py2
-rw-r--r--lib/sqlalchemy/engine/base.py38
-rw-r--r--lib/sqlalchemy/exc.py4
-rwxr-xr-xlib/sqlalchemy/ext/declarative.py272
-rw-r--r--lib/sqlalchemy/orm/__init__.py390
-rw-r--r--lib/sqlalchemy/orm/attributes.py4
-rw-r--r--lib/sqlalchemy/orm/collections.py2
-rw-r--r--lib/sqlalchemy/orm/dynamic.py39
-rw-r--r--lib/sqlalchemy/orm/interfaces.py2
-rw-r--r--lib/sqlalchemy/orm/mapper.py86
-rw-r--r--lib/sqlalchemy/orm/properties.py112
-rw-r--r--lib/sqlalchemy/orm/query.py6
-rw-r--r--lib/sqlalchemy/orm/session.py5
-rw-r--r--lib/sqlalchemy/orm/strategies.py14
-rw-r--r--lib/sqlalchemy/orm/util.py55
-rw-r--r--lib/sqlalchemy/schema.py53
-rw-r--r--lib/sqlalchemy/sql/expression.py36
-rw-r--r--test/engine/test_reflection.py42
-rw-r--r--test/engine/test_transaction.py1
-rw-r--r--test/ext/test_declarative.py47
-rw-r--r--test/orm/test_collection.py23
-rw-r--r--test/orm/test_dynamic.py14
-rw-r--r--test/orm/test_eager_relations.py29
-rw-r--r--test/orm/test_query.py16
-rw-r--r--test/orm/test_subquery_relations.py25
-rw-r--r--test/sql/test_case_statement.py22
-rw-r--r--test/sql/test_query.py39
-rw-r--r--test/sql/test_selectable.py30
33 files changed, 1126 insertions, 516 deletions
diff --git a/CHANGES b/CHANGES
index ae1763028..0f35f9261 100644
--- a/CHANGES
+++ b/CHANGES
@@ -47,6 +47,27 @@ CHANGES
subclass attributes that "disable" propagation from
the parent - these needed to allow a merge()
operation to pass through without effect.
+
+ - Specifying a non-column based argument
+ for column_mapped_collection, including string,
+ text() etc., will raise an error message that
+ specifically asks for a column element, no longer
+ misleads with incorrect information about
+ text() or literal(). [ticket:1863]
+
+ - Dynamic attributes don't support collection
+ population - added an assertion for when
+ set_committed_value() is called, as well as
+ when joinedload() or subqueryload() options
+ are applied to a dynamic attribute, instead
+ of failure / silent failure. [ticket:1864]
+
+ - Fixed bug whereby generating a Query derived
+ from one which had the same column repeated
+ with different label names, typically
+ in some UNION situations, would fail to
+ propagate the inner columns completely to
+ the outer query. [ticket:1852]
- sql
- Changed the scheme used to generate truncated
@@ -66,7 +87,61 @@ CHANGES
this to appease MySQL who has a max length
of 64 for index names, separate from their
overall max length of 255. [ticket:1412]
+
+ - Calling fetchone() or similar on a result that
+ has already been exhausted, has been closed,
+ or is not a result-returning result now
+ raises ResourceClosedError, a subclass of
+ InvalidRequestError, in all cases, regardless
+ of backend. Previously, some DBAPIs would
+ raise ProgrammingError (i.e. pysqlite), others
+ would return None leading to downstream breakages
+ (i.e. MySQL-python).
+
+ - Connection, ResultProxy, as well as Session use
+ ResourceClosedError for all "this
+ connection/transaction/result is closed" types of
+ errors.
+
+ - the text() construct, if placed in a column
+ oriented situation, will at least return NULLTYPE
+ for its type instead of None, allowing it to
+ be used a little more freely for ad-hoc column
+ expressions than before. literal_column()
+ is still the better choice, however.
+
+ - Added full description of parent table/column,
+ target table/column in error message raised when
+ ForeignKey can't resolve target.
+
+ - Fixed bug whereby replacing composite foreign key
+ columns in a reflected table would cause an attempt
+ to remove the reflected constraint from the table
+ a second time, raising a KeyError. [ticket:1865]
+
+ - the _Label construct, i.e. the one that is produced
+ whenever you say somecol.label(), now counts itself
+ in its "proxy_set" unioned with that of it's
+ contained column's proxy set, instead of
+ directly returning that of the contained column.
+ This allows column correspondence
+ operations which depend on the identity of the
+ _Labels themselves to return the correct result
+ - fixes ORM bug [ticket:1852].
+- declarative
+ - if @classproperty is used with a regular class-bound
+ mapper property attribute, it will be called to get the
+ actual attribute value during initialization. Currently,
+ there's no advantage to using @classproperty on a column
+ or relationship attribute of a declarative class that
+ isn't a mixin - evaluation is at the same time as if
+ @classproperty weren't used. But here we at least allow
+ it to function as expected.
+
+ - Fixed bug where "Can't add additional column" message
+ would display the wrong name.
+
- mssql
- Fixed "default schema" query to work with
pymssql backend.
diff --git a/doc/build/mappers.rst b/doc/build/mappers.rst
index 701e95fac..97677aa08 100644
--- a/doc/build/mappers.rst
+++ b/doc/build/mappers.rst
@@ -204,11 +204,16 @@ And an entire "deferred group", i.e. which uses the ``group`` keyword argument t
SQL Expressions as Mapped Attributes
-------------------------------------
-To add a SQL clause composed of local or external columns as a read-only, mapped column attribute, use the :func:`~sqlalchemy.orm.column_property()` function. Any scalar-returning :class:`~sqlalchemy.sql.expression.ClauseElement` may be used, as long as it has a ``name`` attribute; usually, you'll want to call ``label()`` to give it a specific name::
+To add a SQL clause composed of local or external columns as
+a read-only, mapped column attribute, use the
+:func:`~sqlalchemy.orm.column_property()` function. Any
+scalar-returning
+:class:`~sqlalchemy.sql.expression.ClauseElement` may be
+used. Unlike older versions of SQLAlchemy, there is no :func:`~.sql.expression.label` requirement::
mapper(User, users_table, properties={
'fullname': column_property(
- (users_table.c.firstname + " " + users_table.c.lastname).label('fullname')
+ users_table.c.firstname + " " + users_table.c.lastname
)
})
@@ -221,10 +226,12 @@ Correlated subqueries may be used as well:
select(
[func.count(addresses_table.c.address_id)],
addresses_table.c.user_id==users_table.c.user_id
- ).label('address_count')
+ )
)
})
+The declarative form of the above is described in :ref:`declarative_sql_expressions`.
+
Changing Attribute Behavior
----------------------------
@@ -265,32 +272,39 @@ Validators also receive collection events, when items are added to a collection:
Using Descriptors
~~~~~~~~~~~~~~~~~~
-A more comprehensive way to produce modified behavior for an attribute is to use descriptors. These are commonly used in Python using the ``property()`` function. The standard SQLAlchemy technique for descriptors is to create a plain descriptor, and to have it read/write from a mapped attribute with a different name. To have the descriptor named the same as a column, map the column under a different name, i.e.:
-
-.. sourcecode:: python+sql
+A more comprehensive way to produce modified behavior for an attribute is to use descriptors. These are commonly used in Python using the ``property()`` function. The standard SQLAlchemy technique for descriptors is to create a plain descriptor, and to have it read/write from a mapped attribute with a different name. Below we illustrate
+this using Python 2.6-style properties::
class EmailAddress(object):
- def _set_email(self, email):
- self._email = email
- def _get_email(self):
- return self._email
- email = property(_get_email, _set_email)
+
+ @property
+ def email(self):
+ return self._email
+
+ @email.setter
+ def email(self, email):
+ self._email = email
- mapper(MyAddress, addresses_table, properties={
+ mapper(EmailAddress, addresses_table, properties={
'_email': addresses_table.c.email
})
-However, the approach above is not complete. While our ``EmailAddress`` object will shuttle the value through the ``email`` descriptor and into the ``_email`` mapped attribute, the class level ``EmailAddress.email`` attribute does not have the usual expression semantics usable with :class:`~sqlalchemy.orm.query.Query`. To provide these, we instead use the :func:`~sqlalchemy.orm.synonym` function as follows:
-
-.. sourcecode:: python+sql
+The approach above will work, but there's more we can add.
+While our ``EmailAddress`` object will shuttle the value
+through the ``email`` descriptor and into the ``_email``
+mapped attribute, the class level ``EmailAddress.email``
+attribute does not have the usual expression semantics
+usable with :class:`.Query`. To provide
+these, we instead use the :func:`.synonym`
+function as follows::
mapper(EmailAddress, addresses_table, properties={
'email': synonym('_email', map_column=True)
})
-The ``email`` attribute is now usable in the same way as any other mapped attribute, including filter expressions, get/set operations, etc.:
-
-.. sourcecode:: python+sql
+The ``email`` attribute is now usable in the same way as any
+other mapped attribute, including filter expressions,
+get/set operations, etc.::
address = session.query(EmailAddress).filter(EmailAddress.email == 'some address').one()
@@ -299,7 +313,7 @@ The ``email`` attribute is now usable in the same way as any other mapped attrib
q = session.query(EmailAddress).filter_by(email='some other address')
-If the mapped class does not provide a property, the :func:`~sqlalchemy.orm.synonym` construct will create a default getter/setter object automatically.
+If the mapped class does not provide a property, the :func:`.synonym` construct will create a default getter/setter object automatically.
To use synonyms with :mod:`~sqlalchemy.ext.declarative`, see the section
:ref:`declarative_synonyms`.
@@ -1234,14 +1248,26 @@ Working with the association pattern in its direct form requires that child obje
print assoc.data
print assoc.child
-To enhance the association object pattern such that direct access to the ``Association`` object is optional, SQLAlchemy provides the :ref:`associationproxy`.
-
-**Important Note**: it is strongly advised that the ``secondary`` table argument not be combined with the Association Object pattern, unless the :func:`~sqlalchemy.orm.relationship` which contains the ``secondary`` argument is marked ``viewonly=True``. Otherwise, SQLAlchemy may persist conflicting data to the underlying association table since it is represented by two conflicting mappings. The Association Proxy pattern should be favored in the case where access to the underlying association data is only sometimes needed.
+To enhance the association object pattern such that direct
+access to the ``Association`` object is optional, SQLAlchemy
+provides the :ref:`associationproxy` extension. This
+extension allows the configuration of attributes which will
+access two "hops" with a single access, one "hop" to the
+associated object, and a second to a target attribute.
+
+.. note:: When using the association object pattern, it is
+ advisable that the association-mapped table not be used
+ as the ``secondary`` argument on a :func:`.relationship`
+ elsewhere, unless that :func:`.relationship` contains
+ the option ``viewonly=True``. SQLAlchemy otherwise
+ may attempt to emit redundant INSERT and DELETE
+ statements on the same table, if similar state is detected
+ on the related attribute as well as the associated
+ object.
Adjacency List Relationships
-----------------------------
-
The **adjacency list** pattern is a common relational pattern whereby a table contains a foreign key reference to itself. This is the most common and simple way to represent hierarchical data in flat tables. The other way is the "nested sets" model, sometimes called "modified preorder". Despite what many online articles say about modified preorder, the adjacency list model is probably the most appropriate pattern for the large majority of hierarchical storage needs, for reasons of concurrency, reduced complexity, and that modified preorder has little advantage over an application which can fully load subtrees into the application space.
SQLAlchemy commonly refers to an adjacency list relationship as a **self-referential mapper**. In this example, we'll work with a single table called ``treenodes`` to represent a tree structure::
diff --git a/doc/build/ormtutorial.rst b/doc/build/ormtutorial.rst
index b5d7c83f0..2d6a3325e 100644
--- a/doc/build/ormtutorial.rst
+++ b/doc/build/ormtutorial.rst
@@ -3,7 +3,44 @@
==========================
Object Relational Tutorial
==========================
-In this tutorial we will cover a basic SQLAlchemy object-relational mapping scenario, where we store and retrieve Python objects from a database representation. The tutorial is in doctest format, meaning each ``>>>`` line represents something you can type at a Python command prompt, and the following text represents the expected return value.
+
+Introduction
+============
+
+The SQLAlchemy Object Relational Mapper presents a method of associating
+user-defined Python classes with database tables, and instances of those
+classes (objects) with rows in their corresponding tables. It includes a
+system that transparently synchronizes all changes in state between objects
+and their related rows, called a `unit of work
+<http://martinfowler.com/eaaCatalog/unitOfWork.html>`_, as well as a system
+for expressing database queries in terms of the user defined classes and their
+defined relationships between each other.
+
+The ORM is in contrast to the SQLAlchemy Expression Language, upon which the
+ORM is constructed. Whereas the SQL Expression Language, introduced in
+:ref:`sqlexpression_toplevel`, presents a system of representing the primitive
+constructs of the relational database directly without opinion, the ORM
+presents a high level and abstracted pattern of usage, which itself is an
+example of applied usage of the Expression Language.
+
+While there is overlap among the usage patterns of the ORM and the Expression
+Language, the similarities are more superficial than they may at first appear.
+One approaches the structure and content of data from the perspective of a
+user-defined `domain model
+<http://en.wikipedia.org/wiki/Domain_model>`_ which is transparently
+persisted and refreshed from its underlying storage model. The other
+approaches it from the perspective of literal schema and SQL expression
+representations which are explicitly composed into messages consumed
+individually by the database.
+
+A successful application may be constructed using the Object Relational Mapper
+exclusively. In advanced situations, an application constructed with the ORM
+may make occasional usage of the Expression Language directly in certain areas
+where specific database interactions are required.
+
+The following tutorial is in doctest format, meaning each ``>>>`` line
+represents something you can type at a Python command prompt, and the
+following text represents the expected return value.
Version Check
=============
diff --git a/doc/build/sqlexpression.rst b/doc/build/sqlexpression.rst
index 15116a273..23190a143 100644
--- a/doc/build/sqlexpression.rst
+++ b/doc/build/sqlexpression.rst
@@ -4,7 +4,50 @@
SQL Expression Language Tutorial
================================
-This tutorial will cover SQLAlchemy SQL Expressions, which are Python constructs that represent SQL statements. The tutorial is in doctest format, meaning each ``>>>`` line represents something you can type at a Python command prompt, and the following text represents the expected return value. The tutorial has no prerequisites.
+Introduction
+=============
+
+The SQLAlchemy Expression Language presents a system of representing
+relational database structures and expressions using Python constructs. These
+constructs are modeled to resemble those of the underlying database as closely
+as possible, while providing a modicum of abstraction of the various
+implementation differences between database backends. While the constructs
+attempt to represent equivalent concepts between backends with consistent
+structures, they do not conceal useful concepts that are unique to particular
+subsets of backends. The Expression Language therefore presents a method of
+writing backend-neutral SQL expressions, but does not attempt to enforce that
+expressions are backend-neutral.
+
+The Expression Language is in contrast to the Object Relational Mapper, which
+is a distinct API that builds on top of the Expression Language. Whereas the
+ORM, introduced in :ref:`ormtutorial_toplevel`, presents a high level and
+abstracted pattern of usage, which itself is an example of applied usage of
+the Expression Language, the Expression Language presents a system of
+representing the primitive constructs of the relational database directly
+without opinion.
+
+While there is overlap among the usage patterns of the ORM and the Expression
+Language, the similarities are more superficial than they may at first appear.
+One approaches the structure and content of data from the perspective of a
+user-defined `domain model
+<http://en.wikipedia.org/wiki/Domain_model>`_ which is transparently
+persisted and refreshed from its underlying storage model. The other
+approaches it from the perspective of literal schema and SQL expression
+representations which are explicitly composed into messages consumed
+individually by the database.
+
+A successful application may be constructed using the Expression Language
+exclusively, though the application will need to define its own system of
+translating application concepts into individual database messages and from
+individual database result sets. Alternatively, an application constructed
+with the ORM may, in advanced scenarios, make occasional usage of the
+Expression Language directly in certain areas where specific database
+interactions are required.
+
+The following tutorial is in doctest format, meaning each ``>>>`` line
+represents something you can type at a Python command prompt, and the
+following text represents the expected return value. The tutorial has no
+prerequisites.
Version Check
=============
diff --git a/doc/build/static/docs.css b/doc/build/static/docs.css
index 9127e980b..c0d634557 100644
--- a/doc/build/static/docs.css
+++ b/doc/build/static/docs.css
@@ -159,7 +159,8 @@ div.note, div.warning {
background-color:#EEFFEF;
}
-div.admonition {
+
+div.admonition, div.topic {
border:1px solid #CCCCCC;
margin:5px 5px 5px 5px;
padding:5px 5px 5px 35px;
diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py
index 2d9e56baf..b84b18e68 100644
--- a/lib/sqlalchemy/dialects/sqlite/base.py
+++ b/lib/sqlalchemy/dialects/sqlite/base.py
@@ -600,5 +600,5 @@ def _pragma_cursor(cursor):
"""work around SQLite issue whereby cursor.description is blank when PRAGMA returns no rows."""
if cursor.closed:
- cursor._fetchone_impl = lambda: None
+ cursor.fetchone = lambda: None
return cursor
diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py
index 8533e19fc..4260e923a 100644
--- a/lib/sqlalchemy/engine/base.py
+++ b/lib/sqlalchemy/engine/base.py
@@ -908,7 +908,7 @@ class Connection(Connectable):
self.__connection = self.engine.raw_connection()
self.__invalid = False
return self.__connection
- raise exc.InvalidRequestError("This Connection is closed")
+ raise exc.ResourceClosedError("This Connection is closed")
@property
def info(self):
@@ -956,7 +956,7 @@ class Connection(Connectable):
"""
if self.closed:
- raise exc.InvalidRequestError("This Connection is closed")
+ raise exc.ResourceClosedError("This Connection is closed")
if self.__connection.is_valid:
self.__connection.invalidate(exception)
@@ -2359,7 +2359,9 @@ class ResultProxy(object):
if _autoclose_connection and \
self.connection.should_close_with_result:
self.connection.close()
-
+ # allow consistent errors
+ self.cursor = None
+
def __iter__(self):
while True:
row = self.fetchone()
@@ -2441,14 +2443,32 @@ class ResultProxy(object):
return self.dialect.supports_sane_multi_rowcount
def _fetchone_impl(self):
- return self.cursor.fetchone()
+ try:
+ return self.cursor.fetchone()
+ except AttributeError:
+ self._non_result()
def _fetchmany_impl(self, size=None):
- return self.cursor.fetchmany(size)
+ try:
+ return self.cursor.fetchmany(size)
+ except AttributeError:
+ self._non_result()
def _fetchall_impl(self):
- return self.cursor.fetchall()
-
+ try:
+ return self.cursor.fetchall()
+ except AttributeError:
+ self._non_result()
+
+ def _non_result(self):
+ if self._metadata is None:
+ raise exc.ResourceClosedError(
+ "This result object does not return rows. "
+ "It has been closed automatically.",
+ )
+ else:
+ raise exc.ResourceClosedError("This result object is closed.")
+
def process_rows(self, rows):
process_row = self._process_row
metadata = self._metadata
@@ -2505,7 +2525,6 @@ class ResultProxy(object):
Else the cursor is automatically closed and None is returned.
"""
-
try:
row = self._fetchone_impl()
if row is not None:
@@ -2525,6 +2544,9 @@ class ResultProxy(object):
Returns None if no row is present.
"""
+ if self._metadata is None:
+ self._non_result()
+
try:
row = self._fetchone_impl()
except Exception, e:
diff --git a/lib/sqlalchemy/exc.py b/lib/sqlalchemy/exc.py
index 31826f44f..1c412824c 100644
--- a/lib/sqlalchemy/exc.py
+++ b/lib/sqlalchemy/exc.py
@@ -60,6 +60,10 @@ class InvalidRequestError(SQLAlchemyError):
"""
+class ResourceClosedError(InvalidRequestError):
+ """An operation was requested from a connection, cursor, or other
+ object that's in a closed state."""
+
class NoSuchColumnError(KeyError, InvalidRequestError):
"""A nonexistent column is requested from a ``RowProxy``."""
diff --git a/lib/sqlalchemy/ext/declarative.py b/lib/sqlalchemy/ext/declarative.py
index 3370a764c..2310f01ce 100755
--- a/lib/sqlalchemy/ext/declarative.py
+++ b/lib/sqlalchemy/ext/declarative.py
@@ -2,13 +2,14 @@
Synopsis
========
-SQLAlchemy object-relational configuration involves the use of
-:class:`~sqlalchemy.schema.Table`, :func:`~sqlalchemy.orm.mapper`, and
-class objects to define the three areas of configuration.
-:mod:`~sqlalchemy.ext.declarative` allows all three types of
-configuration to be expressed declaratively on an individual
-mapped class. Regular SQLAlchemy schema elements and ORM constructs
-are used in most cases.
+SQLAlchemy object-relational configuration involves the
+combination of :class:`.Table`, :func:`.mapper`, and class
+objects to define a mapped class.
+:mod:`~sqlalchemy.ext.declarative` allows all three to be
+expressed at once within the class declaration. As much as
+possible, regular SQLAlchemy schema and ORM constructs are
+used directly, so that configuration between "classical" ORM
+usage and declarative remain highly similar.
As a simple example::
@@ -23,72 +24,74 @@ As a simple example::
Above, the :func:`declarative_base` callable returns a new base class from
which all mapped classes should inherit. When the class definition is
-completed, a new :class:`~sqlalchemy.schema.Table` and
-:class:`~sqlalchemy.orm.mapper` will have been generated, accessible via the
-``__table__`` and ``__mapper__`` attributes on the ``SomeClass`` class.
+completed, a new :class:`.Table` and
+:func:`.mapper` will have been generated.
+
+The resulting table and mapper are accessible via
+``__table__`` and ``__mapper__`` attributes on the
+``SomeClass`` class::
+
+ # access the mapped Table
+ SomeClass.__table__
+
+ # access the Mapper
+ SomeClass.__mapper__
Defining Attributes
===================
-In the above example, the :class:`~sqlalchemy.schema.Column` objects are
+In the previous example, the :class:`.Column` objects are
automatically named with the name of the attribute to which they are
assigned.
-They can also be explicitly named, and that name does not have to be
-the same as name assigned on the class.
-The column will be assigned to the :class:`~sqlalchemy.schema.Table` using the
-given name, and mapped to the class using the attribute name::
+To name columns explicitly with a name distinct from their mapped attribute,
+just give the column a name. Below, column "some_table_id" is mapped to the
+"id" attribute of `SomeClass`, but in SQL will be represented as "some_table_id"::
class SomeClass(Base):
__tablename__ = 'some_table'
id = Column("some_table_id", Integer, primary_key=True)
- name = Column("name", String(50))
Attributes may be added to the class after its construction, and they will be
-added to the underlying :class:`~sqlalchemy.schema.Table` and
-:func:`~sqlalchemy.orm.mapper()` definitions as appropriate::
+added to the underlying :class:`.Table` and
+:func:`.mapper()` definitions as appropriate::
SomeClass.data = Column('data', Unicode)
SomeClass.related = relationship(RelatedInfo)
-Classes which are mapped explicitly using
-:func:`~sqlalchemy.orm.mapper()` can interact freely with declarative
-classes.
+Classes which are constructed using declarative can interact freely
+with classes that are mapped explicitly with :func:`mapper`.
It is recommended, though not required, that all tables
share the same underlying :class:`~sqlalchemy.schema.MetaData` object,
so that string-configured :class:`~sqlalchemy.schema.ForeignKey`
references can be resolved without issue.
-Association of Metadata and Engine
-==================================
+Accessing the MetaData
+=======================
The :func:`declarative_base` base class contains a
-:class:`~sqlalchemy.schema.MetaData` object where newly
-defined :class:`~sqlalchemy.schema.Table` objects are collected. This
-is accessed via the :class:`~sqlalchemy.schema.MetaData` class level
-accessor, so to create tables we can say::
+:class:`.MetaData` object where newly defined
+:class:`.Table` objects are collected. This object is
+intended to be accessed directly for
+:class:`.MetaData`-specific operations. Such as, to issue
+CREATE statements for all tables::
engine = create_engine('sqlite://')
Base.metadata.create_all(engine)
-The :class:`~sqlalchemy.engine.base.Engine` created above may also be
-directly associated with the declarative base class using the ``bind``
-keyword argument, where it will be associated with the underlying
-:class:`~sqlalchemy.schema.MetaData` object and allow SQL operations
-involving that metadata and its tables to make use of that engine
-automatically::
+The usual techniques of associating :class:`.MetaData:` with :class:`.Engine`
+apply, such as assigning to the ``bind`` attribute::
- Base = declarative_base(bind=create_engine('sqlite://'))
+ Base.metadata.bind = create_engine('sqlite://')
-Alternatively, by way of the normal
-:class:`~sqlalchemy.schema.MetaData` behavior, the ``bind`` attribute
-of the class level accessor can be assigned at any time as follows::
+To associate the engine with the :func:`declarative_base` at time
+of construction, the ``bind`` argument is accepted::
- Base.metadata.bind = create_engine('sqlite://')
+ Base = declarative_base(bind=create_engine('sqlite://'))
-The :func:`declarative_base` can also receive a pre-created
-:class:`~sqlalchemy.schema.MetaData` object, which allows a
+:func:`declarative_base` can also receive a pre-existing
+:class:`.MetaData` object, which allows a
declarative setup to be associated with an already
existing traditional collection of :class:`~sqlalchemy.schema.Table`
objects::
@@ -157,12 +160,13 @@ class after the fact::
Configuring Many-to-Many Relationships
======================================
-There's nothing special about many-to-many with declarative. The
-``secondary`` argument to :func:`~sqlalchemy.orm.relationship` still
-requires a :class:`~sqlalchemy.schema.Table` object, not a declarative
-class. The :class:`~sqlalchemy.schema.Table` should share the same
-:class:`~sqlalchemy.schema.MetaData` object used by the declarative
-base::
+Many-to-many relationships are also declared in the same way
+with declarative as with traditional mappings. The
+``secondary`` argument to
+:func:`.relationship` is as usual passed a
+:class:`.Table` object, which is typically declared in the
+traditional way. The :class:`.Table` usually shares
+the :class:`.MetaData` object used by the declarative base::
keywords = Table(
'keywords', Base.metadata,
@@ -175,10 +179,11 @@ base::
id = Column(Integer, primary_key=True)
keywords = relationship("Keyword", secondary=keywords)
-You should generally **not** map a class and also specify its table in
-a many-to-many relationship, since the ORM may issue duplicate INSERT and
-DELETE statements.
-
+As with traditional mapping, its generally not a good idea to use
+a :class:`.Table` as the "secondary" argument which is also mapped to
+a class, unless the :class:`.relationship` is declared with ``viewonly=True``.
+Otherwise, the unit-of-work system may attempt duplicate INSERT and
+DELETE statements against the underlying table.
.. _declarative_synonyms:
@@ -187,18 +192,25 @@ Defining Synonyms
Synonyms are introduced in :ref:`synonyms`. To define a getter/setter
which proxies to an underlying attribute, use
-:func:`~sqlalchemy.orm.synonym` with the ``descriptor`` argument::
+:func:`~.synonym` with the ``descriptor`` argument. Here we present
+using Python 2.6 style properties::
class MyClass(Base):
__tablename__ = 'sometable'
+ id = Column(Integer, primary_key=True)
+
_attr = Column('attr', String)
- def _get_attr(self):
- return self._some_attr
- def _set_attr(self, attr):
- self._some_attr = attr
- attr = synonym('_attr', descriptor=property(_get_attr, _set_attr))
+ @property
+ def attr(self):
+ return self._attr
+
+ @attr.setter
+ def attr(self, attr):
+ self._attr = attr
+
+ attr = synonym('_attr', descriptor=attr)
The above synonym is then usable as an instance attribute as well as a
class-level expression construct::
@@ -212,16 +224,17 @@ conjunction with ``@property``::
class MyClass(Base):
__tablename__ = 'sometable'
-
+
+ id = Column(Integer, primary_key=True)
_attr = Column('attr', String)
@synonym_for('_attr')
@property
def attr(self):
- return self._some_attr
+ return self._attr
Similarly, :func:`comparable_using` is a front end for the
-:func:`~sqlalchemy.orm.comparable_property` ORM function::
+:func:`~.comparable_property` ORM function::
class MyClass(Base):
__tablename__ = 'sometable'
@@ -233,6 +246,61 @@ Similarly, :func:`comparable_using` is a front end for the
def uc_name(self):
return self.name.upper()
+.. _declarative_sql_expressions:
+
+Defining SQL Expressions
+========================
+
+The usage of :func:`.column_property` with Declarative is
+pretty much the same as that described in
+:ref:`mapper_sql_expressions`. Local columns within the same
+class declaration can be referenced directly::
+
+ class User(Base):
+ __tablename__ = 'user'
+ id = Column(Integer, primary_key=True)
+ firstname = Column(String)
+ lastname = Column(String)
+ fullname = column_property(
+ firstname + " " + lastname
+ )
+
+Correlated subqueries reference the :class:`Column` objects they
+need either from the local class definition or from remote
+classes::
+
+ from sqlalchemy.sql import func
+
+ class Address(Base):
+ __tablename__ = 'address'
+
+ id = Column('id', Integer, primary_key=True)
+ user_id = Column(Integer, ForeignKey('user.id'))
+
+ class User(Base):
+ __tablename__ = 'user'
+
+ id = Column(Integer, primary_key=True)
+ name = Column(String)
+
+ address_count = column_property(
+ select([func.count(Address.id)]).\\
+ where(Address.user_id==id)
+ )
+
+In the case that the ``address_count`` attribute above doesn't have access to
+``Address`` when ``User`` is defined, the ``address_count`` attribute should
+be added to ``User`` when both ``User`` and ``Address`` are available (i.e.
+there is no string based "late compilation" feature like there is with
+:func:`.relationship` at this time). Note we reference the ``id`` column
+attribute of ``User`` with its class when we are no longer in the declaration
+of the ``User`` class::
+
+ User.address_count = column_property(
+ select([func.count(Address.id)]).\\
+ where(Address.user_id==User.id)
+ )
+
Table Configuration
===================
@@ -263,6 +331,9 @@ arguments to be specified as well (usually constraints)::
Note that the keyword parameters dictionary is required in the tuple
form even if empty.
+Using a Hybrid Approach with __table__
+=======================================
+
As an alternative to ``__tablename__``, a direct
:class:`~sqlalchemy.schema.Table` construct may be used. The
:class:`~sqlalchemy.schema.Column` objects, which in this case require
@@ -275,23 +346,48 @@ to a table::
Column('name', String(50))
)
+``__table__`` provides a more focused point of control for establishing
+table metadata, while still getting most of the benefits of using declarative.
+An application that uses reflection might want to load table metadata elsewhere
+and simply pass it to declarative classes::
+
+ from sqlalchemy.ext.declarative import declarative_base
+
+ Base = declarative_base()
+ Base.metadata.reflect(some_engine)
+
+ class User(Base):
+ __table__ = metadata['user']
+
+ class Address(Base):
+ __table__ = metadata['address']
+
+Some configuration schemes may find it more appropriate to use ``__table__``,
+such as those which already take advantage of the data-driven nature of
+:class:`.Table` to customize and/or automate schema definition.
+
Mapper Configuration
====================
-Configuration of mappers is done with the
-:func:`~sqlalchemy.orm.mapper` function and all the possible mapper
-configuration parameters can be found in the documentation for that
-function.
-
-:func:`~sqlalchemy.orm.mapper` is still used by declaratively mapped
-classes and keyword parameters to the function can be passed by
-placing them in the ``__mapper_args__`` class variable::
+Declarative makes use of the :func:`~.orm.mapper` function internally
+when it creates the mapping to the declared table. The options
+for :func:`~.orm.mapper` are passed directly through via the ``__mapper_args__``
+class attribute. As always, arguments which reference locally
+mapped columns can reference them directly from within the
+class declaration::
+ from datetime import datetime
+
class Widget(Base):
__tablename__ = 'widgets'
+
id = Column(Integer, primary_key=True)
+ timestamp = Column(DateTime, nullable=False)
- __mapper_args__ = {'extension': MyWidgetExtension()}
+ __mapper_args__ = {
+ 'version_id_col': timestamp,
+ 'version_id_generator': lambda v:datetime.now()
+ }
Inheritance Configuration
=========================
@@ -531,12 +627,12 @@ Mixing in Relationships
~~~~~~~~~~~~~~~~~~~~~~~
Relationships created by :func:`~sqlalchemy.orm.relationship` are provided
-exclusively using the :func:`~sqlalchemy.util.classproperty` approach,
-eliminating any ambiguity which could arise when copying a relationship
-and its possibly column-bound contents. Below is an example which
-combines a foreign key column and a relationship so that two classes
-``Foo`` and ``Bar`` can both be configured to reference a common
-target class via many-to-one::
+with declarative mixin classes exclusively using the
+:func:`~sqlalchemy.util.classproperty` approach, eliminating any ambiguity
+which could arise when copying a relationship and its possibly column-bound
+contents. Below is an example which combines a foreign key column and a
+relationship so that two classes ``Foo`` and ``Bar`` can both be configured to
+reference a common target class via many-to-one::
class RefTargetMixin(object):
@classproperty
@@ -586,9 +682,9 @@ Mixing in deferred(), column_property(), etc.
Like :func:`~sqlalchemy.orm.relationship`, all
:class:`~sqlalchemy.orm.interfaces.MapperProperty` subclasses such as
:func:`~sqlalchemy.orm.deferred`, :func:`~sqlalchemy.orm.column_property`,
-etc. ultimately involve references to columns, and therefore have the
-:func:`~sqlalchemy.util.classproperty` requirement so that no reliance on
-copying is needed::
+etc. ultimately involve references to columns, and therefore, when
+used with declarative mixins, have the :func:`~sqlalchemy.util.classproperty`
+requirement so that no reliance on copying is needed::
class SomethingMixin(object):
@@ -607,7 +703,8 @@ Controlling table inheritance with mixins
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``__tablename__`` attribute in conjunction with the hierarchy of
-the classes involved controls what type of table inheritance, if any,
+classes involved in a declarative mixin scenario controls what type of
+table inheritance, if any,
is configured by the declarative extension.
If the ``__tablename__`` is computed by a mixin, you may need to
@@ -700,12 +797,13 @@ classes::
Combining Table/Mapper Arguments from Multiple Mixins
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-In the case of ``__table_args__`` or ``__mapper_args__``, you may want
-to combine some parameters from several mixins with those you wish to
-define on the class iteself. The
-:func:`~sqlalchemy.util.classproperty` decorator can be used here
-to create user-defined collation routines that pull from multiple
-collections::
+In the case of ``__table_args__`` or ``__mapper_args__``
+specified with declarative mixins, you may want to combine
+some parameters from several mixins with those you wish to
+define on the class iteself. The
+:func:`~sqlalchemy.util.classproperty` decorator can be used
+here to create user-defined collation routines that pull
+from multiple collections::
from sqlalchemy.util import classproperty
@@ -866,8 +964,12 @@ def _as_declarative(cls, classname, dict_):
cls._decl_class_registry[classname] = cls
our_stuff = util.OrderedDict()
+
for k in dict_:
value = dict_[k]
+ if isinstance(value, util.classproperty):
+ value = getattr(cls, k)
+
if (isinstance(value, tuple) and len(value) == 1 and
isinstance(value[0], (Column, MapperProperty))):
util.warn("Ignoring declarative-like tuple value of attribute "
@@ -933,7 +1035,7 @@ def _as_declarative(cls, classname, dict_):
if not table.c.contains_column(c):
raise exceptions.ArgumentError(
"Can't add additional column %r when "
- "specifying __table__" % key
+ "specifying __table__" % c.key
)
if 'inherits' not in mapper_args:
diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py
index 77281ee56..2004dccd1 100644
--- a/lib/sqlalchemy/orm/__init__.py
+++ b/lib/sqlalchemy/orm/__init__.py
@@ -1,5 +1,6 @@
# sqlalchemy/orm/__init__.py
-# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Michael Bayer mike_mp@zzzcomputing.com
+# Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010 Michael Bayer
+# mike_mp@zzzcomputing.com
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -51,11 +52,11 @@ from sqlalchemy.orm import strategies
from sqlalchemy.orm.query import AliasOption, Query
from sqlalchemy.sql import util as sql_util
from sqlalchemy.orm.session import Session as _Session
-from sqlalchemy.orm.session import object_session, sessionmaker, make_transient
+from sqlalchemy.orm.session import object_session, sessionmaker, \
+ make_transient
from sqlalchemy.orm.scoping import ScopedSession
from sqlalchemy import util as sa_util
-
__all__ = (
'EXT_CONTINUE',
'EXT_STOP',
@@ -174,10 +175,11 @@ def relationship(argument, secondary=None, **kwargs):
"""Provide a relationship of a primary Mapper to a secondary Mapper.
.. note:: This function is known as :func:`relation` in all versions
- of SQLAlchemy prior to version 0.6beta2, including the 0.5 and 0.4 series.
- :func:`~sqlalchemy.orm.relationship()` is only available starting with
- SQLAlchemy 0.6beta2. The :func:`relation` name will remain available for
- the foreseeable future in order to enable cross-compatibility.
+ of SQLAlchemy prior to version 0.6beta2, including the 0.5 and 0.4
+ series. :func:`~sqlalchemy.orm.relationship()` is only available
+ starting with SQLAlchemy 0.6beta2. The :func:`relation` name will
+ remain available for the foreseeable future in order to enable
+ cross-compatibility.
This corresponds to a parent-child or associative table relationship. The
constructed class is an instance of :class:`RelationshipProperty`.
@@ -223,7 +225,7 @@ def relationship(argument, secondary=None, **kwargs):
Available cascades are:
- * ``save-update`` - cascade the :meth:`~sqlalchemy.orm.session.Session.add`
+ * ``save-update`` - cascade the :meth:`.Session.add`
operation. This cascade applies both to future and
past calls to :meth:`~sqlalchemy.orm.session.Session.add`,
meaning new items added to a collection or scalar relationship
@@ -234,10 +236,10 @@ def relationship(argument, secondary=None, **kwargs):
* ``merge`` - cascade the :meth:`~sqlalchemy.orm.session.Session.merge`
operation
- * ``expunge`` - cascade the :meth:`~sqlalchemy.orm.session.Session.expunge`
+ * ``expunge`` - cascade the :meth:`.Session.expunge`
operation
- * ``delete`` - cascade the :meth:`~sqlalchemy.orm.session.Session.delete`
+ * ``delete`` - cascade the :meth:`.Session.delete`
operation
* ``delete-orphan`` - if an item of the child's type with no
@@ -245,7 +247,7 @@ def relationship(argument, secondary=None, **kwargs):
option prevents a pending item of the child's class from being
persisted without a parent present.
- * ``refresh-expire`` - cascade the :meth:`~sqlalchemy.orm.session.Session.expire`
+ * ``refresh-expire`` - cascade the :meth:`.Session.expire`
and :meth:`~sqlalchemy.orm.session.Session.refresh` operations
* ``all`` - shorthand for "save-update,merge, refresh-expire,
@@ -501,7 +503,8 @@ def dynamic_loader(argument, secondary=None, primaryjoin=None,
return RelationshipProperty(
argument, secondary=secondary, primaryjoin=primaryjoin,
- secondaryjoin=secondaryjoin, foreign_keys=foreign_keys, backref=backref,
+ secondaryjoin=secondaryjoin, foreign_keys=foreign_keys,
+ backref=backref,
post_update=post_update, cascade=cascade, remote_side=remote_side,
enable_typechecks=enable_typechecks, passive_deletes=passive_deletes,
order_by=order_by, comparator_factory=comparator_factory,doc=doc,
@@ -522,9 +525,9 @@ def column_property(*args, **kwargs):
:param \*cols:
list of Column objects to be mapped.
- :param comparator_factory:
- a class which extends :class:`sqlalchemy.orm.properties.ColumnProperty.Comparator`
- which provides custom SQL clause generation for comparison operations.
+ :param comparator_factory: a class which extends
+ :class:`.ColumnProperty.Comparator` which provides custom SQL clause
+ generation for comparison operations.
:param group:
a group name for this property when marked as deferred.
@@ -571,9 +574,9 @@ def composite(class_, *cols, **kwargs):
load immediately, and is instead loaded when the attribute is first
accessed on an instance. See also :func:`~sqlalchemy.orm.deferred`.
- :param comparator_factory:
- a class which extends :class:`sqlalchemy.orm.properties.CompositeProperty.Comparator`
- which provides custom SQL clause generation for comparison operations.
+ :param comparator_factory: a class which extends
+ :class:`.CompositeProperty.Comparator` which provides custom SQL clause
+ generation for comparison operations.
:param doc:
optional string that will be applied as the doc on the
@@ -616,140 +619,150 @@ def mapper(class_, local_table=None, *args, **params):
:param class\_: The class to be mapped.
- :param local_table: The table to which the class is mapped, or None if this mapper
- inherits from another mapper using concrete table inheritance.
-
- :param always_refresh: If True, all query operations for this mapped class will overwrite all
- data within object instances that already exist within the session,
- erasing any in-memory changes with whatever information was loaded
- from the database. Usage of this flag is highly discouraged; as an
- alternative, see the method `populate_existing()` on
- :class:`~sqlalchemy.orm.query.Query`.
-
- :param allow_null_pks: This flag is deprecated - this is stated as allow_partial_pks
- which defaults to True.
-
- :param allow_partial_pks: Defaults to True. Indicates that a composite primary key with
- some NULL values should be considered as possibly existing
- within the database. This affects whether a mapper will assign
- an incoming row to an existing identity, as well as if
- session.merge() will check the database first for a particular
- primary key value. A "partial primary key" can occur if one
- has mapped to an OUTER JOIN, for example.
-
- :param batch: Indicates that save operations of multiple entities can be batched
- together for efficiency. setting to False indicates that an instance
- will be fully saved before saving the next instance, which includes
- inserting/updating all table rows corresponding to the entity as well
- as calling all :class:`MapperExtension` methods corresponding to the save
- operation.
-
- :param column_prefix: A string which will be prepended to the `key` name of all Columns when
- creating column-based properties from the given Table. Does not
- affect explicitly specified column-based properties
-
- :param concrete: If True, indicates this mapper should use concrete table inheritance
- with its parent mapper.
-
- :param exclude_properties: A list of properties not to map. Columns present in the mapped table
- and present in this list will not be automatically converted into
- properties. Note that neither this option nor include_properties will
- allow an end-run around Python inheritance. If mapped class ``B``
- inherits from mapped class ``A``, no combination of includes or
- excludes will allow ``B`` to have fewer properties than its
- superclass, ``A``.
-
-
- :param extension: A :class:`~sqlalchemy.orm.interfaces.MapperExtension` instance or list of
- :class:`~sqlalchemy.orm.interfaces.MapperExtension` instances which will be applied to all
- operations by this :class:`~sqlalchemy.orm.mapper.Mapper`.
-
- :param include_properties: An inclusive list of properties to map. Columns present in the mapped
- table but not present in this list will not be automatically converted
- into properties.
+ :param local_table: The table to which the class is mapped, or None if
+ this mapper inherits from another mapper using concrete table
+ inheritance.
+
+ :param always_refresh: If True, all query operations for this mapped
+ class will overwrite all data within object instances that already
+ exist within the session, erasing any in-memory changes with
+ whatever information was loaded from the database. Usage of this
+ flag is highly discouraged; as an alternative, see the method
+ `populate_existing()` on :class:`~sqlalchemy.orm.query.Query`.
+
+ :param allow_null_pks: This flag is deprecated - this is stated as
+ allow_partial_pks which defaults to True.
+
+ :param allow_partial_pks: Defaults to True. Indicates that a
+ composite primary key with some NULL values should be considered as
+ possibly existing within the database. This affects whether a
+ mapper will assign an incoming row to an existing identity, as well
+ as if session.merge() will check the database first for a
+ particular primary key value. A "partial primary key" can occur if
+ one has mapped to an OUTER JOIN, for example.
+
+ :param batch: Indicates that save operations of multiple entities
+ can be batched together for efficiency. setting to False indicates
+ that an instance will be fully saved before saving the next
+ instance, which includes inserting/updating all table rows
+ corresponding to the entity as well as calling all
+ :class:`MapperExtension` methods corresponding to the save
+ operation.
+
+ :param column_prefix: A string which will be prepended to the `key`
+ name of all Columns when creating column-based properties from the
+ given Table. Does not affect explicitly specified column-based
+ properties
+
+ :param concrete: If True, indicates this mapper should use concrete
+ table inheritance with its parent mapper.
+
+ :param exclude_properties: A list of properties not to map. Columns
+ present in the mapped table and present in this list will not be
+ automatically converted into properties. Note that neither this
+ option nor include_properties will allow an end-run around Python
+ inheritance. If mapped class ``B`` inherits from mapped class
+ ``A``, no combination of includes or excludes will allow ``B`` to
+ have fewer properties than its superclass, ``A``.
+
+ :param extension: A :class:`.MapperExtension` instance or
+ list of :class:`~sqlalchemy.orm.interfaces.MapperExtension`
+ instances which will be applied to all operations by this
+ :class:`~sqlalchemy.orm.mapper.Mapper`.
+
+ :param include_properties: An inclusive list of properties to map.
+ Columns present in the mapped table but not present in this list
+ will not be automatically converted into properties.
:param inherits: Another :class:`~sqlalchemy.orm.Mapper` for which
this :class:`~sqlalchemy.orm.Mapper` will have an inheritance
relationship with.
-
- :param inherit_condition: For joined table inheritance, a SQL expression (constructed
- :class:`~sqlalchemy.expression.sql.ClauseElement`) which will define how the two tables are joined;
- defaults to a natural join between the two tables.
-
- :param inherit_foreign_keys: When inherit_condition is used and the condition contains no
- ForeignKey columns, specify the "foreign" columns of the join
- condition in this list. else leave as None.
-
- :param non_primary: Construct a :class:`Mapper` that will define only the selection of
- instances, not their persistence. Any number of non_primary mappers
- may be created for a particular class.
-
- :param order_by: A single :class:`Column` or list of :class:`Column` objects for which
- selection operations should use as the default ordering for entities.
- Defaults to the OID/ROWID of the table if any, or the first primary
- key column of the table.
-
- :param passive_updates: Indicates UPDATE behavior of foreign keys when a primary key changes
- on a joined-table inheritance or other joined table mapping.
-
+ :param inherit_condition: For joined table inheritance, a SQL
+ expression (constructed
+ :class:`~sqlalchemy.expression.sql.ClauseElement`) which will
+ define how the two tables are joined; defaults to a natural join
+ between the two tables.
+
+ :param inherit_foreign_keys: When inherit_condition is used and the
+ condition contains no ForeignKey columns, specify the "foreign"
+ columns of the join condition in this list. else leave as None.
+
+ :param non_primary: Construct a :class:`Mapper` that will define only
+ the selection of instances, not their persistence. Any number of
+ non_primary mappers may be created for a particular class.
+
+ :param order_by: A single :class:`Column` or list of :class:`Column`
+ objects for which selection operations should use as the default
+ ordering for entities. Defaults to the OID/ROWID of the table if
+ any, or the first primary key column of the table.
+
+ :param passive_updates: Indicates UPDATE behavior of foreign keys
+ when a primary key changes on a joined-table inheritance or other
+ joined table mapping.
+
When True, it is assumed that ON UPDATE CASCADE is configured on
- the foreign key in the database, and that the database will
- handle propagation of an UPDATE from a source column to
- dependent rows. Note that with databases which enforce
- referential integrity (i.e. PostgreSQL, MySQL with InnoDB tables),
- ON UPDATE CASCADE is required for this operation. The
- relationship() will update the value of the attribute on related
- items which are locally present in the session during a flush.
-
+ the foreign key in the database, and that the database will handle
+ propagation of an UPDATE from a source column to dependent rows.
+ Note that with databases which enforce referential integrity (i.e.
+ PostgreSQL, MySQL with InnoDB tables), ON UPDATE CASCADE is
+ required for this operation. The relationship() will update the
+ value of the attribute on related items which are locally present
+ in the session during a flush.
+
When False, it is assumed that the database does not enforce
- referential integrity and will not be issuing its own CASCADE
- operation for an update. The relationship() will issue the
- appropriate UPDATE statements to the database in response to the
- change of a referenced key, and items locally present in the
- session during a flush will also be refreshed.
-
+ referential integrity and will not be issuing its own CASCADE
+ operation for an update. The relationship() will issue the
+ appropriate UPDATE statements to the database in response to the
+ change of a referenced key, and items locally present in the
+ session during a flush will also be refreshed.
+
This flag should probably be set to False if primary key changes
- are expected and the database in use doesn't support CASCADE
- (i.e. SQLite, MySQL MyISAM tables).
-
+ are expected and the database in use doesn't support CASCADE (i.e.
+ SQLite, MySQL MyISAM tables).
+
Also see the passive_updates flag on :func:`relationship()`.
-
+
A future SQLAlchemy release will provide a "detect" feature for
- this flag.
-
- :param polymorphic_on: Used with mappers in an inheritance relationship, a ``Column`` which
- will identify the class/mapper combination to be used with a
- particular row. Requires the ``polymorphic_identity`` value to be set
- for all mappers in the inheritance hierarchy. The column specified by
- ``polymorphic_on`` is usually a column that resides directly within
- the base mapper's mapped table; alternatively, it may be a column that
- is only present within the <selectable> portion of the
- ``with_polymorphic`` argument.
-
- :param polymorphic_identity: A value which will be stored in the Column denoted by polymorphic_on,
- corresponding to the *class identity* of this mapper.
-
- :param properties: A dictionary mapping the string names of object attributes to
- ``MapperProperty`` instances, which define the persistence behavior of
- that attribute. Note that the columns in the mapped table are
- automatically converted into ``ColumnProperty`` instances based on the
- `key` property of each ``Column`` (although they can be overridden
- using this dictionary).
-
- :param primary_key: A list of ``Column`` objects which define the *primary key* to be used
- against this mapper's selectable unit. This is normally simply the
- primary key of the `local_table`, but can be overridden here.
-
- :param version_id_col: A ``Column`` which must have an integer type that will be used to keep
- a running *version id* of mapped entities in the database. this is
- used during save operations to ensure that no other thread or process
- has updated the instance during the lifetime of the entity, else a
- :class:`StaleDataError` exception is thrown.
-
- :param version_id_generator: A callable which defines the algorithm used to generate new version
- ids. Defaults to an integer generator. Can be replaced with one that
- generates timestamps, uuids, etc. e.g.::
+ this flag.
+
+ :param polymorphic_on: Used with mappers in an inheritance
+ relationship, a ``Column`` which will identify the class/mapper
+ combination to be used with a particular row. Requires the
+ ``polymorphic_identity`` value to be set for all mappers in the
+ inheritance hierarchy. The column specified by ``polymorphic_on``
+ is usually a column that resides directly within the base mapper's
+ mapped table; alternatively, it may be a column that is only
+ present within the <selectable> portion of the ``with_polymorphic``
+ argument.
+
+ :param polymorphic_identity: A value which will be stored in the
+ Column denoted by polymorphic_on, corresponding to the *class
+ identity* of this mapper.
+
+ :param properties: A dictionary mapping the string names of object
+ attributes to ``MapperProperty`` instances, which define the
+ persistence behavior of that attribute. Note that the columns in
+ the mapped table are automatically converted into
+ ``ColumnProperty`` instances based on the `key` property of each
+ ``Column`` (although they can be overridden using this dictionary).
+
+ :param primary_key: A list of ``Column`` objects which define the
+ *primary key* to be used against this mapper's selectable unit.
+ This is normally simply the primary key of the `local_table`, but
+ can be overridden here.
+
+ :param version_id_col: A ``Column`` which must have an integer type
+ that will be used to keep a running *version id* of mapped entities
+ in the database. this is used during save operations to ensure that
+ no other thread or process has updated the instance during the
+ lifetime of the entity, else a :class:`StaleDataError` exception is
+ thrown.
+
+ :param version_id_generator: A callable which defines the algorithm
+ used to generate new version ids. Defaults to an integer
+ generator. Can be replaced with one that generates timestamps,
+ uuids, etc. e.g.::
import uuid
@@ -761,29 +774,31 @@ def mapper(class_, local_table=None, *args, **params):
The callable receives the current version identifier as its
single argument.
- :param with_polymorphic: A tuple in the form ``(<classes>, <selectable>)`` indicating the
- default style of "polymorphic" loading, that is, which tables are
- queried at once. <classes> is any single or list of mappers and/or
- classes indicating the inherited classes that should be loaded at
- once. The special value ``'*'`` may be used to indicate all descending
- classes should be loaded immediately. The second tuple argument
- <selectable> indicates a selectable that will be used to query for
- multiple classes. Normally, it is left as None, in which case this
- mapper will form an outer join from the base mapper's table to that of
- all desired sub-mappers. When specified, it provides the selectable
- to be used for polymorphic loading. When with_polymorphic includes
- mappers which load from a "concrete" inheriting table, the
- <selectable> argument is required, since it usually requires more
- complex UNION queries.
+ :param with_polymorphic: A tuple in the form ``(<classes>,
+ <selectable>)`` indicating the default style of "polymorphic"
+ loading, that is, which tables are queried at once. <classes> is
+ any single or list of mappers and/or classes indicating the
+ inherited classes that should be loaded at once. The special value
+ ``'*'`` may be used to indicate all descending classes should be
+ loaded immediately. The second tuple argument <selectable>
+ indicates a selectable that will be used to query for multiple
+ classes. Normally, it is left as None, in which case this mapper
+ will form an outer join from the base mapper's table to that of
+ all desired sub-mappers. When specified, it provides the
+ selectable to be used for polymorphic loading. When
+ with_polymorphic includes mappers which load from a "concrete"
+ inheriting table, the <selectable> argument is required, since it
+ usually requires more complex UNION queries.
-
"""
return Mapper(class_, local_table, *args, **params)
-def synonym(name, map_column=False, descriptor=None, comparator_factory=None, doc=None):
+def synonym(name, map_column=False, descriptor=None,
+ comparator_factory=None, doc=None):
"""Set up `name` as a synonym to another mapped property.
- Used with the ``properties`` dictionary sent to :func:`~sqlalchemy.orm.mapper`.
+ Used with the ``properties`` dictionary sent to
+ :func:`~sqlalchemy.orm.mapper`.
Any existing attributes on the class which map the key name sent
to the ``properties`` dictionary will be used by the synonym to provide
@@ -848,12 +863,13 @@ def comparable_property(comparator_factory, descriptor=None):
mapper(MyClass, mytable, properties={
'myprop': comparable_property(MyComparator)})
- Used with the ``properties`` dictionary sent to :func:`~sqlalchemy.orm.mapper`.
+ Used with the ``properties`` dictionary sent to
+ :func:`~sqlalchemy.orm.mapper`.
Note that :func:`comparable_property` is usually not needed for basic
- needs. The recipe at :mod:`.derived_attributes` offers a simpler pure-Python
- method of achieving a similar result using class-bound attributes with
- SQLAlchemy expression constructs.
+ needs. The recipe at :mod:`.derived_attributes` offers a simpler
+ pure-Python method of achieving a similar result using class-bound
+ attributes with SQLAlchemy expression constructs.
:param comparator_factory:
A PropComparator subclass or factory that defines operator behavior
@@ -913,9 +929,9 @@ def joinedload(*keys, **kw):
name into an joined eager load.
.. note:: This function is known as :func:`eagerload` in all versions
- of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4 series.
- :func:`eagerload` will remain available for
- the foreseeable future in order to enable cross-compatibility.
+ of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4
+ series. :func:`eagerload` will remain available for the foreseeable
+ future in order to enable cross-compatibility.
Used with :meth:`~sqlalchemy.orm.query.Query.options`.
@@ -937,10 +953,11 @@ def joinedload(*keys, **kw):
query(Order).options(joinedload(Order.user, innerjoin=True))
- Note that the join created by :func:`joinedload` is aliased such that
- no other aspects of the query will affect what it loads. To use joined eager
- loading with a join that is constructed manually using :meth:`~sqlalchemy.orm.query.Query.join`
- or :func:`~sqlalchemy.orm.join`, see :func:`contains_eager`.
+ Note that the join created by :func:`joinedload` is aliased such that no
+ other aspects of the query will affect what it loads. To use joined eager
+ loading with a join that is constructed manually using
+ :meth:`~sqlalchemy.orm.query.Query.join` or :func:`~sqlalchemy.orm.join`,
+ see :func:`contains_eager`.
See also: :func:`subqueryload`, :func:`lazyload`
@@ -960,9 +977,9 @@ def joinedload_all(*keys, **kw):
given dot-separated path into an joined eager load.
.. note:: This function is known as :func:`eagerload_all` in all versions
- of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4 series.
- :func:`eagerload_all` will remain available for
- the foreseeable future in order to enable cross-compatibility.
+ of SQLAlchemy prior to version 0.6beta3, including the 0.5 and 0.4
+ series. :func:`eagerload_all` will remain available for the
+ foreseeable future in order to enable cross-compatibility.
Used with :meth:`~sqlalchemy.orm.query.Query.options`.
@@ -977,8 +994,9 @@ def joinedload_all(*keys, **kw):
query.options(joinedload_all(User.orders, Order.items, Item.keywords))
- The keyword arguments accept a flag `innerjoin=True|False` which will
- override the value of the `innerjoin` flag specified on the relationship().
+ The keyword arguments accept a flag `innerjoin=True|False` which will
+ override the value of the `innerjoin` flag specified on the
+ relationship().
See also: :func:`subqueryload_all`, :func:`lazyload`
@@ -1043,7 +1061,8 @@ def subqueryload_all(*keys):
Individual descriptors are accepted as arguments as well::
- query.options(subqueryload_all(User.orders, Order.items, Item.keywords))
+ query.options(subqueryload_all(User.orders, Order.items,
+ Item.keywords))
See also: :func:`joinedload_all`, :func:`lazyload`
@@ -1102,10 +1121,10 @@ def contains_eager(*keys, **kwargs):
``User`` entity, and the returned ``Order`` objects would have the
``Order.user`` attribute pre-populated.
- :func:`contains_eager` also accepts an `alias` argument, which
- is the string name of an alias, an :func:`~sqlalchemy.sql.expression.alias`
- construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this
- when the eagerly-loaded rows are to come from an aliased table::
+ :func:`contains_eager` also accepts an `alias` argument, which is the
+ string name of an alias, an :func:`~sqlalchemy.sql.expression.alias`
+ construct, or an :func:`~sqlalchemy.orm.aliased` construct. Use this when
+ the eagerly-loaded rows are to come from an aliased table::
user_alias = aliased(User)
sess.query(Order).\\
@@ -1118,12 +1137,11 @@ def contains_eager(*keys, **kwargs):
"""
alias = kwargs.pop('alias', None)
if kwargs:
- raise exceptions.ArgumentError("Invalid kwargs for contains_eager: %r" % kwargs.keys())
-
- return (
- strategies.EagerLazyOption(keys, lazy='joined', propagate_to_loaders=False),
- strategies.LoadEagerFromAliasOption(keys, alias=alias)
- )
+ raise exceptions.ArgumentError('Invalid kwargs for contains_eag'
+ 'er: %r' % kwargs.keys())
+ return strategies.EagerLazyOption(keys, lazy='joined',
+ propagate_to_loaders=False), \
+ strategies.LoadEagerFromAliasOption(keys, alias=alias)
@sa_util.accepts_a_list_as_starargs(list_deprecation='deprecated')
def defer(*keys):
diff --git a/lib/sqlalchemy/orm/attributes.py b/lib/sqlalchemy/orm/attributes.py
index 2701fc879..a09463a4f 100644
--- a/lib/sqlalchemy/orm/attributes.py
+++ b/lib/sqlalchemy/orm/attributes.py
@@ -484,6 +484,7 @@ class ScalarAttributeImpl(AttributeImpl):
accepts_scalar_loader = True
uses_objects = False
+ supports_population = True
def delete(self, state, dict_):
@@ -538,6 +539,7 @@ class MutableScalarAttributeImpl(ScalarAttributeImpl):
"""
uses_objects = False
+ supports_population = True
def __init__(self, class_, key, callable_, events,
class_manager, copy_function=None,
@@ -604,6 +606,7 @@ class ScalarObjectAttributeImpl(ScalarAttributeImpl):
accepts_scalar_loader = False
uses_objects = True
+ supports_population = True
def __init__(self, class_, key, callable_, events,
trackparent=False, extension=None, copy_function=None,
@@ -694,6 +697,7 @@ class CollectionAttributeImpl(AttributeImpl):
"""
accepts_scalar_loader = False
uses_objects = True
+ supports_population = True
def __init__(self, class_, key, callable_, events,
typecallable=None, trackparent=False, extension=None,
diff --git a/lib/sqlalchemy/orm/collections.py b/lib/sqlalchemy/orm/collections.py
index 0ea17cd8b..b5c4353b3 100644
--- a/lib/sqlalchemy/orm/collections.py
+++ b/lib/sqlalchemy/orm/collections.py
@@ -129,7 +129,7 @@ def column_mapped_collection(mapping_spec):
from sqlalchemy.orm.util import _state_mapper
from sqlalchemy.orm.attributes import instance_state
- cols = [expression._no_literals(q) for q in util.to_list(mapping_spec)]
+ cols = [expression._only_column_elements(q) for q in util.to_list(mapping_spec)]
if len(cols) == 1:
def keyfunc(value):
state = instance_state(value)
diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py
index d0aa2c20b..d55838011 100644
--- a/lib/sqlalchemy/orm/dynamic.py
+++ b/lib/sqlalchemy/orm/dynamic.py
@@ -43,10 +43,12 @@ log.class_logger(DynaLoader)
class DynamicAttributeImpl(attributes.AttributeImpl):
uses_objects = True
accepts_scalar_loader = False
-
+ supports_population = False
+
def __init__(self, class_, key, typecallable,
target_mapper, order_by, query_class=None, **kwargs):
- super(DynamicAttributeImpl, self).__init__(class_, key, typecallable, **kwargs)
+ super(DynamicAttributeImpl, self).\
+ __init__(class_, key, typecallable, **kwargs)
self.target_mapper = target_mapper
self.order_by = order_by
if not query_class:
@@ -58,15 +60,18 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
def get(self, state, dict_, passive=False):
if passive:
- return self._get_collection_history(state, passive=True).added_items
+ return self._get_collection_history(state,
+ passive=True).added_items
else:
return self.query_class(self, state)
def get_collection(self, state, dict_, user_data=None, passive=True):
if passive:
- return self._get_collection_history(state, passive=passive).added_items
+ return self._get_collection_history(state,
+ passive=passive).added_items
else:
- history = self._get_collection_history(state, passive=passive)
+ history = self._get_collection_history(state,
+ passive=passive)
return history.added_items + history.unchanged_items
def fire_append_event(self, state, dict_, value, initiator):
@@ -105,30 +110,36 @@ class DynamicAttributeImpl(attributes.AttributeImpl):
dict_[self.key] = True
return state.committed_state[self.key]
- def set(self, state, dict_, value, initiator, passive=attributes.PASSIVE_OFF):
+ def set(self, state, dict_, value, initiator,
+ passive=attributes.PASSIVE_OFF):
if initiator is self:
return
self._set_iterable(state, dict_, value)
def _set_iterable(self, state, dict_, iterable, adapter=None):
-
collection_history = self._modified_event(state, dict_)
new_values = list(iterable)
-
if state.has_identity:
old_collection = list(self.get(state, dict_))
else:
old_collection = []
-
- collections.bulk_replace(new_values, DynCollectionAdapter(self, state, old_collection), DynCollectionAdapter(self, state, new_values))
+ collections.bulk_replace(new_values, DynCollectionAdapter(self,
+ state, old_collection),
+ DynCollectionAdapter(self, state,
+ new_values))
def delete(self, *args, **kwargs):
raise NotImplementedError()
+ def set_committed_value(self, state, dict_, value):
+ raise NotImplementedError("Dynamic attributes don't support "
+ "collection population.")
+
def get_history(self, state, dict_, passive=False):
c = self._get_collection_history(state, passive)
- return attributes.History(c.added_items, c.unchanged_items, c.deleted_items)
+ return attributes.History(c.added_items, c.unchanged_items,
+ c.deleted_items)
def _get_collection_history(self, state, passive=False):
if self.key in state.committed_state:
@@ -193,7 +204,8 @@ class AppenderMixin(object):
def __session(self):
sess = object_session(self.instance)
- if sess is not None and self.autoflush and sess.autoflush and self.instance in sess:
+ if sess is not None and self.autoflush and sess.autoflush \
+ and self.instance in sess:
sess.flush()
if not has_identity(self.instance):
return None
@@ -283,7 +295,8 @@ class CollectionHistory(object):
deleted = util.IdentitySet(apply_to.deleted_items)
added = apply_to.added_items
coll = AppenderQuery(attr, state).autoflush(False)
- self.unchanged_items = [o for o in util.IdentitySet(coll) if o not in deleted]
+ self.unchanged_items = [o for o in util.IdentitySet(coll)
+ if o not in deleted]
self.added_items = apply_to.added_items
self.deleted_items = apply_to.deleted_items
else:
diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py
index 6ecc202b5..945b657e4 100644
--- a/lib/sqlalchemy/orm/interfaces.py
+++ b/lib/sqlalchemy/orm/interfaces.py
@@ -474,7 +474,7 @@ class MapperProperty(object):
return iter(())
- def set_parent(self, parent):
+ def set_parent(self, parent, init):
self.parent = parent
def instrument_class(self, mapper):
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index b566610f1..b268cf6b5 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -57,8 +57,6 @@ _COMPILE_MUTEX = util.threading.RLock()
# initialize these lazily
ColumnProperty = None
-SynonymProperty = None
-ComparableProperty = None
RelationshipProperty = None
ConcreteInheritedProperty = None
_expire_state = None
@@ -708,35 +706,11 @@ class Mapper(object):
for col in col.proxy_set:
self._columntoproperty[col] = prop
- elif isinstance(prop, (ComparableProperty, SynonymProperty)) and \
- setparent:
- if prop.descriptor is None:
- desc = getattr(self.class_, key, None)
- if self._is_userland_descriptor(desc):
- prop.descriptor = desc
- if getattr(prop, 'map_column', False):
- if key not in self.mapped_table.c:
- raise sa_exc.ArgumentError(
- "Can't compile synonym '%s': no column on table "
- "'%s' named '%s'"
- % (prop.name, self.mapped_table.description, key))
- elif self.mapped_table.c[key] in self._columntoproperty and \
- self._columntoproperty[
- self.mapped_table.c[key]
- ].key == prop.name:
- raise sa_exc.ArgumentError(
- "Can't call map_column=True for synonym %r=%r, "
- "a ColumnProperty already exists keyed to the name "
- "%r for column %r" %
- (key, prop.name, prop.name, key)
- )
- p = ColumnProperty(self.mapped_table.c[key])
- self._configure_property(
- prop.name, p,
- init=init,
- setparent=setparent)
- p._mapped_by_synonym = key
-
+ prop.key = key
+
+ if setparent:
+ prop.set_parent(self, init)
+
if key in self._props and \
getattr(self._props[key], '_mapped_by_synonym', False):
syn = self._props[key]._mapped_by_synonym
@@ -747,10 +721,6 @@ class Mapper(object):
)
self._props[key] = prop
- prop.key = key
-
- if setparent:
- prop.set_parent(self)
if not self.non_primary:
prop.instrument_class(self)
@@ -904,26 +874,36 @@ class Mapper(object):
def has_property(self, key):
return key in self._props
- def get_property(self, key, resolve_synonyms=False, raiseerr=True):
- """return a MapperProperty associated with the given key."""
+ def get_property(self, key,
+ resolve_synonyms=False,
+ raiseerr=True, _compile_mappers=True):
+
+ """return a :class:`.MapperProperty` associated with the given key.
+
+ resolve_synonyms=False and raiseerr=False are deprecated.
+
+ """
- if not self.compiled:
+ if _compile_mappers and not self.compiled:
self.compile()
- return self._get_property(key,
- resolve_synonyms=resolve_synonyms,
- raiseerr=raiseerr)
-
- def _get_property(self, key, resolve_synonyms=False, raiseerr=True):
- prop = self._props.get(key, None)
- if resolve_synonyms:
- while isinstance(prop, SynonymProperty):
- prop = self._props.get(prop.name, None)
- if prop is None and raiseerr:
- raise sa_exc.InvalidRequestError(
- "Mapper '%s' has no property '%s'" %
- (self, key))
- return prop
-
+
+ if not resolve_synonyms:
+ prop = self._props.get(key, None)
+ if prop is None and raiseerr:
+ raise sa_exc.InvalidRequestError(
+ "Mapper '%s' has no property '%s'" %
+ (self, key))
+ return prop
+ else:
+ try:
+ return getattr(self.class_, key).property
+ except AttributeError:
+ if raiseerr:
+ raise sa_exc.InvalidRequestError(
+ "Mapper '%s' has no property '%s'" % (self, key))
+ else:
+ return None
+
@property
def iterate_properties(self):
"""return an iterator of all MapperProperty objects."""
diff --git a/lib/sqlalchemy/orm/properties.py b/lib/sqlalchemy/orm/properties.py
index cf5f31162..cbfba91f3 100644
--- a/lib/sqlalchemy/orm/properties.py
+++ b/lib/sqlalchemy/orm/properties.py
@@ -237,7 +237,22 @@ class CompositeProperty(ColumnProperty):
def __str__(self):
return str(self.parent.class_.__name__) + "." + self.key
-class ConcreteInheritedProperty(MapperProperty):
+class DescriptorProperty(MapperProperty):
+ """:class:`MapperProperty` which proxies access to a
+ plain descriptor."""
+
+ def setup(self, context, entity, path, adapter, **kwargs):
+ pass
+
+ def create_row_processor(self, selectcontext, path, mapper, row, adapter):
+ return (None, None)
+
+ def merge(self, session, source_state, source_dict,
+ dest_state, dest_dict, load, _recursive):
+ pass
+
+
+class ConcreteInheritedProperty(DescriptorProperty):
"""A 'do nothing' :class:`MapperProperty` that disables
an attribute on a concrete subclass that is only present
on the inherited mapper, not the concrete classes' mapper.
@@ -254,18 +269,6 @@ class ConcreteInheritedProperty(MapperProperty):
"""
- extension = None
-
- def setup(self, context, entity, path, adapter, **kwargs):
- pass
-
- def create_row_processor(self, selectcontext, path, mapper, row, adapter):
- return (None, None)
-
- def merge(self, session, source_state, source_dict, dest_state,
- dest_dict, load, _recursive):
- pass
-
def instrument_class(self, mapper):
def warn():
raise AttributeError("Concrete %s does not implement "
@@ -284,7 +287,7 @@ class ConcreteInheritedProperty(MapperProperty):
comparator_callable = None
# TODO: put this process into a deferred callable?
for m in self.parent.iterate_to_root():
- p = m._get_property(self.key)
+ p = m.get_property(self.key, _compile_mappers=False)
if not isinstance(p, ConcreteInheritedProperty):
comparator_callable = p.comparator_factory
break
@@ -299,9 +302,7 @@ class ConcreteInheritedProperty(MapperProperty):
)
-class SynonymProperty(MapperProperty):
-
- extension = None
+class SynonymProperty(DescriptorProperty):
def __init__(self, name, map_column=None,
descriptor=None, comparator_factory=None,
@@ -313,14 +314,40 @@ class SynonymProperty(MapperProperty):
self.doc = doc or (descriptor and descriptor.__doc__) or None
util.set_creation_order(self)
- def setup(self, context, entity, path, adapter, **kwargs):
- pass
+ def set_parent(self, parent, init):
+ if self.map_column:
+ # implement the 'map_column' option.
+ if self.key not in parent.mapped_table.c:
+ raise sa_exc.ArgumentError(
+ "Can't compile synonym '%s': no column on table "
+ "'%s' named '%s'"
+ % (self.name, parent.mapped_table.description, self.key))
+ elif parent.mapped_table.c[self.key] in \
+ parent._columntoproperty and \
+ parent._columntoproperty[
+ parent.mapped_table.c[self.key]
+ ].key == self.name:
+ raise sa_exc.ArgumentError(
+ "Can't call map_column=True for synonym %r=%r, "
+ "a ColumnProperty already exists keyed to the name "
+ "%r for column %r" %
+ (self.key, self.name, self.name, self.key)
+ )
+ p = ColumnProperty(parent.mapped_table.c[self.key])
+ parent._configure_property(
+ self.name, p,
+ init=init,
+ setparent=True)
+ p._mapped_by_synonym = self.key
- def create_row_processor(self, selectcontext, path, mapper, row, adapter):
- return (None, None)
+ self.parent = parent
def instrument_class(self, mapper):
- class_ = self.parent.class_
+
+ if self.descriptor is None:
+ desc = getattr(mapper.class_, self.key, None)
+ if mapper._is_userland_descriptor(desc):
+ self.descriptor = desc
if self.descriptor is None:
class SynonymProp(object):
@@ -337,8 +364,9 @@ class SynonymProperty(MapperProperty):
def comparator_callable(prop, mapper):
def comparator():
- prop = self.parent._get_property(
- self.key, resolve_synonyms=True)
+ prop = mapper.get_property(
+ self.name, resolve_synonyms=True,
+ _compile_mappers=False)
if self.comparator_factory:
return self.comparator_factory(prop, mapper)
else:
@@ -355,17 +383,10 @@ class SynonymProperty(MapperProperty):
doc=self.doc
)
- def merge(self, session, source_state, source_dict, dest_state,
- dest_dict, load, _recursive):
- pass
-log.class_logger(SynonymProperty)
-
-class ComparableProperty(MapperProperty):
+class ComparableProperty(DescriptorProperty):
"""Instruments a Python property for use in query expressions."""
- extension = None
-
def __init__(self, comparator_factory, descriptor=None, doc=None):
self.descriptor = descriptor
self.comparator_factory = comparator_factory
@@ -374,6 +395,11 @@ class ComparableProperty(MapperProperty):
def instrument_class(self, mapper):
"""Set up a proxy to the unmanaged descriptor."""
+
+ if self.descriptor is None:
+ desc = getattr(mapper.class_, self.key, None)
+ if mapper._is_userland_descriptor(desc):
+ self.descriptor = desc
attributes.register_descriptor(
mapper.class_,
@@ -385,16 +411,6 @@ class ComparableProperty(MapperProperty):
doc=self.doc,
)
- def setup(self, context, entity, path, adapter, **kwargs):
- pass
-
- def create_row_processor(self, selectcontext, path, mapper, row, adapter):
- return (None, None)
-
- def merge(self, session, source_state, source_dict,
- dest_state, dest_dict, load, _recursive):
- pass
-
class RelationshipProperty(StrategizedProperty):
"""Describes an object property that holds a single item or list
@@ -837,7 +853,7 @@ class RelationshipProperty(StrategizedProperty):
yield (c, instance_mapper, instance_state)
def _add_reverse_property(self, key):
- other = self.mapper._get_property(key)
+ other = self.mapper.get_property(key, _compile_mappers=False)
self._reverse_property.add(other)
other._reverse_property.add(self)
@@ -924,8 +940,7 @@ class RelationshipProperty(StrategizedProperty):
if not self.parent.concrete:
for inheriting in self.parent.iterate_to_root():
if inheriting is not self.parent \
- and inheriting._get_property(self.key,
- raiseerr=False):
+ and inheriting.has_property(self.key):
util.warn("Warning: relationship '%s' on mapper "
"'%s' supercedes the same relationship "
"on inherited mapper '%s'; this can "
@@ -1216,7 +1231,7 @@ class RelationshipProperty(StrategizedProperty):
def _assert_is_primary(self):
if not self.is_primary() \
and not mapper.class_mapper(self.parent.class_,
- compile=False)._get_property(self.key, raiseerr=False):
+ compile=False).has_property(self.key):
raise sa_exc.ArgumentError("Attempting to assign a new "
"relationship '%s' to a non-primary mapper on "
"class '%s'. New relationships can only be added "
@@ -1234,8 +1249,7 @@ class RelationshipProperty(StrategizedProperty):
else:
backref_key, kwargs = self.backref
mapper = self.mapper.primary_mapper()
- if mapper._get_property(backref_key, raiseerr=False) \
- is not None:
+ if mapper.has_property(backref_key):
raise sa_exc.ArgumentError("Error creating backref "
"'%s' on relationship '%s': property of that "
"name exists on mapper '%s'" % (backref_key,
@@ -1416,7 +1430,5 @@ PropertyLoader = RelationProperty = RelationshipProperty
log.class_logger(RelationshipProperty)
mapper.ColumnProperty = ColumnProperty
-mapper.SynonymProperty = SynonymProperty
-mapper.ComparableProperty = ComparableProperty
mapper.RelationshipProperty = RelationshipProperty
mapper.ConcreteInheritedProperty = ConcreteInheritedProperty
diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py
index cc6d15a74..c269c8bb9 100644
--- a/lib/sqlalchemy/orm/query.py
+++ b/lib/sqlalchemy/orm/query.py
@@ -864,7 +864,7 @@ class Query(object):
"""apply the given filtering criterion to the query and return
the newly resulting ``Query``."""
- clauses = [_entity_descriptor(self._joinpoint_zero(), key)[0] == value
+ clauses = [_entity_descriptor(self._joinpoint_zero(), key) == value
for key, value in kwargs.iteritems()]
return self.filter(sql.and_(*clauses))
@@ -1158,7 +1158,7 @@ class Query(object):
if isinstance(onclause, basestring):
left_entity = self._joinpoint_zero()
- descriptor, prop = _entity_descriptor(left_entity, onclause)
+ descriptor = _entity_descriptor(left_entity, onclause)
onclause = descriptor
# check for q.join(Class.propname, from_joinpoint=True)
@@ -1171,7 +1171,7 @@ class Query(object):
_entity_info(self._joinpoint_zero())
if left_mapper is left_entity:
left_entity = self._joinpoint_zero()
- descriptor, prop = _entity_descriptor(left_entity,
+ descriptor = _entity_descriptor(left_entity,
onclause.key)
onclause = descriptor
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index d092375a6..8516a22de 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -235,7 +235,7 @@ class SessionTransaction(object):
def _assert_is_open(self, error_msg="The transaction is closed"):
if self.session is None:
- raise sa_exc.InvalidRequestError(error_msg)
+ raise sa_exc.ResourceClosedError(error_msg)
@property
def _is_transaction_boundary(self):
@@ -427,7 +427,8 @@ class SessionTransaction(object):
return self
def __exit__(self, type, value, traceback):
- self._assert_is_open("Cannot end transaction context. The transaction was closed from within the context")
+ self._assert_is_open("Cannot end transaction context. The transaction "
+ "was closed from within the context")
if self.session.transaction is None:
return
if type is None:
diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py
index 62602ff37..1c4571aed 100644
--- a/lib/sqlalchemy/orm/strategies.py
+++ b/lib/sqlalchemy/orm/strategies.py
@@ -806,6 +806,12 @@ class SubqueryLoader(AbstractRelationshipLoader):
]
def create_row_processor(self, context, path, mapper, row, adapter):
+ if not self.parent.class_manager[self.key].impl.supports_population:
+ raise sa_exc.InvalidRequestError(
+ "'%s' does not support object "
+ "population - eager loading cannot be applied." %
+ self)
+
path = path + (self.key,)
path = interfaces._reduce_path(path)
@@ -876,6 +882,7 @@ class EagerLoader(AbstractRelationshipLoader):
**kwargs):
"""Add a left outer join to the statement thats being constructed."""
+
if not context.query._enable_eagerloads:
return
@@ -1069,7 +1076,14 @@ class EagerLoader(AbstractRelationshipLoader):
return False
def create_row_processor(self, context, path, mapper, row, adapter):
+ if not self.parent.class_manager[self.key].impl.supports_population:
+ raise sa_exc.InvalidRequestError(
+ "'%s' does not support object "
+ "population - eager loading cannot be applied." %
+ self)
+
path = path + (self.key,)
+
eager_adapter = self._create_eager_adapter(
context,
diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py
index ef5413724..0f4adec00 100644
--- a/lib/sqlalchemy/orm/util.py
+++ b/lib/sqlalchemy/orm/util.py
@@ -347,9 +347,12 @@ class AliasedClass(object):
return queryattr
def __getattr__(self, key):
- prop = self.__mapper._get_property(key, raiseerr=False)
- if prop:
- return self.__adapt_prop(prop)
+ if self.__mapper.has_property(key):
+ return self.__adapt_prop(
+ self.__mapper.get_property(
+ key, _compile_mappers=False
+ )
+ )
for base in self.__target.__mro__:
try:
@@ -537,40 +540,22 @@ def _entity_info(entity, compile=True):
return mapper, mapper._with_polymorphic_selectable, False
def _entity_descriptor(entity, key):
- """Return attribute/property information given an entity and string name.
-
- Returns a 2-tuple representing InstrumentedAttribute/MapperProperty.
+ """Return a class attribute given an entity and string name.
+
+ May return :class:`.InstrumentedAttribute` or user-defined
+ attribute.
"""
- if isinstance(entity, AliasedClass):
- try:
- desc = getattr(entity, key)
- return desc, desc.property
- except AttributeError:
- raise sa_exc.InvalidRequestError(
- "Entity '%s' has no property '%s'" %
- (entity, key)
- )
-
- elif isinstance(entity, type):
- try:
- desc = attributes.manager_of_class(entity)[key]
- return desc, desc.property
- except KeyError:
- raise sa_exc.InvalidRequestError(
- "Entity '%s' has no property '%s'" %
- (entity, key)
- )
-
- else:
- try:
- desc = entity.class_manager[key]
- return desc, desc.property
- except KeyError:
- raise sa_exc.InvalidRequestError(
- "Entity '%s' has no property '%s'" %
- (entity, key)
- )
+ if not isinstance(entity, (AliasedClass, type)):
+ entity = entity.class_
+
+ try:
+ return getattr(entity, key)
+ except AttributeError:
+ raise sa_exc.InvalidRequestError(
+ "Entity '%s' has no property '%s'" %
+ (entity, key)
+ )
def _orm_columns(entity):
mapper, selectable, is_aliased_class = _entity_info(entity)
diff --git a/lib/sqlalchemy/schema.py b/lib/sqlalchemy/schema.py
index 75bbfd070..5f82a0200 100644
--- a/lib/sqlalchemy/schema.py
+++ b/lib/sqlalchemy/schema.py
@@ -806,7 +806,11 @@ class Column(SchemaItem, expression.ColumnClause):
for fk in col.foreign_keys:
col.foreign_keys.remove(fk)
table.foreign_keys.remove(fk)
- table.constraints.remove(fk.constraint)
+ if fk.constraint in table.constraints:
+ # this might have been removed
+ # already, if it's a composite constraint
+ # and more than one col being replaced
+ table.constraints.remove(fk.constraint)
table._columns.replace(self)
@@ -1019,7 +1023,20 @@ class ForeignKey(SchemaItem):
return "ForeignKey(%r)" % self._get_colspec()
def copy(self, schema=None):
- """Produce a copy of this ForeignKey object."""
+ """Produce a copy of this :class:`ForeignKey` object.
+
+ The new :class:`ForeignKey` will not be bound
+ to any :class:`Column`.
+
+ This method is usually used by the internal
+ copy procedures of :class:`Column`, :class:`Table`,
+ and :class:`MetaData`.
+
+ :param schema: The returned :class:`ForeignKey` will
+ reference the original table and column name, qualified
+ by the given string schema name.
+
+ """
return ForeignKey(
self._get_colspec(schema=schema),
@@ -1033,6 +1050,12 @@ class ForeignKey(SchemaItem):
)
def _get_colspec(self, schema=None):
+ """Return a string based 'column specification' for this :class:`ForeignKey`.
+
+ This is usually the equivalent of the string-based "tablename.colname"
+ argument first passed to the object's constructor.
+
+ """
if schema:
return schema + "." + self.column.table.name + \
"." + self.column.key
@@ -1048,15 +1071,16 @@ class ForeignKey(SchemaItem):
target_fullname = property(_get_colspec)
def references(self, table):
- """Return True if the given table is referenced by this ForeignKey."""
+ """Return True if the given :class:`Table` is referenced by this :class:`ForeignKey`."""
return table.corresponding_column(self.column) is not None
def get_referent(self, table):
- """Return the column in the given table referenced by this ForeignKey.
+ """Return the :class:`.Column` in the given :class:`.Table`
+ referenced by this :class:`ForeignKey`.
- Returns None if this ``ForeignKey`` does not reference the given
- table.
+ Returns None if this :class:`ForeignKey` does not reference the given
+ :class:`Table`.
"""
@@ -1064,6 +1088,18 @@ class ForeignKey(SchemaItem):
@util.memoized_property
def column(self):
+ """Return the target :class:`.Column` referenced by this :class:`.ForeignKey`.
+
+ If this :class:`ForeignKey` was created using a
+ string-based target column specification, this
+ attribute will on first access initiate a resolution
+ process to locate the referenced remote
+ :class:`.Column`. The resolution process traverses
+ to the parent :class:`.Column`, :class:`.Table`, and
+ :class:`.MetaData` to proceed - if any of these aren't
+ yet present, an error is raised.
+
+ """
# ForeignKey inits its remote column as late as possible, so tables
# can be defined without dependencies
if isinstance(self._colspec, basestring):
@@ -1108,8 +1144,9 @@ class ForeignKey(SchemaItem):
if _get_table_key(tname, schema) not in parenttable.metadata:
raise exc.NoReferencedTableError(
- "Could not find table '%s' with which to generate a "
- "foreign key" % tname)
+ "Foreign key assocated with column '%s' could not find "
+ "table '%s' with which to generate a "
+ "foreign key to target column '%s'" % (self.parent, tname, colname))
table = Table(tname, parenttable.metadata,
mustexist=True, schema=schema)
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 050b5c05b..0a5edb42f 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -428,8 +428,8 @@ def case(whens, value=None, else_=None):
The expressions used for THEN and ELSE,
when specified as strings, will be interpreted
as bound values. To specify textual SQL expressions
- for these, use the literal_column(<string>) or
- text(<string>) construct.
+ for these, use the :func:`literal_column`
+ construct.
The expressions used for the WHEN criterion
may only be literal strings when "value" is
@@ -1035,6 +1035,14 @@ def _no_literals(element):
else:
return element
+def _only_column_elements(element):
+ if hasattr(element, '__clause_element__'):
+ element = element.__clause_element__()
+ if not isinstance(element, ColumnElement):
+ raise exc.ArgumentError("Column-based expression object expected; "
+ "got: %r" % element)
+ return element
+
def _corresponding_column_or_error(fromclause, column, require_embedded=False):
c = fromclause.corresponding_column(column,
require_embedded=require_embedded)
@@ -1813,7 +1821,7 @@ class ColumnElement(ClauseElement, _CompareMixin):
else:
name = str(self)
co = ColumnClause(self.anon_label, selectable, type_=getattr(self, 'type', None))
-
+
co.proxies = [self]
selectable.columns[name] = co
return co
@@ -2428,7 +2436,7 @@ class _TextClause(Executable, ClauseElement):
if self.typemap is not None and len(self.typemap) == 1:
return list(self.typemap)[0]
else:
- return None
+ return sqltypes.NULLTYPE
def self_group(self, against=None):
if against is operators.in_op:
@@ -3179,7 +3187,8 @@ class _Label(ColumnElement):
self._element = element
self._type = type_
self.quote = element.quote
-
+ self.proxies = [element]
+
@util.memoized_property
def type(self):
return sqltypes.to_instance(
@@ -3190,17 +3199,13 @@ class _Label(ColumnElement):
def element(self):
return self._element.self_group(against=operators.as_)
- def _proxy_attr(name):
- get = attrgetter(name)
- def attr(self):
- return get(self.element)
- return property(attr)
+ @property
+ def primary_key(self):
+ return self.element.primary_key
- proxies = _proxy_attr('proxies')
- base_columns = _proxy_attr('base_columns')
- proxy_set = _proxy_attr('proxy_set')
- primary_key = _proxy_attr('primary_key')
- foreign_keys = _proxy_attr('foreign_keys')
+ @property
+ def foreign_keys(self):
+ return self.element.foreign_keys
def get_children(self, **kwargs):
return self.element,
@@ -3217,6 +3222,7 @@ class _Label(ColumnElement):
e = self.element._make_proxy(selectable, name=self.name)
else:
e = column(self.name)._make_proxy(selectable=selectable)
+
e.proxies.append(self)
return e
diff --git a/test/engine/test_reflection.py b/test/engine/test_reflection.py
index 1cbc9ab59..a82f1ec52 100644
--- a/test/engine/test_reflection.py
+++ b/test/engine/test_reflection.py
@@ -326,7 +326,44 @@ class ReflectionTest(TestBase, ComparesTables):
assert len(a4.constraints) == 2
finally:
meta.drop_all()
+
+ @testing.provide_metadata
+ def test_override_composite_fk(self):
+ """Test double-remove of composite foreign key, when replaced."""
+
+ a = Table('a',
+ metadata,
+ Column('x', sa.Integer, primary_key=True),
+ Column('y', sa.Integer, primary_key=True),
+ )
+
+ b = Table('b',
+ metadata,
+ Column('x', sa.Integer, primary_key=True),
+ Column('y', sa.Integer, primary_key=True),
+ sa.ForeignKeyConstraint(['x', 'y'], ['a.x', 'a.y'])
+ )
+
+ metadata.create_all()
+ meta2 = MetaData()
+
+ c1 = Column('x', sa.Integer, primary_key=True)
+ c2 = Column('y', sa.Integer, primary_key=True)
+ f1 = sa.ForeignKeyConstraint(['x', 'y'], ['a.x', 'a.y'])
+ b1 = Table('b',
+ meta2, c1, c2, f1,
+ autoload=True,
+ autoload_with=testing.db
+ )
+
+ assert b1.c.x is c1
+ assert b1.c.y is c2
+ assert f1 in b1.constraints
+ assert len(b1.constraints) == 2
+
+
+
def test_override_keys(self):
"""test that columns can be overridden with a 'key',
and that ForeignKey targeting during reflection still works."""
@@ -510,8 +547,9 @@ class ReflectionTest(TestBase, ComparesTables):
)
assert_raises_message(sa.exc.InvalidRequestError,
- "Could not find table 'pkgs' with which "
- "to generate a foreign key",
+ "Foreign key assocated with column 'slots.pkg_id' "
+ "could not find table 'pkgs' with which to generate "
+ "a foreign key to target column 'pkg_id'",
metadata.create_all)
def test_composite_pks(self):
diff --git a/test/engine/test_transaction.py b/test/engine/test_transaction.py
index bc0985bec..e7e2fe1b8 100644
--- a/test/engine/test_transaction.py
+++ b/test/engine/test_transaction.py
@@ -868,6 +868,7 @@ class TLTransactionTest(TestBase):
assert r2.connection.closed
assert tlengine.closed
+ @testing.crashes('oracle+cx_oracle', 'intermittent failures on the buildbot')
def test_dispose(self):
eng = create_engine(testing.db.url, strategy='threadlocal')
result = eng.execute(select([1]))
diff --git a/test/ext/test_declarative.py b/test/ext/test_declarative.py
index 4da826d38..71e31233b 100644
--- a/test/ext/test_declarative.py
+++ b/test/ext/test_declarative.py
@@ -690,6 +690,40 @@ class DeclarativeTest(DeclarativeTestBase):
eq_(sess.query(User).all(), [User(name='u1', address_count=2,
addresses=[Address(email='one'), Address(email='two')])])
+ def test_useless_classproperty(self):
+ class Address(Base, ComparableEntity):
+
+ __tablename__ = 'addresses'
+ id = Column('id', Integer, primary_key=True,
+ test_needs_autoincrement=True)
+ email = Column('email', String(50))
+ user_id = Column('user_id', Integer, ForeignKey('users.id'))
+
+ class User(Base, ComparableEntity):
+
+ __tablename__ = 'users'
+ id = Column('id', Integer, primary_key=True,
+ test_needs_autoincrement=True)
+ name = Column('name', String(50))
+ addresses = relationship('Address', backref='user')
+
+ @classproperty
+ def address_count(cls):
+ # this doesn't really gain us anything. but if
+ # one is used, lets have it function as expected...
+ return sa.orm.column_property(sa.select([sa.func.count(Address.id)]).
+ where(Address.user_id == cls.id))
+
+ Base.metadata.create_all()
+ u1 = User(name='u1', addresses=[Address(email='one'),
+ Address(email='two')])
+ sess = create_session()
+ sess.add(u1)
+ sess.flush()
+ sess.expunge_all()
+ eq_(sess.query(User).all(), [User(name='u1', address_count=2,
+ addresses=[Address(email='one'), Address(email='two')])])
+
def test_column(self):
class User(Base, ComparableEntity):
@@ -2585,17 +2619,18 @@ class DeclarativeMixinTest(DeclarativeTestBase):
def test_table_in_model_and_different_named_column_in_mixin(self):
class ColumnMixin:
-
tada = Column(Integer)
-
+
+
def go():
class Model(Base, ColumnMixin):
- __table__ = Table('foo', Base.metadata, Column('data',
- Integer), Column('id', Integer,
- primary_key=True))
-
+ __table__ = Table('foo', Base.metadata,
+ Column('data',Integer),
+ Column('id', Integer,primary_key=True))
+ foo = relationship("Dest")
+
assert_raises_message(sa.exc.ArgumentError,
"Can't add additional column 'tada' when "
"specifying __table__", go)
diff --git a/test/orm/test_collection.py b/test/orm/test_collection.py
index 7dcc5c56f..405829f74 100644
--- a/test/orm/test_collection.py
+++ b/test/orm/test_collection.py
@@ -7,12 +7,12 @@ from sqlalchemy.orm.collections import collection
import sqlalchemy as sa
from sqlalchemy.test import testing
-from sqlalchemy import Integer, String, ForeignKey
+from sqlalchemy import Integer, String, ForeignKey, text
from sqlalchemy.test.schema import Table, Column
from sqlalchemy import util, exc as sa_exc
from sqlalchemy.orm import create_session, mapper, relationship, attributes
from test.orm import _base
-from sqlalchemy.test.testing import eq_, assert_raises
+from sqlalchemy.test.testing import eq_, assert_raises, assert_raises_message
class Canary(sa.orm.interfaces.AttributeExtension):
def __init__(self):
@@ -1561,6 +1561,25 @@ class DictHelpersTest(_base.MappedTest):
eq_(Bar.foos.property.collection_class().keyfunc(Foo(id=3)), 3)
eq_(Bar.foos2.property.collection_class().keyfunc(Foo(id=3, bar_id=12)), (3, 12))
+
+ @testing.resolve_artifact_names
+ def test_column_mapped_assertions(self):
+ assert_raises_message(
+ sa_exc.ArgumentError,
+ "Column-based expression object expected; got: 'a'",
+ collections.column_mapped_collection, "a",
+ )
+ assert_raises_message(
+ sa_exc.ArgumentError,
+ "Column-based expression object expected; got",
+ collections.column_mapped_collection, text("a"),
+ )
+ assert_raises_message(
+ sa_exc.ArgumentError,
+ "Column-based expression object expected; got",
+ collections.column_mapped_collection, text("a"),
+ )
+
@testing.resolve_artifact_names
def test_column_mapped_collection(self):
diff --git a/test/orm/test_dynamic.py b/test/orm/test_dynamic.py
index b2fac6005..5d822fa3d 100644
--- a/test/orm/test_dynamic.py
+++ b/test/orm/test_dynamic.py
@@ -6,7 +6,7 @@ from sqlalchemy import Integer, String, ForeignKey, desc, select, func
from sqlalchemy.test.schema import Table, Column
from sqlalchemy.orm import mapper, relationship, create_session, Query, attributes
from sqlalchemy.orm.dynamic import AppenderMixin
-from sqlalchemy.test.testing import eq_, AssertsCompiledSQL
+from sqlalchemy.test.testing import eq_, AssertsCompiledSQL, assert_raises_message
from sqlalchemy.util import function_named
from test.orm import _base, _fixtures
@@ -123,6 +123,18 @@ class DynamicTest(_fixtures.FixtureTest, AssertsCompiledSQL):
self.assert_sql_count(testing.db, go, 2)
@testing.resolve_artifact_names
+ def test_no_populate(self):
+ mapper(User, users, properties={
+ 'addresses':dynamic_loader(mapper(Address, addresses))
+ })
+ u1 = User()
+ assert_raises_message(
+ NotImplementedError,
+ "Dynamic attributes don't support collection population.",
+ attributes.set_committed_value, u1, 'addresses', []
+ )
+
+ @testing.resolve_artifact_names
def test_m2m(self):
mapper(Order, orders, properties={
'items':relationship(Item, secondary=order_items, lazy="dynamic",
diff --git a/test/orm/test_eager_relations.py b/test/orm/test_eager_relations.py
index de397f47b..b96014a57 100644
--- a/test/orm/test_eager_relations.py
+++ b/test/orm/test_eager_relations.py
@@ -1,13 +1,17 @@
-"""basic tests of eager loaded attributes"""
+"""tests of joined-eager loaded attributes"""
from sqlalchemy.test.testing import eq_, is_, is_not_
import sqlalchemy as sa
from sqlalchemy.test import testing
-from sqlalchemy.orm import joinedload, deferred, undefer, joinedload_all, backref
-from sqlalchemy import Integer, String, Date, ForeignKey, and_, select, func
+from sqlalchemy.orm import joinedload, deferred, undefer, \
+ joinedload_all, backref
+from sqlalchemy import Integer, String, Date, ForeignKey, and_, select, \
+ func
from sqlalchemy.test.schema import Table, Column
-from sqlalchemy.orm import mapper, relationship, create_session, lazyload, aliased
-from sqlalchemy.test.testing import eq_, assert_raises
+from sqlalchemy.orm import mapper, relationship, create_session, \
+ lazyload, aliased
+from sqlalchemy.test.testing import eq_, assert_raises, \
+ assert_raises_message
from sqlalchemy.test.assertsql import CompiledSQL
from test.orm import _base, _fixtures
from sqlalchemy.util import OrderedDict as odict
@@ -283,7 +287,20 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
)
self.assert_sql_count(testing.db, go, count)
-
+ @testing.resolve_artifact_names
+ def test_disable_dynamic(self):
+ """test no joined option on a dynamic."""
+
+ mapper(User, users, properties={
+ 'addresses':relationship(Address, lazy="dynamic")
+ })
+ mapper(Address, addresses)
+ sess = create_session()
+ assert_raises_message(
+ sa.exc.InvalidRequestError,
+ "User.addresses' does not support object population - eager loading cannot be applied.",
+ sess.query(User).options(joinedload(User.addresses)).first,
+ )
@testing.resolve_artifact_names
def test_many_to_many(self):
diff --git a/test/orm/test_query.py b/test/orm/test_query.py
index ebe72b565..cc2f046cd 100644
--- a/test/orm/test_query.py
+++ b/test/orm/test_query.py
@@ -1132,6 +1132,22 @@ class SetOpsTest(QueryTest, AssertsCompiledSQL):
(User(id=10, name=u'chuck'), u'y')
]
)
+
+ c1, c2 = column('c1'), column('c2')
+ q1 = s.query(User, c1.label('foo'), c1.label('bar'))
+ q2 = s.query(User, c1.label('foo'), c2.label('bar'))
+ q3 = q1.union(q2)
+ self.assert_compile(
+ q3,
+ "SELECT anon_1.users_id AS anon_1_users_id, "
+ "anon_1.users_name AS anon_1_users_name, "
+ "anon_1.foo AS anon_1_foo, anon_1.bar AS anon_1_bar "
+ "FROM (SELECT users.id AS users_id, users.name AS users_name, "
+ "c1 AS foo, c1 AS bar FROM users UNION SELECT users.id AS "
+ "users_id, users.name AS users_name, c1 AS foo, c2 AS bar "
+ "FROM users) AS anon_1",
+ use_default_dialect=True
+ )
@testing.fails_on('mysql', "mysql doesn't support intersect")
def test_intersect(self):
diff --git a/test/orm/test_subquery_relations.py b/test/orm/test_subquery_relations.py
index 827630c3d..71f87a726 100644
--- a/test/orm/test_subquery_relations.py
+++ b/test/orm/test_subquery_relations.py
@@ -3,10 +3,10 @@ from sqlalchemy.test import testing
from sqlalchemy.test.schema import Table, Column
from sqlalchemy import Integer, String, ForeignKey, bindparam
from sqlalchemy.orm import backref, subqueryload, subqueryload_all, \
- mapper, relationship, clear_mappers,\
- create_session, lazyload, aliased, joinedload,\
- deferred, undefer
-from sqlalchemy.test.testing import eq_, assert_raises
+ mapper, relationship, clear_mappers, create_session, lazyload, \
+ aliased, joinedload, deferred, undefer
+from sqlalchemy.test.testing import eq_, assert_raises, \
+ assert_raises_message
from sqlalchemy.test.assertsql import CompiledSQL
from test.orm import _base, _fixtures
import sqlalchemy as sa
@@ -80,6 +80,23 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
self.assert_sql_count(testing.db, go, 2)
+ @testing.resolve_artifact_names
+ def test_disable_dynamic(self):
+ """test no subquery option on a dynamic."""
+
+ mapper(User, users, properties={
+ 'addresses':relationship(Address, lazy="dynamic")
+ })
+ mapper(Address, addresses)
+ sess = create_session()
+
+ # previously this would not raise, but would emit
+ # the query needlessly and put the result nowhere.
+ assert_raises_message(
+ sa.exc.InvalidRequestError,
+ "User.addresses' does not support object population - eager loading cannot be applied.",
+ sess.query(User).options(subqueryload(User.addresses)).first,
+ )
@testing.resolve_artifact_names
def test_many_to_many(self):
diff --git a/test/sql/test_case_statement.py b/test/sql/test_case_statement.py
index 3f3abe7e1..645822fa7 100644
--- a/test/sql/test_case_statement.py
+++ b/test/sql/test_case_statement.py
@@ -1,4 +1,4 @@
-from sqlalchemy.test.testing import assert_raises, assert_raises_message
+from sqlalchemy.test.testing import assert_raises, assert_raises_message, eq_
import sys
from sqlalchemy import *
from sqlalchemy.test import *
@@ -99,7 +99,25 @@ class CaseTest(TestBase, AssertsCompiledSQL):
self.assert_compile(case([("x", "y")], value=t.c.col1), "CASE test.col1 WHEN :param_1 THEN :param_2 END")
self.assert_compile(case([(t.c.col1==7, "y")], else_="z"), "CASE WHEN (test.col1 = :col1_1) THEN :param_1 ELSE :param_2 END")
-
+
+ def test_text_doesnt_explode(self):
+
+ for s in [
+ select([case([(info_table.c.info == 'pk_4_data',
+ text("'yes'"))], else_=text("'no'"
+ ))]).order_by(info_table.c.info),
+
+ select([case([(info_table.c.info == 'pk_4_data',
+ literal_column("'yes'"))], else_=literal_column("'no'"
+ ))]).order_by(info_table.c.info),
+
+ ]:
+ eq_(s.execute().fetchall(), [
+ (u'no', ), (u'no', ), (u'no', ), (u'yes', ),
+ (u'no', ), (u'no', ),
+ ])
+
+
@testing.fails_on('firebird', 'FIXME: unknown')
@testing.fails_on('maxdb', 'FIXME: unknown')
diff --git a/test/sql/test_query.py b/test/sql/test_query.py
index e8f9d118b..0a496906d 100644
--- a/test/sql/test_query.py
+++ b/test/sql/test_query.py
@@ -16,15 +16,19 @@ class QueryTest(TestBase):
users = Table('query_users', metadata,
Column('user_id', INT, primary_key=True, test_needs_autoincrement=True),
Column('user_name', VARCHAR(20)),
+ test_needs_acid=True
)
addresses = Table('query_addresses', metadata,
Column('address_id', Integer, primary_key=True, test_needs_autoincrement=True),
Column('user_id', Integer, ForeignKey('query_users.user_id')),
- Column('address', String(30)))
+ Column('address', String(30)),
+ test_needs_acid=True
+ )
users2 = Table('u2', metadata,
Column('user_id', INT, primary_key = True),
Column('user_name', VARCHAR(20)),
+ test_needs_acid=True
)
metadata.create_all()
@@ -615,6 +619,39 @@ class QueryTest(TestBase):
eq_(r[users.c.user_name], 'jack')
eq_(r.user_name, 'jack')
+ def test_graceful_fetch_on_non_rows(self):
+ """test that calling fetchone() etc. on a result that doesn't
+ return rows fails gracefully.
+
+ """
+
+ # these proxies don't work with no cursor.description present.
+ # so they don't apply to this test at the moment.
+ # base.FullyBufferedResultProxy,
+ # base.BufferedRowResultProxy,
+ # base.BufferedColumnResultProxy
+
+ conn = testing.db.connect()
+ for meth in ('fetchone', 'fetchall', 'first', 'scalar', 'fetchmany'):
+ trans = conn.begin()
+ result = conn.execute(users.insert(), user_id=1)
+ assert_raises_message(
+ exc.ResourceClosedError,
+ "This result object does not return rows. "
+ "It has been closed automatically.",
+ getattr(result, meth),
+ )
+ trans.rollback()
+
+ def test_fetchone_til_end(self):
+ result = testing.db.execute("select * from query_users")
+ eq_(result.fetchone(), None)
+ assert_raises_message(
+ exc.ResourceClosedError,
+ "This result object is closed.",
+ result.fetchone
+ )
+
def test_result_case_sensitivity(self):
"""test name normalization for result sets."""
diff --git a/test/sql/test_selectable.py b/test/sql/test_selectable.py
index 062ed5f1b..5bebbe05f 100644
--- a/test/sql/test_selectable.py
+++ b/test/sql/test_selectable.py
@@ -28,20 +28,40 @@ table2 = Table('table2', metadata,
class SelectableTest(TestBase, AssertsExecutionResults):
- def test_distance_on_labels(self):
-
+ def test_indirect_correspondence_on_labels(self):
+ # this test depends upon 'distance' to
+ # get the right result
+
# same column three times
s = select([table1.c.col1.label('c2'), table1.c.col1,
table1.c.col1.label('c1')])
- # didnt do this yet...col.label().make_proxy() has same
- # "distance" as col.make_proxy() so far assert
- # s.corresponding_column(table1.c.col1) is s.c.col1
+ # this tests the same thing as
+ # test_direct_correspondence_on_labels below -
+ # that the presence of label() affects the 'distance'
+ assert s.corresponding_column(table1.c.col1) is s.c.col1
assert s.corresponding_column(s.c.col1) is s.c.col1
assert s.corresponding_column(s.c.c1) is s.c.c1
+ def test_direct_correspondence_on_labels(self):
+ # this test depends on labels being part
+ # of the proxy set to get the right result
+
+ l1, l2 = table1.c.col1.label('foo'), table1.c.col1.label('bar')
+ sel = select([l1, l2])
+
+ sel2 = sel.alias()
+ assert sel2.corresponding_column(l1) is sel2.c.foo
+ assert sel2.corresponding_column(l2) is sel2.c.bar
+
+ sel2 = select([table1.c.col1.label('foo'), table1.c.col2.label('bar')])
+
+ sel3 = sel.union(sel2).alias()
+ assert sel3.corresponding_column(l1) is sel3.c.foo
+ assert sel3.corresponding_column(l2) is sel3.c.bar
+
def test_distance_on_aliases(self):
a1 = table1.alias('a1')
for s in (select([a1, table1], use_labels=True),