summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2012-01-27 20:32:52 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2012-01-27 20:32:52 -0500
commitf9c2f85a3ac35cd0158dbd818ba0b9d209003304 (patch)
tree794f4cac1bed82cfc8a7a901e090f82d15566e93
parent535fc8b1500cc11cc800ee3189d30bc6d8de51a7 (diff)
downloadsqlalchemy-f9c2f85a3ac35cd0158dbd818ba0b9d209003304.tar.gz
- [bug] Fixed issue where modified session state
established after a failed flush would be committed as part of the subsequent transaction that begins automatically after manual call to rollback(). The state of the session is checked within rollback(), and if new state is present, a warning is emitted and restore_snapshot() is called a second time, discarding those changes. [ticket:2389] - repaired testing.assert_warnings to also verify that any warnings were emitted
-rw-r--r--CHANGES10
-rw-r--r--lib/sqlalchemy/orm/session.py22
-rw-r--r--test/lib/testing.py6
-rw-r--r--test/orm/test_session.py63
4 files changed, 89 insertions, 12 deletions
diff --git a/CHANGES b/CHANGES
index ba7327399..1859b9145 100644
--- a/CHANGES
+++ b/CHANGES
@@ -6,6 +6,16 @@ CHANGES
0.7.5
=====
- orm
+ - [bug] Fixed issue where modified session state
+ established after a failed flush would be committed
+ as part of the subsequent transaction that
+ begins automatically after manual call
+ to rollback(). The state of the session is
+ checked within rollback(), and if new state
+ is present, a warning is emitted and
+ restore_snapshot() is called a second time,
+ discarding those changes. [ticket:2389]
+
- [feature] Added "class_registry" argument to
declarative_base(). Allows two or more declarative
bases to share the same registry of class names.
diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py
index eb03a5b81..4299290d0 100644
--- a/lib/sqlalchemy/orm/session.py
+++ b/lib/sqlalchemy/orm/session.py
@@ -343,6 +343,16 @@ class SessionTransaction(object):
sess = self.session
+ if self.session._enable_transaction_accounting and \
+ not sess._is_clean():
+ # if items were added, deleted, or mutated
+ # here, we need to re-restore the snapshot
+ util.warn(
+ "Session's state has been changed on "
+ "a non-active transaction - this state "
+ "will be discarded.")
+ self._restore_snapshot()
+
self.close()
if self._parent and _capture_exception:
self._parent._rollback_exception = sys.exc_info()[1]
@@ -575,7 +585,7 @@ class Session(object):
if self.transaction is not None:
if subtransactions or nested:
self.transaction = self.transaction._begin(
- nested=nested)
+ nested=nested)
else:
raise sa_exc.InvalidRequestError(
"A transaction is already begun. Use subtransactions=True "
@@ -1542,16 +1552,20 @@ class Session(object):
if self._flushing:
raise sa_exc.InvalidRequestError("Session is already flushing")
+ if self._is_clean():
+ return
try:
self._flushing = True
self._flush(objects)
finally:
self._flushing = False
+ def _is_clean(self):
+ return not self.identity_map.check_modified() and \
+ not self._deleted and \
+ not self._new
+
def _flush(self, objects=None):
- if (not self.identity_map.check_modified() and
- not self._deleted and not self._new):
- return
dirty = self._dirty_states
if not dirty and not self._deleted and not self._new:
diff --git a/test/lib/testing.py b/test/lib/testing.py
index 1d00d04d8..a84c5a7ae 100644
--- a/test/lib/testing.py
+++ b/test/lib/testing.py
@@ -358,14 +358,18 @@ def emits_warning_on(db, *warnings):
def assert_warnings(fn, warnings):
"""Assert that each of the given warnings are emitted by fn."""
+ canary = []
orig_warn = util.warn
def capture_warnings(*args, **kw):
orig_warn(*args, **kw)
popwarn = warnings.pop(0)
+ canary.append(popwarn)
eq_(args[0], popwarn)
util.warn = util.langhelpers.warn = capture_warnings
- return emits_warning()(fn)()
+ result = emits_warning()(fn)()
+ assert canary, "No warning was emitted"
+ return result
def uses_deprecated(*messages):
"""Mark a test as immune from fatal deprecation warnings.
diff --git a/test/orm/test_session.py b/test/orm/test_session.py
index a90e15497..d1544ba99 100644
--- a/test/orm/test_session.py
+++ b/test/orm/test_session.py
@@ -1,5 +1,5 @@
from test.lib.testing import eq_, assert_raises, \
- assert_raises_message
+ assert_raises_message, assert_warnings
from test.lib.util import gc_collect
from test.lib import pickleable
from sqlalchemy.util import pickle
@@ -712,7 +712,7 @@ class SessionTest(_fixtures.FixtureTest):
eq_(len(sess.query(User).all()), 1)
- def test_error_on_using_inactive_session(self):
+ def test_error_on_using_inactive_session_commands(self):
users, User = self.tables.users, self.classes.User
mapper(User, users)
@@ -730,17 +730,67 @@ class SessionTest(_fixtures.FixtureTest):
sess.begin, subtransactions=True)
sess.close()
- def test_preserve_flush_error(self):
+ def _inactive_flushed_session_fixture(self):
users, User = self.tables.users, self.classes.User
mapper(User, users)
sess = Session()
+ u1 = User(id=1, name='u1')
+ sess.add(u1)
+ sess.commit()
- sess.add(User(id=5))
+ sess.add(User(id=1, name='u2'))
assert_raises(
- sa.exc.DBAPIError,
- sess.commit
+ orm_exc.FlushError, sess.flush
)
+ return sess, u1
+
+ def test_warning_on_using_inactive_session_new(self):
+ User = self.classes.User
+
+ sess, u1 = self._inactive_flushed_session_fixture()
+ u2 = User(name='u2')
+ sess.add(u2)
+ def go():
+ sess.rollback()
+ assert_warnings(go,
+ ["Session's state has been changed on a "
+ "non-active transaction - this state "
+ "will be discarded."],
+ )
+ assert u2 not in sess
+ assert u1 in sess
+
+ def test_warning_on_using_inactive_session_dirty(self):
+ sess, u1 = self._inactive_flushed_session_fixture()
+ u1.name = 'newname'
+ def go():
+ sess.rollback()
+ assert_warnings(go,
+ ["Session's state has been changed on a "
+ "non-active transaction - this state "
+ "will be discarded."],
+ )
+ assert u1 in sess
+ assert u1 not in sess.dirty
+
+ def test_warning_on_using_inactive_session_delete(self):
+ sess, u1 = self._inactive_flushed_session_fixture()
+ sess.delete(u1)
+ def go():
+ sess.rollback()
+ assert_warnings(go,
+ ["Session's state has been changed on a "
+ "non-active transaction - this state "
+ "will be discarded."],
+ )
+ assert u1 in sess
+ assert u1 not in sess.deleted
+
+ def test_preserve_flush_error(self):
+ User = self.classes.User
+
+ sess, u1 = self._inactive_flushed_session_fixture()
for i in range(5):
assert_raises_message(sa.exc.InvalidRequestError,
@@ -755,7 +805,6 @@ class SessionTest(_fixtures.FixtureTest):
sess.add(User(id=5, name='some name'))
sess.commit()
-
def test_no_autocommit_with_explicit_commit(self):
User, users = self.classes.User, self.tables.users