sequel 5.70.0 → 5.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +134 -0
  3. data/README.rdoc +7 -5
  4. data/doc/dataset_basics.rdoc +1 -1
  5. data/doc/mass_assignment.rdoc +1 -1
  6. data/doc/migration.rdoc +15 -0
  7. data/doc/opening_databases.rdoc +6 -2
  8. data/doc/querying.rdoc +6 -1
  9. data/doc/release_notes/5.71.0.txt +21 -0
  10. data/doc/release_notes/5.72.0.txt +33 -0
  11. data/doc/release_notes/5.73.0.txt +66 -0
  12. data/doc/release_notes/5.74.0.txt +45 -0
  13. data/doc/release_notes/5.75.0.txt +35 -0
  14. data/doc/release_notes/5.76.0.txt +86 -0
  15. data/doc/release_notes/5.77.0.txt +63 -0
  16. data/doc/release_notes/5.78.0.txt +67 -0
  17. data/doc/release_notes/5.79.0.txt +28 -0
  18. data/doc/release_notes/5.80.0.txt +40 -0
  19. data/doc/schema_modification.rdoc +2 -2
  20. data/doc/testing.rdoc +4 -2
  21. data/lib/sequel/adapters/ibmdb.rb +1 -1
  22. data/lib/sequel/adapters/jdbc/h2.rb +3 -0
  23. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
  24. data/lib/sequel/adapters/jdbc/postgresql.rb +3 -0
  25. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
  26. data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
  27. data/lib/sequel/adapters/jdbc.rb +10 -6
  28. data/lib/sequel/adapters/mysql2.rb +2 -2
  29. data/lib/sequel/adapters/odbc/mssql.rb +1 -1
  30. data/lib/sequel/adapters/postgres.rb +6 -5
  31. data/lib/sequel/adapters/shared/db2.rb +12 -0
  32. data/lib/sequel/adapters/shared/mssql.rb +30 -2
  33. data/lib/sequel/adapters/shared/mysql.rb +68 -3
  34. data/lib/sequel/adapters/shared/oracle.rb +4 -6
  35. data/lib/sequel/adapters/shared/postgres.rb +116 -6
  36. data/lib/sequel/adapters/shared/sqlanywhere.rb +10 -4
  37. data/lib/sequel/adapters/shared/sqlite.rb +20 -3
  38. data/lib/sequel/adapters/sqlite.rb +42 -3
  39. data/lib/sequel/connection_pool.rb +4 -2
  40. data/lib/sequel/database/misc.rb +3 -2
  41. data/lib/sequel/database/schema_methods.rb +11 -4
  42. data/lib/sequel/database/transactions.rb +6 -0
  43. data/lib/sequel/dataset/actions.rb +8 -6
  44. data/lib/sequel/dataset/dataset_module.rb +1 -1
  45. data/lib/sequel/dataset/features.rb +10 -1
  46. data/lib/sequel/dataset/graph.rb +1 -0
  47. data/lib/sequel/dataset/query.rb +58 -9
  48. data/lib/sequel/dataset/sql.rb +47 -34
  49. data/lib/sequel/exceptions.rb +5 -0
  50. data/lib/sequel/extensions/any_not_empty.rb +2 -2
  51. data/lib/sequel/extensions/async_thread_pool.rb +7 -0
  52. data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
  53. data/lib/sequel/extensions/caller_logging.rb +2 -1
  54. data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
  55. data/lib/sequel/extensions/index_caching.rb +5 -1
  56. data/lib/sequel/extensions/migration.rb +64 -14
  57. data/lib/sequel/extensions/named_timezones.rb +1 -1
  58. data/lib/sequel/extensions/pg_array.rb +10 -0
  59. data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
  60. data/lib/sequel/extensions/pg_extended_date_support.rb +4 -4
  61. data/lib/sequel/extensions/pg_json_ops.rb +52 -0
  62. data/lib/sequel/extensions/pg_range.rb +2 -2
  63. data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
  64. data/lib/sequel/extensions/provenance.rb +108 -0
  65. data/lib/sequel/extensions/round_timestamps.rb +1 -1
  66. data/lib/sequel/extensions/schema_caching.rb +1 -1
  67. data/lib/sequel/extensions/sqlite_json_ops.rb +76 -18
  68. data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
  69. data/lib/sequel/model/associations.rb +9 -2
  70. data/lib/sequel/model/base.rb +26 -13
  71. data/lib/sequel/model/exceptions.rb +15 -3
  72. data/lib/sequel/plugins/column_encryption.rb +27 -6
  73. data/lib/sequel/plugins/defaults_setter.rb +16 -0
  74. data/lib/sequel/plugins/list.rb +5 -2
  75. data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
  76. data/lib/sequel/plugins/optimistic_locking.rb +9 -42
  77. data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
  78. data/lib/sequel/plugins/paged_operations.rb +181 -0
  79. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +5 -1
  80. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
  81. data/lib/sequel/plugins/rcte_tree.rb +7 -4
  82. data/lib/sequel/plugins/static_cache_cache.rb +5 -1
  83. data/lib/sequel/plugins/validation_helpers.rb +1 -1
  84. data/lib/sequel/version.rb +1 -1
  85. metadata +44 -3
