diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-03-10 19:56:59 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-03-10 19:56:59 -0400 |
commit | 710021d22e8a5a053e1c4edc4a30612f6e10b83e (patch) | |
tree | 158a8e8ad555c255fdddadf891d981b40c6b87ec | |
parent | 201ba16fc88439fa100023369dfdfe22625253c9 (diff) | |
download | sqlalchemy-710021d22e8a5a053e1c4edc4a30612f6e10b83e.tar.gz |
- Added a new event suite :class:`.QueryEvents`. The
:meth:`.QueryEvents.before_compile` event allows the creation
of functions which may place additional modifications to
:class:`.Query` objects before the construction of the SELECT
statement. It is hoped that this event be made much more
useful via the advent of a new inspection system that will
allow for detailed modifications to be made against
:class:`.Query` objects in an automated fashion.
fixes #3317
-rw-r--r-- | doc/build/changelog/changelog_10.rst | 18 | ||||
-rw-r--r-- | doc/build/orm/events.rst | 8 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/events.py | 55 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 13 | ||||
-rw-r--r-- | test/orm/test_events.py | 38 |
5 files changed, 125 insertions, 7 deletions
diff --git a/doc/build/changelog/changelog_10.rst b/doc/build/changelog/changelog_10.rst index 8f6fd3f37..1776625f8 100644 --- a/doc/build/changelog/changelog_10.rst +++ b/doc/build/changelog/changelog_10.rst @@ -25,6 +25,24 @@ .. change:: :tags: feature, orm + :tickets: 3317 + + Added a new event suite :class:`.QueryEvents`. The + :meth:`.QueryEvents.before_compile` event allows the creation + of functions which may place additional modifications to + :class:`.Query` objects before the construction of the SELECT + statement. It is hoped that this event be made much more + useful via the advent of a new inspection system that will + allow for detailed modifications to be made against + :class:`.Query` objects in an automated fashion. + + .. seealso:: + + :class:`.QueryEvents` + + + .. change:: + :tags: feature, orm :tickets: 3249 The subquery wrapping which occurs when joined eager loading diff --git a/doc/build/orm/events.rst b/doc/build/orm/events.rst index 6f2e0cb29..e9673bed0 100644 --- a/doc/build/orm/events.rst +++ b/doc/build/orm/events.rst @@ -9,7 +9,7 @@ The ORM includes a wide variety of hooks available for subscription. The event supersedes the previous system of "extension" classes. For an introduction to the event API, see :ref:`event_toplevel`. Non-ORM events -such as those regarding connections and low-level statement execution are described in +such as those regarding connections and low-level statement execution are described in :ref:`core_event_toplevel`. Attribute Events @@ -36,6 +36,12 @@ Session Events .. autoclass:: sqlalchemy.orm.events.SessionEvents :members: +Query Events +------------- + +.. autoclass:: sqlalchemy.orm.events.QueryEvents + :members: + Instrumentation Events ----------------------- diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 7e23248b5..233cd66a6 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -17,7 +17,7 @@ from . import mapperlib, instrumentation from .session import Session, sessionmaker from .scoping import scoped_session from .attributes import QueryableAttribute - +from .query import Query class InstrumentationEvents(event.Events): """Events related to class instrumentation events. @@ -1651,3 +1651,56 @@ class AttributeEvents(event.Events): the :class:`.collection.linker` hook. """ + + +class QueryEvents(event.Events): + """Represent events within the construction of a :class:`.Query` object. + + The events here are intended to be used with an as-yet-unreleased + inspection system for :class:`.Query`. Some very basic operations + are possible now, however the inspection system is intended to allow + complex query manipulations to be automated. + + .. versionadded:: 1.0.0 + + """ + + _target_class_doc = "SomeQuery" + _dispatch_target = Query + + def before_compile(self, query): + """Receive the :class:`.Query` object before it is composed into a + core :class:`.Select` object. + + This event is intended to allow changes to the query given:: + + @event.listens_for(Query, "before_compile", retval=True) + def no_deleted(query): + for desc in query.column_descriptions: + if desc['type'] is User: + entity = desc['expr'] + query = query.filter(entity.deleted == False) + return query + + The event should normally be listened with the ``retval=True`` + parameter set, so that the modified query may be returned. + + + """ + + @classmethod + def _listen( + cls, event_key, retval=False, **kw): + fn = event_key._listen_fn + + if not retval: + def wrap(*arg, **kw): + if not retval: + query = arg[0] + fn(*arg, **kw) + return query + else: + return fn(*arg, **kw) + event_key = event_key.with_wrapper(wrap) + + event_key.base_listen(**kw) diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 9792b4e88..65c72e5e1 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -2934,6 +2934,12 @@ class Query(object): return update_op.rowcount def _compile_context(self, labels=True): + if self.dispatch.before_compile: + for fn in self.dispatch.before_compile: + new_query = fn(self) + if new_query is not None: + self = new_query + context = QueryContext(self) if context.statement is not None: @@ -2954,10 +2960,8 @@ class Query(object): # "load from explicit FROMs" mode, # i.e. when select_from() or join() is used context.froms = list(context.from_clause) - else: - # "load from discrete FROMs" mode, - # i.e. when each _MappedEntity has its own FROM - context.froms = context.froms + # else "load from discrete FROMs" mode, + # i.e. when each _MappedEntity has its own FROM if self._enable_single_crit: self._adjust_for_single_inheritance(context) @@ -2977,6 +2981,7 @@ class Query(object): context.statement = self._compound_eager_statement(context) else: context.statement = self._simple_statement(context) + return context def _compound_eager_statement(self, context): diff --git a/test/orm/test_events.py b/test/orm/test_events.py index 904293102..179f914fc 100644 --- a/test/orm/test_events.py +++ b/test/orm/test_events.py @@ -5,12 +5,13 @@ from sqlalchemy import Integer, String from sqlalchemy.testing.schema import Table, Column from sqlalchemy.orm import mapper, relationship, \ create_session, class_mapper, \ - Mapper, column_property, \ + Mapper, column_property, query, \ Session, sessionmaker, attributes, configure_mappers from sqlalchemy.orm.instrumentation import ClassManager from sqlalchemy.orm import instrumentation, events from sqlalchemy.testing import eq_ from sqlalchemy.testing import fixtures +from sqlalchemy.testing import AssertsCompiledSQL from sqlalchemy.testing.util import gc_collect from test.orm import _fixtures from sqlalchemy import event @@ -22,6 +23,7 @@ class _RemoveListeners(object): events.InstanceEvents._clear() events.SessionEvents._clear() events.InstrumentationEvents._clear() + events.QueryEvents._clear() super(_RemoveListeners, self).teardown() @@ -1881,3 +1883,37 @@ class SessionExtensionTest(_fixtures.FixtureTest): assert not s.dispatch.after_commit assert len(s.dispatch.before_commit) == 1 + +class QueryEventsTest( + _RemoveListeners, _fixtures.FixtureTest, AssertsCompiledSQL): + run_inserts = None + __dialect__ = 'default' + + @classmethod + def setup_mappers(cls): + User = cls.classes.User + users = cls.tables.users + + mapper(User, users) + + def test_before_compile(self): + @event.listens_for(query.Query, "before_compile", retval=True) + def no_deleted(query): + for desc in query.column_descriptions: + if desc['type'] is User: + entity = desc['expr'] + query = query.filter(entity.id != 10) + return query + + User = self.classes.User + s = Session() + + q = s.query(User).filter_by(id=7) + self.assert_compile( + q, + "SELECT users.id AS users_id, users.name AS users_name " + "FROM users " + "WHERE users.id = :id_1 AND users.id != :id_2", + checkparams={'id_2': 10, 'id_1': 7} + ) + |