diff options
author | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-08-28 17:43:46 -0400 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2015-09-02 17:55:15 -0400 |
commit | 108c60f460c723a0f48c47597928d938a3b0a42d (patch) | |
tree | faad446079cfacfe6aacec92900aa5d2bbfa70ac | |
parent | 8be93c23ee566de7cefd7d1b8ef044324132a70f (diff) | |
download | sqlalchemy-ticket_2677.tar.gz |
- The :class:`.SessionEvents` suite now includes events to allowticket_2677
unambiguous tracking of all object lifecycle state transitions
in terms of the :class:`.Session` itself, e.g. pending,
transient, persistent, detached. The state of the object
within each event is also defined.
fixes #2677
- Added a new session lifecycle state :term:`deleted`. This new state
represents an object that has been deleted from the :term:`persistent`
state and will move to the :term:`detached` state once the transaction
is committed. This resolves the long-standing issue that objects
which were deleted existed in a gray area between persistent and
detached. The :attr:`.InstanceState.persistent` accessor will
**no longer** report on a deleted object as persistent; the
:attr:`.InstanceState.deleted` accessor will instead be True for
these objects, until they become detached.
- The :paramref:`.Session.weak_identity_map` parameter is deprecated.
See the new recipe at :ref:`session_referencing_behavior` for
an event-based approach to maintaining strong identity map behavior.
references #3517
-rw-r--r-- | doc/build/changelog/changelog_11.rst | 44 | ||||
-rw-r--r-- | doc/build/changelog/migration_11.rst | 90 | ||||
-rw-r--r-- | doc/build/glossary.rst | 23 | ||||
-rw-r--r-- | doc/build/orm/session_events.rst | 237 | ||||
-rw-r--r-- | doc/build/orm/session_state_management.rst | 117 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/events.py | 240 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/identity.py | 39 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/loading.py | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 241 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/state.py | 112 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 9 | ||||
-rw-r--r-- | test/orm/test_events.py | 500 | ||||
-rw-r--r-- | test/orm/test_hasparent.py | 4 | ||||
-rw-r--r-- | test/orm/test_load_on_fks.py | 3 | ||||
-rw-r--r-- | test/orm/test_session.py | 105 | ||||
-rw-r--r-- | test/orm/test_transaction.py | 8 | ||||
-rw-r--r-- | test/profiles.txt | 58 |
17 files changed, 1593 insertions, 247 deletions
diff --git a/doc/build/changelog/changelog_11.rst b/doc/build/changelog/changelog_11.rst index 4fff4ab64..27dc8fd46 100644 --- a/doc/build/changelog/changelog_11.rst +++ b/doc/build/changelog/changelog_11.rst @@ -22,6 +22,50 @@ :version: 1.1.0b1 .. change:: + :tags: feature, orm + :tickets: 2677 + + The :class:`.SessionEvents` suite now includes events to allow + unambiguous tracking of all object lifecycle state transitions + in terms of the :class:`.Session` itself, e.g. pending, + transient, persistent, detached. The state of the object + within each event is also defined. + + .. seealso:: + + :ref:`change_2677` + + .. change:: + :tags: feature, orm + :tickets: 2677 + + Added a new session lifecycle state :term:`deleted`. This new state + represents an object that has been deleted from the :term:`persistent` + state and will move to the :term:`detached` state once the transaction + is committed. This resolves the long-standing issue that objects + which were deleted existed in a gray area between persistent and + detached. The :attr:`.InstanceState.persistent` accessor will + **no longer** report on a deleted object as persistent; the + :attr:`.InstanceState.deleted` accessor will instead be True for + these objects, until they become detached. + + .. seealso:: + + :ref:`change_2677` + + .. change:: + :tags: change, orm + :tickets: 2677 + + The :paramref:`.Session.weak_identity_map` parameter is deprecated. + See the new recipe at :ref:`session_referencing_behavior` for + an event-based approach to maintaining strong identity map behavior. + + .. seealso:: + + :ref:`change_2677` + + .. change:: :tags: bug, sql :tickets: 2919 diff --git a/doc/build/changelog/migration_11.rst b/doc/build/changelog/migration_11.rst index 412f42d27..ff26a00bd 100644 --- a/doc/build/changelog/migration_11.rst +++ b/doc/build/changelog/migration_11.rst @@ -16,7 +16,7 @@ What's New in SQLAlchemy 1.1? some issues may be moved to later milestones in order to allow for a timely release. - Document last updated: August 26, 2015 + Document last updated: September 2, 2015 Introduction ============ @@ -66,6 +66,94 @@ as it relies on deprecated features of setuptools. New Features and Improvements - ORM =================================== +.. _change_2677: + +New Session lifecycle events +---------------------------- + +The :class:`.Session` has long supported events that allow some degree +of tracking of state changes to objects, including +:meth:`.SessionEvents.before_attach`, :meth:`.SessionEvents.after_attach`, +and :meth:`.SessionEvents.before_flush`. The Session documentation also +documents major object states at :ref:`session_object_states`. However, +there has never been system of tracking objects specifically as they +pass through these transitions. Additionally, the status of "deleted" objects +has historically been murky as the objects act somewhere between +the "persistent" and "detached" states. + +To clean up this area and allow the realm of session state transition +to be fully transparent, a new series of events have been added that +are intended to cover every possible way that an object might transition +between states, and additionally the "deleted" status has been given +its own official state name within the realm of session object states. + +New State Transition Events +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Transitions between all states of an object such as :term:`persistent`, +:term:`pending` and others can now be intercepted in terms of a +session-level event intended to cover a specific transition. +Transitions as objects move into a :class:`.Session`, move out of a +:class:`.Session`, and even all the transitions which occur when the +transaction is rolled back using :meth:`.Session.rollback` +are explicitly present in the interface of :class:`.SessionEvents`. + +In total, there are **ten new events**. A summary of these events is in a +newly written documentation section :ref:`session_lifecycle_events`. + + +New Object State "deleted" is added, deleted objects no longer "persistent" +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The :term:`persistent` state of an object in the :class:`.Session` has +always been documented as an object that has a valid database identity; +however in the case of objects that were deleted within a flush, they +have always been in a grey area where they are not really "detached" +from the :class:`.Session` yet, because they can still be restored +within a rollback, but are not really "persistent" because their database +identity has been deleted and they aren't present in the identity map. + +To resolve this grey area given the new events, a new object state +:term:`deleted` is introduced. This state exists between the "persistent" and +"detached" states. An object that is marked for deletion via +:meth:`.Session.delete` remains in the "persistent" state until a flush +proceeds; at that point, it is removed from the identity map, moves +to the "deleted" state, and the :meth:`.SessionEvents.persistent_to_deleted` +hook is invoked. If the :class:`.Session` object's transaction is rolled +back, the object is restored as persistent; the +:meth:`.SessionEvents.deleted_to_persistent` transition is called. Otherwise +if the :class:`.Session` object's transaction is committed, +the :meth:`.SessionEvents.deleted_to_detached` transition is invoked. + +Additionally, the :attr:`.InstanceState.persistent` accessor **no longer returns +True** for an object that is in the new "deleted" state; instead, the +:attr:`.InstanceState.deleted` accessor has been enhanced to reliably +report on this new state. When the object is detached, the :attr:`.InstanceState.deleted` +returns False and the :attr:`.InstanceState.detached` accessor is True +instead. To determine if an object was deleted either in the current +transaction or in a previous transaction, use the +:attr:`.InstanceState.was_deleted` accessor. + +Strong Identity Map is Deprecated +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +One of the inspirations for the new series of transition events was to enable +leak-proof tracking of objects as they move in and out of the identity map, +so that a "strong reference" may be maintained mirroring the object +moving in and out of this map. With this new capability, there is no longer +any need for the :paramref:`.Session.weak_identity_map` parameter and the +corresponding :class:`.StrongIdentityMap` object. This option has remained +in SQLAlchemy for many years as the "strong-referencing" behavior used to be +the only behavior available, and many applications were written to assume +this behavior. It has long been recommended that strong-reference tracking +of objects not be an intrinsic job of the :class:`.Session` and instead +be an application-level construct built as needed by the application; the +new event model allows even the exact behavior of the strong identity map +to be replicated. See :ref:`session_referencing_behavior` for a new +recipe illustrating how to replace the strong identity map. + +:ticket:`2677` + .. _change_3499: Changes regarding "unhashable" types diff --git a/doc/build/glossary.rst b/doc/build/glossary.rst index c0ecee84b..9c1395f14 100644 --- a/doc/build/glossary.rst +++ b/doc/build/glossary.rst @@ -1019,7 +1019,7 @@ Glossary http://en.wikipedia.org/wiki/Unique_key#Defining_unique_keys transient - This describes one of the four major object states which + This describes one of the major object states which an object can have within a :term:`session`; a transient object is a new object that doesn't have any database identity and has not been associated with a session yet. When the @@ -1031,7 +1031,7 @@ Glossary :ref:`session_object_states` pending - This describes one of the four major object states which + This describes one of the major object states which an object can have within a :term:`session`; a pending object is a new object that doesn't have any database identity, but has been recently associated with a session. When @@ -1042,8 +1042,23 @@ Glossary :ref:`session_object_states` + deleted + This describes one of the major object states which + an object can have within a :term:`session`; a deleted object + is an object that was formerly persistent and has had a + DELETE statement emitted to the database within a flush + to delete its row. The object will move to the :term:`detached` + state once the session's transaction is committed; alternatively, + if the session's transaction is rolled back, the DELETE is + reverted and the object moves back to the :term:`persistent` + state. + + .. seealso:: + + :ref:`session_object_states` + persistent - This describes one of the four major object states which + This describes one of the major object states which an object can have within a :term:`session`; a persistent object is an object that has a database identity (i.e. a primary key) and is currently associated with a session. Any object @@ -1058,7 +1073,7 @@ Glossary :ref:`session_object_states` detached - This describes one of the four major object states which + This describes one of the major object states which an object can have within a :term:`session`; a detached object is an object that has a database identity (i.e. a primary key) but is not associated with any session. An object that diff --git a/doc/build/orm/session_events.rst b/doc/build/orm/session_events.rst index 231311aa0..dc7ad39e0 100644 --- a/doc/build/orm/session_events.rst +++ b/doc/build/orm/session_events.rst @@ -148,24 +148,239 @@ Object Lifecycle Events Another use case for events is to track the lifecycle of objects. This refers to the states first introduced at :ref:`session_object_states`. -As of SQLAlchemy 1.0, there is no direct event interface for tracking of -these states. Events that can be used at the moment to track the state of -objects include: +.. versionadded:: 1.1 added a system of events that intercept all possible + state transitions of an object within the :class:`.Session`. -* :meth:`.InstanceEvents.init` +All the states above can be tracked fully with events. Each event +represents a distinct state transition, meaning, the starting state +and the destination state are both part of what are tracked. With the +exception of the initial transient event, all the events are in terms of +the :class:`.Session` object or class, meaning they can be associated either +with a specific :class:`.Session` object:: -* :meth:`.InstanceEvents.load` + from sqlalchemy import event + from sqlalchemy.orm import Session -* :meth:`.SessionEvents.before_attach` + session = Session() -* :meth:`.SessionEvents.after_attach` + @event.listens_for(session, 'transient_to_pending') + def object_is_pending(session, obj): + print("new pending: %s" % obj) -* :meth:`.SessionEvents.before_flush` - by scanning the session's collections +Or with the :class:`.Session` class itself, as well as with a specific +:class:`.sessionmaker`, which is likely the most useful form:: -* :meth:`.SessionEvents.after_flush` - by scanning the session's collections + from sqlalchemy import event + from sqlalchemy.orm import sessionmaker -SQLAlchemy 1.1 will introduce a comprehensive event system to track -the object persistence states fully and unambiguously. + maker = sessionmaker() + + @event.listens_for(maker, 'transient_to_pending') + def object_is_pending(session, obj): + print("new pending: %s" % obj) + +The listeners can of course be stacked on top of one function, as is +likely to be common. For example, to track all objects that are +entering the persistent state:: + + @event.listens_for(maker, "pending_to_persistent") + @event.listens_for(maker, "deleted_to_persistent") + @event.listens_for(maker, "detached_to_persistent") + @event.listens_for(maker, "loaded_as_persistent") + def detect_all_persistent(session, instance): + print("object is now persistent: %s" % instance) + +Transient +^^^^^^^^^ + +All mapped objects when first constructed start out as :term:`transient`. +In this state, the object exists alone and doesn't have an association with +any :class:`.Session`. For this initial state, there's no specific +"transition" event since there is no :class:`.Session`, however if one +wanted to intercept when any transient object is created, the +:meth:`.InstanceEvents.init` method is probably the best event. This +event is applied to a specific class or superclass. For example, to +intercept all new objects for a particular declarative base:: + + from sqlalchemy.ext.declarative import declarative_base + from sqlalchemy import event + + Base = declarative_base() + + @event.listens_for(Base, "init", propagate=True) + def intercept_init(instance, args, kwargs): + print("new transient: %s" % instance) + + +Transient to Pending +^^^^^^^^^^^^^^^^^^^^ + +The transient object becomes :term:`pending` when it is first associated +with a :class:`.Session` via the :meth:`.Session.add` or :meth:`.Session.add_all` +method. An object may also become part of a :class:`.Session` as a result +of a :ref:`"cascade" <unitofwork_cascades>` from a referencing object that was +explicitly added. The transient to pending transition is detectable using +the :meth:`.SessionEvents.transient_to_pending` event:: + + @event.listens_for(sessionmaker, "transient_to_pending") + def intercept_transient_to_pending(session, object_): + print("transient to pending: %s" % object_) + + +Pending to Persistent +^^^^^^^^^^^^^^^^^^^^^ + +The :term:`pending` object becomes :term:`persistent` when a flush +proceeds and an INSERT statement takes place for the instance. The object +now has an identity key. Track pending to persistent with the +:meth:`.SessionEvents.pending_to_persistent` event:: + + @event.listens_for(sessionmaker, "pending_to_persistent") + def intercept_pending_to_persistent(session, object_): + print("pending to persistent: %s" % object_) + +Pending to Transient +^^^^^^^^^^^^^^^^^^^^^^^ + +The :term:`pending` object can revert back to :term:`transient` if the +:meth:`.Session.rollback` method is called before the pending object +has been flushed, or if the :meth:`.Session.expunge` method is called +for the object before it is flushed. Track pending to transient with the +:meth:`.SessionEvents.pending_to_transient` event:: + + @event.listens_for(sessionmaker, "pending_to_transient") + def intercept_pending_to_transient(session, object_): + print("transient to pending: %s" % object_) + +Loaded as Persistent +^^^^^^^^^^^^^^^^^^^^^^^ + +Objects can appear in the :class:`.Session` directly in the :term:`persistent` +state when they are loaded from the database. Tracking this state transition +is synonymous with tracking objects as they are loaded, and is synonomous +with using the :meth:`.InstanceEvents.load` instance-level event. However, the +:meth:`.SessionEvents.loaded_as_persistent` event is provided as a +session-centric hook for intercepting objects as they enter the persistent +state via this particular avenue:: + + @event.listens_for(sessionmaker, "loaded_as_persistent") + def intercept_loaded_as_persistent(session, object_): + print("object loaded into persistent state: %s" % object_) + + +Persistent to Transient +^^^^^^^^^^^^^^^^^^^^^^^ + +The persistent object can revert to the transient state if the +:meth:`.Session.rollback` method is called for a transaction where the +object was first added as pending. In the case of the ROLLBACK, the +INSERT statement that made this object persistent is rolled back, and +the object is evicted from the :class:`.Session` to again become transient. +Track objects that were reverted to transient from +persistent using the :meth:`.SessionEvents.persistent_to_transient` +event hook:: + + @event.listens_for(sessionmaker, "persistent_to_transient") + def intercept_persistent_to_transient(session, object_): + print("persistent to transient: %s" % object_) + +Persistent to Deleted +^^^^^^^^^^^^^^^^^^^^^ + +The persistent object enters the :term:`deleted` state when an object +marked for deletion is deleted from the database within the flush +process. Note that this is **not the same** as when the :meth:`.Session.delete` +method is called for a target object. The :meth:`.Session.delete` +method only **marks** the object for deletion; the actual DELETE statement +is not emitted until the flush proceeds. It is subsequent to the flush +that the "deleted" state is present for the target object. + +Within the "deleted" state, the object is only marginally associated +with the :class:`.Session`. It is not present in the identity map +nor is it present in the :attr:`.Session.deleted` collection that refers +to when it was pending for deletion. + +From the "deleted" state, the object can go either to the detached state +when the transaction is committed, or back to the persistent state +if the transaction is instead rolled back. + +Track the persistent to deleted transition with +:meth:`.SessionEvents.persistent_to_deleted`:: + + @event.listens_for(sessionmaker, "persistent_to_deleted") + def intercept_persistent_to_deleted(session, object_): + print("object was DELETEd, is now in deleted state: %s" % object_) + + +Deleted to Detached +^^^^^^^^^^^^^^^^^^^^ + +The deleted object becomes :term:`detached` when the session's transaction +is committed. After the :meth:`.Session.commit` method is called, the +database transaction is final and the :class:`.Session` now fully discards +the deleted object and removes all associations to it. Track +the deleted to detached transition using :meth:`.SessionEvents.deleted_to_detached`:: + + @event.listens_for(sessionmaker, "deleted_to_detached") + def intercept_deleted_to_detached(session, object_): + print("deleted to detached: %s" % object_) + + +.. note:: + + While the object is in the deleted state, the :attr:`.InstanceState.deleted` + attribute, accessible using ``inspect(object).deleted``, returns True. However + when the object is detached, :attr:`.InstanceState.deleted` will again + return False. To detect that an object was deleted, regardless of whether + or not it is detached, use the :attr:`.InstanceState.was_deleted` + accessor. + + +Persistent to Detached +^^^^^^^^^^^^^^^^^^^^^^^ + +The persistent object becomes :term:`detached` when the object is de-associated +with the :class:`.Session`, via the :meth:`.Session.expunge`, +:meth:`.Session.expunge_all`, or :meth:`.Session.close` methods. + +.. note:: + + An object may also become **implicitly detached** if its owning + :class:`.Session` is dereferenced by the application and discarded due to + garbage collection. In this case, **no event is emitted**. + +Track objects as they move from persistent to detached using the +:meth:`.SessionEvents.persistent_to_detached` event:: + + @event.listens_for(sessionmaker, "persistent_to_detached") + def intecept_persistent_to_detached(session, object_): + print("object became detached: %s" % object_) + +Detached to Persistent +^^^^^^^^^^^^^^^^^^^^^^^ + +The detached object becomes persistent when it is re-associated with a +session using the :meth:`.Session.add` or equivalent method. Track +objects moving back to persistent from detached using the +:meth:`.SessionEvents.detached_to_persistent` event:: + + @event.listens_for(sessionmaker, "detached_to_persistent") + def intecept_detached_to_persistent(session, object_): + print("object became persistent again: %s" % object_) + + +Deleted to Persistent +^^^^^^^^^^^^^^^^^^^^^^^ + +The :term:`deleted` object can be reverted to the :term:`persistent` +state when the transaction in which it was DELETEd was rolled back +using the :meth:`.Session.rollback` method. Track deleted objects +moving back to the persistent state using the +:meth:`.SessionEvents.deleted_to_persistent` event:: + + @event.listens_for(sessionmaker, "transient_to_pending") + def intercept_transient_to_pending(session, object_): + print("transient to pending: %s" % object_) .. _session_transaction_events: diff --git a/doc/build/orm/session_state_management.rst b/doc/build/orm/session_state_management.rst index 18673fde1..090bf7674 100644 --- a/doc/build/orm/session_state_management.rst +++ b/doc/build/orm/session_state_management.rst @@ -23,16 +23,15 @@ It's helpful to know the states which an instance can have within a session: existing instances (or moving persistent instances from other sessions into your local session). - .. note:: +* **Deleted** - An instance which has been deleted within a flush, but + the transaction has not yet completed. Objects in this state are essentially + in the opposite of "pending" state; when the session's transaction is committed, + the object will move to the detached state. Alternatively, when + the session's transaction is rolled back, a deleted object moves + *back* to the persistent state. - An object that is marked as deleted, e.g. via the - :meth:`.Session.delete` method, is still considered persistent. The - object remains in the identity map until the flush proceeds and a DELETE - state is emitted, at which point the object moves to the state that is - for most practical purposes "detached" - after the session's transaction - is committed, the object becomes fully detached. SQLAlchemy 1.1 will - introduce a new object state called "deleted" which represents - this "deleted but not quite detached" state explicitly. + .. versionchanged:: 1.1 The 'deleted' state is a newly added session + object state distinct from the 'persistent' state. * **Detached** - an instance which corresponds, or previously corresponded, to a record in the database, but is not currently in any session. @@ -43,10 +42,9 @@ It's helpful to know the states which an instance can have within a session: load unloaded attributes or attributes that were previously marked as "expired". -Knowing these states is important, since the -:class:`.Session` tries to be strict about ambiguous -operations (such as trying to save the same object to two different sessions -at the same time). +For a deeper dive into all possible state transitions, see the +section :ref:`session_lifecycle_events` which describes each transition +as well as how to programmatically track each one. Getting the Current State of an Object ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -67,6 +65,8 @@ the :func:`.inspect` system:: :attr:`.InstanceState.persistent` + :attr:`.InstanceState.deleted` + :attr:`.InstanceState.detached` .. _session_attributes: @@ -107,7 +107,13 @@ all objects which have had changes since they were last loaded or saved (i.e. (Documentation: :attr:`.Session.new`, :attr:`.Session.dirty`, :attr:`.Session.deleted`, :attr:`.Session.identity_map`). -Note that objects within the session are *weakly referenced*. This + +.. _session_referencing_behavior: + +Session Referencing Behavior +---------------------------- + +Objects within the session are *weakly referenced*. This means that when they are dereferenced in the outside application, they fall out of scope from within the :class:`~sqlalchemy.orm.session.Session` as well and are subject to garbage collection by the Python interpreter. The @@ -116,30 +122,65 @@ as deleted, or persistent objects which have pending changes on them. After a full flush, these collections are all empty, and all objects are again weakly referenced. -.. note:: - - To disable the weak referencing behavior and force all objects - within the session to remain until explicitly expunged, configure - :class:`.sessionmaker` with the ``weak_identity_map=False`` - setting. However note that this option is **deprecated**; - it is present only to allow compatibility with older - applications, typically those that were made back before SQLAlchemy - had the ability to effectively weak-reference all objects. - It is recommended that strong references to objects - be maintained by the calling application externally to the - :class:`.Session` itself, to the extent that is required by the application. - This eliminates the - :class:`.Session` as a possible source of unbounded memory growth in the case - where large numbers of objects are being loaded and/or persisted. - - Simple examples of externally managed strong-referencing behavior - include loading objects into a local dictionary keyed to their primary key, - or into lists or sets for the span of time that they need to remain referenced. - These collections can be associated with a :class:`.Session`, if desired, - by placing them into the :attr:`.Session.info` dictionary. Events such - as the :meth:`.SessionEvents.after_attach` and :meth:`.MapperEvents.load` - event may also be of use for intercepting objects as they are associated - with a :class:`.Session`. +To cause objects in the :class:`.Session` to remain strongly +referenced, usually a simple approach is all that's needed. Examples +of externally managed strong-referencing behavior include loading +objects into a local dictionary keyed to their primary key, or into +lists or sets for the span of time that they need to remain +referenced. These collections can be associated with a +:class:`.Session`, if desired, by placing them into the +:attr:`.Session.info` dictionary. + +An event based approach is also feasable. A simple recipe that provides +"strong referencing" behavior for all objects as they remain within +the :term:`persistent` state is as follows:: + + from sqlalchemy import event + + def strong_reference_session(session): + @event.listens_for(session, "pending_to_persistent") + @event.listens_for(session, "deleted_to_persistent") + @event.listens_for(session, "detached_to_persistent") + @event.listens_for(session, "loaded_as_persistent") + def strong_ref_object(sess, instance): + if 'refs' not in sess.info: + sess.info['refs'] = refs = set() + else: + refs = sess.info['refs'] + + refs.add(instance) + + + @event.listens_for(session, "persistent_to_detached") + @event.listens_for(session, "persistent_to_deleted") + @event.listens_for(session, "persistent_to_transient") + def deref_object(sess, instance): + sess.info['refs'].discard(instance) + +Above, we intercept the :meth:`.SessionEvents.pending_to_persistent`, +:meth:`.SessionEvents.detached_to_persistent`, +:meth:`.SessionEvents.deleted_to_persistent` and +:meth:`.SessionEvents.loaded_as_persistent` event hooks in order to intercept +objects as they enter the :term:`persistent` transition, and the +:meth:`.SessionEvents.persistent_to_detached` and +:meth:`.SessionEvents.persistent_to_deleted` hooks to intercept +objects as they leave the persistent state. + +The above function may be called for any :class:`.Session` in order to +provide strong-referencing behavior on a per-:class:`.Session` basis:: + + from sqlalchemy.orm import Session + + my_session = Session() + strong_reference_session(my_session) + +It may also be called for any :class:`.sessionmaker`:: + + from sqlalchemy.orm import sessionmaker + + maker = sessionmaker() + strong_reference_session(maker) + .. _unitofwork_merging: diff --git a/lib/sqlalchemy/orm/events.py b/lib/sqlalchemy/orm/events.py index 29bdbaa8c..993385e15 100644 --- a/lib/sqlalchemy/orm/events.py +++ b/lib/sqlalchemy/orm/events.py @@ -316,6 +316,8 @@ class InstanceEvents(event.Events): :meth:`.InstanceEvents.refresh` + :meth:`.SessionEvents.loaded_as_persistent` + """ def refresh(self, target, context, attrs): @@ -1510,6 +1512,244 @@ class SessionEvents(event.Events): """ + def transient_to_pending(self, session, instance): + """Intercept the "transient to pending" transition for a specific object. + + This event is a specialization of the + :meth:`.SessionEvents.after_attach` event which is only invoked + for this specific transition. It is invoked typically during the + :meth:`.Session.add` call. + + :param session: target :class:`.Session` + + :param instance: the ORM-mapped instance being operated upon. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def pending_to_transient(self, session, instance): + """Intercept the "pending to transient" transition for a specific object. + + This less common transition occurs when an pending object that has + not been flushed is evicted from the session; this can occur + when the :meth:`.Session.rollback` method rolls back the transaction, + or when the :meth:`.Session.expunge` method is used. + + :param session: target :class:`.Session` + + :param instance: the ORM-mapped instance being operated upon. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def persistent_to_transient(self, session, instance): + """Intercept the "persistent to transient" transition for a specific object. + + This less common transition occurs when an pending object that has + has been flushed is evicted from the session; this can occur + when the :meth:`.Session.rollback` method rolls back the transaction. + + :param session: target :class:`.Session` + + :param instance: the ORM-mapped instance being operated upon. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def pending_to_persistent(self, session, instance): + """Intercept the "pending to persistent"" transition for a specific object. + + This event is invoked within the flush process, and is + similar to scanning the :attr:`.Session.new` collection within + the :meth:`.SessionEvents.after_flush` event. However, in this + case the object has already been moved to the persistent state + when the event is called. + + :param session: target :class:`.Session` + + :param instance: the ORM-mapped instance being operated upon. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def detached_to_persistent(self, session, instance): + """Intercept the "detached to persistent" transition for a specific object. + + This event is a specialization of the + :meth:`.SessionEvents.after_attach` event which is only invoked + for this specific transition. It is invoked typically during the + :meth:`.Session.add` call, as well as during the + :meth:`.Session.delete` call if the object was not previously + associated with the + :class:`.Session` (note that an object marked as "deleted" remains + in the "persistent" state until the flush proceeds). + + .. note:: + + If the object becomes persistent as part of a call to + :meth:`.Session.delete`, the object is **not** yet marked as + deleted when this event is called. To detect deleted objects, + check the ``deleted`` flag sent to the + :meth:`.SessionEvents.persistent_to_detached` to event after the + flush proceeds, or check the :attr:`.Session.deleted` collection + within the :meth:`.SessionEvents.before_flush` event if deleted + objects need to be intercepted before the flush. + + :param session: target :class:`.Session` + + :param instance: the ORM-mapped instance being operated upon. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def loaded_as_persistent(self, session, instance): + """Intercept the "loaded as peristent" transition for a specific object. + + This event is invoked within the ORM loading process, and is invoked + very similarly to the :meth:`.InstanceEvents.load` event. However, + the event here is linkable to a :class:`.Session` class or instance, + rather than to a mapper or class hierarchy, and integrates + with the other session lifecycle events smoothly. The object + is guaranteed to be present in the session's identity map when + this event is called. + + + :param session: target :class:`.Session` + + :param instance: the ORM-mapped instance being operated upon. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def persistent_to_deleted(self, session, instance): + """Intercept the "persistent to deleted" transition for a specific object. + + This event is invoked when a persistent object's identity + is deleted from the database within a flush, however the object + still remains associated with the :class:`.Session` until the + transaction completes. + + If the transaction is rolled back, the object moves again + to the persistent state, and the + :meth:`.SessionEvents.deleted_to_persistent` event is called. + If the transaction is committed, the object becomes detached, + which will emit the :meth:`.SessionEvents.deleted_to_detached` + event. + + Note that while the :meth:`.Session.delete` method is the primary + public interface to mark an object as deleted, many objects + get deleted due to cascade rules, which are not always determined + until flush time. Therefore, there's no way to catch + every object that will be deleted until the flush has proceeded. + the :meth:`.SessionEvents.persistent_to_deleted` event is therefore + invoked at the end of a flush. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def deleted_to_persistent(self, session, instance): + """Intercept the "deleted to persistent" transition for a specific object. + + This transition occurs only when an object that's been deleted + successfully in a flush is restored due to a call to + :meth:`.Session.rollback`. The event is not called under + any other circumstances. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def deleted_to_detached(self, session, instance): + """Intercept the "deleted to detached" transition for a specific object. + + This event is invoked when a deleted object is evicted + from the session. The typical case when this occurs is when + the transaction for a :class:`.Session` in which the object + was deleted is committed; the object moves from the deleted + state to the detached state. + + It is also invoked for objects that were deleted in a flush + when the :meth:`.Session.expunge_all` or :meth:`.Session.close` + events are called, as well as if the object is individually + expunged from its deleted state via :meth:`.Session.expunge`. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + + def persistent_to_detached(self, session, instance): + """Intercept the "persistent to detached" transition for a specific object. + + This event is invoked when a persistent object is evicted + from the session. There are many conditions that cause this + to happen, including: + + * using a method such as :meth:`.Session.expunge` + or :meth:`.Session.close` + + * Calling the :meth:`.Session.rollback` method, when the object + was part of an INSERT statement for that session's transaction + + + :param session: target :class:`.Session` + + :param instance: the ORM-mapped instance being operated upon. + + :param deleted: boolean. If True, indicates this object moved + to the detached state because it was marked as deleted and flushed. + + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`session_lifecycle_events` + + """ + class AttributeEvents(event.Events): """Define events for object attributes. diff --git a/lib/sqlalchemy/orm/identity.py b/lib/sqlalchemy/orm/identity.py index b42703855..2dfe3fd5c 100644 --- a/lib/sqlalchemy/orm/identity.py +++ b/lib/sqlalchemy/orm/identity.py @@ -8,7 +8,8 @@ import weakref from . import attributes from .. import util - +from .. import exc as sa_exc +from . import util as orm_util class IdentityMap(object): def __init__(self): @@ -126,16 +127,18 @@ class WeakInstanceDict(IdentityMap): if existing_state is not state: o = existing_state.obj() if o is not None: - raise AssertionError( - "A conflicting state is already " - "present in the identity map for key %r" - % (key, )) + raise sa_exc.InvalidRequestError( + "Can't attach instance " + "%s; another instance with key %s is already " + "present in this session." % ( + orm_util.state_str(state), state.key)) else: - return + return False except KeyError: pass self._dict[key] = state self._manage_incoming_state(state) + return True def _add_unpresent(self, state, key): # inlined form of add() called by loading.py @@ -210,13 +213,13 @@ class WeakInstanceDict(IdentityMap): class StrongInstanceDict(IdentityMap): """A 'strong-referencing' version of the identity map. - .. deprecated:: this object is present in order to fulfill - the ``weak_identity_map=False`` option of the Session. - This option is present to allow compatibility with older applications, - but it is recommended that strong references to objects - be maintained by the calling application - externally to the :class:`.Session` itself, to the degree - that is needed by the application. + .. deprecated 1.1:: + The strong + reference identity map is legacy. See the + recipe at :ref:`session_referencing_behavior` for + an event-based approach to maintaining strong identity + references. + """ @@ -268,12 +271,16 @@ class StrongInstanceDict(IdentityMap): def add(self, state): if state.key in self: if attributes.instance_state(self._dict[state.key]) is not state: - raise AssertionError('A conflicting state is already ' - 'present in the identity map for key %r' - % (state.key, )) + raise sa_exc.InvalidRequestError( + "Can't attach instance " + "%s; another instance with key %s is already " + "present in this session." % ( + orm_util.state_str(state), state.key)) + return False else: self._dict[state.key] = state.obj() self._manage_incoming_state(state) + return True def _add_unpresent(self, state, key): # inlined form of add() called by loading.py diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index d8bf662fc..c90308a69 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -339,6 +339,9 @@ def _instance_processor( populate_existing = context.populate_existing or mapper.always_refresh load_evt = bool(mapper.class_manager.dispatch.load) refresh_evt = bool(mapper.class_manager.dispatch.refresh) + persistent_evt = bool(context.session.dispatch.loaded_as_persistent) + if persistent_evt: + loaded_as_persistent = context.session.dispatch.loaded_as_persistent instance_state = attributes.instance_state instance_dict = attributes.instance_dict session_id = context.session.hash_key @@ -432,8 +435,11 @@ def _instance_processor( loaded_instance, populate_existing, populators) if isnew: - if loaded_instance and load_evt: - state.manager.dispatch.load(state, context) + if loaded_instance: + if load_evt: + state.manager.dispatch.load(state, context) + if persistent_evt: + loaded_as_persistent(context.session, state.obj()) elif refresh_evt: state.manager.dispatch.refresh( state, context, only_load_props) diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index 6c3f392ba..96a24067e 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -272,10 +272,9 @@ class SessionTransaction(object): def _restore_snapshot(self, dirty_only=False): assert self._is_transaction_boundary - for s in set(self._new).union(self.session._new): - self.session._expunge_state(s) - if s.key: - del s.key + self.session._expunge_states( + set(self._new).union(self.session._new), + to_transient=True) for s, (oldkey, newkey) in self._key_switches.items(): self.session.identity_map.safe_discard(s) @@ -283,10 +282,7 @@ class SessionTransaction(object): self.session.identity_map.replace(s) for s in set(self._deleted).union(self.session._deleted): - if s.deleted: - # assert s in self._deleted - del s.deleted - self.session._update_impl(s, discard_existing=True) + self.session._update_impl(s, revert_deletion=True) assert not self.session._deleted @@ -300,8 +296,9 @@ class SessionTransaction(object): if not self.nested and self.session.expire_on_commit: for s in self.session.identity_map.all_states(): s._expire(s.dict, self.session.identity_map._modified) - for s in list(self._deleted): - s._detach() + + statelib.InstanceState._detach_states( + list(self._deleted), self.session) self._deleted.clear() elif self.nested: self._parent._new.update(self._new) @@ -629,12 +626,11 @@ class Session(_SessionClassMethods): :param weak_identity_map: Defaults to ``True`` - when set to ``False``, objects placed in the :class:`.Session` will be strongly referenced until explicitly removed or the - :class:`.Session` is closed. **Deprecated** - this option - is present to allow compatibility with older applications, but - it is recommended that strong references to objects - be maintained by the calling application - externally to the :class:`.Session` itself, - to the extent that is required by the application. + :class:`.Session` is closed. **Deprecated** - The strong + reference identity map is legacy. See the + recipe at :ref:`session_referencing_behavior` for + an event-based approach to maintaining strong identity + references. """ @@ -643,12 +639,9 @@ class Session(_SessionClassMethods): else: util.warn_deprecated( "weak_identity_map=False is deprecated. " - "It is present to allow compatibility with older " - "applications, but " - "it is recommended that strong references to " - "objects be maintained by the calling application " - "externally to the :class:`.Session` itself, " - "to the extent that is required by the application.") + "See the documentation on 'Session Referencing Behavior' " + "for an event-based approach to maintaining strong identity " + "references.") self._identity_cls = identity.StrongInstanceDict self.identity_map = self._identity_cls() @@ -1097,16 +1090,15 @@ class Session(_SessionClassMethods): ``Session``. """ - for state in self.identity_map.all_states() + list(self._new): - state._detach() + all_states = self.identity_map.all_states() + list(self._new) self.identity_map = self._identity_cls() self._new = {} self._deleted = {} - # TODO: need much more test coverage for bind_mapper() and similar ! - # TODO: + crystallize + document resolution order - # vis. bind_mapper/bind_table + statelib.InstanceState._detach_states( + all_states, self + ) def _add_bind(self, key, bind): try: @@ -1448,7 +1440,7 @@ class Session(_SessionClassMethods): state._expire(state.dict, self.identity_map._modified) elif state in self._new: self._new.pop(state) - state._detach() + state._detach(self) @util.deprecated("0.7", "The non-weak-referencing identity map " "feature is no longer needed.") @@ -1483,23 +1475,26 @@ class Session(_SessionClassMethods): cascaded = list(state.manager.mapper.cascade_iterator( 'expunge', state)) - self._expunge_state(state) - for o, m, st_, dct_ in cascaded: - self._expunge_state(st_) + self._expunge_states( + [state] + [st_ for o, m, st_, dct_ in cascaded] + ) - def _expunge_state(self, state): - if state in self._new: - self._new.pop(state) - state._detach() - elif self.identity_map.contains_state(state): - self.identity_map.safe_discard(state) - self._deleted.pop(state, None) - state._detach() - elif self.transaction: - self.transaction._deleted.pop(state, None) - state._detach() + def _expunge_states(self, states, to_transient=False): + for state in states: + if state in self._new: + self._new.pop(state) + elif self.identity_map.contains_state(state): + self.identity_map.safe_discard(state) + self._deleted.pop(state, None) + elif self.transaction: + # state is "detached" from being deleted, but still present + # in the transaction snapshot + self.transaction._deleted.pop(state, None) + statelib.InstanceState._detach_states( + states, self, to_transient=to_transient) def _register_newly_persistent(self, states): + pending_to_persistent = self.dispatch.pending_to_persistent or None for state in states: mapper = _state_mapper(state) @@ -1546,6 +1541,11 @@ class Session(_SessionClassMethods): ) self._register_altered(states) + + if pending_to_persistent is not None: + for state in states: + pending_to_persistent(self, state.obj()) + # remove from new last, might be the last strong ref for state in set(states).intersection(self._new): self._new.pop(state) @@ -1559,13 +1559,19 @@ class Session(_SessionClassMethods): self.transaction._dirty[state] = True def _remove_newly_deleted(self, states): + persistent_to_deleted = self.dispatch.persistent_to_deleted or None for state in states: if self._enable_transaction_accounting and self.transaction: self.transaction._deleted[state] = True self.identity_map.safe_discard(state) self._deleted.pop(state, None) - state.deleted = True + state._deleted = True + # can't call state._detach() here, because this state + # is still in the transaction snapshot and needs to be + # tracked as part of that + if persistent_to_deleted is not None: + persistent_to_deleted(self, state.obj()) def add(self, instance, _warn=True): """Place an object in the ``Session``. @@ -1620,30 +1626,39 @@ class Session(_SessionClassMethods): except exc.NO_STATE: raise exc.UnmappedInstanceError(instance) + self._delete_impl(state, head=True) + + def _delete_impl(self, state, head): + if state.key is None: - raise sa_exc.InvalidRequestError( - "Instance '%s' is not persisted" % - state_str(state)) + if head: + raise sa_exc.InvalidRequestError( + "Instance '%s' is not persisted" % + state_str(state)) + else: + return + + to_attach = self._before_attach(state) if state in self._deleted: return - # ensure object is attached to allow the - # cascade operation to load deferred attributes - # and collections - self._attach(state, include_before=True) + if to_attach: + self.identity_map.add(state) + self._after_attach(state) - # grab the cascades before adding the item to the deleted list - # so that autoflush does not delete the item - # the strong reference to the instance itself is significant here - cascade_states = list(state.manager.mapper.cascade_iterator( - 'delete', state)) + if head: + # grab the cascades before adding the item to the deleted list + # so that autoflush does not delete the item + # the strong reference to the instance itself is significant here + cascade_states = list(state.manager.mapper.cascade_iterator( + 'delete', state)) self._deleted[state] = state.obj() - self.identity_map.add(state) - for o, m, st_, dct_ in cascade_states: - self._delete_impl(st_) + if head: + for o, m, st_, dct_ in cascade_states: + self._delete_impl(st_, False) def merge(self, instance, load=True): """Copy the state of a given instance into a corresponding instance @@ -1820,35 +1835,46 @@ class Session(_SessionClassMethods): "Object '%s' already has an identity - " "it can't be registered as pending" % state_str(state)) - self._before_attach(state) + to_attach = self._before_attach(state) if state not in self._new: self._new[state] = state.obj() state.insert_order = len(self._new) - self._attach(state) - - def _update_impl(self, state, discard_existing=False): - if (self.identity_map.contains_state(state) and - state not in self._deleted): - return + if to_attach: + self._after_attach(state) + def _update_impl(self, state, revert_deletion=False): if state.key is None: raise sa_exc.InvalidRequestError( "Instance '%s' is not persisted" % state_str(state)) - if state.deleted: - raise sa_exc.InvalidRequestError( - "Instance '%s' has been deleted. Use the make_transient() " - "function to send this object back to the transient state." % - state_str(state) - ) - self._before_attach(state, check_identity_map=False) + if state._deleted: + if revert_deletion: + if not state._attached: + return + del state._deleted + else: + raise sa_exc.InvalidRequestError( + "Instance '%s' has been deleted. " + "Use the make_transient() " + "function to send this object back " + "to the transient state." % + state_str(state) + ) + + to_attach = self._before_attach(state) + + self._deleted.pop(state, None) - if discard_existing: + if revert_deletion: self.identity_map.replace(state) else: self.identity_map.add(state) - self._attach(state) + + if to_attach: + self._after_attach(state) + elif revert_deletion and self.dispatch.deleted_to_persistent: + self.dispatch.deleted_to_persistent(self, state.obj()) def _save_or_update_impl(self, state): if state.key is None: @@ -1856,17 +1882,6 @@ class Session(_SessionClassMethods): else: self._update_impl(state) - def _delete_impl(self, state): - if state in self._deleted: - return - - if state.key is None: - return - - self._attach(state, include_before=True) - self._deleted[state] = state.obj() - self.identity_map.add(state) - def enable_relationship_loading(self, obj): """Associate an object with this :class:`.Session` for related object loading. @@ -1919,40 +1934,36 @@ class Session(_SessionClassMethods): """ state = attributes.instance_state(obj) - self._attach(state, include_before=True) + to_attach = self._before_attach(state) state._load_pending = True + if to_attach: + self._after_attach(state) - def _before_attach(self, state, check_identity_map=True): - if state.session_id != self.hash_key and \ - self.dispatch.before_attach: - self.dispatch.before_attach(self, state.obj()) - - if check_identity_map and state.key and \ - state.key in self.identity_map and \ - not self.identity_map.contains_state(state): - raise sa_exc.InvalidRequestError( - "Can't attach instance " - "%s; another instance with key %s is already " - "present in this session." % (state_str(state), state.key)) + def _before_attach(self, state): + if state.session_id == self.hash_key: + return False - if state.session_id and \ - state.session_id is not self.hash_key and \ - state.session_id in _sessions: + if state.session_id and state.session_id in _sessions: raise sa_exc.InvalidRequestError( "Object '%s' is already attached to session '%s' " "(this is '%s')" % (state_str(state), state.session_id, self.hash_key)) - def _attach(self, state, include_before=False): + if self.dispatch.before_attach: + self.dispatch.before_attach(self, state.obj()) + + return True - if state.session_id != self.hash_key: - if include_before: - self._before_attach(state) - state.session_id = self.hash_key - if state.modified and state._strong_obj is None: - state._strong_obj = state.obj() - if self.dispatch.after_attach: - self.dispatch.after_attach(self, state.obj()) + def _after_attach(self, state): + state.session_id = self.hash_key + if state.modified and state._strong_obj is None: + state._strong_obj = state.obj() + if self.dispatch.after_attach: + self.dispatch.after_attach(self, state.obj()) + if state.persistent and self.dispatch.detached_to_persistent: + self.dispatch.detached_to_persistent(self, state.obj()) + elif state.pending and self.dispatch.transient_to_pending: + self.dispatch.transient_to_pending(self, state.obj()) def __contains__(self, instance): """Return True if the instance is associated with this session. @@ -2711,7 +2722,7 @@ def make_transient(instance): state = attributes.instance_state(instance) s = _state_session(state) if s: - s._expunge_state(state) + s._expunge_states([state]) # remove expired state state.expired_attributes.clear() @@ -2722,8 +2733,8 @@ def make_transient(instance): if state.key: del state.key - if state.deleted: - del state.deleted + if state._deleted: + del state._deleted def make_transient_to_detached(instance): @@ -2755,8 +2766,8 @@ def make_transient_to_detached(instance): raise sa_exc.InvalidRequestError( "Given object must be transient") state.key = state.mapper._identity_key_from_state(state) - if state.deleted: - del state.deleted + if state._deleted: + del state._deleted state._commit_all(state.dict) state._expire_attributes(state.dict, state.unloaded) diff --git a/lib/sqlalchemy/orm/state.py b/lib/sqlalchemy/orm/state.py index 6c9f07bff..9f1190339 100644 --- a/lib/sqlalchemy/orm/state.py +++ b/lib/sqlalchemy/orm/state.py @@ -58,7 +58,7 @@ class InstanceState(interfaces.InspectionAttr): _strong_obj = None modified = False expired = False - deleted = False + _deleted = False _load_pending = False is_instance = True @@ -89,7 +89,6 @@ class InstanceState(interfaces.InspectionAttr): see also the ``unmodified`` collection which is intersected against this set when a refresh operation occurs.""" - @util.memoized_property def attrs(self): """Return a namespace representing each attribute on @@ -135,16 +134,80 @@ class InstanceState(interfaces.InspectionAttr): self._attached @property + def deleted(self): + """Return true if the object is :term:`deleted`. + + An object that is in the deleted state is guaranteed to + not be within the :attr:`.Session.identity_map` of its parent + :class:`.Session`; however if the session's transaction is rolled + back, the object will be restored to the persistent state and + the identity map. + + .. note:: + + The :attr:`.InstanceState.deleted` attribute refers to a specific + state of the object that occurs between the "persistent" and + "detached" states; once the object is :term:`detached`, the + :attr:`.InstanceState.deleted` attribute **no longer returns + True**; in order to detect that a state was deleted, regardless + of whether or not the object is associated with a :class:`.Session`, + use the :attr:`.InstanceState.was_deleted` accessor. + + .. versionadded: 1.1 + + .. seealso:: + + :ref:`session_object_states` + + """ + return self.key is not None and \ + self._attached and self._deleted + + @property + def was_deleted(self): + """Return True if this object is or was previously in the + "deleted" state and has not been reverted to persistent. + + This flag returns True once the object was deleted in flush. + When the object is expunged from the session either explicitly + or via transaction commit and enters the "detached" state, + this flag will continue to report True. + + .. versionadded:: 1.1 - added a local method form of + :func:`.orm.util.was_deleted`. + + .. seealso:: + + :attr:`.InstanceState.deleted` - refers to the "deleted" state + + :func:`.orm.util.was_deleted` - standalone function + + :ref:`session_object_states` + + """ + return self._deleted + + @property def persistent(self): """Return true if the object is :term:`persistent`. + An object that is in the persistent state is guaranteed to + be within the :attr:`.Session.identity_map` of its parent + :class:`.Session`. + + .. versionchanged:: 1.1 The :attr:`.InstanceState.persistent` + accessor no longer returns True for an object that was + "deleted" within a flush; use the :attr:`.InstanceState.deleted` + accessor to detect this state. This allows the "persistent" + state to guarantee membership in the identity map. + .. seealso:: :ref:`session_object_states` """ return self.key is not None and \ - self._attached + self._attached and not self._deleted @property def detached(self): @@ -155,8 +218,7 @@ class InstanceState(interfaces.InspectionAttr): :ref:`session_object_states` """ - return self.key is not None and \ - not self._attached + return self.key is not None and not self._attached @property @util.dependencies("sqlalchemy.orm.session") @@ -243,8 +305,44 @@ class InstanceState(interfaces.InspectionAttr): """ return bool(self.key) - def _detach(self): - self.session_id = self._strong_obj = None + @classmethod + def _detach_states(self, states, session, to_transient=False): + persistent_to_detached = \ + session.dispatch.persistent_to_detached or None + deleted_to_detached = \ + session.dispatch.deleted_to_detached or None + pending_to_transient = \ + session.dispatch.pending_to_transient or None + persistent_to_transient = \ + session.dispatch.persistent_to_transient or None + + for state in states: + deleted = state._deleted + persistent = state.key is not None and not deleted + pending = state.key is None + + state.session_id = None + + if to_transient and state.key: + del state.key + if persistent: + if to_transient: + if persistent_to_transient is not None: + persistent_to_transient(session, state.obj()) + elif persistent_to_detached is not None: + persistent_to_detached(session, state.obj()) + elif deleted and deleted_to_detached is not None: + deleted_to_detached(session, state.obj()) + elif pending and pending_to_transient is not None: + pending_to_transient(session, state.obj()) + + state._strong_obj = None + + def _detach(self, session=None): + if session: + InstanceState._detach_states([self], session) + else: + self.session_id = self._strong_obj = None def _dispose(self): self._detach() diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 6d3869679..4351c8dc6 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -985,12 +985,19 @@ def was_deleted(object): """Return True if the given object was deleted within a session flush. + This is regardless of whether or not the object is + persistent or detached. + .. versionadded:: 0.8.0 + .. seealso:: + + :attr:`.InstanceState.was_deleted` + """ state = attributes.instance_state(object) - return state.deleted + return state.was_deleted def randomize_unitofwork(): diff --git a/test/orm/test_events.py b/test/orm/test_events.py index b9fafb105..ab61077ae 100644 --- a/test/orm/test_events.py +++ b/test/orm/test_events.py @@ -1617,6 +1617,506 @@ class SessionEventsTest(_RemoveListeners, _fixtures.FixtureTest): ) +class SessionLifecycleEventsTest(_RemoveListeners, _fixtures.FixtureTest): + run_inserts = None + + def _fixture(self, include_address=False): + users, User = self.tables.users, self.classes.User + + if include_address: + addresses, Address = self.tables.addresses, self.classes.Address + mapper(User, users, properties={ + "addresses": relationship( + Address, cascade="all, delete-orphan") + }) + mapper(Address, addresses) + else: + mapper(User, users) + + listener = Mock() + + sess = Session() + + def start_events(): + event.listen( + sess, "transient_to_pending", listener.transient_to_pending) + event.listen( + sess, "pending_to_transient", listener.pending_to_transient) + event.listen( + sess, "persistent_to_transient", + listener.persistent_to_transient) + event.listen( + sess, "pending_to_persistent", listener.pending_to_persistent) + event.listen( + sess, "detached_to_persistent", + listener.detached_to_persistent) + event.listen( + sess, "loaded_as_persistent", listener.loaded_as_persistent) + + event.listen( + sess, "persistent_to_detached", + listener.persistent_to_detached) + event.listen( + sess, "deleted_to_detached", listener.deleted_to_detached) + + event.listen( + sess, "persistent_to_deleted", listener.persistent_to_deleted) + event.listen( + sess, "deleted_to_persistent", listener.deleted_to_persistent) + return listener + + if include_address: + return sess, User, Address, start_events + else: + return sess, User, start_events + + def test_transient_to_pending(self): + sess, User, start_events = self._fixture() + + listener = start_events() + + @event.listens_for(sess, "transient_to_pending") + def trans_to_pending(session, instance): + assert instance in session + listener.flag_checked(instance) + + u1 = User(name='u1') + sess.add(u1) + + eq_( + listener.mock_calls, + [ + call.transient_to_pending(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_pending_to_transient_via_rollback(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + + listener = start_events() + + @event.listens_for(sess, "pending_to_transient") + def test_deleted_flag(session, instance): + assert instance not in session + listener.flag_checked(instance) + + sess.rollback() + assert u1 not in sess + + eq_( + listener.mock_calls, + [ + call.pending_to_transient(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_pending_to_transient_via_expunge(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + + listener = start_events() + + @event.listens_for(sess, "pending_to_transient") + def test_deleted_flag(session, instance): + assert instance not in session + listener.flag_checked(instance) + + sess.expunge(u1) + assert u1 not in sess + + eq_( + listener.mock_calls, + [ + call.pending_to_transient(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_pending_to_persistent(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + + listener = start_events() + + @event.listens_for(sess, "pending_to_persistent") + def test_flag(session, instance): + assert instance in session + assert instance._sa_instance_state.persistent + assert instance._sa_instance_state.key in session.identity_map + listener.flag_checked(instance) + + sess.flush() + + eq_( + listener.mock_calls, + [ + call.pending_to_persistent(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_detached_to_persistent(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.flush() + + sess.expunge(u1) + + listener = start_events() + + @event.listens_for(sess, "detached_to_persistent") + def test_deleted_flag(session, instance): + assert instance not in session.deleted + assert instance in session + listener.flag_checked() + + sess.add(u1) + + eq_( + listener.mock_calls, + [ + call.detached_to_persistent(sess, u1), + call.flag_checked() + ] + ) + + def test_loaded_as_persistent(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.commit() + sess.close() + + listener = start_events() + + @event.listens_for(sess, "loaded_as_persistent") + def test_identity_flag(session, instance): + assert instance in session + assert instance._sa_instance_state.persistent + assert instance._sa_instance_state.key in session.identity_map + assert not instance._sa_instance_state.deleted + assert not instance._sa_instance_state.detached + assert instance._sa_instance_state.persistent + listener.flag_checked(instance) + + u1 = sess.query(User).filter_by(name='u1').one() + + eq_( + listener.mock_calls, + [ + call.loaded_as_persistent(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_detached_to_persistent_via_deleted(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.commit() + sess.close() + + listener = start_events() + + @event.listens_for(sess, "detached_to_persistent") + def test_deleted_flag_persistent(session, instance): + assert instance not in session.deleted + assert instance in session + assert not instance._sa_instance_state.deleted + assert not instance._sa_instance_state.detached + assert instance._sa_instance_state.persistent + listener.dtp_flag_checked(instance) + + @event.listens_for(sess, "persistent_to_deleted") + def test_deleted_flag_detached(session, instance): + assert instance not in session.deleted + assert instance not in session + assert not instance._sa_instance_state.persistent + assert instance._sa_instance_state.deleted + assert not instance._sa_instance_state.detached + listener.ptd_flag_checked(instance) + + sess.delete(u1) + assert u1 in sess.deleted + + eq_( + listener.mock_calls, + [ + call.detached_to_persistent(sess, u1), + call.dtp_flag_checked(u1) + ] + ) + + sess.flush() + + eq_( + listener.mock_calls, + [ + call.detached_to_persistent(sess, u1), + call.dtp_flag_checked(u1), + call.persistent_to_deleted(sess, u1), + call.ptd_flag_checked(u1), + ] + ) + + def test_detached_to_persistent_via_cascaded_delete(self): + sess, User, Address, start_events = self._fixture(include_address=True) + + u1 = User(name='u1') + sess.add(u1) + a1 = Address(email_address='e1') + u1.addresses.append(a1) + sess.commit() + u1.addresses # ensure u1.addresses refers to a1 before detachment + sess.close() + + listener = start_events() + + @event.listens_for(sess, "detached_to_persistent") + def test_deleted_flag(session, instance): + assert instance not in session.deleted + assert instance in session + assert not instance._sa_instance_state.deleted + assert not instance._sa_instance_state.detached + assert instance._sa_instance_state.persistent + listener.flag_checked(instance) + + sess.delete(u1) + assert u1 in sess.deleted + assert a1 in sess.deleted + + eq_( + listener.mock_calls, + [ + call.detached_to_persistent(sess, u1), + call.flag_checked(u1), + call.detached_to_persistent(sess, a1), + call.flag_checked(a1), + ] + ) + + sess.flush() + + def test_persistent_to_deleted(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.commit() + + listener = start_events() + + @event.listens_for(sess, "persistent_to_deleted") + def test_deleted_flag(session, instance): + assert instance not in session.deleted + assert instance not in session + assert instance._sa_instance_state.deleted + assert not instance._sa_instance_state.detached + assert not instance._sa_instance_state.persistent + listener.flag_checked(instance) + + sess.delete(u1) + assert u1 in sess.deleted + + eq_( + listener.mock_calls, + [] + ) + + sess.flush() + assert u1 not in sess + + eq_( + listener.mock_calls, + [ + call.persistent_to_deleted(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_persistent_to_detached_via_expunge(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.flush() + + listener = start_events() + + @event.listens_for(sess, "persistent_to_detached") + def test_deleted_flag(session, instance): + assert instance not in session.deleted + assert instance not in session + assert not instance._sa_instance_state.deleted + assert instance._sa_instance_state.detached + assert not instance._sa_instance_state.persistent + listener.flag_checked(instance) + + assert u1 in sess + sess.expunge(u1) + assert u1 not in sess + + eq_( + listener.mock_calls, + [ + call.persistent_to_detached(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_persistent_to_detached_via_expunge_all(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.flush() + + listener = start_events() + + @event.listens_for(sess, "persistent_to_detached") + def test_deleted_flag(session, instance): + assert instance not in session.deleted + assert instance not in session + assert not instance._sa_instance_state.deleted + assert instance._sa_instance_state.detached + assert not instance._sa_instance_state.persistent + listener.flag_checked(instance) + + assert u1 in sess + sess.expunge_all() + assert u1 not in sess + + eq_( + listener.mock_calls, + [ + call.persistent_to_detached(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_persistent_to_transient_via_rollback(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.flush() + + listener = start_events() + + @event.listens_for(sess, "persistent_to_transient") + def test_deleted_flag(session, instance): + assert instance not in session.deleted + assert instance not in session + assert not instance._sa_instance_state.deleted + assert not instance._sa_instance_state.detached + assert not instance._sa_instance_state.persistent + assert instance._sa_instance_state.transient + listener.flag_checked(instance) + + sess.rollback() + + eq_( + listener.mock_calls, + [ + call.persistent_to_transient(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_deleted_to_persistent_via_rollback(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.commit() + + sess.delete(u1) + sess.flush() + + listener = start_events() + + @event.listens_for(sess, "deleted_to_persistent") + def test_deleted_flag(session, instance): + assert instance not in session.deleted + assert instance in session + assert not instance._sa_instance_state.deleted + assert not instance._sa_instance_state.detached + assert instance._sa_instance_state.persistent + listener.flag_checked(instance) + + assert u1 not in sess + assert u1._sa_instance_state.deleted + assert not u1._sa_instance_state.persistent + assert not u1._sa_instance_state.detached + + sess.rollback() + + assert u1 in sess + assert u1._sa_instance_state.persistent + assert not u1._sa_instance_state.deleted + assert not u1._sa_instance_state.detached + + eq_( + listener.mock_calls, + [ + call.deleted_to_persistent(sess, u1), + call.flag_checked(u1) + ] + ) + + def test_deleted_to_detached_via_commit(self): + sess, User, start_events = self._fixture() + + u1 = User(name='u1') + sess.add(u1) + sess.commit() + + sess.delete(u1) + sess.flush() + + listener = start_events() + + @event.listens_for(sess, "deleted_to_detached") + def test_detached_flag(session, instance): + assert instance not in session.deleted + assert instance not in session + assert not instance._sa_instance_state.deleted + assert instance._sa_instance_state.detached + listener.flag_checked(instance) + + assert u1 not in sess + assert u1._sa_instance_state.deleted + assert not u1._sa_instance_state.persistent + assert not u1._sa_instance_state.detached + + sess.commit() + + assert u1 not in sess + assert not u1._sa_instance_state.deleted + assert u1._sa_instance_state.detached + + eq_( + listener.mock_calls, + [ + call.deleted_to_detached(sess, u1), + call.flag_checked(u1) + ] + ) + + class MapperExtensionTest(_fixtures.FixtureTest): """Superseded by MapperEventsTest - test backwards diff --git a/test/orm/test_hasparent.py b/test/orm/test_hasparent.py index fd246b527..df4b05980 100644 --- a/test/orm/test_hasparent.py +++ b/test/orm/test_hasparent.py @@ -116,7 +116,7 @@ class ParentRemovalTest(fixtures.MappedTest): User = self.classes.User s, u1, a1 = self._fixture() - s._expunge_state(attributes.instance_state(u1)) + s._expunge_states([attributes.instance_state(u1)]) del u1 gc_collect() @@ -178,7 +178,7 @@ class ParentRemovalTest(fixtures.MappedTest): u2 = User(addresses=[a1]) s.add(u2) s.flush() - s._expunge_state(attributes.instance_state(u2)) + s._expunge_states([attributes.instance_state(u2)]) del u2 gc_collect() diff --git a/test/orm/test_load_on_fks.py b/test/orm/test_load_on_fks.py index 813d8d17a..471c8665a 100644 --- a/test/orm/test_load_on_fks.py +++ b/test/orm/test_load_on_fks.py @@ -301,7 +301,8 @@ class LoadOnFKsTest(AssertsExecutionResults, fixtures.TestBase): c2 = Child() if attach: - sess._attach(instance_state(c2)) + state = instance_state(c2) + state.session_id = sess.hash_key if enable_relationship_rel: sess.enable_relationship_loading(c2) diff --git a/test/orm/test_session.py b/test/orm/test_session.py index 58551d763..f6ddcb566 100644 --- a/test/orm/test_session.py +++ b/test/orm/test_session.py @@ -493,8 +493,10 @@ class SessionStateTest(_fixtures.FixtureTest): 'is already attached to session', s2.delete, user) u2 = s2.query(User).get(user.id) - assert_raises_message(sa.exc.InvalidRequestError, - 'another instance with key', s.delete, u2) + s2.expunge(u2) + assert_raises_message( + sa.exc.InvalidRequestError, + 'another instance .* is already present', s.delete, u2) s.expire(user) s.expunge(user) assert user not in s @@ -543,8 +545,14 @@ class SessionStateTest(_fixtures.FixtureTest): s.expunge(u2) s.identity_map.add(sa.orm.attributes.instance_state(u1)) - assert_raises(AssertionError, s.identity_map.add, - sa.orm.attributes.instance_state(u2)) + assert_raises_message( + sa.exc.InvalidRequestError, + "Can't attach instance <User.*?>; another instance " + "with key .*? is already " + "present in this session.", + s.identity_map.add, + sa.orm.attributes.instance_state(u2) + ) def test_pickled_update(self): users, User = self.tables.users, pickleable.User @@ -581,7 +589,13 @@ class SessionStateTest(_fixtures.FixtureTest): assert u2 is not None and u2 is not u1 assert u2 in sess - assert_raises(AssertionError, lambda: sess.add(u1)) + assert_raises_message( + sa.exc.InvalidRequestError, + "Can't attach instance <User.*?>; another instance " + "with key .*? is already " + "present in this session.", + sess.add, u1 + ) sess.expunge(u2) assert u2 not in sess @@ -1124,11 +1138,56 @@ class WeakIdentityMapTest(_fixtures.FixtureTest): class StrongIdentityMapTest(_fixtures.FixtureTest): run_inserts = None + def _strong_ident_fixture(self): + sess = create_session(weak_identity_map=False) + return sess, sess.prune + + def _event_fixture(self): + session = create_session() + + @event.listens_for(session, "pending_to_persistent") + @event.listens_for(session, "deleted_to_persistent") + @event.listens_for(session, "detached_to_persistent") + @event.listens_for(session, "loaded_as_persistent") + def strong_ref_object(sess, instance): + if 'refs' not in sess.info: + sess.info['refs'] = refs = set() + else: + refs = sess.info['refs'] + + refs.add(instance) + + @event.listens_for(session, "persistent_to_detached") + @event.listens_for(session, "persistent_to_deleted") + @event.listens_for(session, "persistent_to_transient") + def deref_object(sess, instance): + sess.info['refs'].discard(instance) + + def prune(): + if 'refs' not in session.info: + return 0 + + sess_size = len(session.identity_map) + session.info['refs'].clear() + gc_collect() + session.info['refs'] = set( + s.obj() for s in session.identity_map.all_states()) + return sess_size - len(session.identity_map) + + return session, prune + @testing.uses_deprecated() - def test_strong_ref(self): + def test_strong_ref_imap(self): + self._test_strong_ref(self._strong_ident_fixture) + + def test_strong_ref_events(self): + self._test_strong_ref(self._event_fixture) + + def _test_strong_ref(self, fixture): + s, prune = fixture() + users, User = self.tables.users, self.classes.User - s = create_session(weak_identity_map=False) mapper(User, users) # save user @@ -1148,12 +1207,19 @@ class StrongIdentityMapTest(_fixtures.FixtureTest): eq_(users.select().execute().fetchall(), [(user.id, 'u2')]) @testing.uses_deprecated() + def test_prune_imap(self): + self._test_prune(self._strong_ident_fixture) + + def test_prune_events(self): + self._test_prune(self._event_fixture) + @testing.fails_if(lambda: pypy, "pypy has a real GC") @testing.fails_on('+zxjdbc', 'http://www.sqlalchemy.org/trac/ticket/1473') - def test_prune(self): + def _test_prune(self, fixture): + s, prune = fixture() + users, User = self.tables.users, self.classes.User - s = create_session(weak_identity_map=False) mapper(User, users) for o in [User(name='u%s' % x) for x in range(10)]: @@ -1161,43 +1227,44 @@ class StrongIdentityMapTest(_fixtures.FixtureTest): # o is still live after this loop... self.assert_(len(s.identity_map) == 0) - self.assert_(s.prune() == 0) + eq_(prune(), 0) s.flush() gc_collect() - self.assert_(s.prune() == 9) + eq_(prune(), 9) + # o is still in local scope here, so still present self.assert_(len(s.identity_map) == 1) id = o.id del o - self.assert_(s.prune() == 1) + eq_(prune(), 1) self.assert_(len(s.identity_map) == 0) u = s.query(User).get(id) - self.assert_(s.prune() == 0) + eq_(prune(), 0) self.assert_(len(s.identity_map) == 1) u.name = 'squiznart' del u - self.assert_(s.prune() == 0) + eq_(prune(), 0) self.assert_(len(s.identity_map) == 1) s.flush() - self.assert_(s.prune() == 1) + eq_(prune(), 1) self.assert_(len(s.identity_map) == 0) s.add(User(name='x')) - self.assert_(s.prune() == 0) + eq_(prune(), 0) self.assert_(len(s.identity_map) == 0) s.flush() self.assert_(len(s.identity_map) == 1) - self.assert_(s.prune() == 1) + eq_(prune(), 1) self.assert_(len(s.identity_map) == 0) u = s.query(User).get(id) s.delete(u) del u - self.assert_(s.prune() == 0) + eq_(prune(), 0) self.assert_(len(s.identity_map) == 1) s.flush() - self.assert_(s.prune() == 0) + eq_(prune(), 0) self.assert_(len(s.identity_map) == 0) diff --git a/test/orm/test_transaction.py b/test/orm/test_transaction.py index 91846a67e..73c6b977a 100644 --- a/test/orm/test_transaction.py +++ b/test/orm/test_transaction.py @@ -895,7 +895,13 @@ class AutoExpireTest(_LocalFixture): assert u1_state.obj() is None s.rollback() - assert u1_state in s.identity_map.all_states() + # new in 1.1, not in identity map if the object was + # gc'ed and we restore snapshot; we've changed update_impl + # to just skip this object + assert u1_state not in s.identity_map.all_states() + + # in any version, the state is replaced by the query + # because the identity map would switch it u1 = s.query(User).filter_by(name='ed').one() assert u1_state not in s.identity_map.all_states() assert s.scalar(users.count()) == 1 diff --git a/test/profiles.txt b/test/profiles.txt index 691d1a54d..80563e8cf 100644 --- a/test/profiles.txt +++ b/test/profiles.txt @@ -38,7 +38,7 @@ test.aaa_profiling.test_compiler.CompileTest.test_insert 3.4_sqlite_pysqlite_noc test.aaa_profiling.test_compiler.CompileTest.test_select 2.6_sqlite_pysqlite_nocextensions 157 test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_mysql_mysqldb_cextensions 153 test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_mysql_mysqldb_nocextensions 153 -test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_postgresql_psycopg2_cextensions 153 +test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_postgresql_psycopg2_cextensions 157 test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_postgresql_psycopg2_nocextensions 153 test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_sqlite_pysqlite_cextensions 153 test.aaa_profiling.test_compiler.CompileTest.test_select 2.7_sqlite_pysqlite_nocextensions 153 @@ -60,7 +60,7 @@ test.aaa_profiling.test_compiler.CompileTest.test_select 3.4_sqlite_pysqlite_noc test.aaa_profiling.test_compiler.CompileTest.test_select_labels 2.6_sqlite_pysqlite_nocextensions 190 test.aaa_profiling.test_compiler.CompileTest.test_select_labels 2.7_mysql_mysqldb_cextensions 188 test.aaa_profiling.test_compiler.CompileTest.test_select_labels 2.7_mysql_mysqldb_nocextensions 188 -test.aaa_profiling.test_compiler.CompileTest.test_select_labels 2.7_postgresql_psycopg2_cextensions 188 +test.aaa_profiling.test_compiler.CompileTest.test_select_labels 2.7_postgresql_psycopg2_cextensions 190 test.aaa_profiling.test_compiler.CompileTest.test_select_labels 2.7_postgresql_psycopg2_nocextensions 188 test.aaa_profiling.test_compiler.CompileTest.test_select_labels 2.7_sqlite_pysqlite_cextensions 188 test.aaa_profiling.test_compiler.CompileTest.test_select_labels 2.7_sqlite_pysqlite_nocextensions 188 @@ -104,7 +104,7 @@ test.aaa_profiling.test_compiler.CompileTest.test_update 3.4_sqlite_pysqlite_noc test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 2.6_sqlite_pysqlite_nocextensions 146 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 2.7_mysql_mysqldb_cextensions 146 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 2.7_mysql_mysqldb_nocextensions 146 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 2.7_postgresql_psycopg2_cextensions 146 +test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 2.7_postgresql_psycopg2_cextensions 147 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 2.7_postgresql_psycopg2_nocextensions 146 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 2.7_sqlite_pysqlite_cextensions 146 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 2.7_sqlite_pysqlite_nocextensions 146 @@ -117,7 +117,7 @@ test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.3_sqlite_ test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.4_mysql_pymysql_cextensions 146 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.4_mysql_pymysql_nocextensions 146 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.4_postgresql_psycopg2_cextensions 146 -test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.4_postgresql_psycopg2_nocextensions 146 +test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.4_postgresql_psycopg2_nocextensions 147 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.4_sqlite_pysqlite_cextensions 146 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.4_sqlite_pysqlite_nocextensions 146 @@ -126,7 +126,7 @@ test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.4_sqlite_ test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.6_sqlite_pysqlite_nocextensions 4262 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_mysql_mysqldb_cextensions 4262 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_mysql_mysqldb_nocextensions 4262 -test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_postgresql_psycopg2_cextensions 4262 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_postgresql_psycopg2_cextensions 4257 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_postgresql_psycopg2_nocextensions 4262 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_sqlite_pysqlite_cextensions 4262 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 2.7_sqlite_pysqlite_nocextensions 4262 @@ -139,7 +139,7 @@ test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.3_sqlite_ test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.4_mysql_pymysql_cextensions 4263 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.4_mysql_pymysql_nocextensions 4263 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.4_postgresql_psycopg2_cextensions 4263 -test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.4_postgresql_psycopg2_nocextensions 4263 +test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.4_postgresql_psycopg2_nocextensions 4258 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.4_sqlite_pysqlite_cextensions 4263 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set 3.4_sqlite_pysqlite_nocextensions 4263 @@ -170,7 +170,7 @@ test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 2.6_sqlite_pysqlite_nocextensions 26358 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 2.7_mysql_mysqldb_cextensions 16194 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 2.7_mysql_mysqldb_nocextensions 25197 -test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 2.7_postgresql_psycopg2_cextensions 28177 +test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 2.7_postgresql_psycopg2_cextensions 29184 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 2.7_postgresql_psycopg2_nocextensions 37180 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 2.7_sqlite_pysqlite_cextensions 16329 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 2.7_sqlite_pysqlite_nocextensions 25332 @@ -183,7 +183,7 @@ test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.3_sqlite_pysqlite_n test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.4_mysql_pymysql_cextensions 83733 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.4_mysql_pymysql_nocextensions 92736 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.4_postgresql_psycopg2_cextensions 18221 -test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.4_postgresql_psycopg2_nocextensions 27224 +test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.4_postgresql_psycopg2_nocextensions 27201 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.4_sqlite_pysqlite_cextensions 18393 test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.4_sqlite_pysqlite_nocextensions 27396 @@ -192,7 +192,7 @@ test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.4_sqlite_pysqlite_n test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.6_sqlite_pysqlite_nocextensions 26282 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_mysql_mysqldb_cextensions 22212 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_mysql_mysqldb_nocextensions 25215 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_postgresql_psycopg2_cextensions 22183 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_postgresql_psycopg2_cextensions 23196 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_postgresql_psycopg2_nocextensions 25186 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_sqlite_pysqlite_cextensions 22269 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_sqlite_pysqlite_nocextensions 25272 @@ -205,7 +205,7 @@ test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.3_sqlite_pys test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_mysql_pymysql_cextensions 47353 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_mysql_pymysql_nocextensions 50356 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_postgresql_psycopg2_cextensions 24215 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_postgresql_psycopg2_nocextensions 27218 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_postgresql_psycopg2_nocextensions 27220 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_sqlite_pysqlite_cextensions 24321 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_sqlite_pysqlite_nocextensions 27324 @@ -236,7 +236,7 @@ test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_ test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.6_sqlite_pysqlite_nocextensions 161101 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_mysql_mysqldb_cextensions 127101 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_mysql_mysqldb_nocextensions 128851 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_postgresql_psycopg2_cextensions 120101 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_postgresql_psycopg2_cextensions 123351 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_postgresql_psycopg2_nocextensions 121851 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_sqlite_pysqlite_cextensions 156351 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_sqlite_pysqlite_nocextensions 158054 @@ -249,7 +249,7 @@ test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_ test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_mysql_pymysql_cextensions 187056 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_mysql_pymysql_nocextensions 188855 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_postgresql_psycopg2_cextensions 128556 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_postgresql_psycopg2_nocextensions 130306 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_postgresql_psycopg2_nocextensions 130356 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_sqlite_pysqlite_cextensions 168806 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_sqlite_pysqlite_nocextensions 170556 @@ -258,7 +258,7 @@ test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_ test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.6_sqlite_pysqlite_nocextensions 21505 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_mysql_mysqldb_cextensions 19393 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_mysql_mysqldb_nocextensions 19597 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_postgresql_psycopg2_cextensions 18881 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_postgresql_psycopg2_cextensions 19024 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_postgresql_psycopg2_nocextensions 19085 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_sqlite_pysqlite_cextensions 21186 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_sqlite_pysqlite_nocextensions 21437 @@ -271,7 +271,7 @@ test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3. test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_mysql_pymysql_cextensions 23716 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_mysql_pymysql_nocextensions 23871 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_postgresql_psycopg2_cextensions 19552 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_postgresql_psycopg2_nocextensions 19744 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_postgresql_psycopg2_nocextensions 19731 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_sqlite_pysqlite_cextensions 22051 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_sqlite_pysqlite_nocextensions 22255 @@ -280,7 +280,7 @@ test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3. test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.6_sqlite_pysqlite_nocextensions 1520 test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_mysql_mysqldb_cextensions 1400 test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_mysql_mysqldb_nocextensions 1415 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_postgresql_psycopg2_cextensions 1319 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_postgresql_psycopg2_cextensions 1309 test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_postgresql_psycopg2_nocextensions 1334 test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_sqlite_pysqlite_cextensions 1527 test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_sqlite_pysqlite_nocextensions 1542 @@ -293,7 +293,7 @@ test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.3_sqlite_pysqlite_nocext test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_mysql_pymysql_cextensions 2038 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_mysql_pymysql_nocextensions 2053 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_postgresql_psycopg2_cextensions 1335 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_postgresql_psycopg2_nocextensions 1350 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_postgresql_psycopg2_nocextensions 1354 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_sqlite_pysqlite_cextensions 1577 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_sqlite_pysqlite_nocextensions 1592 @@ -302,7 +302,7 @@ test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_sqlite_pysqlite_nocext test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 2.6_sqlite_pysqlite_nocextensions 89,19 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 2.7_mysql_mysqldb_cextensions 93,19 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 2.7_mysql_mysqldb_nocextensions 93,19 -test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 2.7_postgresql_psycopg2_cextensions 93,19 +test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 2.7_postgresql_psycopg2_cextensions 101,19 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 2.7_postgresql_psycopg2_nocextensions 93,19 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 2.7_sqlite_pysqlite_cextensions 93,19 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 2.7_sqlite_pysqlite_nocextensions 93,19 @@ -315,7 +315,7 @@ test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.3_sqlite_pysqlite_noc test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.4_mysql_pymysql_cextensions 92,20 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.4_mysql_pymysql_nocextensions 92,20 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.4_postgresql_psycopg2_cextensions 92,20 -test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.4_postgresql_psycopg2_nocextensions 92,20 +test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.4_postgresql_psycopg2_nocextensions 104,20 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.4_sqlite_pysqlite_cextensions 92,20 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.4_sqlite_pysqlite_nocextensions 92,20 @@ -324,7 +324,7 @@ test.aaa_profiling.test_orm.MergeTest.test_merge_no_load 3.4_sqlite_pysqlite_noc test.aaa_profiling.test_orm.QueryTest.test_query_cols 2.6_sqlite_pysqlite_nocextensions 8064 test.aaa_profiling.test_orm.QueryTest.test_query_cols 2.7_mysql_mysqldb_cextensions 6220 test.aaa_profiling.test_orm.QueryTest.test_query_cols 2.7_mysql_mysqldb_nocextensions 6750 -test.aaa_profiling.test_orm.QueryTest.test_query_cols 2.7_postgresql_psycopg2_cextensions 6790 +test.aaa_profiling.test_orm.QueryTest.test_query_cols 2.7_postgresql_psycopg2_cextensions 6798 test.aaa_profiling.test_orm.QueryTest.test_query_cols 2.7_postgresql_psycopg2_nocextensions 7320 test.aaa_profiling.test_orm.QueryTest.test_query_cols 2.7_sqlite_pysqlite_cextensions 7564 test.aaa_profiling.test_orm.QueryTest.test_query_cols 2.7_sqlite_pysqlite_nocextensions 8094 @@ -337,7 +337,7 @@ test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.3_sqlite_pysqlite_nocext test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.4_mysql_pymysql_cextensions 13744 test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.4_mysql_pymysql_nocextensions 14274 test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.4_postgresql_psycopg2_cextensions 6234 -test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.4_postgresql_psycopg2_nocextensions 6674 +test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.4_postgresql_psycopg2_nocextensions 6702 test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.4_sqlite_pysqlite_cextensions 7846 test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.4_sqlite_pysqlite_nocextensions 8376 @@ -346,7 +346,7 @@ test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.4_sqlite_pysqlite_nocext test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.6_sqlite_pysqlite_nocextensions 1156 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_mysql_mysqldb_cextensions 1145 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_mysql_mysqldb_nocextensions 1148 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_postgresql_psycopg2_cextensions 1160 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_postgresql_psycopg2_cextensions 1139 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_postgresql_psycopg2_nocextensions 1161 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_sqlite_pysqlite_cextensions 1151 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_sqlite_pysqlite_nocextensions 1145 @@ -359,7 +359,7 @@ test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.3_sqlite_pysqlite_noc test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_mysql_pymysql_cextensions 1254 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_mysql_pymysql_nocextensions 1280 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_postgresql_psycopg2_cextensions 1247 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_postgresql_psycopg2_nocextensions 1262 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_postgresql_psycopg2_nocextensions 1270 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_sqlite_pysqlite_cextensions 1238 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_sqlite_pysqlite_nocextensions 1272 @@ -368,7 +368,7 @@ test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_sqlite_pysqlite_noc test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect 2.6_sqlite_pysqlite_nocextensions 97 test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect 2.7_mysql_mysqldb_cextensions 95 test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect 2.7_mysql_mysqldb_nocextensions 95 -test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect 2.7_postgresql_psycopg2_cextensions 95 +test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect 2.7_postgresql_psycopg2_cextensions 96 test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect 2.7_postgresql_psycopg2_nocextensions 95 test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect 2.7_sqlite_pysqlite_cextensions 95 test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect 2.7_sqlite_pysqlite_nocextensions 95 @@ -500,7 +500,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_contains_doesnt_compile 3.4 test.aaa_profiling.test_resultset.ResultSetTest.test_string 2.6_sqlite_pysqlite_nocextensions 15439 test.aaa_profiling.test_resultset.ResultSetTest.test_string 2.7_mysql_mysqldb_cextensions 488 test.aaa_profiling.test_resultset.ResultSetTest.test_string 2.7_mysql_mysqldb_nocextensions 15488 -test.aaa_profiling.test_resultset.ResultSetTest.test_string 2.7_postgresql_psycopg2_cextensions 20477 +test.aaa_profiling.test_resultset.ResultSetTest.test_string 2.7_postgresql_psycopg2_cextensions 20497 test.aaa_profiling.test_resultset.ResultSetTest.test_string 2.7_postgresql_psycopg2_nocextensions 35477 test.aaa_profiling.test_resultset.ResultSetTest.test_string 2.7_sqlite_pysqlite_cextensions 419 test.aaa_profiling.test_resultset.ResultSetTest.test_string 2.7_sqlite_pysqlite_nocextensions 15419 @@ -522,7 +522,7 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_string 3.4_sqlite_pysqlite_ test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 2.6_sqlite_pysqlite_nocextensions 15439 test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 2.7_mysql_mysqldb_cextensions 488 test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 2.7_mysql_mysqldb_nocextensions 45488 -test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 2.7_postgresql_psycopg2_cextensions 20477 +test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 2.7_postgresql_psycopg2_cextensions 20497 test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 2.7_postgresql_psycopg2_nocextensions 35477 test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 2.7_sqlite_pysqlite_cextensions 419 test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 2.7_sqlite_pysqlite_nocextensions 15419 @@ -541,18 +541,18 @@ test.aaa_profiling.test_resultset.ResultSetTest.test_unicode 3.4_sqlite_pysqlite # TEST: test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation -test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_cextensions 5811,295,3577,11462,1134,1973,2434 +test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_cextensions 5823,295,3721,11938,1146,2017,2481 test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_nocextensions 5833,295,3681,12720,1241,1980,2655 test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 3.3_postgresql_psycopg2_cextensions 5591,277,3569,11458,1134,1924,2489 test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 3.3_postgresql_psycopg2_nocextensions 5613,277,3665,12630,1228,1931,2681 test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 3.4_postgresql_psycopg2_cextensions 5619,277,3705,11902,1144,1966,2532 -test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 3.4_postgresql_psycopg2_nocextensions 5624,277,3801,13074,1238,1970,2724 +test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 3.4_postgresql_psycopg2_nocextensions 5625,277,3809,13108,1241,1975,2729 # TEST: test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation -test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_cextensions 6256,402,6599,17140,1146,2569 +test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_cextensions 6437,410,6761,17665,1159,2627 test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_nocextensions 6341,407,6703,18167,1244,2598 test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.3_postgresql_psycopg2_cextensions 6228,393,6747,17582,1148,2623 test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.3_postgresql_psycopg2_nocextensions 6318,398,6851,18609,1234,2652 test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.4_postgresql_psycopg2_cextensions 6257,393,6891,18056,1159,2671 -test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.4_postgresql_psycopg2_nocextensions 6341,398,6995,19083,1245,2700 +test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.4_postgresql_psycopg2_nocextensions 6508,406,7005,19115,1248,2707 |