@@ -0,0 +1,78 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The transaction_connection_validator extension automatically
4
+ # retries a transaction on a connection if an disconnect error
5
+ # is raised when sending the statement to begin a new
6
+ # transaction, as long as the user has not already checked out
7
+ # a connection. This is safe to do because no other queries
8
+ # have been issued on the connection, and no user-level code
9
+ # is run before retrying.
10
+ #
11
+ # This approach to connection validation can be significantly
12
+ # lower overhead than the connection_validator extension,
13
+ # though it does not handle all cases handled by the
14
+ # connection_validator extension. However, it performs the
15
+ # validation checks on every new transaction, so it will
16
+ # automatically handle disconnected connections in some cases
17
+ # where the connection_validator extension will not by default
18
+ # (as the connection_validator extension only checks
19
+ # connections if they have not been used in the last hour by
20
+ # default).
21
+ #
22
+ # Related module: Sequel::TransactionConnectionValidator
23
+
24
+ #
25
+ module Sequel
26
+ module TransactionConnectionValidator
27
+ class DisconnectRetry < DatabaseDisconnectError
28
+ # The connection that raised the disconnect error
29
+ attr_accessor :connection
30
+
31
+ # The underlying disconnect error, in case it needs to be reraised.
32
+ attr_accessor :database_error
33
+ end
34
+
35
+ # Rescue disconnect errors raised when beginning a new transaction. If there
36
+ # is a disconnnect error, it should be safe to retry the transaction using a
37
+ # new connection, as we haven't yielded control to the user yet.
38
+ def transaction(opts=OPTS)
39
+ super
40
+ rescue DisconnectRetry => e
41
+ if synchronize(opts[:server]){|conn| conn.equal?(e.connection)}
42
+ # If retrying would use the same connection, that means the
43
+ # connection was not removed from the pool, which means the caller has
44
+ # already checked out the connection, and retrying will not be successful.
45
+ # In this case, we can only reraise the exception.
46
+ raise e.database_error
47
+ end
48
+
49
+ num_retries ||= 0
50
+ num_retries += 1
51
+ retry if num_retries < 5
52
+
53
+ raise e.database_error
54
+ end
55
+
56
+ private
57
+
58
+ # Reraise disconnect errors as DisconnectRetry so they can be retried.
59
+ def begin_new_transaction(conn, opts)
60
+ super
61
+ rescue Sequel::DatabaseDisconnectError, *database_error_classes => e
62
+ if e.is_a?(Sequel::DatabaseDisconnectError) || disconnect_error?(e, OPTS)
63
+ exception = DisconnectRetry.new(e.message)
64
+ exception.set_backtrace([])
65
+ exception.connection = conn
66
+ unless e.is_a?(Sequel::DatabaseError)
67
+ e = Sequel.convert_exception_class(e, database_error_class(e, OPTS))
68
+ end
69
+ exception.database_error = e
70
+ raise exception
71
+ end
72
+
73
+ raise
74
+ end
75
+ end
76
+
77
+ Database.register_extension(:transaction_connection_validator, TransactionConnectionValidator)
78
+ end
@@ -3387,8 +3387,15 @@ module Sequel
3387
3387
  local_opts = ds.opts[:eager_graph][:local]
