sequel 5.40.0 → 5.45.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +52 -0
  3. data/MIT-LICENSE +1 -1
  4. data/doc/release_notes/5.41.0.txt +25 -0
  5. data/doc/release_notes/5.42.0.txt +136 -0
  6. data/doc/release_notes/5.43.0.txt +98 -0
  7. data/doc/release_notes/5.44.0.txt +32 -0
  8. data/doc/release_notes/5.45.0.txt +34 -0
  9. data/doc/sql.rdoc +1 -1
  10. data/doc/testing.rdoc +3 -0
  11. data/doc/virtual_rows.rdoc +1 -1
  12. data/lib/sequel/adapters/ado.rb +16 -16
  13. data/lib/sequel/adapters/odbc.rb +5 -1
  14. data/lib/sequel/adapters/shared/postgres.rb +4 -14
  15. data/lib/sequel/adapters/shared/sqlite.rb +8 -4
  16. data/lib/sequel/core.rb +11 -0
  17. data/lib/sequel/database/misc.rb +1 -2
  18. data/lib/sequel/database/schema_generator.rb +35 -47
  19. data/lib/sequel/database/schema_methods.rb +4 -0
  20. data/lib/sequel/dataset/query.rb +1 -3
  21. data/lib/sequel/dataset/sql.rb +7 -0
  22. data/lib/sequel/extensions/async_thread_pool.rb +438 -0
  23. data/lib/sequel/extensions/blank.rb +2 -0
  24. data/lib/sequel/extensions/date_arithmetic.rb +32 -23
  25. data/lib/sequel/extensions/inflector.rb +2 -0
  26. data/lib/sequel/extensions/named_timezones.rb +5 -1
  27. data/lib/sequel/extensions/pg_enum.rb +1 -1
  28. data/lib/sequel/extensions/pg_interval.rb +12 -2
  29. data/lib/sequel/extensions/pg_loose_count.rb +3 -1
  30. data/lib/sequel/model/associations.rb +70 -14
  31. data/lib/sequel/model/base.rb +2 -2
  32. data/lib/sequel/plugins/async_thread_pool.rb +39 -0
  33. data/lib/sequel/plugins/auto_validations.rb +15 -1
  34. data/lib/sequel/plugins/auto_validations_constraint_validations_presence_message.rb +68 -0
  35. data/lib/sequel/plugins/column_encryption.rb +728 -0
  36. data/lib/sequel/plugins/composition.rb +2 -1
  37. data/lib/sequel/plugins/concurrent_eager_loading.rb +174 -0
  38. data/lib/sequel/plugins/json_serializer.rb +37 -22
  39. data/lib/sequel/plugins/nested_attributes.rb +5 -2
  40. data/lib/sequel/plugins/pg_array_associations.rb +6 -4
  41. data/lib/sequel/plugins/rcte_tree.rb +27 -19
  42. data/lib/sequel/plugins/serialization.rb +8 -3
  43. data/lib/sequel/plugins/serialization_modification_detection.rb +1 -1
  44. data/lib/sequel/plugins/validation_helpers.rb +6 -2
  45. data/lib/sequel/version.rb +1 -1
  46. metadata +18 -3
@@ -7,9 +7,11 @@
7
7
  # Sequel.extension :blank
8
8
 
9
9
  [FalseClass, Object, NilClass, Numeric, String, TrueClass].each do |klass|
10
+ # :nocov:
10
11
  if klass.method_defined?(:blank?)
11
12
  klass.send(:alias_method, :blank?, :blank?)
12
13
  end
14
+ # :nocov:
13
15
  end
14
16
 
15
17
  class FalseClass
@@ -8,9 +8,10 @@
8
8
  # DB.extension :date_arithmetic
9
9
  #
10
10
  # Then you can use the Sequel.date_add and Sequel.date_sub methods
11
- # to return Sequel expressions:
11
+ # to return Sequel expressions (this example shows the only supported
12
+ # keys for the second argument):
12
13
  #
