sequel 5.39.0 → 5.44.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +52 -0
  3. data/MIT-LICENSE +1 -1
  4. data/doc/release_notes/5.40.0.txt +40 -0
  5. data/doc/release_notes/5.41.0.txt +25 -0
  6. data/doc/release_notes/5.42.0.txt +136 -0
  7. data/doc/release_notes/5.43.0.txt +98 -0
  8. data/doc/release_notes/5.44.0.txt +32 -0
  9. data/doc/sql.rdoc +1 -1
  10. data/doc/testing.rdoc +3 -0
  11. data/lib/sequel/adapters/ado.rb +16 -16
  12. data/lib/sequel/adapters/jdbc.rb +2 -2
  13. data/lib/sequel/adapters/shared/postgres.rb +4 -2
  14. data/lib/sequel/adapters/shared/sqlite.rb +37 -3
  15. data/lib/sequel/core.rb +11 -0
  16. data/lib/sequel/database/misc.rb +1 -2
  17. data/lib/sequel/database/schema_generator.rb +35 -47
  18. data/lib/sequel/database/schema_methods.rb +4 -0
  19. data/lib/sequel/dataset/features.rb +10 -0
  20. data/lib/sequel/dataset/prepared_statements.rb +2 -0
  21. data/lib/sequel/dataset/sql.rb +32 -10
  22. data/lib/sequel/extensions/async_thread_pool.rb +438 -0
  23. data/lib/sequel/extensions/blank.rb +8 -0
  24. data/lib/sequel/extensions/date_arithmetic.rb +36 -24
  25. data/lib/sequel/extensions/eval_inspect.rb +2 -0
  26. data/lib/sequel/extensions/inflector.rb +8 -0
  27. data/lib/sequel/extensions/migration.rb +2 -0
  28. data/lib/sequel/extensions/named_timezones.rb +5 -1
  29. data/lib/sequel/extensions/pg_array.rb +1 -0
  30. data/lib/sequel/extensions/pg_enum.rb +1 -1
  31. data/lib/sequel/extensions/pg_interval.rb +34 -8
  32. data/lib/sequel/extensions/pg_row.rb +1 -0
  33. data/lib/sequel/extensions/query.rb +2 -0
  34. data/lib/sequel/model/associations.rb +68 -13
  35. data/lib/sequel/model/base.rb +23 -6
  36. data/lib/sequel/model/plugins.rb +5 -0
  37. data/lib/sequel/plugins/association_proxies.rb +2 -0
  38. data/lib/sequel/plugins/async_thread_pool.rb +39 -0
  39. data/lib/sequel/plugins/auto_validations.rb +15 -1
  40. data/lib/sequel/plugins/column_encryption.rb +728 -0
  41. data/lib/sequel/plugins/composition.rb +7 -2
  42. data/lib/sequel/plugins/concurrent_eager_loading.rb +174 -0
  43. data/lib/sequel/plugins/constraint_validations.rb +2 -1
  44. data/lib/sequel/plugins/dataset_associations.rb +4 -1
  45. data/lib/sequel/plugins/json_serializer.rb +37 -22
  46. data/lib/sequel/plugins/nested_attributes.rb +8 -3
  47. data/lib/sequel/plugins/pg_array_associations.rb +10 -4
  48. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +2 -0
  49. data/lib/sequel/plugins/rcte_tree.rb +27 -19
  50. data/lib/sequel/plugins/serialization.rb +8 -3
  51. data/lib/sequel/plugins/serialization_modification_detection.rb +1 -1
  52. data/lib/sequel/plugins/validation_helpers.rb +6 -2
  53. data/lib/sequel/version.rb +1 -1
  54. metadata +36 -22
@@ -143,10 +143,14 @@ module Sequel
143
143
  compositions[name] = send(composer_meth)
144
144
  end
145
145
  end
146
- define_method("#{name}=") do |v|
146
+ alias_method(name, name)
147
+
148
+ meth = :"#{name}="
149
+ define_method(meth) do |v|
147
150
  modified!
