Commits

Mike Bayer committed 5a68954

- 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.

Comments (0)

Files changed (7)

doc/build/changelog/changelog_09.rst

 
         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.
+        or other database-provided versioning features, or via an optional programmatic
+        value, by setting ``version_id_generator=False``.
+        When using a server-generated version identfier, the ORM will use RETURNING when
+        available to immediately
+        load the new version value, else it will emit a second SELECT.
 
     .. change::
         :tags: feature, orm

doc/build/changelog/migration_09.rst

 
 The versioning feature of the ORM (now also documented at :ref:`mapper_version_counter`)
 can now make use of server-side version counting schemes, such as those produced
-by triggers or database system columns.   By providing the value ``False``
-to the ``version_id_generator`` parameter, the ORM will fetch the version identifier
-from each row at the same time the INSERT or UPDATE is emitted.   It is strongly
+by triggers or database system columns, as well as conditional programmatic schemes outside
+of the version_id_counter function itself.  By providing the value ``False``
+to the ``version_id_generator`` parameter, the ORM will use the already-set version
+identifier, or alternatively fetch the version identifier
+from each row at the same time the INSERT or UPDATE is emitted.   When using a
+server-generated version identifier, it is strongly
 recommended that this feature be used only on a backend where RETURNING can also
 be used, else the additional SELECT statements will add significant performance
 overhead.   The example provided at :ref:`server_side_version_counter` illustrates

doc/build/orm/mapper_config.rst

 .. _server_side_version_counter:
 
 Server Side Version Counters
------------------------------
+----------------------------
 
 The ``version_id_generator`` can also be configured to rely upon a value
 that is generated by the database.  In this case, the database would need
 
     Support for server side version identifier tracking.
 
+Programmatic or Conditional Version Counters
+---------------------------------------------
+
+When ``version_id_generator`` is set to False, we can also programmatically
+(and conditionally) set the version identifier on our object in the same way
+we assign any other mapped attribute.  Such as if we used our UUID example, but
+set ``version_id_generator`` to ``False``, we can set the version identifier
+at our choosing::
+
+    import uuid
+
+    class User(Base):
+        __tablename__ = 'user'
+
+        id = Column(Integer, primary_key=True)
+        version_uuid = Column(String(32))
+        name = Column(String(50), nullable=False)
+
+        __mapper_args__ = {
+            'version_id_col':version_uuid,
+            'version_id_generator': False
+        }
+
+    u1 = User(name='u1', version_uuid=uuid.uuid4())
+
+    session.add(u1)
+
+    session.commit()
+
+    u1.name = 'u2'
+    u1.version_uuid = uuid.uuid4()
+
+    session.commit()
+
+We can update our ``User`` object without incrementing the version counter
+as well; the value of the counter will remain unchanged, and the UPDATE
+statement will still check against the previous value.  This may be useful
+for schemes where only certain classes of UPDATE are sensitive to concurrency
+issues::
+
+    # will leave version_uuid unchanged
+    u1.name = 'u3'
+    session.commit()
+
+.. versionadded:: 0.9.0
+
+    Support for programmatic and conditional version identifier tracking.
+
 
 Class Mapping API
 =================

lib/sqlalchemy/orm/mapper.py

               def generate_version(version):
                   return next_version
 
-          Alternatively, server-side versioning functions such as triggers
-          may be used as well, by specifying the value ``False``.
+          Alternatively, server-side versioning functions such as triggers,
+          or programmatic versioning schemes outside of the version id generator
+          may be used, by specifying the value ``False``.
           Please see :ref:`server_side_version_counter` for a discussion
           of important points when using this option.
 

lib/sqlalchemy/orm/persistence.py

         has_all_pks = True
         has_all_defaults = True
         for col in mapper._cols_by_table[table]:
-            if col is mapper.version_id_col:
-                if mapper.version_id_generator is not False:
-                    val = mapper.version_id_generator(None)
-                    params[col.key] = val
+            if col is mapper.version_id_col and \
+                mapper.version_id_generator is not False:
+                val = mapper.version_id_generator(None)
+                params[col.key] = val
             else:
                 # pull straight from the dict for
                 # pending objects
                         mapper._get_state_attr_by_column(
                                         state,
                                         state_dict, col)
+
             elif col in post_update_cols:
                 prop = mapper._columntoproperty[col]
                 history = attributes.get_state_history(

lib/sqlalchemy/orm/strategies.py

         coltype = self.columns[0].type
         # TODO: check all columns ?  check for foreign key as well?
         active_history = self.parent_property.active_history or \
-                            self.columns[0].primary_key
+                            self.columns[0].primary_key or \
+                            mapper.version_id_col in set(self.columns)
 
         _register_attribute(self, mapper, useobject=False,
             compare_function=coltype.compare_values,

test/orm/test_versioning.py

             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)
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.