From d6ce68727f8ad4c77cc64ac6bbc5fc17ecd2b8e3 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 25 Aug 2013 14:03:54 -0400 Subject: - The ``version_id_generator`` parameter of ``Mapper`` can now be specified to rely upon server generated version identifiers, using triggers or other database-provided versioning features, by passing the value ``False``. The ORM will use RETURNING when available to immediately load the new version identifier, else it will emit a second SELECT. [ticket:2793] - The ``eager_defaults`` flag of :class:`.Mapper` will now allow the newly generated default values to be fetched using an inline RETURNING clause, rather than a second SELECT statement, for backends that support RETURNING. - Added a new variant to :meth:`.ValuesBase.returning` called :meth:`.ValuesBase.return_defaults`; this allows arbitrary columns to be added to the RETURNING clause of the statement without interfering with the compilers usual "implicit returning" feature, which is used to efficiently fetch newly generated primary key values. For supporting backends, a dictionary of all fetched values is present at :attr:`.ResultProxy.returned_defaults`. - add a glossary entry for RETURNING - add documentation for version id generation, [ticket:867] --- test/orm/test_versioning.py | 195 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 191 insertions(+), 4 deletions(-) (limited to 'test/orm/test_versioning.py') diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py index abb08c536..d8d92830f 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,190 @@ 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(counter.next()) + 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 + ) + + -- cgit v1.2.1 From e9c748a7bf1acb1423efa92ad797e9a0fbcf1cbb Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Sun, 25 Aug 2013 17:37:59 -0400 Subject: - ensure rowcount is returned for an UPDATE with no implicit returning - modernize test for that - use py3k compatible next() in test_returning/test_versioning --- test/orm/test_versioning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'test/orm/test_versioning.py') diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py index d8d92830f..026793c97 100644 --- a/test/orm/test_versioning.py +++ b/test/orm/test_versioning.py @@ -668,7 +668,7 @@ class ServerVersioningTest(fixtures.MappedTest): if hasattr(stmt, "_counter"): return stmt._counter else: - stmt._counter = str(counter.next()) + stmt._counter = str(next(counter)) return stmt._counter Table('version_table', metadata, -- cgit v1.2.1 From 5a6895471fb6bf9afe9bdf017f1fa2c6246ae303 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 6 Sep 2013 21:39:36 -0400 Subject: - modify what we did in [ticket:2793] so that we can also set the version id programmatically outside of the generator. using this system, we can also leave the version id alone. --- test/orm/test_versioning.py | 86 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) (limited to 'test/orm/test_versioning.py') diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py index 026793c97..9379543ed 100644 --- a/test/orm/test_versioning.py +++ b/test/orm/test_versioning.py @@ -829,4 +829,90 @@ class ServerVersioningTest(fixtures.MappedTest): 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 -- cgit v1.2.1