sequel 5.60.1 → 5.81.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 (166) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +280 -0
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +27 -24
  5. data/doc/advanced_associations.rdoc +16 -14
  6. data/doc/association_basics.rdoc +53 -17
  7. data/doc/cheat_sheet.rdoc +3 -3
  8. data/doc/code_order.rdoc +5 -3
  9. data/doc/dataset_basics.rdoc +1 -1
  10. data/doc/mass_assignment.rdoc +1 -1
  11. data/doc/migration.rdoc +15 -0
  12. data/doc/model_hooks.rdoc +1 -1
  13. data/doc/object_model.rdoc +8 -8
  14. data/doc/opening_databases.rdoc +18 -7
  15. data/doc/postgresql.rdoc +8 -8
  16. data/doc/querying.rdoc +7 -2
  17. data/doc/release_notes/5.61.0.txt +43 -0
  18. data/doc/release_notes/5.62.0.txt +132 -0
  19. data/doc/release_notes/5.63.0.txt +33 -0
  20. data/doc/release_notes/5.64.0.txt +50 -0
  21. data/doc/release_notes/5.65.0.txt +21 -0
  22. data/doc/release_notes/5.66.0.txt +24 -0
  23. data/doc/release_notes/5.67.0.txt +32 -0
  24. data/doc/release_notes/5.68.0.txt +61 -0
  25. data/doc/release_notes/5.69.0.txt +26 -0
  26. data/doc/release_notes/5.70.0.txt +35 -0
  27. data/doc/release_notes/5.71.0.txt +21 -0
  28. data/doc/release_notes/5.72.0.txt +33 -0
  29. data/doc/release_notes/5.73.0.txt +66 -0
  30. data/doc/release_notes/5.74.0.txt +45 -0
  31. data/doc/release_notes/5.75.0.txt +35 -0
  32. data/doc/release_notes/5.76.0.txt +86 -0
  33. data/doc/release_notes/5.77.0.txt +63 -0
  34. data/doc/release_notes/5.78.0.txt +67 -0
  35. data/doc/release_notes/5.79.0.txt +28 -0
  36. data/doc/release_notes/5.80.0.txt +40 -0
  37. data/doc/release_notes/5.81.0.txt +31 -0
  38. data/doc/schema_modification.rdoc +3 -3
  39. data/doc/security.rdoc +9 -9
  40. data/doc/sharding.rdoc +3 -1
  41. data/doc/sql.rdoc +14 -14
  42. data/doc/testing.rdoc +16 -12
  43. data/doc/transactions.rdoc +6 -6
  44. data/doc/virtual_rows.rdoc +1 -1
  45. data/lib/sequel/adapters/ibmdb.rb +1 -1
  46. data/lib/sequel/adapters/jdbc/h2.rb +3 -0
  47. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -0
  48. data/lib/sequel/adapters/jdbc/postgresql.rb +3 -0
  49. data/lib/sequel/adapters/jdbc/sqlanywhere.rb +15 -0
  50. data/lib/sequel/adapters/jdbc/sqlserver.rb +4 -0
  51. data/lib/sequel/adapters/jdbc.rb +10 -6
  52. data/lib/sequel/adapters/mysql.rb +19 -7
  53. data/lib/sequel/adapters/mysql2.rb +2 -2
  54. data/lib/sequel/adapters/odbc/mssql.rb +1 -1
  55. data/lib/sequel/adapters/oracle.rb +1 -0
  56. data/lib/sequel/adapters/postgres.rb +10 -5
  57. data/lib/sequel/adapters/shared/access.rb +9 -1
  58. data/lib/sequel/adapters/shared/db2.rb +12 -0
  59. data/lib/sequel/adapters/shared/mssql.rb +71 -9
  60. data/lib/sequel/adapters/shared/mysql.rb +82 -3
  61. data/lib/sequel/adapters/shared/oracle.rb +17 -7
  62. data/lib/sequel/adapters/shared/postgres.rb +469 -160
  63. data/lib/sequel/adapters/shared/sqlanywhere.rb +18 -5
  64. data/lib/sequel/adapters/shared/sqlite.rb +39 -3
  65. data/lib/sequel/adapters/sqlite.rb +42 -3
  66. data/lib/sequel/adapters/trilogy.rb +117 -0
  67. data/lib/sequel/connection_pool/sharded_threaded.rb +16 -11
  68. data/lib/sequel/connection_pool/sharded_timed_queue.rb +374 -0
  69. data/lib/sequel/connection_pool/threaded.rb +14 -8
  70. data/lib/sequel/connection_pool/timed_queue.rb +270 -0
  71. data/lib/sequel/connection_pool.rb +57 -31
  72. data/lib/sequel/database/connecting.rb +25 -1
  73. data/lib/sequel/database/dataset.rb +16 -6
  74. data/lib/sequel/database/misc.rb +65 -14
  75. data/lib/sequel/database/query.rb +72 -1
  76. data/lib/sequel/database/schema_generator.rb +1 -1
  77. data/lib/sequel/database/schema_methods.rb +12 -5
  78. data/lib/sequel/database/transactions.rb +6 -0
  79. data/lib/sequel/dataset/actions.rb +60 -13
  80. data/lib/sequel/dataset/dataset_module.rb +1 -1
  81. data/lib/sequel/dataset/deprecated_singleton_class_methods.rb +42 -0
  82. data/lib/sequel/dataset/features.rb +15 -1
  83. data/lib/sequel/dataset/graph.rb +1 -0
  84. data/lib/sequel/dataset/misc.rb +12 -2
  85. data/lib/sequel/dataset/placeholder_literalizer.rb +20 -9
  86. data/lib/sequel/dataset/query.rb +119 -45
  87. data/lib/sequel/dataset/sql.rb +58 -36
  88. data/lib/sequel/dataset.rb +4 -0
  89. data/lib/sequel/exceptions.rb +5 -0
  90. data/lib/sequel/extensions/_model_pg_row.rb +0 -12
  91. data/lib/sequel/extensions/_pretty_table.rb +1 -1
  92. data/lib/sequel/extensions/any_not_empty.rb +2 -2
  93. data/lib/sequel/extensions/async_thread_pool.rb +21 -13
  94. data/lib/sequel/extensions/auto_cast_date_and_time.rb +94 -0
  95. data/lib/sequel/extensions/auto_literal_strings.rb +1 -1
  96. data/lib/sequel/extensions/caller_logging.rb +4 -1
  97. data/lib/sequel/extensions/connection_expiration.rb +15 -9
  98. data/lib/sequel/extensions/connection_validator.rb +16 -11
  99. data/lib/sequel/extensions/constraint_validations.rb +1 -1
  100. data/lib/sequel/extensions/date_arithmetic.rb +1 -1
  101. data/lib/sequel/extensions/duplicate_columns_handler.rb +10 -9
  102. data/lib/sequel/extensions/index_caching.rb +5 -1
  103. data/lib/sequel/extensions/looser_typecasting.rb +3 -0
  104. data/lib/sequel/extensions/migration.rb +65 -15
  105. data/lib/sequel/extensions/named_timezones.rb +22 -6
  106. data/lib/sequel/extensions/pg_array.rb +33 -4
  107. data/lib/sequel/extensions/pg_auto_parameterize.rb +509 -0
  108. data/lib/sequel/extensions/pg_auto_parameterize_in_array.rb +110 -0
  109. data/lib/sequel/extensions/pg_enum.rb +1 -2
  110. data/lib/sequel/extensions/pg_extended_date_support.rb +38 -27
  111. data/lib/sequel/extensions/pg_extended_integer_support.rb +116 -0
  112. data/lib/sequel/extensions/pg_hstore.rb +5 -0
  113. data/lib/sequel/extensions/pg_inet.rb +10 -11
  114. data/lib/sequel/extensions/pg_interval.rb +10 -11
  115. data/lib/sequel/extensions/pg_json.rb +10 -10
  116. data/lib/sequel/extensions/pg_json_ops.rb +3 -3
  117. data/lib/sequel/extensions/pg_multirange.rb +6 -11
  118. data/lib/sequel/extensions/pg_range.rb +9 -14
  119. data/lib/sequel/extensions/pg_row.rb +20 -19
  120. data/lib/sequel/extensions/pg_timestamptz.rb +27 -3
  121. data/lib/sequel/extensions/provenance.rb +110 -0
  122. data/lib/sequel/extensions/round_timestamps.rb +1 -1
  123. data/lib/sequel/extensions/schema_caching.rb +1 -1
  124. data/lib/sequel/extensions/schema_dumper.rb +32 -9
  125. data/lib/sequel/extensions/server_block.rb +2 -1
  126. data/lib/sequel/extensions/set_literalizer.rb +58 -0
  127. data/lib/sequel/extensions/sqlite_json_ops.rb +76 -18
  128. data/lib/sequel/extensions/temporarily_release_connection.rb +178 -0
  129. data/lib/sequel/extensions/transaction_connection_validator.rb +78 -0
  130. data/lib/sequel/model/associations.rb +38 -11
  131. data/lib/sequel/model/base.rb +32 -18
  132. data/lib/sequel/model/dataset_module.rb +3 -0
  133. data/lib/sequel/model/exceptions.rb +15 -3
  134. data/lib/sequel/plugins/auto_validations.rb +53 -15
  135. data/lib/sequel/plugins/class_table_inheritance.rb +2 -2
  136. data/lib/sequel/plugins/column_encryption.rb +28 -7
  137. data/lib/sequel/plugins/composition.rb +2 -2
  138. data/lib/sequel/plugins/concurrent_eager_loading.rb +4 -4
  139. data/lib/sequel/plugins/constraint_validations.rb +8 -5
  140. data/lib/sequel/plugins/defaults_setter.rb +16 -0
  141. data/lib/sequel/plugins/dirty.rb +1 -1
  142. data/lib/sequel/plugins/finder.rb +4 -2
  143. data/lib/sequel/plugins/input_transformer.rb +1 -1
  144. data/lib/sequel/plugins/list.rb +5 -2
  145. data/lib/sequel/plugins/many_through_many.rb +1 -1
  146. data/lib/sequel/plugins/mssql_optimistic_locking.rb +8 -38
  147. data/lib/sequel/plugins/nested_attributes.rb +4 -4
  148. data/lib/sequel/plugins/optimistic_locking.rb +9 -42
  149. data/lib/sequel/plugins/optimistic_locking_base.rb +55 -0
  150. data/lib/sequel/plugins/paged_operations.rb +181 -0
  151. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +9 -3
  152. data/lib/sequel/plugins/pg_xmin_optimistic_locking.rb +109 -0
  153. data/lib/sequel/plugins/prepared_statements.rb +2 -1
  154. data/lib/sequel/plugins/prepared_statements_safe.rb +2 -1
  155. data/lib/sequel/plugins/primary_key_lookup_check_values.rb +154 -0
  156. data/lib/sequel/plugins/rcte_tree.rb +7 -4
  157. data/lib/sequel/plugins/single_table_inheritance.rb +8 -0
  158. data/lib/sequel/plugins/sql_comments.rb +1 -1
  159. data/lib/sequel/plugins/static_cache.rb +38 -0
  160. data/lib/sequel/plugins/static_cache_cache.rb +5 -1
  161. data/lib/sequel/plugins/tactical_eager_loading.rb +14 -14
  162. data/lib/sequel/plugins/validate_associated.rb +22 -12
  163. data/lib/sequel/plugins/validation_helpers.rb +29 -2
  164. data/lib/sequel/plugins/validation_helpers_generic_type_messages.rb +73 -0
  165. data/lib/sequel/version.rb +2 -2
  166. metadata +79 -6