148
151
  compositions[name] = v
149
152
  end
153
+ alias_method(meth, meth)
150
154
  end
151
155
  end
152
156
 
@@ -167,8 +171,9 @@ module Sequel
167
171
 
168
172
  # Freeze compositions hash when freezing model instance.
169
173
  def freeze
170
- compositions.freeze
174
+ compositions
171
175
  super
176
+ compositions.freeze
172
177
  end
173
178
 
174
179
  # For each composition, set the columns in the model class based
@@ -0,0 +1,174 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ extension 'async_thread_pool'
5
+
6
+ module Plugins
7
+ # The concurrent_eager_loading plugin allows for eager loading multiple associations
8
+ # concurrently in separate threads. You must load the async_thread_pool Database
9
+ # extension into the Database object the model class uses in order for this plugin
10
+ # to work.
11
+ #
12
+ # By default in Sequel, eager loading happens in a serial manner. If you have code
13
+ # such as:
14
+ #
15
+ # Album.eager(:artist, :genre, :tracks)
16
+ #
17
+ # Sequel will load the albums, then the artists for the albums, then
18
+ # the genres for the albums, then the tracks for the albums.
19
+ #
20
+ # With the concurrent_eager_loading plugin, you can use the +eager_load_concurrently+
21
+ # method to allow for concurrent eager loading:
22
+ #
23
+ # Album.eager_load_concurrently.eager(:artist, :genre, :tracks)
24
+ #
25
+ # This will load the albums, first, since it needs to load the albums to know
26
+ # which artists, genres, and tracks to eagerly load. However, it will load the
27
+ # artists, genres, and tracks for the albums concurrently in separate threads.
28
+ # This can significantly improve performance, especially if there is significant
29
+ # latency between the application and the database. Note that using separate threads
30
+ # is only used in the case where there are multiple associations to eagerly load.
31
+ # With only a single association to eagerly load, there is no reason to use a
32
+ # separate thread, since it would not improve performance.
33
+ #
34
+ # If you want to make concurrent eager loading the default, you can load the
35
+ # plugin with the +:always+ option. In this case, all eager loads will be
36
+ # concurrent. If you want to force a non-concurrent eager load, you can use
37
+ # +eager_load_serially+:
38
+ #
39
+ # Album.eager_load_serially.eager(:artist, :genre, :tracks)
40
+ #
41
+ # Note that making concurrent eager loading the default is probably a bad idea
42
+ # if you are eager loading inside transactions and want the eager load to
43
+ # reflect changes made inside the transaction, unless you plan to use
44
+ # +eager_load_serially+ for such cases. See the async_thread_pool
45
+ # Database extension documentation for more general caveats regarding its use.
46
+ #
47
+ # The default eager loaders for all of the association types that ship with Sequel
48
+ # support safe concurrent eager loading. However, if you are specifying a custom
49
+ # +:eager_loader+ for an association, it may not work safely unless it it modified to
50
+ # support concurrent eager loading. Taking this example from the
51
+ # {Advanced Associations guide}[rdoc-ref:doc/advanced_associations.rdoc]
52
+ #
53
+ # Album.many_to_one :artist, :eager_loader=>(proc do |eo_opts|
54
+ # eo_opts[:rows].each{|album| album.associations[:artist] = nil}
55
+ # id_map = eo_opts[:id_map]
56
+ # Artist.where(:id=>id_map.keys).all do |artist|
57
+ # if albums = id_map[artist.id]
58
+ # albums.each do |album|
59
+ # album.associations[:artist] = artist
60
+ # end
61
+ # end
62
+ # end
63
+ # end)
64
+ #
65
+ # This would not support concurrent eager loading safely. To support safe
66
+ # concurrent eager loading, you need to make sure you are not modifying
67
+ # the associations for objects concurrently by separate threads. This is
68
+ # implemented using a mutex, which you can access via <tt>eo_opts[:mutex]</tt>.
69
+ # To keep things simple, you can use +Sequel.synchronize_with+ to only
70
+ # use this mutex if it is available. You want to use the mutex around the
71
+ # code that initializes the associations (usually to +nil+ or <tt>[]</tt>),
72
+ # and also around the code that sets the associatied objects appropriately
73
+ # after they have been retreived. You do not want to use the mutex around
74
+ # the code that loads the objects, since that will prevent concurrent loading.
75
+ # So after the changes, the custom eager loader would look like this:
76
+ #
77
+ # Album.many_to_one :artist, :eager_loader=>(proc do |eo_opts|
78
+ # Sequel.synchronize_with(eo[:mutex]) do
79
+ # eo_opts[:rows].each{|album| album.associations[:artist] = nil}
80
+ # end
81
+ # id_map = eo_opts[:id_map]
82
+ # rows = Artist.where(:id=>id_map.keys).all
83
+ # Sequel.synchronize_with(eo[:mutex]) do
84
+ # rows.each do |artist|
85
+ # if albums = id_map[artist.id]
86
+ # albums.each do |album|
87
+ # album.associations[:artist] = artist
88
+ # end
89
+ # end
90
+ # end
91
+ # end
92
+ # end)
93
+ #
94
+ # Usage:
95
+ #
96
+ # # Make all model subclass datasets support concurrent eager loading
97
+ # Sequel::Model.plugin :concurrent_eager_loading
98
+ #
99
+ # # Make the Album class datasets support concurrent eager loading
100
+ # Album.plugin :concurrent_eager_loading
101
+ #
102
+ # # Make all model subclass datasets concurrently eager load by default
103
+ # Sequel::Model.plugin :concurrent_eager_loading, always: true
104
+ module ConcurrentEagerLoading
105
+ def self.configure(mod, opts=OPTS)
106
+ if opts.has_key?(:always)
107
+ mod.instance_variable_set(:@always_eager_load_concurrently, opts[:always])
108
+ end
109
+ end
110
+
111
+ module ClassMethods
112
+ Plugins.inherited_instance_variables(self, :@always_eager_load_concurrently => nil)
113
+ Plugins.def_dataset_methods(self, [:eager_load_concurrently, :eager_load_serially])
114
+
115
+ # Whether datasets for this class should eager load concurrently by default.
116
+ def always_eager_load_concurrently?
117
+ @always_eager_load_concurrently
118
+ end
119
+ end
120
+
121
+ module DatasetMethods
122
+ # Return a cloned dataset that will eager load associated results concurrently
123
+ # using the async thread pool.
124
+ def eager_load_concurrently
125
+ cached_dataset(:_eager_load_concurrently) do
126
+ clone(:eager_load_concurrently=>true)
127
+ end
128
+ end
129
+
130
+ # Return a cloned dataset that will noteager load associated results concurrently
131
+ # using the async thread pool. Only useful if the current dataset has been marked
132
+ # as loading concurrently, or loading concurrently is the model's default behavior.
133
+ def eager_load_serially
134
+ cached_dataset(:_eager_load_serially) do
135
+ clone(:eager_load_concurrently=>false)
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ # Whether this particular dataset will eager load results concurrently.
142
+ def eager_load_concurrently?
143
+ v = @opts[:eager_load_concurrently]
144
+ v.nil? ? model.always_eager_load_concurrently? : v
145
+ end
146
+
147
+ # If performing eager loads concurrently, and at least 2 associations are being
148
+ # eagerly loaded, create a single mutex used for all eager loads. After the
149
+ # eager loads have been performed, force loading of any async results, so that
150
+ # all eager loads will have been completed before this method returns.
151
+ def perform_eager_loads(eager_load_data)
152
+ return super if !eager_load_concurrently? || eager_load_data.length < 2
153
+
154
+ mutex = Mutex.new
155
+ eager_load_data.each_value do |eo|
156
+ eo[:mutex] = mutex
157
+ end
158
+
159
+ super.each do |v|
160
+ if Sequel::Database::AsyncThreadPool::BaseProxy === v
161
+ v.__value
162
+ end
163
+ end
164
+ end
165
+
166
+ # If performing eager loads concurrently, perform this eager load using the
167
+ # async thread pool.
168
+ def perform_eager_load(loader, eo)
169
+ eo[:mutex] ? db.send(:async_run){super} : super
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
@@ -220,7 +220,8 @@ module Sequel
220
220
  '%'