3388
3388
  limit_strategy = r.eager_graph_limit_strategy(local_opts[:limit_strategy])
3389
3389
 
3390
- if r[:conditions] && !Sequel.condition_specifier?(r[:conditions]) && !r[:orig_opts].has_key?(:graph_conditions) && !r[:orig_opts].has_key?(:graph_only_conditions) && !r.has_key?(:graph_block)
3391
- raise Error, "Cannot eager_graph association when :conditions specified and not a hash or an array of pairs. Specify :graph_conditions, :graph_only_conditions, or :graph_block for the association. Model: #{r[:model]}, association: #{r[:name]}"
3390
+ # SEQUEL6: remove and integrate the auto_restrict_eager_graph plugin
3391
+ if !r[:orig_opts].has_key?(:graph_conditions) && !r[:orig_opts].has_key?(:graph_only_conditions) && !r.has_key?(:graph_block) && !r[:allow_eager_graph]
3392
+ if r[:conditions] && !Sequel.condition_specifier?(r[:conditions])
3393
+ raise Error, "Cannot eager_graph association when :conditions specified and not a hash or an array of pairs. Specify :graph_conditions, :graph_only_conditions, or :graph_block for the association. Model: #{r[:model]}, association: #{r[:name]}"
3394
+ end
3395
+
3396
+ if r[:block] && !r[:graph_use_association_block]
3397
+ warn "eager_graph used for association when association given a block without graph options. The block is ignored in this case. This will result in an exception starting in Sequel 6. Model: #{r[:model]}, association: #{r[:name]}"
3398
+ end
3392
3399
  end
3393
3400
 
3394
3401
  ds = loader.call(:self=>ds, :table_alias=>assoc_table_alias, :implicit_qualifier=>(ta == ds.opts[:eager_graph][:master]) ? first_source : qualifier_from_alias_symbol(ta, first_source), :callback=>callback, :join_type=>join_type || local_opts[:join_type], :join_only=>local_opts[:join_only], :limit_strategy=>limit_strategy, :from_self_alias=>ds.opts[:eager_graph][:master])
@@ -17,7 +17,7 @@ module Sequel
17
17
  # natural_join, natural_left_join, natural_right_join, offset, order, order_append, order_by,
18
18
  # order_more, order_prepend, paged_each, qualify, reverse, reverse_order, right_join,
19
19
  # right_outer_join, select, select_all, select_append, select_group, select_hash,
20
- # select_hash_groups, select_map, select_more, select_order_map, server,
20
+ # select_hash_groups, select_map, select_more, select_order_map, select_prepend, server,
21
21
  # single_record, single_record!, single_value, single_value!, sum, to_hash, to_hash_groups,
22
22
  # truncate, unfiltered, ungraphed, ungrouped, union, unlimited, unordered, where, where_all,
23
23
  # where_each, where_single_value, with, with_recursive, with_sql
@@ -1244,18 +1244,21 @@ module Sequel
1244
1244
  @errors ||= errors_class.new
1245
1245
  end
1246
1246
 
1247
+ EXISTS_SELECT_ = SQL::AliasedExpression.new(1, :one)
1248
+ private_constant :EXISTS_SELECT_
1249
+
1247
1250
  # Returns true when current instance exists, false otherwise.
1248
1251
  # Generally an object that isn't new will exist unless it has
1249
1252
  # been deleted. Uses a database query to check for existence,
1250
1253
  # unless the model object is new, in which case this is always
1251
1254
  # false.
1252
1255
  #
1253
- # Artist[1].exists? # SELECT 1 FROM artists WHERE (id = 1)
1256
+ # Artist[1].exists? # SELECT 1 AS one FROM artists WHERE (id = 1)
1254
1257
  # # => true
1255
1258
  # Artist.new.exists?
1256
1259
  # # => false
1257
1260
  def exists?
1258
- new? ? false : !this.get(SQL::AliasedExpression.new(1, :one)).nil?
1261
+ new? ? false : !this.get(EXISTS_SELECT_).nil?
1259
1262
  end
1260
1263
 