@@ -50,10 +50,10 @@ module Sequel
50
50
  # support concurrent eager loading. Taking this example from the
51
51
  # {Advanced Associations guide}[rdoc-ref:doc/advanced_associations.rdoc]
52
52
  #
53
- # Album.many_to_one :artist, :eager_loader=>(proc do |eo_opts|
53
+ # Album.many_to_one :artist, eager_loader: (proc do |eo_opts|
54
54
  # eo_opts[:rows].each{|album| album.associations[:artist] = nil}
55
55
  # id_map = eo_opts[:id_map]
56
- # Artist.where(:id=>id_map.keys).all do |artist|
56
+ # Artist.where(id: id_map.keys).all do |artist|
57
57
  # if albums = id_map[artist.id]
58
58
  # albums.each do |album|
59
59
  # album.associations[:artist] = artist
@@ -74,12 +74,12 @@ module Sequel
74
74
  # the code that loads the objects, since that will prevent concurrent loading.
75
75
  # So after the changes, the custom eager loader would look like this:
76
76
  #
77
- # Album.many_to_one :artist, :eager_loader=>(proc do |eo_opts|
77
+ # Album.many_to_one :artist, eager_loader: (proc do |eo_opts|
78
78
  # Sequel.synchronize_with(eo[:mutex]) do