221
221
  when '%'
222
222
  '.*'
223
- when '_'
223
+ else
224
+ #when '_'
224
225
  '.'
225
226
  end
226
227
  end
@@ -62,7 +62,10 @@ module Sequel
62
62
  ret = super
63
63
  r = association_reflection(name)
64
64
  meth = r.returns_array? ? name : pluralize(name).to_sym
65
- dataset_module{define_method(meth){associated(name)}}
65
+ dataset_module do
66
+ define_method(meth){associated(name)}
67
+ alias_method(meth, meth)
68
+ end
66
69
  ret
67
70
  end
68
71
 
@@ -133,21 +133,39 @@ module Sequel
133
133
  end
134
134
  end
135
135
 
136
- # Helper class used for making sure that cascading options
137
- # for model associations works correctly. Cascaded options
138
- # work by creating instances of this class, which take a
139
- # literal JSON string and have +to_json+ return it.
136
+ # SEQUEL6: Remove
137
+ # :nocov:
140
138
  class Literal
141
- # Store the literal JSON to use
142
139
  def initialize(json)
143
140
  @json = json
144
141
  end
145
142
 
146
- # Return the literal JSON to use
147
143
  def to_json(*a)
148
144
  @json
149
145
  end
150
146
  end
147
+ # :nocov:
148
+ Sequel::Deprecation.deprecate_constant(self, :Literal)
149
+
150
+ # Convert the given object to a JSON data structure using the given arguments.
151
+ def self.object_to_json_data(obj, *args, &block)
152
+ if obj.is_a?(Array)
153
+ obj.map{|x| object_to_json_data(x, *args, &block)}
154
+ else
155
+ if obj.respond_to?(:to_json_data)
156
+ obj.to_json_data(*args, &block)
157
+ else
158
+ begin
159
+ Sequel.parse_json(Sequel.object_to_json(obj, *args, &block))
160
+ # :nocov:
161
+ rescue Sequel.json_parser_error_class
162
+ # Support for old Ruby code that only supports parsing JSON object/array
163
+ Sequel.parse_json(Sequel.object_to_json([obj], *args, &block))[0]
164
+ # :nocov:
165
+ end
166
+ end
167
+ end
168
+ end
151
169
 
