From: Mike Bayer Date: Tue, 16 Mar 2010 16:58:13 +0000 (-0400) Subject: - Fixed bug which caused "row switch" logic, that is an X-Git-Tag: rel_0_6beta2~45^2~5 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=84c6857d214725246c28f97b4dba9d52385f9a37;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git - Fixed bug which caused "row switch" logic, that is an INSERT and DELETE replaced by an UPDATE, to fail when version_id_col was in use. [ticket:1692] - Added "version_id_generator" argument to Mapper, this is a callable that, given the current value of the "version_id_col", returns the next version number. Can be used for alternate versioning schemes such as uuid, timestamps. [ticket:1692] --- diff --git a/.hgignore b/.hgignore index d8aed38730..6ecede1f37 100755 --- a/.hgignore +++ b/.hgignore @@ -2,3 +2,4 @@ syntax:regexp ^build/ ^doc/build/output .pyc$ +.orig$ diff --git a/CHANGES b/CHANGES index d2ed4672e1..327ed33b2f 100644 --- a/CHANGES +++ b/CHANGES @@ -126,7 +126,16 @@ CHANGES - query.scalar() now raises an exception if more than one row is returned. All other behavior remains the same. [ticket:1735] + + - Fixed bug which caused "row switch" logic, that is an + INSERT and DELETE replaced by an UPDATE, to fail when + version_id_col was in use. [ticket:1692] + - Added "version_id_generator" argument to Mapper, this is a + callable that, given the current value of the "version_id_col", + returns the next version number. Can be used for alternate + versioning schemes such as uuid, timestamps. [ticket:1692] + - sql - The most common result processors conversion function were moved to the new "processors" module. Dialect authors are diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index 37baa03bef..6c19a12e71 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -610,178 +610,169 @@ def deferred(*columns, **kwargs): def mapper(class_, local_table=None, *args, **params): """Return a new :class:`~sqlalchemy.orm.Mapper` object. - class\_ - The class to be mapped. - - local_table - The table to which the class is mapped, or None if this mapper - inherits from another mapper using concrete table inheritance. - - always_refresh - If True, all query operations for this mapped class will overwrite all - data within object instances that already exist within the session, - erasing any in-memory changes with whatever information was loaded - from the database. Usage of this flag is highly discouraged; as an - alternative, see the method `populate_existing()` on - :class:`~sqlalchemy.orm.query.Query`. - - allow_null_pks - This flag is deprecated - this is stated as allow_partial_pks - which defaults to True. - - allow_partial_pks - Defaults to True. Indicates that a composite primary key with - some NULL values should be considered as possibly existing - within the database. This affects whether a mapper will assign - an incoming row to an existing identity, as well as if - session.merge() will check the database first for a particular - primary key value. A "partial primary key" can occur if one - has mapped to an OUTER JOIN, for example. - - batch - Indicates that save operations of multiple entities can be batched - together for efficiency. setting to False indicates that an instance - will be fully saved before saving the next instance, which includes - inserting/updating all table rows corresponding to the entity as well - as calling all ``MapperExtension`` methods corresponding to the save - operation. - - column_prefix - A string which will be prepended to the `key` name of all Columns when - creating column-based properties from the given Table. Does not - affect explicitly specified column-based properties - - concrete - If True, indicates this mapper should use concrete table inheritance - with its parent mapper. - - extension - A :class:`~sqlalchemy.orm.MapperExtension` instance or list of - ``MapperExtension`` instances which will be applied to all - operations by this ``Mapper``. - - inherits - Another ``Mapper`` for which this ``Mapper`` will have an inheritance - relationship with. - - inherit_condition - For joined table inheritance, a SQL expression (constructed - ``ClauseElement``) which will define how the two tables are joined; - defaults to a natural join between the two tables. - - inherit_foreign_keys - when inherit_condition is used and the condition contains no - ForeignKey columns, specify the "foreign" columns of the join - condition in this list. else leave as None. - - order_by - A single ``Column`` or list of ``Columns`` for which - selection operations should use as the default ordering for entities. - Defaults to the OID/ROWID of the table if any, or the first primary - key column of the table. - - non_primary - Construct a ``Mapper`` that will define only the selection of - instances, not their persistence. Any number of non_primary mappers - may be created for a particular class. - - passive_updates - Indicates UPDATE behavior of foreign keys when a primary key changes - on a joined-table inheritance or other joined table mapping. - - When True, it is assumed that ON UPDATE CASCADE is configured on - the foreign key in the database, and that the database will - handle propagation of an UPDATE from a source column to - dependent rows. Note that with databases which enforce - referential integrity (i.e. PostgreSQL, MySQL with InnoDB tables), - ON UPDATE CASCADE is required for this operation. The - relation() will update the value of the attribute on related - items which are locally present in the session during a flush. - - When False, it is assumed that the database does not enforce - referential integrity and will not be issuing its own CASCADE - operation for an update. The relation() will issue the - appropriate UPDATE statements to the database in response to the - change of a referenced key, and items locally present in the - session during a flush will also be refreshed. - - This flag should probably be set to False if primary key changes - are expected and the database in use doesn't support CASCADE - (i.e. SQLite, MySQL MyISAM tables). - - Also see the passive_updates flag on ``relation()``. - - A future SQLAlchemy release will provide a "detect" feature for - this flag. - - polymorphic_on - Used with mappers in an inheritance relationship, a ``Column`` which - will identify the class/mapper combination to be used with a - particular row. Requires the ``polymorphic_identity`` value to be set - for all mappers in the inheritance hierarchy. The column specified by - ``polymorphic_on`` is usually a column that resides directly within - the base mapper's mapped table; alternatively, it may be a column that - is only present within the portion of the - ``with_polymorphic`` argument. - - _polymorphic_map - Used internally to propagate the full map of polymorphic identifiers - to surrogate mappers. - - polymorphic_identity - A value which will be stored in the Column denoted by polymorphic_on, - corresponding to the *class identity* of this mapper. - - properties - A dictionary mapping the string names of object attributes to - ``MapperProperty`` instances, which define the persistence behavior of - that attribute. Note that the columns in the mapped table are - automatically converted into ``ColumnProperty`` instances based on the - `key` property of each ``Column`` (although they can be overridden - using this dictionary). - - include_properties - An inclusive list of properties to map. Columns present in the mapped - table but not present in this list will not be automatically converted - into properties. - - exclude_properties - A list of properties not to map. Columns present in the mapped table - and present in this list will not be automatically converted into - properties. Note that neither this option nor include_properties will - allow an end-run around Python inheritance. If mapped class ``B`` - inherits from mapped class ``A``, no combination of includes or - excludes will allow ``B`` to have fewer properties than its - superclass, ``A``. - - primary_key - A list of ``Column`` objects which define the *primary key* to be used - against this mapper's selectable unit. This is normally simply the - primary key of the `local_table`, but can be overridden here. - - with_polymorphic - A tuple in the form ``(, )`` indicating the - default style of "polymorphic" loading, that is, which tables are - queried at once. is any single or list of mappers and/or - classes indicating the inherited classes that should be loaded at - once. The special value ``'*'`` may be used to indicate all descending - classes should be loaded immediately. The second tuple argument - indicates a selectable that will be used to query for - multiple classes. Normally, it is left as None, in which case this - mapper will form an outer join from the base mapper's table to that of - all desired sub-mappers. When specified, it provides the selectable - to be used for polymorphic loading. When with_polymorphic includes - mappers which load from a "concrete" inheriting table, the - argument is required, since it usually requires more - complex UNION queries. - - version_id_col - A ``Column`` which must have an integer type that will be used to keep - a running *version id* of mapped entities in the database. this is - used during save operations to ensure that no other thread or process - has updated the instance during the lifetime of the entity, else a - ``ConcurrentModificationError`` exception is thrown. - + :param class\_: The class to be mapped. + + :param local_table: The table to which the class is mapped, or None if this mapper + inherits from another mapper using concrete table inheritance. + + :param always_refresh: If True, all query operations for this mapped class will overwrite all + data within object instances that already exist within the session, + erasing any in-memory changes with whatever information was loaded + from the database. Usage of this flag is highly discouraged; as an + alternative, see the method `populate_existing()` on + :class:`~sqlalchemy.orm.query.Query`. + + :param allow_null_pks: This flag is deprecated - this is stated as allow_partial_pks + which defaults to True. + + :param allow_partial_pks: Defaults to True. Indicates that a composite primary key with + some NULL values should be considered as possibly existing + within the database. This affects whether a mapper will assign + an incoming row to an existing identity, as well as if + session.merge() will check the database first for a particular + primary key value. A "partial primary key" can occur if one + has mapped to an OUTER JOIN, for example. + + :param batch: Indicates that save operations of multiple entities can be batched + together for efficiency. setting to False indicates that an instance + will be fully saved before saving the next instance, which includes + inserting/updating all table rows corresponding to the entity as well + as calling all ``MapperExtension`` methods corresponding to the save + operation. + + :param column_prefix: A string which will be prepended to the `key` name of all Columns when + creating column-based properties from the given Table. Does not + affect explicitly specified column-based properties + + :param concrete: If True, indicates this mapper should use concrete table inheritance + with its parent mapper. + + :param exclude_properties: A list of properties not to map. Columns present in the mapped table + and present in this list will not be automatically converted into + properties. Note that neither this option nor include_properties will + allow an end-run around Python inheritance. If mapped class ``B`` + inherits from mapped class ``A``, no combination of includes or + excludes will allow ``B`` to have fewer properties than its + superclass, ``A``. + + + :param extension: A :class:`~sqlalchemy.orm.interfaces.MapperExtension` instance or list of + :class:`~sqlalchemy.orm.interfaces.MapperExtension` instances which will be applied to all + operations by this :class:`~sqlalchemy.orm.mapper.Mapper`. + + :param include_properties: An inclusive list of properties to map. Columns present in the mapped + table but not present in this list will not be automatically converted + into properties. + + :param inherits: Another :class:`~sqlalchemy.orm.Mapper` for which + this :class:`~sqlalchemy.orm.Mapper` will have an inheritance + relationship with. + + + :param inherit_condition: For joined table inheritance, a SQL expression (constructed + ``ClauseElement``) which will define how the two tables are joined; + defaults to a natural join between the two tables. + + :param inherit_foreign_keys: When inherit_condition is used and the condition contains no + ForeignKey columns, specify the "foreign" columns of the join + condition in this list. else leave as None. + + :param non_primary: Construct a ``Mapper`` that will define only the selection of + instances, not their persistence. Any number of non_primary mappers + may be created for a particular class. + + :param order_by: A single ``Column`` or list of ``Columns`` for which + selection operations should use as the default ordering for entities. + Defaults to the OID/ROWID of the table if any, or the first primary + key column of the table. + + :param passive_updates: Indicates UPDATE behavior of foreign keys when a primary key changes + on a joined-table inheritance or other joined table mapping. + + When True, it is assumed that ON UPDATE CASCADE is configured on + the foreign key in the database, and that the database will + handle propagation of an UPDATE from a source column to + dependent rows. Note that with databases which enforce + referential integrity (i.e. PostgreSQL, MySQL with InnoDB tables), + ON UPDATE CASCADE is required for this operation. The + relation() will update the value of the attribute on related + items which are locally present in the session during a flush. + + When False, it is assumed that the database does not enforce + referential integrity and will not be issuing its own CASCADE + operation for an update. The relation() will issue the + appropriate UPDATE statements to the database in response to the + change of a referenced key, and items locally present in the + session during a flush will also be refreshed. + + This flag should probably be set to False if primary key changes + are expected and the database in use doesn't support CASCADE + (i.e. SQLite, MySQL MyISAM tables). + + Also see the passive_updates flag on :func:`relation()`. + + A future SQLAlchemy release will provide a "detect" feature for + this flag. + + :param polymorphic_on: Used with mappers in an inheritance relationship, a ``Column`` which + will identify the class/mapper combination to be used with a + particular row. Requires the ``polymorphic_identity`` value to be set + for all mappers in the inheritance hierarchy. The column specified by + ``polymorphic_on`` is usually a column that resides directly within + the base mapper's mapped table; alternatively, it may be a column that + is only present within the portion of the + ``with_polymorphic`` argument. + + :param polymorphic_identity: A value which will be stored in the Column denoted by polymorphic_on, + corresponding to the *class identity* of this mapper. + + :param properties: A dictionary mapping the string names of object attributes to + ``MapperProperty`` instances, which define the persistence behavior of + that attribute. Note that the columns in the mapped table are + automatically converted into ``ColumnProperty`` instances based on the + `key` property of each ``Column`` (although they can be overridden + using this dictionary). + + :param primary_key: A list of ``Column`` objects which define the *primary key* to be used + against this mapper's selectable unit. This is normally simply the + primary key of the `local_table`, but can be overridden here. + + :param version_id_col: A ``Column`` which must have an integer type that will be used to keep + a running *version id* of mapped entities in the database. this is + used during save operations to ensure that no other thread or process + has updated the instance during the lifetime of the entity, else a + ``ConcurrentModificationError`` exception is thrown. + + :param version_id_generator: A callable which defines the algorithm used to generate new version + ids. Defaults to an integer generator. Can be replaced with one that + generates timestamps, uuids, etc. e.g.:: + + import uuid + + mapper(Cls, table, + version_id_col=table.c.version_uuid, + version_id_generator=lambda version:uuid.uuid4().hex + ) + + The callable receives the current version identifier as its + single argument. + + :param with_polymorphic: A tuple in the form ``(, )`` indicating the + default style of "polymorphic" loading, that is, which tables are + queried at once. is any single or list of mappers and/or + classes indicating the inherited classes that should be loaded at + once. The special value ``'*'`` may be used to indicate all descending + classes should be loaded immediately. The second tuple argument + indicates a selectable that will be used to query for + multiple classes. Normally, it is left as None, in which case this + mapper will form an outer join from the base mapper's table to that of + all desired sub-mappers. When specified, it provides the selectable + to be used for polymorphic loading. When with_polymorphic includes + mappers which load from a "concrete" inheriting table, the + argument is required, since it usually requires more + complex UNION queries. + + """ return Mapper(class_, local_table, *args, **params) diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index d868892958..30b6dd070a 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -84,6 +84,7 @@ class Mapper(object): order_by = False, always_refresh = False, version_id_col = None, + version_id_generator = None, polymorphic_on=None, _polymorphic_map=None, polymorphic_identity=None, @@ -118,6 +119,7 @@ class Mapper(object): self.always_refresh = always_refresh self.version_id_col = version_id_col + self.version_id_generator = version_id_generator or (lambda x:(x or 0) + 1) self.concrete = concrete self.single = False self.inherits = inherits @@ -252,6 +254,7 @@ class Mapper(object): if self.version_id_col is None: self.version_id_col = self.inherits.version_id_col + self.version_id_generator = self.inherits.version_id_generator for mapper in self.iterate_to_root(): util.reset_memoized(mapper, '_equivalent_columns') @@ -1303,7 +1306,7 @@ class Mapper(object): if 'before_update' in mapper.extension: mapper.extension.before_update(mapper, connection, state.obj()) - row_switches = set() + row_switches = {} if not postupdate: for state, mapper, connection, has_identity, instance_key in tups: # detect if we have a "pending" instance (i.e. has no instance_key attached to it), @@ -1324,7 +1327,7 @@ class Mapper(object): # remove the "delete" flag from the existing element uowtransaction.set_row_switch(existing) - row_switches.add(state) + row_switches[state] = existing table_to_mapper = self._sorted_tables @@ -1347,7 +1350,7 @@ class Mapper(object): if isinsert: for col in mapper._cols_by_table[table]: if col is mapper.version_id_col: - params[col.key] = 1 + params[col.key] = mapper.version_id_generator(None) elif mapper.polymorphic_on is not None and \ mapper.polymorphic_on.shares_lineage(col): value = mapper.polymorphic_identity @@ -1372,8 +1375,8 @@ class Mapper(object): else: for col in mapper._cols_by_table[table]: if col is mapper.version_id_col: - params[col._label] = mapper._get_state_attr_by_column(state, col) - params[col.key] = params[col._label] + 1 + params[col._label] = mapper._get_state_attr_by_column(row_switches.get(state, state), col) + params[col.key] = mapper.version_id_generator(params[col._label]) for prop in mapper._columntoproperty.itervalues(): history = attributes.get_state_history(state, prop.key, passive=True) if history.added: diff --git a/test/orm/test_relationships.py b/test/orm/test_relationships.py index 5223c692cc..1d2869291d 100644 --- a/test/orm/test_relationships.py +++ b/test/orm/test_relationships.py @@ -596,12 +596,12 @@ class RelationToUniqueTest(_base.MappedTest): def define_tables(cls, metadata): Table("table_a", metadata, Column("id", Integer, primary_key=True, test_needs_autoincrement=True), - Column("ident", String, nullable=False, unique=True), + Column("ident", String(10), nullable=False, unique=True), ) Table("table_b", metadata, Column("id", Integer, primary_key=True, test_needs_autoincrement=True), - Column("a_ident", String, ForeignKey('table_a.ident'), nullable=False), + Column("a_ident", String(10), ForeignKey('table_a.ident'), nullable=False), ) @classmethod diff --git a/test/orm/test_unitofwork.py b/test/orm/test_unitofwork.py index b961e2d25a..e1150a6205 100644 --- a/test/orm/test_unitofwork.py +++ b/test/orm/test_unitofwork.py @@ -52,141 +52,6 @@ class HistoryTest(_fixtures.FixtureTest): assert u.addresses[0].user == u session.close() - -class VersioningTest(_base.MappedTest): - @classmethod - def define_tables(cls, metadata): - Table('version_table', metadata, - Column('id', Integer, primary_key=True, - test_needs_autoincrement=True), - Column('version_id', Integer, nullable=False), - Column('value', String(40), nullable=False)) - - @classmethod - def setup_classes(cls): - class Foo(_base.ComparableEntity): - pass - - @engines.close_open_connections - @testing.resolve_artifact_names - def test_notsane_warning(self): - # clear the warning module's ignores to force the SAWarning this - # test relies on to be emitted (it may have already been ignored - # forever by other VersioningTests) - try: - del __warningregistry__ - except NameError: - pass - - save = testing.db.dialect.supports_sane_rowcount - testing.db.dialect.supports_sane_rowcount = False - try: - mapper(Foo, version_table, - version_id_col=version_table.c.version_id) - - s1 = create_session(autocommit=False) - f1 = Foo(value='f1') - f2 = Foo(value='f2') - s1.add_all((f1, f2)) - s1.commit() - - f1.value='f1rev2' - assert_raises(sa.exc.SAWarning, s1.commit) - finally: - testing.db.dialect.supports_sane_rowcount = save - - @testing.emits_warning(r'.*does not support updated rowcount') - @engines.close_open_connections - @testing.resolve_artifact_names - def test_basic(self): - mapper(Foo, version_table, - version_id_col=version_table.c.version_id) - - s1 = create_session(autocommit=False) - f1 = Foo(value='f1') - f2 = Foo(value='f2') - s1.add_all((f1, f2)) - s1.commit() - - f1.value='f1rev2' - s1.commit() - - s2 = create_session(autocommit=False) - f1_s = s2.query(Foo).get(f1.id) - f1_s.value='f1rev3' - s2.commit() - - f1.value='f1rev3mine' - - # Only dialects with a sane rowcount can detect the - # ConcurrentModificationError - if testing.db.dialect.supports_sane_rowcount: - assert_raises(sa.orm.exc.ConcurrentModificationError, s1.commit) - s1.rollback() - else: - s1.commit() - - # new in 0.5 ! dont need to close the session - f1 = s1.query(Foo).get(f1.id) - f2 = s1.query(Foo).get(f2.id) - - f1_s.value='f1rev4' - s2.commit() - - s1.delete(f1) - s1.delete(f2) - - if testing.db.dialect.supports_sane_multi_rowcount: - assert_raises(sa.orm.exc.ConcurrentModificationError, s1.commit) - else: - s1.commit() - - @testing.emits_warning(r'.*does not support updated rowcount') - @engines.close_open_connections - @testing.resolve_artifact_names - def test_versioncheck(self): - """query.with_lockmode performs a 'version check' on an already loaded instance""" - - s1 = create_session(autocommit=False) - - mapper(Foo, version_table, version_id_col=version_table.c.version_id) - f1s1 = Foo(value='f1 value') - s1.add(f1s1) - s1.commit() - - s2 = create_session(autocommit=False) - f1s2 = s2.query(Foo).get(f1s1.id) - f1s2.value='f1 new value' - s2.commit() - - # load, version is wrong - assert_raises(sa.orm.exc.ConcurrentModificationError, s1.query(Foo).with_lockmode('read').get, f1s1.id) - - # reload it - s1.query(Foo).populate_existing().get(f1s1.id) - # now assert version OK - s1.query(Foo).with_lockmode('read').get(f1s1.id) - - # assert brand new load is OK too - s1.close() - s1.query(Foo).with_lockmode('read').get(f1s1.id) - - @testing.emits_warning(r'.*does not support updated rowcount') - @engines.close_open_connections - @testing.resolve_artifact_names - def test_noversioncheck(self): - """test query.with_lockmode works when the mapper has no version id col""" - s1 = create_session(autocommit=False) - mapper(Foo, version_table) - f1s1 = Foo(value="foo", version_id=0) - s1.add(f1s1) - s1.commit() - - s2 = create_session(autocommit=False) - f1s2 = s2.query(Foo).with_lockmode('read').get(f1s1.id) - assert f1s2.id == f1s1.id - assert f1s2.value == f1s1.value - class UnicodeTest(_base.MappedTest): __requires__ = ('unicode_connections',) diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py new file mode 100644 index 0000000000..e97a3ad985 --- /dev/null +++ b/test/orm/test_versioning.py @@ -0,0 +1,307 @@ +import sqlalchemy as sa +from sqlalchemy.test import engines, testing +from sqlalchemy import Integer, String, ForeignKey, literal_column, orm +from sqlalchemy.test.schema import Table, Column +from sqlalchemy.orm import mapper, relation, create_session, column_property, sessionmaker +from sqlalchemy.test.testing import eq_, ne_, assert_raises, assert_raises_message +from test.orm import _base, _fixtures +from test.engine import _base as engine_base + + +_uuids =['1fc614acbb904742a2990f86af6ded95', '23e253786f4d491b9f9d6189dc33de9b', 'fc44910db37e43fd93e9ec8165b885cf', + '0187a1832b4249e6b48911821d86de58', '778af6ea2fb74a009d8d2f5abe5dc29a', '51a6ce031aff47e4b5f2895c4161f120', + '7434097cd319401fb9f15fa443ccbbbb', '9bc548a8128e4a85ac18060bc3f4b7d3', '59548715e3c440b7bcb96417d06f7930', + 'd7647c7004734de196885ca2bd73adf8', '70cef121d3ff48d39906b6d1ac77f41a', 'ee37a8a6430c466aa322b8a215a0dd70', + '782a5f04b4364a53a6fce762f48921c1', 'bef510f2420f4476a7629013ead237f5'] + +def make_uuid(): + """generate uuids even on Python 2.4 which has no 'uuid'""" + return _uuids.pop(0) + +class VersioningTest(_base.MappedTest): + @classmethod + def define_tables(cls, metadata): + Table('version_table', metadata, + Column('id', Integer, primary_key=True, + test_needs_autoincrement=True), + Column('version_id', Integer, nullable=False), + Column('value', String(40), nullable=False)) + + @classmethod + def setup_classes(cls): + class Foo(_base.ComparableEntity): + pass + + @engines.close_open_connections + @testing.resolve_artifact_names + def test_notsane_warning(self): + # clear the warning module's ignores to force the SAWarning this + # test relies on to be emitted (it may have already been ignored + # forever by other VersioningTests) + try: + del __warningregistry__ + except NameError: + pass + + save = testing.db.dialect.supports_sane_rowcount + testing.db.dialect.supports_sane_rowcount = False + try: + mapper(Foo, version_table, + version_id_col=version_table.c.version_id) + + s1 = create_session(autocommit=False) + f1 = Foo(value='f1') + f2 = Foo(value='f2') + s1.add_all((f1, f2)) + s1.commit() + + f1.value='f1rev2' + assert_raises(sa.exc.SAWarning, s1.commit) + finally: + testing.db.dialect.supports_sane_rowcount = save + + @testing.emits_warning(r'.*does not support updated rowcount') + @engines.close_open_connections + @testing.resolve_artifact_names + def test_basic(self): + mapper(Foo, version_table, + version_id_col=version_table.c.version_id) + + s1 = create_session(autocommit=False) + f1 = Foo(value='f1') + f2 = Foo(value='f2') + s1.add_all((f1, f2)) + s1.commit() + + f1.value='f1rev2' + s1.commit() + + s2 = create_session(autocommit=False) + f1_s = s2.query(Foo).get(f1.id) + f1_s.value='f1rev3' + s2.commit() + + f1.value='f1rev3mine' + + # Only dialects with a sane rowcount can detect the + # ConcurrentModificationError + if testing.db.dialect.supports_sane_rowcount: + assert_raises(sa.orm.exc.ConcurrentModificationError, s1.commit) + s1.rollback() + else: + s1.commit() + + # new in 0.5 ! dont need to close the session + f1 = s1.query(Foo).get(f1.id) + f2 = s1.query(Foo).get(f2.id) + + f1_s.value='f1rev4' + s2.commit() + + s1.delete(f1) + s1.delete(f2) + + if testing.db.dialect.supports_sane_multi_rowcount: + assert_raises(sa.orm.exc.ConcurrentModificationError, s1.commit) + else: + s1.commit() + + @testing.emits_warning(r'.*does not support updated rowcount') + @engines.close_open_connections + @testing.resolve_artifact_names + def test_versioncheck(self): + """query.with_lockmode performs a 'version check' on an already loaded instance""" + + s1 = create_session(autocommit=False) + + mapper(Foo, version_table, version_id_col=version_table.c.version_id) + f1s1 = Foo(value='f1 value') + s1.add(f1s1) + s1.commit() + + s2 = create_session(autocommit=False) + f1s2 = s2.query(Foo).get(f1s1.id) + f1s2.value='f1 new value' + s2.commit() + + # load, version is wrong + assert_raises(sa.orm.exc.ConcurrentModificationError, s1.query(Foo).with_lockmode('read').get, f1s1.id) + + # reload it + s1.query(Foo).populate_existing().get(f1s1.id) + # now assert version OK + s1.query(Foo).with_lockmode('read').get(f1s1.id) + + # assert brand new load is OK too + s1.close() + s1.query(Foo).with_lockmode('read').get(f1s1.id) + + @testing.emits_warning(r'.*does not support updated rowcount') + @engines.close_open_connections + @testing.resolve_artifact_names + def test_noversioncheck(self): + """test query.with_lockmode works when the mapper has no version id col""" + s1 = create_session(autocommit=False) + mapper(Foo, version_table) + f1s1 = Foo(value="foo", version_id=0) + s1.add(f1s1) + s1.commit() + + s2 = create_session(autocommit=False) + f1s2 = s2.query(Foo).with_lockmode('read').get(f1s1.id) + assert f1s2.id == f1s1.id + assert f1s2.value == f1s1.value + + + +class RowSwitchTest(_base.MappedTest): + @classmethod + def define_tables(cls, metadata): + Table('p', metadata, + Column('id', String(10), primary_key=True), + Column('version_id', Integer, default=1, nullable=False), + Column('data', String(50)) + ) + Table('c', metadata, + Column('id', String(10), ForeignKey('p.id'), primary_key=True), + Column('version_id', Integer, default=1, nullable=False), + Column('data', String(50)) + ) + + @classmethod + def setup_classes(cls): + class P(_base.ComparableEntity): + pass + class C(_base.ComparableEntity): + pass + + @classmethod + @testing.resolve_artifact_names + def setup_mappers(cls): + mapper(P, p, version_id_col=p.c.version_id, + properties={ + 'c':relation(C, uselist=False, cascade='all, delete-orphan') + }) + mapper(C, c, version_id_col=c.c.version_id) + + @testing.resolve_artifact_names + def test_row_switch(self): + session = sessionmaker()() + session.add(P(id='P1', data='P version 1')) + session.commit() + session.close() + + p = session.query(P).first() + session.delete(p) + session.add(P(id='P1', data="really a row-switch")) + session.commit() + + @testing.resolve_artifact_names + def test_child_row_switch(self): + assert P.c.property.strategy.use_get + + session = sessionmaker()() + session.add(P(id=1, data='P version 1')) + session.commit() + session.close() + + p = session.query(P).first() + p.c = C(data='child version 1') + session.commit() + + p = session.query(P).first() + p.c = C(data='child row-switch') + session.commit() + +class AlternateGeneratorTest(_base.MappedTest): + @classmethod + def define_tables(cls, metadata): + Table('p', metadata, + Column('id', String(10), primary_key=True), + Column('version_id', String(32), nullable=False), + Column('data', String(50)) + ) + Table('c', metadata, + Column('id', String(10), ForeignKey('p.id'), primary_key=True), + Column('version_id', String(32), nullable=False), + Column('data', String(50)) + ) + + @classmethod + def setup_classes(cls): + class P(_base.ComparableEntity): + pass + class C(_base.ComparableEntity): + pass + + @classmethod + @testing.resolve_artifact_names + def setup_mappers(cls): + mapper(P, p, version_id_col=p.c.version_id, + version_id_generator=lambda x:make_uuid(), + properties={ + 'c':relation(C, uselist=False, cascade='all, delete-orphan') + }) + mapper(C, c, version_id_col=c.c.version_id, + version_id_generator=lambda x:make_uuid(), + ) + + @testing.resolve_artifact_names + def test_row_switch(self): + session = sessionmaker()() + session.add(P(id='P1', data='P version 1')) + session.commit() + session.close() + + p = session.query(P).first() + session.delete(p) + session.add(P(id='P1', data="really a row-switch")) + session.commit() + + @testing.resolve_artifact_names + def test_child_row_switch_one(self): + assert P.c.property.strategy.use_get + + session = sessionmaker()() + session.add(P(id=1, data='P version 1')) + session.commit() + session.close() + + p = session.query(P).first() + p.c = C(data='child version 1') + session.commit() + + p = session.query(P).first() + p.c = C(data='child row-switch') + session.commit() + + @testing.resolve_artifact_names + def test_child_row_switch_two(self): + Session = sessionmaker() + + sess1 = Session() + sess1.add(P(id=1, data='P version 1')) + sess1.commit() + sess1.close() + + p1 = sess1.query(P).first() + + sess2 = Session() + p2 = sess2.query(P).first() + + sess1.delete(p1) + sess1.commit() + + sess1.add(P(id='P1', data='P version 2')) + sess1.commit() + + p2.data = 'P overwritten by concurrent tx' + assert_raises( + orm.exc.ConcurrentModificationError, + sess2.commit + ) + + + + + \ No newline at end of file