79
79
  # eo_opts[:rows].each{|album| album.associations[:artist] = nil}
80
80
  # end
81
81
  # id_map = eo_opts[:id_map]
82
- # rows = Artist.where(:id=>id_map.keys).all
82
+ # rows = Artist.where(id: id_map.keys).all
83
83
  # Sequel.synchronize_with(eo[:mutex]) do
84
84
  # rows.each do |artist|
85
85
  # if albums = id_map[artist.id]
@@ -125,14 +125,15 @@ module Sequel
125
125
  ds = @dataset.with_quote_identifiers(false)
126
126
  table_name = ds.literal(ds.first_source_table)
127
127
  reflections = {}
128
- @constraint_validations = (Sequel.synchronize{hash[table_name]} || []).map{|r| constraint_validation_array(r, reflections)}
128
+ allow_missing_columns = db_schema.select{|col, sch| sch[:allow_null] == false && nil != sch[:default]}.map(&:first)
129
+ @constraint_validations = (Sequel.synchronize{hash[table_name]} || []).map{|r| constraint_validation_array(r, reflections, allow_missing_columns)}
129
130
  @constraint_validation_reflections = reflections
130
131
  end
131
132
  end
132
133
 
133
134
  # Given a specific database constraint validation metadata row hash, transform
134
135
  # it in an validation method call array suitable for splatting to send.