152
170
  module ClassMethods
153
171
  # The default opts to use when serializing model objects to JSON.
@@ -324,20 +342,7 @@ module Sequel
324
342
  end
325
343
 
326
344
  v = v.empty? ? [] : [v]
327
-
328
- objs = public_send(k)
329
-
330
- is_array = if r = model.association_reflection(k)
331
- r.returns_array?
332
- else
333
- objs.is_a?(Array)
334
- end
335
-
336
- h[key_name] = if is_array
337
- objs.map{|obj| Literal.new(Sequel.object_to_json(obj, *v))}
338
- else
339
- Literal.new(Sequel.object_to_json(objs, *v))
340
- end
345
+ h[key_name] = JsonSerializer.object_to_json_data(public_send(k), *v)
341
346
  end
342
347
  else
343
348
  Array(inc).each do |c|
@@ -347,7 +352,8 @@ module Sequel
347
352
  else
348
353
  key_name = c.to_s
349
354
  end
350
- h[key_name] = public_send(c)
355
+
356
+ h[key_name] = JsonSerializer.object_to_json_data(public_send(c))
351
357
  end
352
358
  end
353
359
  end
@@ -362,6 +368,15 @@ module Sequel
362
368
  h = yield h if block_given?
363
369
  Sequel.object_to_json(h, *a)
364
370
  end