1261
1264
  # Ignore the model's setter method cache when this instances extends a module, as the
@@ -1945,8 +1948,10 @@ module Sequel
1945
1948
  end
1946
1949
 
1947
1950
  # If transactions should be used, wrap the yield in a transaction block.
1948
- def checked_transaction(opts=OPTS)
1949
- use_transaction?(opts) ? db.transaction({:server=>this_server}.merge!(opts)){yield} : yield
1951
+ def checked_transaction(opts=OPTS, &block)
1952
+ h = {:server=>this_server}.merge!(opts)
1953
+ h[:skip_transaction] = true unless use_transaction?(opts)
1954
+ db.transaction(h, &block)
1950
1955
  end
1951
1956
 
1952
1957
  # Change the value of the column to given value, recording the change.
@@ -2031,19 +2036,20 @@ module Sequel
2031
2036
  meths = setter_methods(type)
2032
2037
  strict = strict_param_setting
2033
2038
  hash.each do |k,v|
2039
+ k = k.to_s
2034
2040
  m = "#{k}="
2035
2041
  if meths.include?(m)
2036
2042
  set_column_value(m, v)
2037
2043
  elsif strict
2038
2044
  # Avoid using respond_to? or creating symbols from user input
2039
2045
  if public_methods.map(&:to_s).include?(m)
2040
- if Array(model.primary_key).map(&:to_s).member?(k.to_s) && model.restrict_primary_key?
2041
- raise MassAssignmentRestriction, "#{k} is a restricted primary key"
2046
+ if Array(model.primary_key).map(&:to_s).member?(k) && model.restrict_primary_key?
2047
+ raise MassAssignmentRestriction.create("#{k} is a restricted primary key", self, k)
2042
2048
  else
2043
- raise MassAssignmentRestriction, "#{k} is a restricted column"
2049
+ raise MassAssignmentRestriction.create("#{k} is a restricted column", self, k)
2044
2050
  end
2045
2051
  else
2046
- raise MassAssignmentRestriction, "method #{m} doesn't exist"
2052
+ raise MassAssignmentRestriction.create("method #{m} doesn't exist", self, k)
2047
2053
  end
2048
2054
  end
2049
2055
  end
@@ -2147,8 +2153,9 @@ module Sequel
2147
2153
  # # DELETE FROM artists WHERE (id = 2)
2148
2154
  # # ...
2149
2155
  def destroy
2150
- pr = proc{all(&:destroy).length}
2151
- model.use_transactions ? @db.transaction(:server=>opts[:server], &pr) : pr.call
2156
+ @db.transaction(:server=>opts[:server], :skip_transaction=>model.use_transactions == false) do
2157
+ all(&:destroy).length
2158
+ end
2152
2159
  end
2153
2160
 
2154
2161
  # If there is no order already defined on this dataset, order it by
@@ -2228,11 +2235,17 @@ module Sequel
2228
2235
 
2229
2236
  private
2230
2237
 
2238
+ # Return the dataset ordered by the model's primary key. This should not
2239
+ # be used if the model does not have a primary key.
2240
+ def _force_primary_key_order
2241
+ cached_dataset(:_pk_order_ds){order(*model.primary_key)}
2242
+ end
2243
+
2231
2244
  # If the dataset is not already ordered, and the model has a primary key,
2232
2245
  # return a clone ordered by the primary key.
2233
2246
  def _primary_key_order
2234
- if @opts[:order].nil? && model && (pk = model.primary_key)
2235
- cached_dataset(:_pk_order_ds){order(*pk)}
2247
+ if @opts[:order].nil? && model && model.primary_key
2248
+ _force_primary_key_order
2236
2249
  end
2237
2250
  end
2238
2251
 
@@ -24,11 +24,23 @@ module Sequel
24
24
  UndefinedAssociation = Class.new(Error)
25
25
  ).name
26
26
 