135
- def constraint_validation_array(r, reflections)
136
+ def constraint_validation_array(r, reflections, allow_missing_columns=EMPTY_ARRAY)
136
137
  opts = {}
137
138
  opts[:message] = r[:message] if r[:message]
138
139
  opts[:allow_nil] = true if db.typecast_value(:boolean, r[:allow_nil])
@@ -191,11 +192,13 @@ module Sequel
191
192
  reflection_opts[:argument] = arg
192
193
  end
193
194
 
194
- a << column
195
- unless opts.empty?
196
- a << opts
195
+ opts[:from] = :values
196
+ if column.is_a?(Symbol) && allow_missing_columns.include?(column)
197
+ opts[:allow_missing] = true
197
198
  end
198
199
 
200
+ a << column << opts
201
+
199
202
  if column.is_a?(Array) && column.length == 1
200
203
  column = column.first
201
204
  end
@@ -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
@@ -37,7 +37,7 @@ module Sequel
37
37
  #
38
38
  # It also saves the previously changed values after an update:
39
39
  #
40
- # artist.update(:name=>'Bar')
40
+ # artist.update(name: 'Bar')
41
41
  # artist.column_changes # => {}
42
42
  # artist.previous_changes # => {:name=>['Foo', 'Bar']}
43
43
  #
@@ -97,7 +97,9 @@ module Sequel
97
97
  # Artist.first_by_name(nil)
98
98
  # # WHERE (name IS NULL)
99
99
  #
100
- # See Dataset::PlaceholderLiteralizer for additional caveats.
100
+ # See Dataset::PlaceholderLiteralizer for additional caveats. Note that if the model's
101
+ # dataset does not support placeholder literalizers, you will not be able to use this
102
+ # method.
101
103
  def finder(meth=OPTS, opts=OPTS, &block)
102
104
  if block
103
105
  raise Error, "cannot pass both a method name argument and a block of Model.finder" unless meth.is_a?(Hash)
@@ -149,7 +151,7 @@ module Sequel
149
151
  ds
150
152
  end
151
153
 
152
- Sequel::Dataset::PlaceholderLiteralizer.loader(model, &block)
154
+ model.dataset.placeholder_literalizer_class.loader(model, &block)
153
155
  end
154
156
  end
155
157
 
@@ -24,7 +24,7 @@ module Sequel
24
24
  # # Make the Album class support input transformers
25
25
  # Album.plugin :input_transformer
26
26
  module InputTransformer
27
- def self.apply(model, *)
27
+ def self.apply(model, *, &_)
28
28
  model.instance_exec do
29
29
  @input_transformers = {}
30
30
  @skip_input_transformer_columns = {}
@@ -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
@@ -385,7 +385,7 @@ module Sequel
385
385
  iq = nil
386
386
  end
387
387
  fe = opts.final_edge
