sequel 5.40.0 → 5.45.0

Sign up to get free protection for your applications and to get access to all the features.
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