27
- (
28
27
  # Raised when a mass assignment method is called in strict mode with either a restricted column
29
28
  # or a column without a setter method.
30
- MassAssignmentRestriction = Class.new(Error)
31
- ).name
29
+ class MassAssignmentRestriction < Error
30
+ # The Sequel::Model object related to this exception.
31
+ attr_reader :model
32
+
33
+ # The column related to this exception, as a string.
34
+ attr_reader :column
35
+
36
+ # Create an instance of this class with the model and column set.
37
+ def self.create(msg, model, column)
38
+ e = new("#{msg} for class #{model.class.inspect}")
39
+ e.instance_variable_set(:@model, model)
40
+ e.instance_variable_set(:@column, column)
41
+ e
42
+ end
43
+ end
32
44
 
33
45
  # Exception class raised when +raise_on_save_failure+ is set and validation fails
34
46
  class ValidationFailed < Error
@@ -31,7 +31,6 @@ rescue RuntimeError, OpenSSL::Cipher::CipherError
31
31
  # :nocov:
32
32
  end
33
33
 
34
- require 'base64'
35
34
  require 'securerandom'
36
35
 
37
36
  module Sequel
@@ -326,7 +325,7 @@ module Sequel
326
325
  # DB.alter_table(:ce_test) do
327
326
  # c = Sequel[:encrypted_column_name]
328
327
  # add_constraint(:enc_base64) do
329
- # octet_length(decode(regexp_replace(regexp_replace(c, '_', '/', 'g'), '-', '+', 'g'), 'base64')) >= 65}
328
+ # octet_length(decode(regexp_replace(regexp_replace(c, '_', '/', 'g'), '-', '+', 'g'), 'base64')) >= 65
330
329
  # end
331
330
  # end
332
331
  #
@@ -375,7 +374,7 @@ module Sequel
375
374
  # Decrypt using any supported format and any available key.
376
375
  def decrypt(data)
377
376
  begin
378
- data = Base64.urlsafe_decode64(data)
377
+ data = urlsafe_decode64(data)
379
378
  rescue ArgumentError
380
379
  raise Error, "Unable to decode encrypted column: invalid base64"
381
380
  end
@@ -448,7 +447,7 @@ module Sequel
448
447
  # The prefix string of columns for the given search type and the first configured encryption key.
449
448
  # Used to find values that do not use this prefix in order to perform reencryption.
450
449
  def current_key_prefix(search_type)
451
- Base64.urlsafe_encode64("#{search_type.chr}\0#{@key_id.chr}")
450
+ urlsafe_encode64("#{search_type.chr}\0#{@key_id.chr}")
452
451
  end
453
452
 
454
453
  # The prefix values to search for the given data (an array of strings), assuming the column uses
@@ -472,11 +471,33 @@ module Sequel
472
471
 
473
472
  private
474
473
 
474
+ if RUBY_VERSION >= '2.4'
475
+ def decode64(str)
476
+ str.unpack1("m0")
477
+ end
478
+ # :nocov:
479
+ else
480
+ def decode64(str)
481
+ str.unpack("m0")[0]
482
+ end
483
+ # :nocov:
484
+ end
485
+
486
+ def urlsafe_encode64(bin)
487
+ str = [bin].pack("m0")
488
+ str.tr!("+/", "-_")
489
+ str
490
+ end
491
+
492
+ def urlsafe_decode64(str)
493
+ decode64(str.tr("-_", "+/"))
494
+ end
495
+
475
496
  # An array of strings, one for each configured encryption key, to find encypted values matching
476
497
  # the given data and search format.
477
498
  def _search_prefixes(data, search_type)
478
499
  @key_map.map do |key_id, (key, _)|
479
- Base64.urlsafe_encode64(_search_prefix(data, search_type, key_id, key))
500
+ urlsafe_encode64(_search_prefix(data, search_type, key_id, key))
480
501
  end
481
502
  end
482
503
 
@@ -509,7 +530,7 @@ module Sequel
509
530
  cipher_text << cipher.update(data) if data_size > 0
510
531
  cipher_text << cipher.final
511
532
 
512
- Base64.urlsafe_encode64("#{prefix}#{random_data}#{cipher_iv}#{cipher.auth_tag}#{cipher_text}")
533
+ urlsafe_encode64("#{prefix}#{random_data}#{cipher_iv}#{cipher.auth_tag}#{cipher_text}")
513
534
  end
514
535
  end
515
536
 
@@ -1,5 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require 'delegate'
4
+
3
5
  module Sequel