388
- ds.graph(opts.associated_class.dataset, use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>eo[:join_type]||join_type, :join_only=>eo[:join_only], &graph_block)
388
+ ds.graph(eager_graph_dataset(opts, eo), use_only_conditions ? only_conditions : (Array(opts.right_primary_key).zip(Array(fe[:left])) + conditions), :select=>select, :table_alias=>eo[:table_alias], :qualify=>:deep, :join_type=>eo[:join_type]||join_type, :join_only=>eo[:join_only], &graph_block)
389
389
  end
390
390
  end
391
391
  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
@@ -33,7 +33,7 @@ module Sequel
33
33
  # objects. You just need to make sure that the primary key field is filled in for the
34
34
  # associated object:
35
35
  #
36
- # a.update(:albums_attributes => [{id: 1, name: 'T'}])
36
+ # a.update(albums_attributes: [{id: 1, name: 'T'}])
37
37
  #
38
38
  # Since the primary key field is filled in, the plugin will update the album with id 1 instead
39
39
  # of creating a new album.
@@ -42,14 +42,14 @@ module Sequel
42
42
  # entry to the hash, and also pass the :destroy option when calling +nested_attributes+:
43
43
  #
44
44
  # Artist.nested_attributes :albums, destroy: true
45
- # a.update(:albums_attributes => [{id: 1, _delete: true}])
45
+ # a.update(albums_attributes: [{id: 1, _delete: true}])
46
46
  #
47
47
  # This will delete the related associated object from the database. If you want to leave the
48
48
  # associated object in the database, but just remove it from the association, add a _remove
49
49
  # entry in the hash, and also pass the :remove option when calling +nested_attributes+:
50
50
  #
51
51
  # Artist.nested_attributes :albums, remove: true
52
- # a.update(:albums_attributes => [{id: 1, _remove: true}])
52
+ # a.update(albums_attributes: [{id: 1, _remove: true}])
53
53
  #
54
54
  # The above example was for a one_to_many association, but the plugin also works similarly
55
55
  # for other association types. For one_to_one and many_to_one associations, you need to
@@ -84,7 +84,7 @@ module Sequel
84
84
  # nested attributes options for that association. This is useful for per-call filtering
85
85
  # of the allowed fields:
86
86
  #
87
- # a.set_nested_attributes(:albums, params['artist'], :fields=>%w'name')
87
+ # a.set_nested_attributes(:albums, params['artist'], fields: %w'name')
88
88
  module NestedAttributes
89
89
  # Depend on the validate_associated plugin.
90
90
  def self.apply(model)
