summaryrefslogtreecommitdiff
path: root/test/orm/test_versioning.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2014-01-21 20:10:23 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2014-01-21 20:10:23 -0500
commit07fb90c6cc14de6d02cf4be592c57d56831f59f7 (patch)
tree050ef65db988559c60f7aa40f2d0bfe24947e548 /test/orm/test_versioning.py
parent560fd1d5ed643a1b0f95296f3b840c1963bbe67f (diff)
parentee1f4d21037690ad996c5eacf7e1200e92f2fbaa (diff)
downloadsqlalchemy-ticket_2501.tar.gz
Merge branch 'master' into ticket_2501ticket_2501
Conflicts: lib/sqlalchemy/orm/mapper.py
Diffstat (limited to 'test/orm/test_versioning.py')
-rw-r--r--test/orm/test_versioning.py281
1 files changed, 277 insertions, 4 deletions
diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py
index abb08c536..9379543ed 100644
--- a/test/orm/test_versioning.py
+++ b/test/orm/test_versioning.py
@@ -11,7 +11,7 @@ from sqlalchemy.orm import mapper, relationship, Session, \
from sqlalchemy.testing import eq_, ne_, assert_raises, assert_raises_message
from sqlalchemy.testing import fixtures
from test.orm import _fixtures
-from sqlalchemy.testing import fixtures
+from sqlalchemy.testing.assertsql import AllOf, CompiledSQL
_uuids = [
@@ -461,12 +461,12 @@ class AlternateGeneratorTest(fixtures.MappedTest):
cls.classes.P)
mapper(P, p, version_id_col=p.c.version_id,
- version_id_generator=lambda x:make_uuid(),
+ version_id_generator=lambda x: make_uuid(),
properties={
- 'c':relationship(C, uselist=False, cascade='all, delete-orphan')
+ 'c': relationship(C, uselist=False, cascade='all, delete-orphan')
})
mapper(C, c, version_id_col=c.c.version_id,
- version_id_generator=lambda x:make_uuid(),
+ version_id_generator=lambda x: make_uuid(),
)
@testing.emits_warning_on('+zxjdbc', r'.*does not support updated rowcount')
@@ -643,3 +643,276 @@ class InheritanceTwoVersionIdsTest(fixtures.MappedTest):
mapper,
Sub, sub, inherits=Base,
version_id_col=sub.c.version_id)
+
+
+class ServerVersioningTest(fixtures.MappedTest):
+ run_define_tables = 'each'
+
+ @classmethod
+ def define_tables(cls, metadata):
+ from sqlalchemy.sql import ColumnElement
+ from sqlalchemy.ext.compiler import compiles
+ import itertools
+
+ counter = itertools.count(1)
+
+ class IncDefault(ColumnElement):
+ pass
+
+ @compiles(IncDefault)
+ def compile(element, compiler, **kw):
+ # cache the counter value on the statement
+ # itself so the assertsql system gets the same
+ # value when it compiles the statement a second time
+ stmt = compiler.statement
+ if hasattr(stmt, "_counter"):
+ return stmt._counter
+ else:
+ stmt._counter = str(next(counter))
+ return stmt._counter
+
+ Table('version_table', metadata,
+ Column('id', Integer, primary_key=True,
+ test_needs_autoincrement=True),
+ Column('version_id', Integer, nullable=False,
+ default=IncDefault(), onupdate=IncDefault()),
+ Column('value', String(40), nullable=False))
+
+ @classmethod
+ def setup_classes(cls):
+ class Foo(cls.Basic):
+ pass
+ class Bar(cls.Basic):
+ pass
+
+ def _fixture(self, expire_on_commit=True):
+ Foo, version_table = self.classes.Foo, self.tables.version_table
+
+ mapper(Foo, version_table,
+ version_id_col=version_table.c.version_id,
+ version_id_generator=False,
+ )
+
+ s1 = Session(expire_on_commit=expire_on_commit)
+ return s1
+
+ def test_insert_col(self):
+ sess = self._fixture()
+
+ f1 = self.classes.Foo(value='f1')
+ sess.add(f1)
+
+ statements = [
+ # note that the assertsql tests the rule against
+ # "default" - on a "returning" backend, the statement
+ # includes "RETURNING"
+ CompiledSQL(
+ "INSERT INTO version_table (version_id, value) "
+ "VALUES (1, :value)",
+ lambda ctx: [{'value': 'f1'}]
+ )
+ ]
+ if not testing.db.dialect.implicit_returning:
+ # DBs without implicit returning, we must immediately
+ # SELECT for the new version id
+ statements.append(
+ CompiledSQL(
+ "SELECT version_table.version_id AS version_table_version_id "
+ "FROM version_table WHERE version_table.id = :param_1",
+ lambda ctx: [{"param_1": 1}]
+ )
+ )
+ self.assert_sql_execution(testing.db, sess.flush, *statements)
+
+ def test_update_col(self):
+ sess = self._fixture()
+
+ f1 = self.classes.Foo(value='f1')
+ sess.add(f1)
+ sess.flush()
+
+ f1.value = 'f2'
+
+ statements = [
+ # note that the assertsql tests the rule against
+ # "default" - on a "returning" backend, the statement
+ # includes "RETURNING"
+ CompiledSQL(
+ "UPDATE version_table SET version_id=2, value=:value "
+ "WHERE version_table.id = :version_table_id AND "
+ "version_table.version_id = :version_table_version_id",
+ lambda ctx: [{"version_table_id": 1,
+ "version_table_version_id": 1, "value": "f2"}]
+ )
+ ]
+ if not testing.db.dialect.implicit_returning:
+ # DBs without implicit returning, we must immediately
+ # SELECT for the new version id
+ statements.append(
+ CompiledSQL(
+ "SELECT version_table.version_id AS version_table_version_id "
+ "FROM version_table WHERE version_table.id = :param_1",
+ lambda ctx: [{"param_1": 1}]
+ )
+ )
+ self.assert_sql_execution(testing.db, sess.flush, *statements)
+
+
+ def test_delete_col(self):
+ sess = self._fixture()
+
+ f1 = self.classes.Foo(value='f1')
+ sess.add(f1)
+ sess.flush()
+
+ sess.delete(f1)
+
+ statements = [
+ # note that the assertsql tests the rule against
+ # "default" - on a "returning" backend, the statement
+ # includes "RETURNING"
+ CompiledSQL(
+ "DELETE FROM version_table "
+ "WHERE version_table.id = :id AND "
+ "version_table.version_id = :version_id",
+ lambda ctx: [{"id": 1, "version_id": 1}]
+ )
+ ]
+ self.assert_sql_execution(testing.db, sess.flush, *statements)
+
+ def test_concurrent_mod_err_expire_on_commit(self):
+ sess = self._fixture()
+
+ f1 = self.classes.Foo(value='f1')
+ sess.add(f1)
+ sess.commit()
+
+ f1.value
+
+ s2 = Session()
+ f2 = s2.query(self.classes.Foo).first()
+ f2.value = 'f2'
+ s2.commit()
+
+ f1.value = 'f3'
+
+ assert_raises_message(
+ orm.exc.StaleDataError,
+ r"UPDATE statement on table 'version_table' expected to "
+ r"update 1 row\(s\); 0 were matched.",
+ sess.commit
+ )
+
+ def test_concurrent_mod_err_noexpire_on_commit(self):
+ sess = self._fixture(expire_on_commit=False)
+
+ f1 = self.classes.Foo(value='f1')
+ sess.add(f1)
+ sess.commit()
+
+ # here, we're not expired overall, so no load occurs and we
+ # stay without a version id, unless we've emitted
+ # a SELECT for it within the flush.
+ f1.value
+
+ s2 = Session(expire_on_commit=False)
+ f2 = s2.query(self.classes.Foo).first()
+ f2.value = 'f2'
+ s2.commit()
+
+ f1.value = 'f3'
+
+ assert_raises_message(
+ orm.exc.StaleDataError,
+ r"UPDATE statement on table 'version_table' expected to "
+ r"update 1 row\(s\); 0 were matched.",
+ sess.commit
+ )
+
+class ManualVersionTest(fixtures.MappedTest):
+ run_define_tables = 'each'
+
+ @classmethod
+ def define_tables(cls, metadata):
+ Table("a", metadata,
+ Column('id', Integer, primary_key=True, test_needs_autoincrement=True),
+ Column('data', String(30)),
+ Column('vid', Integer)
+ )
+
+ @classmethod
+ def setup_classes(cls):
+ class A(cls.Basic):
+ pass
+
+
+ @classmethod
+ def setup_mappers(cls):
+ mapper(cls.classes.A, cls.tables.a,
+ version_id_col=cls.tables.a.c.vid,
+ version_id_generator=False)
+
+ def test_insert(self):
+ sess = Session()
+ a1 = self.classes.A()
+
+ a1.vid = 1
+ sess.add(a1)
+ sess.commit()
+
+ eq_(a1.vid, 1)
+
+ def test_update(self):
+ sess = Session()
+ a1 = self.classes.A()
+
+ a1.vid = 1
+ a1.data = 'd1'
+ sess.add(a1)
+ sess.commit()
+
+ a1.vid = 2
+ a1.data = 'd2'
+
+ sess.commit()
+
+ eq_(a1.vid, 2)
+
+ def test_update_concurrent_check(self):
+ sess = Session()
+ a1 = self.classes.A()
+
+ a1.vid = 1
+ a1.data = 'd1'
+ sess.add(a1)
+ sess.commit()
+
+ a1.vid = 2
+ sess.execute(self.tables.a.update().values(vid=3))
+ a1.data = 'd2'
+ assert_raises(
+ orm_exc.StaleDataError,
+ sess.commit
+ )
+
+ def test_update_version_conditional(self):
+ sess = Session()
+ a1 = self.classes.A()
+
+ a1.vid = 1
+ a1.data = 'd1'
+ sess.add(a1)
+ sess.commit()
+
+ # change the data and UPDATE without
+ # incrementing version id
+ a1.data = 'd2'
+ sess.commit()
+
+ eq_(a1.vid, 1)
+
+ a1.data = 'd3'
+ a1.vid = 2
+ sess.commit()
+
+ eq_(a1.vid, 2) \ No newline at end of file