371
+
372
+ # Convert the receiver to a JSON data structure using the given arguments.
373
+ def to_json_data(*args, &block)
374
+ if block
375
+ to_json(*args){|x| return block.call(x)}
376
+ else
377
+ to_json(*args){|x| return x}
378
+ end
379
+ end
365
380
  end
366
381
 
367
382
  module DatasetMethods
@@ -420,7 +435,7 @@ module Sequel
420
435
  else
421
436
  all
422
437
  end
423
- array.map{|obj| Literal.new(Sequel.object_to_json(obj, opts, &opts[:instance_block]))}
438
+ JsonSerializer.object_to_json_data(array, opts, &opts[:instance_block])
424
439
  else
425
440
  all
426
441
  end
@@ -108,9 +108,10 @@ module Sequel
108
108
  # array of the allowable fields.
109
109
  # :limit :: For *_to_many associations, a limit on the number of records
110
110
  # that will be processed, to prevent denial of service attacks.
111
- # :reject_if :: A proc that is given each attribute hash before it is
111
+ # :reject_if :: A proc that is called with each attribute hash before it is
112
112
  # passed to its associated object. If the proc returns a truthy
113
113
  # value, the attribute hash is ignored.
114
+ # :reject_nil :: Ignore nil objects passed to nested attributes setter methods.
114
115
  # :remove :: Allow disassociation of nested records (can remove the associated
115
116
  # object from the parent object, but not destroy the associated object).
116
117
  # :require_modification :: Whether to require modification of nested objects when
@@ -145,9 +146,12 @@ module Sequel
145
146
  # class.
146
147
  def def_nested_attribute_method(reflection)
147
148
  @nested_attributes_module.class_eval do
148
- define_method("#{reflection[:name]}_attributes=") do |v|
149
- set_nested_attributes(reflection[:name], v)
149
+ meth = :"#{reflection[:name]}_attributes="
150
+ assoc = reflection[:name]
151
+ define_method(meth) do |v|
152
+ set_nested_attributes(assoc, v)
150
153
  end
154
+ alias_method meth, meth
151
155
  end
152
156
  end
153
157
  end
@@ -159,6 +163,7 @@ module Sequel
159
163
  def set_nested_attributes(assoc, obj, opts=OPTS)
160
164
  raise(Error, "no association named #{assoc} for #{model.inspect}") unless ref = model.association_reflection(assoc)
161
165
  raise(Error, "nested attributes are not enabled for association #{assoc} for #{model.inspect}") unless meta = ref[:nested_attributes]
166
+ return if obj.nil? && meta[:reject_nil]
162
167
  meta = meta.merge(opts)
163
168
  meta[:reflection] = ref
164
169
  if ref.returns_array?
@@ -426,10 +426,12 @@ module Sequel
426
426
  id_map = {}
427
427
  pkm = opts.primary_key_method
428
428
 
429
- rows.each do |object|
430
- if associated_pks = object.get_column_value(key)
431
- associated_pks.each do |apk|
432
- (id_map[apk] ||= []) << object
429
+ Sequel.synchronize_with(eo[:mutex]) do
430
+ rows.each do |object|
431
+ if associated_pks = object.get_column_value(key)
432
+ associated_pks.each do |apk|
433
+ (id_map[apk] ||= []) << object
434
+ end
433
435
  end
434
436
  end
435
437
  end
@@ -520,7 +522,9 @@ module Sequel
520
522
  def many_to_pg_array_association_filter_expression(op, ref, obj)
521
523
  pk = ref.qualify(model.table_name, ref.primary_key)
522
524
  key = ref[:key]
525
+ # :nocov:
523
526
  expr = case obj
527
+ # :nocov:
524
528
  when Sequel::Model
525
529
  if (assoc_pks = obj.get_column_value(key)) && !assoc_pks.empty?
526
530
  Sequel[pk=>assoc_pks.to_a]
@@ -540,7 +544,9 @@ module Sequel
540
544
  # Support filtering by pg_array_to_many associations using a subquery.
