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
@@ -6,6 +6,14 @@
6
6
  #
7
7
  # Sequel.extension :blank
8
8
 
9
+ [FalseClass, Object, NilClass, Numeric, String, TrueClass].each do |klass|
10
+ # :nocov:
11
+ if klass.method_defined?(:blank?)
12
+ klass.send(:alias_method, :blank?, :blank?)
13
+ end
14
+ # :nocov:
15
+ end
16
+
9
17
  class FalseClass
10
18
  # false is always blank
11
19
  def blank?
@@ -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
@@ -49,14 +50,12 @@ module Sequel
49
50
  # Options:
50
51
  # :cast :: Cast to the specified type instead of the default if casting
51
52
  def date_sub(expr, interval, opts=OPTS)
52
- interval = if interval.is_a?(Hash)
53
- h = {}
54
- interval.each{|k,v| h[k] = -v unless v.nil?}
55
- h
56
- else
57
- -interval
53
+ if defined?(ActiveSupport::Duration) && interval.is_a?(ActiveSupport::Duration)
54
+ interval = interval.parts
58
55
  end
59
- DateAdd.new(expr, interval, opts)
56
+ parts = {}
57
+ interval.each{|k,v| parts[k] = -v unless v.nil?}
58
+ DateAdd.new(expr, parts, opts)
60
59
  end
61
60
  end
62
61
 
@@ -113,12 +112,12 @@ module Sequel
113
112
  end
114
113
  when :mssql, :h2, :access, :sqlanywhere
115
114
  units = case db_type
116
- when :mssql, :sqlanywhere
117
- MSSQL_DURATION_UNITS
118
115
  when :h2
119
116
  H2_DURATION_UNITS
120
117
  when :access
121
118
  ACCESS_DURATION_UNITS
119
+ else
120
+ MSSQL_DURATION_UNITS
122
121
  end
123
122
  each_valid_interval_unit(h, units) do |value, sql_unit|
124
123
  expr = Sequel.function(:DATEADD, sql_unit, value, expr)
@@ -186,22 +185,35 @@ module Sequel
186
185
  # ActiveSupport::Duration :: Converted to a hash using the interval's parts.
187
186
  def initialize(expr, interval, opts=OPTS)
188
187
  @expr = expr
189
- @interval = if interval.is_a?(Hash)
190
- interval.each_value do |v|
191
- # Attempt to prevent SQL injection by users who pass untrusted strings
192
- # as interval values.
193
- if v.is_a?(String) && !v.is_a?(LiteralString)
194
- raise Sequel::InvalidValue, "cannot provide String value as interval part: #{v.inspect}"
195
- 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
196
200
  end
197
- Hash[interval]
198
- else
199
- h = Hash.new(0)
200
- interval.parts.each{|unit, value| h[unit] += value}
201
- Hash[h]
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}"
211
+ end
212
+
213
+ h[unit] += value
202
214
  end
203
215
 
204
- @interval.freeze
216
+ @interval = Hash[h].freeze
205
217
  @cast_type = opts[:cast] if opts[:cast]
206
218
  freeze
207
219
  end
@@ -55,6 +55,8 @@ module Sequel
55
55
 
56
56
  module SQL
57
57
  class Expression
58
+ alias inspect inspect
59
+
58
60
  # Attempt to produce a string suitable for eval, such that:
59
61
  #
60
62
  # eval(obj.inspect) == obj
@@ -105,6 +105,14 @@ class String
105
105
  yield Inflections if block_given?
106
106
  Inflections
107
107
  end
108
+
109
+ %w'classify constantize dasherize demodulize foreign_key humanize pluralize singularize tableize underscore'.each do |m|
110
+ # :nocov:
111
+ if method_defined?(m)
112
+ alias_method(m, m)
113
+ end
114
+ # :nocov:
115
+ end
108
116
 
109
117
  # By default, camelize converts the string to UpperCamelCase. If the argument to camelize
110
118
  # is set to :lower then camelize produces lowerCamelCase.
@@ -68,7 +68,9 @@ module Sequel
68
68
  # Allow calling private methods for backwards compatibility
69
69
  @db.send(method_sym, *args, &block)
70
70
  end
71
+ # :nocov:
71
72
  ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
73
+ # :nocov:
72
74
 
73
75
  # This object responds to all methods the database responds to.
74
76
  def respond_to_missing?(meth, include_private)
@@ -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.
@@ -213,6 +213,7 @@ module Sequel
213
213
  scalar_typecast_method = :"typecast_value_#{opts.fetch(:scalar_typecast, type)}"
