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