541
545
  def pg_array_to_many_association_filter_expression(op, ref, obj)
542
546
  key = ref.qualify(model.table_name, ref[:key_column])
547
+ # :nocov:
543
548
  expr = case obj
549
+ # :nocov:
544
550
  when Sequel::Model
545
551
  if pkv = obj.get_column_value(ref.primary_key_method)
546
552
  Sequel.pg_array_op(key).contains(Sequel.pg_array([pkv], ref.array_type))
@@ -250,7 +250,9 @@ module Sequel
250
250
  messages = model.pg_auto_constraint_validations_messages
251
251
 
252
252
  unless override
253
+ # :nocov:
253
254
  case e
255
+ # :nocov:
254
256
  when Sequel::NotNullConstraintViolation
255
257
  if column = info[:column]
256
258
  add_pg_constraint_validation_error([m.call(column)], messages[:not_null])
@@ -170,11 +170,13 @@ module Sequel
170
170
  id_map = eo[:id_map]
171
171
  parent_map = {}
172
172
  children_map = {}
173
- eo[:rows].each do |obj|
174
- parent_map[prkey_conv[obj]] = obj
175
- (children_map[key_conv[obj]] ||= []) << obj
176
- obj.associations[ancestors] = []
177
- obj.associations[parent] = nil
173
+ Sequel.synchronize_with(eo[:mutex]) do
174
+ eo[:rows].each do |obj|
175
+ parent_map[prkey_conv[obj]] = obj
176
+ (children_map[key_conv[obj]] ||= []) << obj
177
+ obj.associations[ancestors] = []
178
+ obj.associations[parent] = nil
179
+ end
178
180
  end
179
181
  r = model.association_reflection(ancestors)
180
182
  base_case = model.where(prkey=>id_map.keys).
@@ -207,10 +209,12 @@ module Sequel
207
209
  root.associations[ancestors] << obj
208
210
  end
209
211
  end
210
- parent_map.each do |parent_id, obj|
211
- if children = children_map[parent_id]
212
- children.each do |child|
213
- child.associations[parent] = obj
212
+ Sequel.synchronize_with(eo[:mutex]) do
213
+ parent_map.each do |parent_id, obj|
214
+ if children = children_map[parent_id]
215
+ children.each do |child|
216
+ child.associations[parent] = obj
217
+ end
214
218
  end
215
219
  end
216
220
  end
@@ -268,10 +272,12 @@ module Sequel
268
272
  associations = eo[:associations]
269
273
  parent_map = {}
270
274
  children_map = {}
271
- eo[:rows].each do |obj|
272
- parent_map[prkey_conv[obj]] = obj
273
- obj.associations[descendants] = []
274
- obj.associations[childrena] = []
275
+ Sequel.synchronize_with(eo[:mutex]) do
276
+ eo[:rows].each do |obj|
277
+ parent_map[prkey_conv[obj]] = obj
278
+ obj.associations[descendants] = []
279
+ obj.associations[childrena] = []
280
+ end
275
281
  end
276
282
  r = model.association_reflection(descendants)
277
283
  base_case = model.where(key=>id_map.keys).
@@ -316,12 +322,14 @@ module Sequel
316
322
 
317
323
  (children_map[key_conv[obj]] ||= []) << obj
318
324
  end
319
- children_map.each do |parent_id, objs|
320
- objs = objs.uniq
321
- parent_obj = parent_map[parent_id]
322
- parent_obj.associations[childrena] = objs
323
- objs.each do |obj|
324
- obj.associations[parent] = parent_obj
325
+ Sequel.synchronize_with(eo[:mutex]) do
326
+ children_map.each do |parent_id, objs|
327
+ objs = objs.uniq
328
+ parent_obj = parent_map[parent_id]
329
+ parent_obj.associations[childrena] = objs
330
+ objs.each do |obj|
331
+ obj.associations[parent] = parent_obj
332
+ end
325
333
  end
326
334
  end
327
335
  end