@@ -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
+
@@ -0,0 +1,181 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The paged_operations plugin adds +paged_update+ and
6
+ # +paged_delete+ dataset methods. These behave similarly to
7
+ # the default +update+ and +delete+ dataset methods, except
8
+ # that the update or deletion is done in potentially multiple
9
+ # queries (by default, affecting 1000 rows per query).
10
+ # For a large table, this prevents the change from
11
+ # locking the table for a long period of time.
12
+ #
13
+ # Because the point of this is to prevent locking tables for
14
+ # long periods of time, the separate queries are not contained
15
+ # in a transaction, which means if a later query fails,
16
+ # earlier queries will still be committed. You could prevent
17
+ # this by using a transaction manually, but that defeats the
18
+ # purpose of using these methods.
19
+ #
20
+ # Examples:
21
+ #
22
+ # Album.where{name <= 'M'}.paged_update(updated_at: Sequel::CURRENT_TIMESTAMP)
23
+ # # SELECT id FROM albums WHERE (name <= 'M') ORDER BY id LIMIT 1 OFFSET 1000
24
+ # # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND ("id" < 1002))
25
+ # # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 1002)) ORDER BY id LIMIT 1 OFFSET 1000
26
+ # # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND ("id" < 2002) AND (id >= 1002))
27
+ # # ...
28
+ # # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 10002)) ORDER BY id LIMIT 1 OFFSET 1000
29
+ # # UPDATE albums SET updated_at = CURRENT_TIMESTAMP WHERE ((name <= 'M') AND (id >= 10002))
30
+ #
31
+ # Album.where{name > 'M'}.paged_delete
32
+ # # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
33
+ # # DELETE FROM albums WHERE ((name > 'M') AND (id < 1002))
34
+ # # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
35
+ # # DELETE FROM albums WHERE ((name > 'M') AND (id < 2002))
36
+ # # ...
37
+ # # SELECT id FROM albums WHERE (name > 'M') ORDER BY id LIMIT 1 OFFSET 1000
38
+ # # DELETE FROM albums WHERE (name > 'M')
39
+ #
40
+ # The plugin also adds a +paged_datasets+ method that will yield
41
+ # separate datasets limited in size that in total handle all
42
+ # rows in the receiver:
43
+ #
44
+ # Album.where{name > 'M'}.paged_datasets{|ds| puts ds.sql}
45
+ # # Runs: SELECT id FROM albums WHERE (name <= 'M') ORDER BY id LIMIT 1 OFFSET 1000
46
+ # # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND ("id" < 1002))
47
+ # # Runs: SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 1002)) ORDER BY id LIMIT 1 OFFSET 1000
48
+ # # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND ("id" < 2002) AND (id >= 1002))
49
+ # # ...
50
+ # # Runs: SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 10002)) ORDER BY id LIMIT 1 OFFSET 1000
51
+ # # Prints: SELECT * FROM albums WHERE ((name <= 'M') AND (id >= 10002))
52
+ #
53
+ # To set the number of rows per page, pass a :rows_per_page option:
54
+ #
55
+ # Album.where{name <= 'M'}.paged_update({x: Sequel[:x] + 1}, rows_per_page: 4)
56
+ # # SELECT id FROM albums WHERE (name <= 'M') ORDER BY id LIMIT 1 OFFSET 4
57
+ # # UPDATE albums SET x = x + 1 WHERE ((name <= 'M') AND ("id" < 5))
58
+ # # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 5)) ORDER BY id LIMIT 1 OFFSET 4
59
+ # # UPDATE albums SET x = x + 1 WHERE ((name <= 'M') AND ("id" < 9) AND (id >= 5))
60
+ # # ...
61
+ # # SELECT id FROM albums WHERE ((name <= 'M') AND (id >= 12345)) ORDER BY id LIMIT 1 OFFSET 4
62
+ # # UPDATE albums SET x = x + 1 WHERE ((name <= 'M') AND (id >= 12345))
63
+ #
64
+ # You should avoid using +paged_update+ or +paged_datasets+
65
+ # with updates that modify the primary key, as such usage is
66
+ # not supported by this plugin.
67
+ #
68
+ # This plugin only supports models with scalar primary keys.
69
+ #
70
+ # Usage:
71
+ #
72
+ # # Make all model subclasses support paged update/delete/datasets
73
+ # # (called before loading subclasses)
74
+ # Sequel::Model.plugin :paged_operations
75
+ #
76
+ # # Make the Album class support paged update/delete/datasets
77
+ # Album.plugin :paged_operations
78
+ module PagedOperations
79
+ module ClassMethods
80
+ Plugins.def_dataset_methods(self, [:paged_datasets, :paged_delete, :paged_update])
81
+ end
82
+
83
+ module DatasetMethods
84
+ # Yield datasets for subsets of the receiver that are limited
85
+ # to no more than 1000 rows (you can configure the number of
86
+ # rows using +:rows_per_page+).
87
+ #
88
+ # Options:
89
+ # :rows_per_page :: The maximum number of rows in each yielded dataset
90
+ # (unless concurrent modifications are made to the table).
91
+ def paged_datasets(opts=OPTS)
92
+ unless defined?(yield)
93
+ return enum_for(:paged_datasets, opts)
94
+ end
95
+
96
+ pk = _paged_operations_pk(:paged_update)
97
+ base_offset_ds = offset_ds = _paged_operations_offset_ds(opts)
98
+ first = nil
99
+
100
+ while last = offset_ds.get(pk)
101
+ ds = where(pk < last)
102
+ ds = ds.where(pk >= first) if first
103
+ yield ds
104
+ first = last
105
+ offset_ds = base_offset_ds.where(pk >= first)
106
+ end
107
+
108
+ ds = self
109
+ ds = ds.where(pk >= first) if first
110
+ yield ds
111
+ nil
112
+ end
113
+
114
+ # Delete all rows of the dataset using using multiple queries so that
115
+ # no more than 1000 rows are deleted at a time (you can configure the
116
+ # number of rows using +:rows_per_page+).
117
+ #
118
+ # Options:
119
+ # :rows_per_page :: The maximum number of rows affected by each DELETE query
120
+ # (unless concurrent modifications are made to the table).
121
+ def paged_delete(opts=OPTS)
122
+ if (db.database_type == :oracle && !supports_fetch_next_rows?) || (db.database_type == :mssql && !is_2012_or_later?)
123
+ raise Error, "paged_delete is not supported on MSSQL/Oracle when using emulated offsets"
124
+ end
125
+ pk = _paged_operations_pk(:paged_delete)
126
+ rows_deleted = 0
127
+ offset_ds = _paged_operations_offset_ds(opts)
128
+ while last = offset_ds.get(pk)
129
+ rows_deleted += where(pk < last).delete
130
+ end
131
+ rows_deleted + delete
132
+ end
133
+
134
+ # Update all rows of the dataset using using multiple queries so that
135
+ # no more than 1000 rows are updated at a time (you can configure the
136
+ # number of rows using +:rows_per_page+). All arguments are
137
+ # passed to Dataset#update.
138
+ #
139
+ # Options:
140
+ # :rows_per_page :: The maximum number of rows affected by each UPDATE query
141
+ # (unless concurrent modifications are made to the table).
142
+ def paged_update(values, opts=OPTS)
143
+ rows_updated = 0
144
+ paged_datasets(opts) do |ds|
145
+ rows_updated += ds.update(values)
146
+ end
147
+ rows_updated
148
+ end
149
+
150
+ private
151
+
152
+ # Run some basic checks common to paged_{datasets,delete,update}
153
+ # and return the primary key to operate on as a Sequel::Identifier.
154
+ def _paged_operations_pk(meth)
155
+ raise Error, "cannot use #{meth} if dataset has a limit or offset" if @opts[:limit] || @opts[:offset]
156
+ if db.database_type == :db2 && db.offset_strategy == :emulate
157
+ raise Error, "the paged_operations plugin is not supported on DB2 when using emulated offsets, set the :offset_strategy Database option to 'limit_offset' or 'offset_fetch'"
158
+ end
159
+
160
+ case pk = model.primary_key
161
+ when Symbol
162
+ Sequel.identifier(pk)
163
+ when Array
164
+ raise Error, "cannot use #{meth} on a model with a composite primary key"
165
+ else
166
+ raise Error, "cannot use #{meth} on a model without a primary key"
167
+ end
168
+ end
169
+
170
+ # The dataset that will be used by paged_{datasets,delete,update}
171
+ # to get the upper limit for the next query.
172
+ def _paged_operations_offset_ds(opts)
173
+ if rows_per_page = opts[:rows_per_page]
174
+ raise Error, ":rows_per_page option must be at least 1" unless rows_per_page >= 1
175
+ end
176
+ _force_primary_key_order.offset(rows_per_page || 1000)
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -28,11 +28,13 @@ module Sequel
28
28
  #