13
- # add = Sequel.date_add(:date_column, years: 1, months: 2, days: 3)
14
+ # add = Sequel.date_add(:date_column, years: 1, months: 2, weeks: 2, days: 1)
14
15
  # sub = Sequel.date_sub(:date_column, hours: 1, minutes: 2, seconds: 3)
15
16
  #
16
17
  # In addition to specifying the interval as a hash, there is also
@@ -52,14 +53,9 @@ module Sequel
52
53
  if defined?(ActiveSupport::Duration) && interval.is_a?(ActiveSupport::Duration)
53
54
  interval = interval.parts
54
55
  end
55
- interval = if interval.is_a?(Enumerable)
56
- h = {}
57
- interval.each{|k,v| h[k] = -v unless v.nil?}
58
- h
59
- else
60
- -interval
61
- end
62
- DateAdd.new(expr, interval, opts)
56
+ parts = {}
57
+ interval.each{|k,v| parts[k] = -v unless v.nil?}
58
+ DateAdd.new(expr, parts, opts)
63
59
  end
64
60
  end
65
61
 
@@ -189,22 +185,35 @@ module Sequel
189
185
  # ActiveSupport::Duration :: Converted to a hash using the interval's parts.
190
186
  def initialize(expr, interval, opts=OPTS)
191
187
  @expr = expr
192
- @interval = if interval.is_a?(Hash)
193
- interval.each_value do |v|
194
- # Attempt to prevent SQL injection by users who pass untrusted strings
195
- # as interval values.
196
- if v.is_a?(String) && !v.is_a?(LiteralString)
197
- raise Sequel::InvalidValue, "cannot provide String value as interval part: #{v.inspect}"
198
- end
188
+
189
+ h = Hash.new(0)
190
+ interval = interval.parts unless interval.is_a?(Hash)
191
+ interval.each do |unit, value|
192
+ # skip nil values
193
+ next unless value
194
+
195
+ # Convert weeks to days, as ActiveSupport::Duration can use weeks,
196
+ # but the database-specific literalizers only support days.
197
+ if unit == :weeks
198
+ unit = :days
199
+ value *= 7
200
+ end
201
+
202
+ unless DatasetMethods::DURATION_UNITS.include?(unit)
203
+ raise Sequel::Error, "Invalid key used in DateAdd interval hash: #{unit.inspect}"
204
+ end
205
+
206
+ # Attempt to prevent SQL injection by users who pass untrusted strings
207
+ # as interval values. It doesn't make sense to support literal strings,
208
+ # due to the numeric adding below.
209
+ if value.is_a?(String)
210
+ raise Sequel::InvalidValue, "cannot provide String value as interval part: #{value.inspect}"
199
211
  end
200
- Hash[interval]
201
- else
202
- h = Hash.new(0)
203
- interval.parts.each{|unit, value| h[unit] += value}
204
- Hash[h]
212
+
213
+ h[unit] += value
205
214
  end
206
215
 
207
- @interval.freeze
216
+ @interval = Hash[h].freeze
208
217
  @cast_type = opts[:cast] if opts[:cast]
209
218
  freeze
210
219
  end
@@ -107,9 +107,11 @@ class String
107
107
  end
108
108
 
109
109
  %w'classify constantize dasherize demodulize foreign_key humanize pluralize singularize tableize underscore'.each do |m|
110
+ # :nocov:
110
111
  if method_defined?(m)
111
112
  alias_method(m, m)
112
113
  end
114
+ # :nocov:
113
115
  end
114
116
 
115
117
  # By default, camelize converts the string to UpperCamelCase. If the argument to camelize
@@ -84,9 +84,9 @@ module Sequel
84
84
  def convert_output_time_other(v, output_timezone)
85
85
  Time.at(v.to_i, :in => output_timezone)
86
86
  end
87
- else
88
87
  # :nodoc:
89
88
  # :nocov:
89
+ else
90
90
  def convert_input_time_other(v, input_timezone)