4
6
  module Plugins
5
7
  # The defaults_setter plugin makes the column getter methods return the default
@@ -106,6 +108,20 @@ module Sequel
106
108
  lambda{Date.today}
107
109
  when Sequel::CURRENT_TIMESTAMP
108
110
  lambda{dataset.current_datetime}
111
+ when Hash, Array
112
+ v = Marshal.dump(v).freeze
113
+ lambda{Marshal.load(v)}
114
+ when Delegator
115
+ # DelegateClass returns an anonymous case, which cannot be marshalled, so marshal the
116
+ # underlying object and create a new instance of the class with the unmarshalled object.
117
+ klass = v.class
118
+ case o = v.__getobj__
119
+ when Hash, Array
120
+ v = Marshal.dump(o).freeze
121
+ lambda{klass.new(Marshal.load(v))}
122
+ else
123
+ v
124
+ end
109
125
  else
110
126
  v
111
127
  end
@@ -185,10 +185,13 @@ module Sequel
185
185
  end
186
186
 
187
187
  # Set the value of the position_field to the maximum value plus 1 unless the
188
- # position field already has a value.
188
+ # position field already has a value. If the list is empty, the position will
189
+ # be set to the model's +top_of_list+ value.
189
190
  def before_validation
190
191
  unless get_column_value(position_field)
191
- set_column_value("#{position_field}=", list_dataset.max(position_field).to_i+1)
192
+ current_max = list_dataset.max(position_field)
193
+ value = current_max.nil? ? model.top_of_list : current_max.to_i + 1
194
+ set_column_value("#{position_field}=", value)
192
195
  end
193
196
  super
194
197
  end
@@ -26,57 +26,27 @@ module Sequel
26
26
  module MssqlOptimisticLocking
27
27
  # Load the instance_filters plugin into the model.
28
28
  def self.apply(model, opts=OPTS)
29
- model.plugin :instance_filters
29
+ model.plugin(:optimistic_locking_base)
30
30
  end
31
31
 
32
- # Set the lock_column to the :lock_column option (default: :timestamp)
32
+ # Set the lock column
33
33
  def self.configure(model, opts=OPTS)
34
- model.lock_column = opts[:lock_column] || :timestamp
34
+ model.lock_column = opts[:lock_column] || model.lock_column || :timestamp
35
35
  end
36
-
37
- module ClassMethods
38
- # The timestamp/rowversion column containing the version for the current row.
39
- attr_accessor :lock_column
40
-
41
- Plugins.inherited_instance_variables(self, :@lock_column=>nil)
42
- end
43
-
36
+
44
37
  module InstanceMethods
45
- # Add the lock column instance filter to the object before destroying it.
46
- def before_destroy
47
- lock_column_instance_filter
48
- super
49
- end
50
-
51
- # Add the lock column instance filter to the object before updating it.
52
- def before_update
53
- lock_column_instance_filter
54
- super
55
- end
56
-
57
38
  private
58
39
 
59
- # Add the lock column instance filter to the object.
60
- def lock_column_instance_filter
61
- lc = model.lock_column
62
- instance_filter(lc=>Sequel.blob(get_column_value(lc)))
63
- end
64
-
65
- # Clear the instance filters when refreshing, so that attempting to
66
- # refresh after a failed save removes the previous lock column filter
67
- # (the new one will be added before updating).
68
- def _refresh(ds)
69
- clear_instance_filters
70
- super
40
+ # Make the instance filter value a blob.
41
+ def lock_column_instance_filter_value
42
+ Sequel.blob(super)
71
43
  end
72
44
 
73
45
  # Remove the lock column from the columns to update.
74
46
  # SQL Server automatically updates the lock column value, and does not like
75
47
  # it to be assigned.
76
48
  def _save_update_all_columns_hash
77
- v = @values.dup
78
- cc = changed_columns
79
- Array(primary_key).each{|x| v.delete(x) unless cc.include?(x)}
49
+ v = super
80
50
  v.delete(model.lock_column)
81
51
  v
82
52
  end
@@ -12,64 +12,31 @@ module Sequel
12
12
  # p1 = Person[1]
13
13
  # p2 = Person[1]
