Skip to content

Loading…

MSSQL optimistic locking #731

Closed
wants to merge 3 commits into from

2 participants

@pinx

Thanks for all the help. You are amazing with Ruby. I learned a lot.
The changes speak for themselves (you wrote most of it...)

@jeremyevans
Owner

This looks OK. Before this can go in, you need to write adapter specs for it (testing it on a real database). Also, your plugin specs are very limited. In those you should be testing for specific SQL used, as well as testing what happens when you set the :lock_column option. Also, you shouldn't be changing the logger in the adapter test.

@jeremyevans
Owner

I'm taking another look at this now and I've realized there are some additional issues with it (you put the adapter spec in the extension spec and don't have real extension specs for it). Plus the patch wasn't rebased against master. I'll make the necessary changes locally and try to merge this later today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 13, 2013
  1. MS SQL optimistic locking with timestamp/rowversion column

    marcelp@bottomline-group.com committed
Commits on Nov 5, 2013
  1. MS SQL optimistic locking with timestamp/rowversion column

    marcelp@bottomline-group.com committed
  2. MS SQL optimistic locking with timestamp/rowversion column

    marcelp@bottomline-group.com committed
View
2 lib/sequel/adapters/shared/mssql.rb
@@ -332,6 +332,8 @@ def schema_column_type(db_type)
:boolean
when /\A(?:(?:small)?money)\z/io
:decimal
+ when /\A(timestamp|rowversion)\z/io
+ :blob
else
super
end
View
91 lib/sequel/plugins/mssql_optimistic_locking.rb
@@ -0,0 +1,91 @@
+module Sequel
+ module Plugins
+ # This plugin implements MS SQL optimistic locking mechanism
+ # to ensure that concurrent updates do not override changes. This is
+ # best implemented by a code example:
+ #
+ # class Person < Sequel::Model
+ # plugin :mssql_optimistic_locking
+ # end
+ # p1 = Person[1]
+ # p2 = Person[1]
+ # p1.update(:name=>'Jim') # works
+ # p2.update(:name=>'Bob') # raises Sequel::Plugins::MssqlOptimisticLocking::Error
+ #
+ # In order for this plugin to work, you need to make sure that the database
+ # table has a column of type timestamp or rowversion.
+ # Assign the name of this column to lock_column.
+ #
+ # This plugin relies on the instance_filters plugin.
+ module MssqlOptimisticLocking
+ # Exception class raised when trying to update or destroy a stale object.
+ Error = Sequel::NoExistingObject
+
+ # Load the instance_filters plugin into the model.
+ def self.apply(model, opts=OPTS)
+ model.plugin :instance_filters
+ end
+
+ # Set the lock_column to the :lock_column option, or :TimeStamp if
+ # that option is not given.
+ def self.configure(model, opts=OPTS)
+ model.lock_column = opts[:lock_column] || :timestamp
+ end
+
+ module ClassMethods
+ # The column holding the version of the lock
+ attr_accessor :lock_column
+
+ Plugins.inherited_instance_variables(self, :@lock_column=>nil)
+ end
+
+ module InstanceMethods
+ # Add the lock column instance filter to the object before destroying it.
+ def before_destroy
+ lock_column_instance_filter
+ super
+ end
+
+ # Add the lock column instance filter to the object before updating it.
+ def before_update
+ lock_column_instance_filter
+ super
+ end
+
+ private
+
+ # Add the lock column instance filter to the object.
+ def lock_column_instance_filter
+ lc = model.lock_column
+ instance_filter(lc=>Sequel.blob(send(lc)))
+ end
+
+ # Clear the instance filters when refreshing, so that attempting to
+ # refresh after a failed save removes the previous lock column filter
+ # (the new one will be added before updating).
+ def _refresh(ds)
+ clear_instance_filters
+ super
+ end
+
+ # Remove the lock column from the columns to update
+ # SQL Server assigns a value to the lock column
+ def _save_update_all_columns_hash
+ v = @values.dup
+ Array(primary_key).each{|x| v.delete(x) unless changed_columns.include?(x)}
+ v.delete(model.lock_column)
+ v
+ end
+
+ # Add an OUTPUT clause to fetch the updated timestamp
+ def _update_without_checking(columns)
+ ds = _update_dataset
+ lc = model.lock_column
+ rows = ds.clone(ds.send(:default_server_opts, :sql=>ds.output(nil, [Sequel.qualify(:inserted, lc)]).update_sql(columns))).all
+ values[lc] = rows.first[lc] unless rows.empty?
+ rows.length
+ end
+ end
+ end
+ end
+end
View
9 spec/adapters/mssql_spec.rb
@@ -5,7 +5,7 @@
def DB.sqls
(@sqls ||= [])
end
-logger = Object.new
+logger = Logger.new($stdout) #Object.new
def logger.method_missing(m, msg)
DB.sqls << msg
end
@@ -382,6 +382,13 @@ def @ds.server_version() 8000760 end
@ds.first(:xid=>h[:xid])[:value].should == 10
end
+ cspecify "should read blobs", :odbc do
+ blob = Sequel::SQL::Blob.new("01234")
+ @db[:test4].insert(:name => 'max varbinary test', :value => blob)
+ b = @db[:test4].where(:name => 'max varbinary test').get(:value)
+ b.should be_kind_of(Sequel::SQL::Blob)
+ end
+
cspecify "should allow large text and binary values", :odbc do
blob = Sequel::SQL::Blob.new("0" * (65*1024))
@db[:test4].insert(:name => 'max varbinary test', :value => blob)
View
38 spec/extensions/mssql_optimistic_locking_spec.rb
@@ -0,0 +1,38 @@
+SEQUEL_ADAPTER_TEST = :mssql
+
+require File.join(File.dirname(File.expand_path(__FILE__)), '../adapters/spec_helper.rb')
+
+describe "MSSSQL optimistic locking plugin" do
+ before do
+ @db = DB
+ @db.create_table! :items do
+ primary_key :id
+ String :name, :size => 20
+ column :timestamp, 'timestamp'
+ end
+ end
+ after do
+ @db.drop_table?(:items)
+ end
+
+ cspecify "timestamp should be filled by server", :odbc do
+ @c = Class.new(Sequel::Model(:items))
+ @c.plugin :mssql_optimistic_locking
+ @o = @c.create(name: 'test')
+ @o = @c.first
+ @o.timestamp.should_not eql nil
+ end
+
+ cspecify "create and update should work", :odbc do
+ @c = Class.new(Sequel::Model(:items))
+ @c.plugin :mssql_optimistic_locking
+ @o = @c.create(name: 'test')
+ @o = @c.first
+ @ts = @o.timestamp
+ @o.name = 'test2'
+ @o.save
+ @o.timestamp.should_not eql @ts
+ end
+
+
+end
Something went wrong with that request. Please try again.