91
91
  local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset
92
92
  Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i
@@ -105,6 +105,8 @@ module Sequel
105
105
  Time.new(1970, 1, 1, 0, 0, 0, local_offset) + v.to_i
106
106
  end
107
107
  end
108
+ # :nodoc:
109
+ # :nocov:
108
110
  end
109
111
 
110
112
  # Handle both TZInfo 1 and TZInfo 2
@@ -142,6 +144,8 @@ module Sequel
142
144
  # Convert timezone offset from UTC to the offset for the output_timezone
143
145
  (v - local_offset).new_offset(local_offset)
144
146
  end
147
+ # :nodoc:
148
+ # :nocov:
145
149
  end
146
150
 
147
151
  # Returns TZInfo::Timezone instance if given a String.
@@ -42,7 +42,7 @@
42
42
  #
43
43
  # This extension integrates with the pg_array extension. If you plan
44
44
  # to use arrays of enum types, load the pg_array extension before the
45
- # pg_interval extension:
45
+ # pg_enum extension:
46
46
  #
47
47
  # DB.extension :pg_array, :pg_enum
48
48
  #
@@ -71,6 +71,16 @@ module Sequel
71
71
  # Whether ActiveSupport::Duration.new takes parts as array instead of hash
72
72
  USE_PARTS_ARRAY = !defined?(ActiveSupport::VERSION::STRING) || ActiveSupport::VERSION::STRING < '5.1'
73
73
 
74
+ if defined?(ActiveSupport::Duration::SECONDS_PER_MONTH)
75
+ SECONDS_PER_MONTH = ActiveSupport::Duration::SECONDS_PER_MONTH
76
+ SECONDS_PER_YEAR = ActiveSupport::Duration::SECONDS_PER_YEAR
77
+ # :nocov:
78
+ else
79
+ SECONDS_PER_MONTH = 2592000
80
+ SECONDS_PER_YEAR = 31557600
81
+ # :nocov:
82
+ end
83
+
74
84
  # Parse the interval input string into an ActiveSupport::Duration instance.
75
85
  def call(string)
76
86
  raise(InvalidValue, "invalid or unhandled interval format: #{string.inspect}") unless matches = /\A([+-]?\d+ years?\s?)?([+-]?\d+ mons?\s?)?([+-]?\d+ days?\s?)?(?:(?:([+-])?(\d{2,10}):(\d\d):(\d\d(\.\d+)?))|([+-]?\d+ hours?\s?)?([+-]?\d+ mins?\s?)?([+-]?\d+(\.\d+)? secs?\s?)?)?\z/.match(string)
@@ -80,12 +90,12 @@ module Sequel
80
90
 
81
91
  if v = matches[1]
82
92
  v = v.to_i
83
- value += 31557600 * v
93
+ value += SECONDS_PER_YEAR * v
84
94
  parts[:years] = v
85
95
  end
86
96
  if v = matches[2]
87
97
  v = v.to_i
88
- value += 2592000 * v
98
+ value += SECONDS_PER_MONTH * v
89
99
  parts[:months] = v
90
100
  end
91
101
  if v = matches[3]
@@ -12,7 +12,9 @@
12
12
  #
13
13
  # How accurate this count is depends on the number of rows
14
14
  # added/deleted from the table since the last time it was
15
- # analyzed.
15
+ # analyzed. If the table has not been vacuumed or analyzed
16
+ # yet, this can return 0 or -1 depending on the PostgreSQL
17
+ # version in use.
16
18
  #
17
19
  # To load the extension into the database:
18
20
  #
@@ -263,7 +263,9 @@ module Sequel
263
263
  # yielding each row to the block.
264
264
  def eager_load_results(eo, &block)
265
265
  rows = eo[:rows]
266
- initialize_association_cache(rows) unless eo[:initialize_rows] == false
266
+ unless eo[:initialize_rows] == false
267
+ Sequel.synchronize_with(eo[:mutex]){initialize_association_cache(rows)}
268
+ end
267
269
  if eo[:id_map]