214
214
  define_method(meth){|v| typecast_value_pg_array(v, creator, scalar_typecast_method)}
215
215
  private meth
216
+ alias_method(meth, meth)
216
217
  end
217
218
 
218
219
  @schema_type_classes[:"#{type}_array"] = PGArray
@@ -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
  #
@@ -34,6 +34,13 @@
34
34
 
35
35
  require 'active_support/duration'
36
36
 
37
+ # :nocov:
38
+ begin
39
+ require 'active_support/version'
40
+ rescue LoadError
41
+ end
42
+ # :nocov:
43
+
37
44
  module Sequel
38
45
  module Postgres
39
46
  module IntervalDatabaseMethods
@@ -61,34 +68,47 @@ module Sequel
61
68
 
62
69
  # Creates callable objects that convert strings into ActiveSupport::Duration instances.
63
70
  class Parser
71
+ # Whether ActiveSupport::Duration.new takes parts as array instead of hash
72
+ USE_PARTS_ARRAY = !defined?(ActiveSupport::VERSION::STRING) || ActiveSupport::VERSION::STRING < '5.1'
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
+
64
84
  # Parse the interval input string into an ActiveSupport::Duration instance.
65
85
  def call(string)
66
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)
67
87
 
68
88
  value = 0
69
- parts = []
89
+ parts = {}
70
90
 
71
91
  if v = matches[1]
72
92
  v = v.to_i
73
- value += 31557600 * v
74
- parts << [:years, v]
93
+ value += SECONDS_PER_YEAR * v
94
+ parts[:years] = v
75
95
  end
76
96
  if v = matches[2]
77
97
  v = v.to_i
78
- value += 2592000 * v
79
- parts << [:months, v]
98
+ value += SECONDS_PER_MONTH * v
99
+ parts[:months] = v
80
100
  end
81
101
  if v = matches[3]
82
102
  v = v.to_i
83
103
  value += 86400 * v
84
- parts << [:days, v]
104
+ parts[:days] = v
85
105
  end
86
106
  if matches[5]
87
107
  seconds = matches[5].to_i * 3600 + matches[6].to_i * 60
88
108
  seconds += matches[8] ? matches[7].to_f : matches[7].to_i
89
109
  seconds *= -1 if matches[4] == '-'
90
110
  value += seconds
91
- parts << [:seconds, seconds]
111
+ parts[:seconds] = seconds
92
112
  elsif matches[9] || matches[10] || matches[11]
93
113
  seconds = 0
94
114
  if v = matches[9]
@@ -101,8 +121,14 @@ module Sequel
101
121
  seconds += matches[12] ? v.to_f : v.to_i
102
122
  end
103
123
  value += seconds
104
- parts << [:seconds, seconds]
124
+ parts[:seconds] = seconds
125
+ end
126
+
127
+ # :nocov:
128
+ if USE_PARTS_ARRAY
129
+ parts = parts.to_a
105
130
  end
131
+ # :nocov:
106
132
 
107
133
  ActiveSupport::Duration.new(value, parts)
108
134
  end
@@ -482,6 +482,7 @@ module Sequel
482
482
  row_type(db_type, v)
483
483
  end
484
484
  private meth
485
+ alias_method(meth, meth)
485
486
  end
486
487
 
487
488
  nil
@@ -74,7 +74,9 @@ module Sequel
74
74
  raise(Sequel::Error, "method #{method.inspect} did not return a dataset") unless @dataset.is_a?(Dataset)
75
75
  self
76
76
  end
77
+ # :nocov:
77
78
  ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
79
+ # :nocov:
78
80
  end
79
81
  end
80
82
 
@@ -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,7 +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)
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)
1933
1951
  end
1934
1952
 
1935
1953
  # Add a private method to the module included in the class.
@@ -1981,17 +1999,17 @@ module Sequel
1981
1999
 
1982
2000
  if adder = opts[:adder]
1983
2001
  association_module_private_def(opts[:_add_method], opts, &adder)
1984
- 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)}
1985
2003
  end
1986
2004
 
1987
2005
  if remover = opts[:remover]
1988
2006
  association_module_private_def(opts[:_remove_method], opts, &remover)
1989
- 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)}
1990
2008
  end
1991
2009
 
1992
2010
  if clearer = opts[:clearer]
1993
2011
  association_module_private_def(opts[:_remove_all_method], opts, &clearer)
1994
- 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)}
1995
2013
  end
1996
2014
  end
1997
2015
 
@@ -2423,6 +2441,9 @@ module Sequel
2423
2441
  run_association_callbacks(opts, :after_add, o)
2424
2442
  o
2425
2443
  end