29
29
  # This plugin only works on the postgres adapter when using the pg 0.16+ driver,
30
30
  # PostgreSQL 9.3+ server, and PostgreSQL 9.3+ client library (libpq). In other cases
31
- # it will be a no-op.
31
+ # it will be a no-op. Additionally, the plugin only handles models that select
32
+ # from tables. It does not handle models that select from subqueries, such as
33
+ # subclasses of models using the class_table_inheritance plugin.
32
34
  #
33
35
  # Example:
34
36
  #
35
- # album = Album.new(:artist_id=>1) # Assume no such artist exists
37
+ # album = Album.new(artist_id: 1) # Assume no such artist exists
36
38
  # begin
37
39
  # album.save
38
40
  # rescue Sequel::ValidationFailed
@@ -131,7 +133,11 @@ module Sequel
131
133
  # Dump the in-memory cached metadata to the cache file.
132
134
  def dump_pg_auto_constraint_validations_cache
133
135
  raise Error, "No pg_auto_constraint_validations setup" unless file = @pg_auto_constraint_validations_cache_file
134
- File.open(file, 'wb'){|f| f.write(Marshal.dump(@pg_auto_constraint_validations_cache))}
136
+ pg_auto_constraint_validations_cache = {}
137
+ @pg_auto_constraint_validations_cache.sort.each do |k, v|
138
+ pg_auto_constraint_validations_cache[k] = v
139
+ end
140
+ File.open(file, 'wb'){|f| f.write(Marshal.dump(pg_auto_constraint_validations_cache))}
135
141
  nil
136
142
  end
137
143