268
270
  ids = eo[:id_map].keys
269
271
  return ids if ids.empty?
@@ -311,7 +313,8 @@ module Sequel
311
313
  objects = loader.all(ids)
312
314
  end
313
315
 
314
- objects.each(&block)
316
+ Sequel.synchronize_with(eo[:mutex]){objects.each(&block)}
317
+
315
318
  if strategy == :ruby
316
319
  apply_ruby_eager_limit_strategy(rows, eager_limit || limit_and_offset)
317
320
  end
@@ -1929,8 +1932,22 @@ module Sequel
1929
1932
  # can be easily overridden in the class itself while allowing for
1930
1933
  # super to be called.
1931
1934
  def association_module_def(name, opts=OPTS, &block)
1932
- association_module(opts).send(:define_method, name, &block)
1933
- association_module(opts).send(:alias_method, name, name)
1935
+ mod = association_module(opts)
1936
+ mod.send(:define_method, name, &block)
1937
+ mod.send(:alias_method, name, name)
1938
+ end
1939
+
1940
+ # Add a method to the module included in the class, so the method
1941
+ # can be easily overridden in the class itself while allowing for
1942
+ # super to be called. This method allows passing keywords through
1943
+ # the defined methods.
1944
+ def association_module_delegate_def(name, opts, &block)
1945
+ mod = association_module(opts)
1946
+ mod.send(:define_method, name, &block)
1947
+ # :nocov:
1948
+ mod.send(:ruby2_keywords, name) if mod.respond_to?(:ruby2_keywords, true)
1949
+ # :nocov:
1950
+ mod.send(:alias_method, name, name)
1934
1951
  end
1935
1952
 
1936
1953
  # Add a private method to the module included in the class.
@@ -1982,17 +1999,17 @@ module Sequel
1982
1999
 
1983
2000
  if adder = opts[:adder]
1984
2001
  association_module_private_def(opts[:_add_method], opts, &adder)
1985
- association_module_def(opts[:add_method], opts){|o,*args| add_associated_object(opts, o, *args)}
2002
+ association_module_delegate_def(opts[:add_method], opts){|o,*args| add_associated_object(opts, o, *args)}
1986
2003
  end
1987
2004
 
1988
2005
  if remover = opts[:remover]
1989
2006
  association_module_private_def(opts[:_remove_method], opts, &remover)
1990
- association_module_def(opts[:remove_method], opts){|o,*args| remove_associated_object(opts, o, *args)}
2007
+ association_module_delegate_def(opts[:remove_method], opts){|o,*args| remove_associated_object(opts, o, *args)}
1991
2008
  end
1992
2009
 
1993
2010
  if clearer = opts[:clearer]
1994
2011
  association_module_private_def(opts[:_remove_all_method], opts, &clearer)
1995
- association_module_def(opts[:remove_all_method], opts){|*args| remove_all_associated_objects(opts, *args)}
2012
+ association_module_delegate_def(opts[:remove_all_method], opts){|*args| remove_all_associated_objects(opts, *args)}
1996
2013
  end
1997
2014
  end
1998
2015
 
@@ -2424,6 +2441,9 @@ module Sequel
2424
2441
  run_association_callbacks(opts, :after_add, o)
2425
2442
  o
2426
2443
  end
2444
+ # :nocov:
2445
+ ruby2_keywords(:add_associated_object) if respond_to?(:ruby2_keywords, true)
2446
+ # :nocov:
2427
2447
 
2428
2448
  # Add/Set the current object to/as the given object's reciprocal association.
2429
2449
  def add_reciprocal_object(opts, o)
@@ -2566,6 +2586,9 @@ module Sequel
2566
2586
  associations[opts[:name]] = []
2567
2587
  ret
2568
2588
  end
2589
+ # :nocov:
2590
+ ruby2_keywords(:remove_all_associated_objects) if respond_to?(:ruby2_keywords, true)
2591
+ # :nocov:
2569
2592
 