2444
+ # :nocov:
2445
+ ruby2_keywords(:add_associated_object) if respond_to?(:ruby2_keywords, true)
2446
+ # :nocov:
2426
2447
 
2427
2448
  # Add/Set the current object to/as the given object's reciprocal association.
2428
2449
  def add_reciprocal_object(opts, o)
@@ -2565,6 +2586,9 @@ module Sequel
2565
2586
  associations[opts[:name]] = []
2566
2587
  ret
2567
2588
  end
2589
+ # :nocov:
2590
+ ruby2_keywords(:remove_all_associated_objects) if respond_to?(:ruby2_keywords, true)
2591
+ # :nocov:
2568
2592
 
2569
2593
  # Remove the given associated object from the given association
2570
2594
  def remove_associated_object(opts, o, *args)
@@ -2586,6 +2610,9 @@ module Sequel
2586
2610
  run_association_callbacks(opts, :after_remove, o)
2587
2611
  o
2588
2612
  end
2613
+ # :nocov:
2614
+ ruby2_keywords(:remove_associated_object) if respond_to?(:ruby2_keywords, true)
2615
+ # :nocov:
2589
2616
 
2590
2617
  # Check that the object from the associated table specified by the primary key
2591
2618
  # is currently associated to the receiver. If it is associated, return the object, otherwise
@@ -3350,15 +3377,30 @@ module Sequel
3350
3377
  egl.dup
3351
3378
  end
3352
3379
 
3353
- # Eagerly load all specified associations
3380
+ # Eagerly load all specified associations.
3354
3381
  def eager_load(a, eager_assoc=@opts[:eager])
3355
3382
  return if a.empty?
3383
+
3384
+ # Reflections for all associations to eager load
3385
+ reflections = eager_assoc.keys.map{|assoc| model.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")}
3386
+
3387
+ perform_eager_loads(prepare_eager_load(a, reflections, eager_assoc))
3388
+
3389
+ reflections.each do |r|
3390
+ a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} if r[:after_load]
3391
+ end
3392
+
3393
+ nil
3394
+ end
3395
+
3396
+ # Prepare a hash loaders and eager options which will be used to implement the eager loading.
3397
+ def prepare_eager_load(a, reflections, eager_assoc)
3398
+ eager_load_data = {}
3399
+
3356
3400
  # Key is foreign/primary key name symbol.
3357
3401
  # Value is hash with keys being foreign/primary key values (generally integers)
3358
3402
  # and values being an array of current model objects with that specific foreign/primary key
3359
3403
  key_hash = {}
3360
- # Reflections for all associations to eager load
3361
- reflections = eager_assoc.keys.map{|assoc| model.association_reflection(assoc) || (raise Sequel::UndefinedAssociation, "Model: #{self}, Association: #{assoc}")}
3362
3404
 
3363
3405
  # Populate the key_hash entry for each association being eagerly loaded
3364
3406
  reflections.each do |r|
@@ -3389,7 +3431,6 @@ module Sequel
3389
3431
  id_map = nil
3390
3432
  end
3391
3433
 
3392
- loader = r[:eager_loader]
3393
3434
  associations = eager_assoc[r[:name]]
3394
3435
  if associations.respond_to?(:call)
3395
3436
  eager_block = associations
@@ -3397,9 +3438,23 @@ module Sequel
3397
3438
  elsif associations.is_a?(Hash) && associations.length == 1 && (pr_assoc = associations.to_a.first) && pr_assoc.first.respond_to?(:call)
3398
3439
  eager_block, associations = pr_assoc
3399
3440
  end
3400
- loader.call(:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block, :id_map=>id_map)
3401
- a.each{|object| object.send(:run_association_callbacks, r, :after_load, object.associations[r[:name]])} if r[:after_load]
3402
- end
3441
+
3442
+ eager_load_data[r[:eager_loader]] = {:key_hash=>key_hash, :rows=>a, :associations=>associations, :self=>self, :eager_block=>eager_block, :id_map=>id_map}
3443
+ end
3444
+
3445
+ eager_load_data
3446
+ end
3447
+
3448
+ # Using the hash of loaders and eager options, perform the eager loading.
3449
+ def perform_eager_loads(eager_load_data)
3450
+ eager_load_data.map do |loader, eo|
3451
+ perform_eager_load(loader, eo)
3452
+ end
3453
+ end
3454
+
3455
+ # Perform eager loading for a single association using the loader and eager options.
3456
+ def perform_eager_load(loader, eo)
3457
+ loader.call(eo)
3403
3458
  end
3404
3459
 
3405
3460
  # Return a subquery expression for filering by a many_to_many association