]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- The :class:`.SessionEvents` suite now includes events to allow
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 28 Aug 2015 21:43:46 +0000 (17:43 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 2 Sep 2015 21:55:15 +0000 (17:55 -0400)
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

17 files changed:
doc/build/changelog/changelog_11.rst
doc/build/changelog/migration_11.rst
doc/build/glossary.rst
doc/build/orm/session_events.rst
doc/build/orm/session_state_management.rst
lib/sqlalchemy/orm/events.py
lib/sqlalchemy/orm/identity.py
lib/sqlalchemy/orm/loading.py
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/orm/state.py
lib/sqlalchemy/orm/util.py
test/orm/test_events.py
test/orm/test_hasparent.py
test/orm/test_load_on_fks.py
test/orm/test_session.py
test/orm/test_transaction.py
test/profiles.txt

index 4fff4ab64983bb341c393d086a6ecedd4690cabe..27dc8fd462efd34782bd4411360c741ffce9b42a 100644 (file)
 .. changelog::
     :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
index 412f42d27a0e945b9178e73308c4c8f78562000c..ff26a00bd79d009d3257fb75c795752d54f84101 100644 (file)
@@ -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
index c0ecee84b4f72bf8483c736a840e0c5e8552e30a..9c1395f14c77f3e315530fc361c0c80fdf4d11ad 100644 (file)
@@ -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
index 231311aa0ca1d6e06dd4fa48772bb9dd7d77bb92..dc7ad39e0769c8de056a7fca3a8246bd687a867f 100644 (file)
@@ -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:
 
index 18673fde1892a42273a4b3d6faec1c59892ee30c..090bf76749697ef7f016dcf5d273955ff93ec78f 100644 (file)
@@ -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:
 
index 29bdbaa8cbe5bb75496716d89b4bd67acae0042d..993385e15ad04685e6de629ef6cf6d667d5bbec3 100644 (file)
@@ -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.
index b42703855d984b0121e8576f720bd84da2af19ad..2dfe3fd5cfde7d0ee4c50c4880ca80cefc00bc40 100644 (file)
@@ -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
index d8bf662fccf84ced2205174372b129a1e550754e..c90308a69341f75109dd07bc1173549a47f26982 100644 (file)
@@ -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)
index 6c3f392bac15b4884d233e8ddfe3d7e749e91b52..96a24067e56f2394eed556102de19326a5263391 100644 (file)
@@ -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)
 
index 6c9f07bffa283c559b6772e66e0a45f813687b93..9f1190339386d54895c741be6e4bc185ecda2f74 100644 (file)
@@ -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
@@ -134,17 +133,81 @@ class InstanceState(interfaces.InspectionAttr):
         return self.key is None and \
             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()
index 6d3869679543802bf135d4158625c98dbbaf5f78..4351c8dc6bccba27d809872f2816285a0dc6f78f 100644 (file)
@@ -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():
index b9fafb105c49afd0fe81e474a00e650838110999..ab61077aecf911dcf1f40609e4d779b752b26c58 100644 (file)
@@ -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
index fd246b527bca001f07429f0015f82e2ccbe7f85c..df4b05980ca5aa0bbd083d4fea4359fcc39c69db 100644 (file)
@@ -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()
 
index 813d8d17ad81bfac1dbdf7de10fe808fcefd7cda..471c8665a12a200adc880854986bd354c4071bf2 100644 (file)
@@ -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)
index 58551d763ede152596fb57e3abc92749b583db4b..f6ddcb56634df0a0d61f2566291f2c8f5af7ebd8 100644 (file)
@@ -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)
 
 
index 91846a67e6102347d512e7be66702c05746215d9..73c6b977ac681fd40287d408937fc8d21af49f05 100644 (file)
@@ -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
index 691d1a54d239ae20e2c85752e8e31ed67e6c3d3b..80563e8cf62ff05957500888cbd168b1d67b5b6b 100644 (file)
@@ -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