2570
2593
  # Remove the given associated object from the given association
2571
2594
  def remove_associated_object(opts, o, *args)
@@ -2587,6 +2610,9 @@ module Sequel
2587
2610
  run_association_callbacks(opts, :after_remove, o)
2588
2611
  o
2589
2612
  end
2613
+ # :nocov:
2614
+ ruby2_keywords(:remove_associated_object) if respond_to?(:ruby2_keywords, true)
2615
+ # :nocov:
2590
2616
 
2591
2617
  # Check that the object from the associated table specified by the primary key
2592
2618
  # is currently associated to the receiver. If it is associated, return the object, otherwise
@@ -2985,6 +3011,8 @@ module Sequel
2985
3011
  # You can specify an custom alias and/or join type on a per-association basis by providing an
2986
3012
  # Sequel::SQL::AliasedExpression object instead of an a Symbol for the association name.
2987
3013
  #
3014
+ # You cannot mix calls to +eager_graph+ and +graph+ on the same dataset.
3015
+ #
2988
3016
  # Examples:
2989
3017
  #
2990
3018
  # # For each album, eager_graph load the artist
@@ -3351,15 +3379,30 @@ module Sequel
3351
3379
  egl.dup
3352
3380
  end
3353
3381
 
3354
- # Eagerly load all specified associations
3382
+ # Eagerly load all specified associations.
3355
3383
  def eager_load(a, eager_assoc=@opts[:eager])
3356
3384
  return if a.empty?
3385
+
3386
+ # Reflections for all associations to eager load
3387
+ reflections = eager_assoc.keys.map{|assoc| model.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")}
3388
+
3389
+ perform_eager_loads(prepare_eager_load(a, reflections, eager_assoc))
3390
+
3391
+ reflections.each do |r|
3392
+ a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} if r[:after_load]
3393
+ end
3394
+
3395
+ nil
3396
+ end
3397
+
3398
+ # Prepare a hash loaders and eager options which will be used to implement the eager loading.
3399
+ def prepare_eager_load(a, reflections, eager_assoc)
3400
+ eager_load_data = {}
3401
+
3357
3402
  # Key is foreign/primary key name symbol.
3358
3403
  # Value is hash with keys being foreign/primary key values (generally integers)
3359
3404
  # and values being an array of current model objects with that specific foreign/primary key
3360
3405
  key_hash = {}
3361
- # Reflections for all associations to eager load
3362
- reflections = eager_assoc.keys.map{|assoc| model.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")}
3363
3406
 
3364
3407
  # Populate the key_hash entry for each association being eagerly loaded
3365
3408
  reflections.each do |r|
@@ -3390,7 +3433,6 @@ module Sequel
3390
3433
  id_map = nil
3391
3434
  end
3392
3435
 
3393
- loader = r[:eager_loader]
3394
3436
  associations = eager_assoc[r[:name]]
3395
3437
  if associations.respond_to?(:call)
3396
3438
  eager_block = associations
@@ -3398,9 +3440,23 @@ module Sequel
3398
3440
  elsif associations.is_a?(Hash) && associations.length == 1 && (pr_assoc = associations.to_a.first) && pr_assoc.first.respond_to?(:call)
3399
3441
  eager_block, associations = pr_assoc
3400
3442
  end
3401
- loader.call(:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block, :id_map=>id_map)
3402
- a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} if r[:after_load]
3403
- end
3443
+
3444
+ eager_load_data[r[:eager_loader]] = {:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block, :id_map=>id_map}
3445
+ end
3446
+
3447
+ eager_load_data
3448
+ end
3449
+
3450
+ # Using the hash of loaders and eager options, perform the eager loading.
3451
+ def perform_eager_loads(eager_load_data)
3452
+ eager_load_data.map do |loader, eo|
3453
+ perform_eager_load(loader, eo)
3454
+ end
3455
+ end
3456
+
3457
+ # Perform eager loading for a single association using the loader and eager options.
3458
+ def perform_eager_load(loader, eo)
3459
+ loader.call(eo)
3404
3460
  end