14
14
  # p1.update(name: 'Jim') # works
15
- # p2.update(name: 'Bob') # raises Sequel::Plugins::OptimisticLocking::Error
15
+ # p2.update(name: 'Bob') # raises Sequel::NoExistingObject
16
16
  #
17
17
  # In order for this plugin to work, you need to make sure that the database
18
- # table has a +lock_version+ column (or other column you name via the lock_column
19
- # class level accessor) that defaults to 0.
18
+ # table has a +lock_version+ column that defaults to 0. To change the column
19
+ # used, provide a +:lock_column+ option when loading the plugin:
20
+ #
21
+ # plugin :optimistic_locking, lock_column: :version
20
22
  #
21
23
  # This plugin relies on the instance_filters plugin.
22
24
  module OptimisticLocking
23
25
  # Exception class raised when trying to update or destroy a stale object.
24
26
  Error = Sequel::NoExistingObject
25
27
 
26
- # Load the instance_filters plugin into the model.
27
28
  def self.apply(model, opts=OPTS)
28
- model.plugin :instance_filters
29
+ model.plugin(:optimistic_locking_base)
29
30
  end
30
31
 
31
- # Set the lock_column to the :lock_column option, or :lock_version if
32
- # that option is not given.
32
+ # Set the lock column
33
33
  def self.configure(model, opts=OPTS)
34
- model.lock_column = opts[:lock_column] || :lock_version
34
+ model.lock_column = opts[:lock_column] || model.lock_column || :lock_version
35
35
  end
36
-
37
- module ClassMethods
38
- # The column holding the version of the lock
39
- attr_accessor :lock_column
40
-
41
- Plugins.inherited_instance_variables(self, :@lock_column=>nil)
42
- end
43
-
36
+
44
37
  module InstanceMethods
45
- # Add the lock column instance filter to the object before destroying it.
46
- def before_destroy
47
- lock_column_instance_filter
48
- super
49
- end
50
-
51
- # Add the lock column instance filter to the object before updating it.
52
- def before_update
53
- lock_column_instance_filter
54
- super
55
- end
56
-
57
38
  private
58
39
 
59
- # Add the lock column instance filter to the object.
60
- def lock_column_instance_filter
61
- lc = model.lock_column
62
- instance_filter(lc=>get_column_value(lc))
63
- end
64
-
65
- # Clear the instance filters when refreshing, so that attempting to
66
- # refresh after a failed save removes the previous lock column filter
67
- # (the new one will be added before updating).
68
- def _refresh(ds)
69
- clear_instance_filters
70
- super
71
- end
72
-
73
40
  # Only update the row if it has the same lock version, and increment the
74
41
  # lock version.
75
42
  def _update_columns(columns)
@@ -0,0 +1,55 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # Base for other optimistic locking plugins
6
+ module OptimisticLockingBase
7
+ # Load the instance_filters plugin into the model.
8
+ def self.apply(model)
9
+ model.plugin :instance_filters
10
+ end
11
+
12
+ module ClassMethods
13
+ # The column holding the version of the lock
14
+ attr_accessor :lock_column
15
+
16
+ Plugins.inherited_instance_variables(self, :@lock_column=>nil)
17
+ end
18
+
19
+ module InstanceMethods
20
+ # Add the lock column instance filter to the object before destroying it.
21
+ def before_destroy
22
+ lock_column_instance_filter
23
+ super
24
+ end
25
+
26
+ # Add the lock column instance filter to the object before updating it.
27
+ def before_update
28
+ lock_column_instance_filter
29
+ super
30
+ end
31
+
32
+ private
33
+
34
+ # Add the lock column instance filter to the object.
35
+ def lock_column_instance_filter
36
+ instance_filter(model.lock_column=>lock_column_instance_filter_value)
37
+ end
38
+
39
+ # Use the current value of the lock column
40
+ def lock_column_instance_filter_value
41
+ public_send(model.lock_column)
42
+ end
43
+
44
+ # Clear the instance filters when refreshing, so that attempting to
45
+ # refresh after a failed save removes the previous lock column filter
46
+ # (the new one will be added before updating).
47
+ def _refresh(ds)
48
+ clear_instance_filters
49
+ super
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+