3405
3461
 
3406
3462
  # Return a subquery expression for filering by a many_to_many association
@@ -1260,12 +1260,12 @@ module Sequel
1260
1260
  # Once an object is frozen, you cannot modify it's values, changed_columns,
1261
1261
  # errors, or dataset.
1262
1262
  def freeze
1263
- values.freeze
1264
- _changed_columns.freeze
1265
1263
  unless errors.frozen?
1266
1264
  validate
1267
1265
  errors.freeze
1268
1266
  end
1267
+ values.freeze
1268
+ _changed_columns.freeze
1269
1269
  this if !new? && model.primary_key
1270
1270
  super
1271
1271
  end
@@ -0,0 +1,39 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ extension 'async_thread_pool'
5
+
6
+ module Plugins
7
+ # The async_thread_pool plugin makes it slightly easier to use the async_thread_pool
8
+ # Database extension with models. It makes Model.async return an async dataset for the
9
+ # model, and support async behavior for #destroy, #with_pk, and #with_pk! for model
10
+ # datasets:
11
+ #
12
+ # # Will load the artist with primary key 1 asynchronously
13
+ # artist = Artist.async.with_pk(1)
14
+ #
15
+ # You must load the async_thread_pool Database extension into the Database object the
16
+ # model class uses in order for async behavior to work.
17
+ #
18
+ # Usage:
19
+ #
20
+ # # Make all model subclass datasets support support async class methods and additional
21
+ # # async dataset methods
22
+ # Sequel::Model.plugin :async_thread_pool
23
+ #
24
+ # # Make the Album class support async class method and additional async dataset methods
25
+ # Album.plugin :async_thread_pool
26
+ module AsyncThreadPool
27
+ module ClassMethods
28
+ Plugins.def_dataset_methods(self, :async)
29
+ end
30
+
31
+ module DatasetMethods
32
+ [:destroy, :with_pk, :with_pk!].each do |meth|
33
+ ::Sequel::Database::AsyncThreadPool::DatasetMethods.define_async_method(self, meth)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
@@ -14,7 +14,9 @@ module Sequel
14
14
  # the plugin looks at the database schema for the model's table. To determine
15
15
  # the unique validations, Sequel looks at the indexes on the table. In order
16
16
  # for this plugin to be fully functional, the underlying database adapter needs
17
- # to support both schema and index parsing.
17
+ # to support both schema and index parsing. Additionally, unique validations are
18
+ # only added for models that select from a simple table, they are not added for models
19
+ # that select from a subquery or joined dataset.
18
20
  #
19
21
  # This plugin uses the validation_helpers plugin underneath to implement the
20
22
  # validations. It does not allow for any per-column validation message
@@ -51,6 +53,11 @@ module Sequel
51
53
  # This works for unique_opts, max_length_opts, schema_types_opts,
52
54
  # explicit_not_null_opts, and not_null_opts.
53
55
  #
56
+ # If you only want auto_validations to add validations to columns that do not already
57
+ # have an error associated with them, you can use the skip_invalid option:
58
+ #
59
+ # Model.plugin :auto_validations, skip_invalid: true
60
+ #
54
61
  # Usage:
55
62
  #
56
63
  # # Make all model subclass use auto validations (called before loading subclasses)
@@ -100,6 +107,13 @@ module Sequel
100
107
  h[type] = h[type].merge(type_opts).freeze
101
108
  end
102
109
  end
110
+
111
+ if opts[:skip_invalid]
112
+ [:not_null, :explicit_not_null, :max_length, :schema_types].each do |type|
113
+ h[type] = h[type].merge(:skip_invalid=>true).freeze
114
+ end
115
+ end
116
+
103
117
  @auto_validate_options = h.freeze
104
118
  end
105
119
  end