sequel 5.60.1 → 5.62.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +44 -0
  3. data/README.rdoc +20 -19
  4. data/doc/advanced_associations.rdoc +13 -13
  5. data/doc/association_basics.rdoc +21 -15
  6. data/doc/cheat_sheet.rdoc +3 -3
  7. data/doc/model_hooks.rdoc +1 -1
  8. data/doc/object_model.rdoc +8 -8
  9. data/doc/opening_databases.rdoc +4 -4
  10. data/doc/postgresql.rdoc +8 -8
  11. data/doc/querying.rdoc +1 -1
  12. data/doc/release_notes/5.61.0.txt +43 -0
  13. data/doc/release_notes/5.62.0.txt +132 -0
  14. data/doc/schema_modification.rdoc +1 -1
  15. data/doc/security.rdoc +9 -9
  16. data/doc/sql.rdoc +13 -13
  17. data/doc/testing.rdoc +13 -11
  18. data/doc/transactions.rdoc +6 -6
  19. data/doc/virtual_rows.rdoc +1 -1
  20. data/lib/sequel/adapters/postgres.rb +4 -0
  21. data/lib/sequel/adapters/shared/access.rb +9 -1
  22. data/lib/sequel/adapters/shared/mssql.rb +9 -5
  23. data/lib/sequel/adapters/shared/mysql.rb +7 -0
  24. data/lib/sequel/adapters/shared/oracle.rb +7 -0
  25. data/lib/sequel/adapters/shared/postgres.rb +275 -152
  26. data/lib/sequel/adapters/shared/sqlanywhere.rb +7 -0
  27. data/lib/sequel/adapters/shared/sqlite.rb +5 -0
  28. data/lib/sequel/connection_pool.rb +42 -28
  29. data/lib/sequel/database/connecting.rb +24 -0
  30. data/lib/sequel/database/misc.rb +62 -12
  31. data/lib/sequel/database/query.rb +37 -0
  32. data/lib/sequel/dataset/actions.rb +31 -11
  33. data/lib/sequel/dataset/features.rb +5 -0
  34. data/lib/sequel/dataset/misc.rb +1 -1
  35. data/lib/sequel/dataset/query.rb +9 -9
  36. data/lib/sequel/dataset/sql.rb +5 -1
  37. data/lib/sequel/extensions/_model_pg_row.rb +0 -12
  38. data/lib/sequel/extensions/_pretty_table.rb +1 -1
  39. data/lib/sequel/extensions/async_thread_pool.rb +11 -11
  40. data/lib/sequel/extensions/auto_literal_strings.rb +1 -1
  41. data/lib/sequel/extensions/constraint_validations.rb +1 -1
  42. data/lib/sequel/extensions/date_arithmetic.rb +1 -1
  43. data/lib/sequel/extensions/looser_typecasting.rb +3 -0
  44. data/lib/sequel/extensions/migration.rb +1 -1
  45. data/lib/sequel/extensions/named_timezones.rb +17 -5
  46. data/lib/sequel/extensions/pg_array.rb +22 -3
  47. data/lib/sequel/extensions/pg_auto_parameterize.rb +478 -0
  48. data/lib/sequel/extensions/pg_extended_date_support.rb +27 -24
  49. data/lib/sequel/extensions/pg_extended_integer_support.rb +116 -0
  50. data/lib/sequel/extensions/pg_hstore.rb +5 -0
  51. data/lib/sequel/extensions/pg_inet.rb +10 -11
  52. data/lib/sequel/extensions/pg_interval.rb +10 -11
  53. data/lib/sequel/extensions/pg_json.rb +10 -10
  54. data/lib/sequel/extensions/pg_json_ops.rb +0 -52
  55. data/lib/sequel/extensions/pg_multirange.rb +5 -10
  56. data/lib/sequel/extensions/pg_range.rb +6 -11
  57. data/lib/sequel/extensions/pg_row.rb +18 -13
  58. data/lib/sequel/model/associations.rb +7 -2
  59. data/lib/sequel/model/base.rb +6 -5
  60. data/lib/sequel/plugins/auto_validations.rb +53 -15
  61. data/lib/sequel/plugins/class_table_inheritance.rb +2 -2
  62. data/lib/sequel/plugins/composition.rb +2 -2
  63. data/lib/sequel/plugins/concurrent_eager_loading.rb +4 -4
  64. data/lib/sequel/plugins/dirty.rb +1 -1
  65. data/lib/sequel/plugins/finder.rb +3 -1
  66. data/lib/sequel/plugins/nested_attributes.rb +4 -4
  67. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +1 -1
  68. data/lib/sequel/plugins/primary_key_lookup_check_values.rb +154 -0
  69. data/lib/sequel/plugins/sql_comments.rb +1 -1
  70. data/lib/sequel/plugins/validation_helpers.rb +20 -0
  71. data/lib/sequel/version.rb +2 -2
  72. metadata +12 -5
@@ -0,0 +1,116 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The pg_extended_integer_support extension supports literalizing
4
+ # Ruby integers outside of PostgreSQL bigint range on PostgreSQL.
5
+ # Sequel by default will raise exceptions when
6
+ # literalizing such integers, as PostgreSQL would treat them
7
+ # as numeric type values instead of integer/bigint type values
8
+ # if unquoted, which can result in unexpected negative performance
9
+ # (e.g. forcing sequential scans when index scans would be used for
10
+ # an integer/bigint type).
11
+ #
12
+ # To load the extension into a Dataset (this returns a new Dataset):
13
+ #
14
+ # dataset = dataset.extension(:pg_extended_integer_support)
15
+ #
16
+ # To load the extension into a Database, so it affects all of the
17
+ # Database's datasets:
18
+ #
19
+ # DB.extension :pg_extended_integer_support
20
+ #
21
+ # By default, the extension will quote integers outside
22
+ # bigint range:
23
+ #
24
+ # DB.literal(2**63) # => "'9223372036854775808'"
25
+ #
26
+ # Quoting the value treats the type as unknown:
27
+ #
28
+ # DB.get{pg_typeof(2**63)} # => 'unknown'
29
+ #
30
+ # PostgreSQL will implicitly cast the unknown type to the appropriate
31
+ # database type, raising an error if it cannot be casted. Be aware this
32
+ # can result in the integer value being implicitly casted to text or
33
+ # any other PostgreSQL type:
34
+ #
35
+ # # Returns a string, not an integer:
36
+ # DB.get{2**63}
37
+ # # => "9223372036854775808"
38
+ #
39
+ # You can use the Dataset#integer_outside_bigint_range_strategy method
40
+ # with the value +:raw+ to change the strategy to not quote the variable:
41
+ #
42
+ # DB.dataset.
43
+ # integer_outside_bigint_range_strategy(:raw).
44
+ # literal(2**63)
45
+ # # => "9223372036854775808"
46
+ #
47
+ # Note that not quoting the value will result in PostgreSQL treating
48
+ # the type as numeric instead of integer:
49
+ #
50
+ # DB.dataset.
51
+ # integer_outside_bigint_range_strategy(:raw).
52
+ # get{pg_typeof(2**63)}
53
+ # # => "numeric"
54
+ #
55
+ # The +:raw+ behavior was Sequel's historical behavior, but unless
56
+ # you fully understand the reprecussions of PostgreSQL using a
57
+ # numeric type for integer values, you should not use it.
58
+ #
59
+ # To get the current default behavior of raising an exception for
60
+ # integers outside of PostgreSQL bigint range, you can use a strategy
61
+ # of +:raise+.
62
+ #
63
+ # To specify a default strategy for handling integers outside
64
+ # bigint range that applies to all of a Database's datasets, you can
65
+ # use the +:integer_outside_bigint_range_strategy+ Database option with
66
+ # a value of +:raise+ or +:raw+:
67
+ #
68
+ # DB.opts[:integer_outside_bigint_range_strategy] = :raw
69
+ #
70
+ # The Database option will be used as a fallback if you did not call
71
+ # the Dataset#integer_outside_bigint_range_strategy method to specify
72
+ # a strategy for the dataset.
73
+ #
74
+ # Related module: Sequel::Postgres::ExtendedIntegerSupport
75
+
76
+ #
77
+ module Sequel
78
+ module Postgres
79
+ module ExtendedIntegerSupport
80
+ # Set the strategy for handling integers outside PostgreSQL
81
+ # bigint range. Supported values:
82
+ #
83
+ # :quote :: Quote the integer value. PostgreSQL will treat
84
+ # the integer as a unknown type, implicitly casting
85
+ # to any other type as needed. This is the default
86
+ # value when using the pg_extended_integer_support
87
+ # extension.
88
+ # :raise :: Raise error when attempting to literalize the integer
89
+ # (the default behavior of Sequel on PostgreSQL when
90
+ # not using the pg_extended_integer_support extension).
91
+ # :raw :: Use raw integer value without quoting. PostgreSQL
92
+ # will treat the integer as a numeric. This was Sequel's
93
+ # historical behavior, but it is unlikely to be desired.
94
+ def integer_outside_bigint_range_strategy(strategy)
95
+ clone(:integer_outside_bigint_range_strategy=>strategy)
96
+ end
97
+
98
+ private
99
+
100
+ # Handle integers outside the bigint range by using
101
+ # the configured strategy.
102
+ def literal_integer_outside_bigint_range(v)
103
+ case @opts[:integer_outside_bigint_range_strategy] || @db.opts[:integer_outside_bigint_range_strategy]
104
+ when :raise
105
+ super
106
+ when :raw
107
+ v.to_s
108
+ else # when :quote
109
+ "'#{v}'"
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ Dataset.register_extension(:pg_extended_integer_support, Postgres::ExtendedIntegerSupport)
116
+ end
@@ -280,6 +280,11 @@ module Sequel
280
280
  str
281
281
  end
282
282
 
283
+ # Allow automatic parameterization.
284
+ def sequel_auto_param_type(ds)
285
+ "::hstore"
286
+ end
287
+
283
288
  private
284
289
 
285
290
  # Return a new hash based on the input hash with string
@@ -75,16 +75,6 @@ module Sequel
75
75
 
76
76
  private
77
77
 
78
- # Handle inet[]/cidr[] types in bound variables.
79
- def bound_variable_array(a)
80
- case a
81
- when IPAddr
82
- "\"#{a.to_s}/#{a.instance_variable_get(:@mask_addr).to_s(2).count('1')}\""
83
- else
84
- super
85
- end
86
- end
87
-
88
78
  # Make the column type detection recognize the inet and cidr types.
89
79
  def schema_column_type(db_type)
90
80
  case db_type
@@ -111,7 +101,7 @@ module Sequel
111
101
  when IPAddr
112
102
  value
113
103
  when String
114
- IPAddr.new(value)
104
+ IPAddr.new(typecast_check_string_length(value, 100))
115
105
  else
116
106
  raise Sequel::InvalidValue, "invalid value for inet/cidr: #{value.inspect}"
117
107
  end
@@ -121,6 +111,15 @@ module Sequel
121
111
  module InetDatasetMethods
122
112
  private
123
113
 
114
+ # Allow auto parameterization of IPAddr instances.
115
+ def auto_param_type_fallback(v)
116
+ if defined?(super) && (type = super)
117
+ type
118
+ elsif IPAddr === v
119
+ "::inet"
120
+ end
121
+ end
122
+
124
123
  # Convert IPAddr value to a string and append a literal version
125
124
  # of the string to the sql.
126
125
  def literal_other_append(sql, value)
@@ -163,16 +163,6 @@ module Sequel
163
163
 
164
164
  private
165
165
 
166
- # Handle arrays of interval types in bound variables.
167
- def bound_variable_array(a)
168
- case a
169
- when ActiveSupport::Duration
170
- "\"#{IntervalDatabaseMethods.literal_duration(a)}\""
171
- else
172
- super
173
- end
174
- end
175
-
176
166
  # Set the :ruby_default value if the default value is recognized as an interval.
177
167
  def schema_post_process(_)
178
168
  super.each do |a|
@@ -197,7 +187,7 @@ module Sequel
197
187
  when Numeric
198
188
  ActiveSupport::Duration.new(value, [[:seconds, value]])
199
189
  when String
200
- PARSER.call(value)
190
+ PARSER.call(typecast_check_string_length(value, 1000))
201
191
  else
202
192
  raise Sequel::InvalidValue, "invalid value for interval type: #{value.inspect}"
203
193
  end
@@ -207,6 +197,15 @@ module Sequel
207
197
  module IntervalDatasetMethods
208
198
  private
209
199
 
200
+ # Allow auto parameterization of ActiveSupport::Duration instances.
201
+ def auto_param_type_fallback(v)
202
+ if defined?(super) && (type = super)
203
+ type
204
+ elsif ActiveSupport::Duration === v
205
+ "::interval"
206
+ end
207
+ end
208
+
210
209
  # Handle literalization of ActiveSupport::Duration objects, treating them as
211
210
  # PostgreSQL intervals.
212
211
  def literal_other_append(sql, v)
@@ -142,6 +142,11 @@ module Sequel
142
142
  ds.literal_append(sql, Sequel.object_to_json(self))
143
143
  sql << '::json'
144
144
  end
145
+
146
+ # Allow automatic parameterization.
147
+ def sequel_auto_param_type(ds)
148
+ "::json"
149
+ end
145
150
  end
146
151
 
147
152
  jsonb_class = Class.new(base_class) do
@@ -151,6 +156,11 @@ module Sequel
151
156
  ds.literal_append(sql, Sequel.object_to_json(self))
152
157
  sql << '::jsonb'
153
158
  end
159
+
160
+ # Allow automatic parameterization.
161
+ def sequel_auto_param_type(ds)
162
+ "::jsonb"
163
+ end
154
164
  end
155
165
 
156
166
  const_set(:"JSON#{name}Base", base_class)
@@ -424,16 +434,6 @@ module Sequel
424
434
  end
425
435
  end
426
436
 
427
- # Handle json[] and jsonb[] types in bound variables.
428
- def bound_variable_array(a)
429
- case a
430
- when JSONObject, JSONBObject
431
- "\"#{Sequel.object_to_json(a).gsub('"', '\\"')}\""
432
- else
433
- super
434
- end
435
- end
436
-
437
437
  # Make the column type detection recognize the json types.
438
438
  def schema_column_type(db_type)
439
439
  case db_type
@@ -123,15 +123,6 @@
123
123
  # c = Sequel.pg_jsonb_op(:c)
124
124
  # DB[:t].update(c['key1'] => 1.to_json, c['key2'] => "a".to_json)
125
125
  #
126
- # On PostgreSQL 15+, the <tt>IS [NOT] JSON</tt> operator is supported:
127
- #
128
- # j.is_json # j IS JSON
129
- # j.is_json(type: :object) # j IS JSON OBJECT
130
- # j.is_json(type: :object, unique: true) # j IS JSON OBJECT WITH UNIQUE
131
- # j.is_not_json # j IS NOT JSON
132
- # j.is_json(type: :array) # j IS NOT JSON ARRAY
133
- # j.is_json(unique: true) # j IS NOT JSON WITH UNIQUE
134
- #
135
126
  # If you are also using the pg_json extension, you should load it before
136
127
  # loading this extension. Doing so will allow you to use the #op method on
137
128
  # JSONHash, JSONHarray, JSONBHash, and JSONBArray, allowing you to perform json/jsonb operations
@@ -160,18 +151,6 @@ module Sequel
160
151
  GET_PATH = ["(".freeze, " #> ".freeze, ")".freeze].freeze
161
152
  GET_PATH_TEXT = ["(".freeze, " #>> ".freeze, ")".freeze].freeze
162
153
 
163
- IS_JSON = ["(".freeze, " IS JSON".freeze, "".freeze, ")".freeze].freeze
164
- IS_NOT_JSON = ["(".freeze, " IS NOT JSON".freeze, "".freeze, ")".freeze].freeze
165
- EMPTY_STRING = Sequel::LiteralString.new('').freeze
166
- WITH_UNIQUE = Sequel::LiteralString.new(' WITH UNIQUE').freeze
167
- IS_JSON_MAP = {
168
- nil => EMPTY_STRING,
169
- :value => Sequel::LiteralString.new(' VALUE').freeze,
170
- :scalar => Sequel::LiteralString.new(' SCALAR').freeze,
171
- :object => Sequel::LiteralString.new(' OBJECT').freeze,
172
- :array => Sequel::LiteralString.new(' ARRAY').freeze
173
- }.freeze
174
-
175
154
  # Get JSON array element or object field as json. If an array is given,
176
155
  # gets the object at the specified path.
177
156
  #
@@ -254,30 +233,6 @@ module Sequel
254
233
  end
255
234
  end
256
235
 
257
- # Return whether the json object can be parsed as JSON.
258
- #
259
- # Options:
260
- # :type :: Check whether the json object can be parsed as a specific type
261
- # of JSON (:value, :scalar, :object, :array).
262
- # :unique :: Check JSON objects for unique keys.
263
- #
264
- # json_op.is_json # json IS JSON
265
- # json_op.is_json(type: :object) # json IS JSON OBJECT
266
- # json_op.is_json(unique: true) # json IS JSON WITH UNIQUE
267
- def is_json(opts=OPTS)
268
- _is_json(IS_JSON, opts)
269
- end
270
-
271
- # Return whether the json object cannot be parsed as JSON. The opposite
272
- # of #is_json. See #is_json for options.
273
- #
274
- # json_op.is_not_json # json IS NOT JSON
275
- # json_op.is_not_json(type: :object) # json IS NOT JSON OBJECT
276
- # json_op.is_not_json(unique: true) # json IS NOT JSON WITH UNIQUE
277
- def is_not_json(opts=OPTS)
278
- _is_json(IS_NOT_JSON, opts)
279
- end
280
-
281
236
  # Returns a set of keys AS text in the json object.
282
237
  #
283
238
  # json_op.keys # json_object_keys(json)
@@ -331,13 +286,6 @@ module Sequel
331
286
 
332
287
  private
333
288
 
334
- # Internals of IS [NOT] JSON support
335
- def _is_json(lit_array, opts)
336
- raise Error, "invalid is_json :type option: #{opts[:type].inspect}" unless type = IS_JSON_MAP[opts[:type]]
337
- unique = opts[:unique] ? WITH_UNIQUE : EMPTY_STRING
338
- Sequel::SQL::BooleanExpression.new(:NOOP, Sequel::SQL::PlaceholderLiteralString.new(lit_array, [self, type, unique]))
339
- end
340
-
341
289
  # Return a placeholder literal with the given str and args, wrapped
342
290
  # in an JSONOp or JSONBOp, used by operators that return json or jsonb.
343
291
  def json_op(str, args)
@@ -220,16 +220,6 @@ module Sequel
220
220
 
221
221
  private
222
222
 
223
- # Handle arrays of multirange types in bound variables.
224
- def bound_variable_array(a)
225
- case a
226
- when PGMultiRange
227
- "\"#{bound_variable_arg(a, nil)}\""
228
- else
229
- super
230
- end
231
- end
232
-
233
223
  # Recognize the registered database multirange types.
234
224
  def schema_column_type(db_type)
235
225
  @pg_multirange_schema_types[db_type] || super
@@ -346,6 +336,11 @@ module Sequel
346
336
 
347
337
  val << "}"
348
338
  end
339
+
340
+ # Allow automatic parameterization.
341
+ def sequel_auto_param_type(ds)
342
+ "::#{db_type}"
343
+ end
349
344
  end
350
345
  end
351
346
 
@@ -233,16 +233,6 @@ module Sequel
233
233
 
234
234
  private
235
235
 
236
- # Handle arrays of range types in bound variables.
237
- def bound_variable_array(a)
238
- case a
239
- when PGRange, Range
240
- "\"#{bound_variable_arg(a, nil)}\""
241
- else
242
- super
243
- end
244
- end
245
-
246
236
  # Recognize the registered database range types.
247
237
  def schema_column_type(db_type)
248
238
  @pg_range_schema_types[db_type] || super
@@ -282,7 +272,7 @@ module Sequel
282
272
  when Range
283
273
  PGRange.from_range(value, parser.db_type)
284
274
  when String
285
- parser.call(value)
275
+ parser.call(typecast_check_string_length(value, 100))
286
276
  else
287
277
  raise Sequel::InvalidValue, "invalid value for range type: #{value.inspect}"
288
278
  end
@@ -491,6 +481,11 @@ module Sequel
491
481
  end
492
482
  end
493
483
 
484
+ # Allow automatic parameterization for ranges with types.
485
+ def sequel_auto_param_type(ds)
486
+ "::#{db_type}" if db_type
487
+ end
488
+
494
489
  private
495
490
 
496
491
  # Escape common range types. Instead of quoting, just backslash escape all
@@ -136,6 +136,15 @@ module Sequel
136
136
  ds.quote_schema_table_append(sql, db_type)
137
137
  end
138
138
  end
139
+
140
+ # Allow automatic parameterization if all values support it.
141
+ def sequel_auto_param_type(ds)
142
+ if db_type && all?{|v| nil == v || ds.send(:auto_param_type, v)}
143
+ s = String.new << "::"
144
+ ds.quote_schema_table_append(s, db_type)
145
+ s
146
+ end
147
+ end
139
148
  end
140
149
 
141
150
  # Class for row-valued/composite types that are treated as hashes.
@@ -208,6 +217,15 @@ module Sequel
208
217
  ds.quote_schema_table_append(sql, db_type)
209
218
  end
210
219
  end
220
+
221
+ # Allow automatic parameterization if all values support it.
222
+ def sequel_auto_param_type(ds)
223
+ if db_type && all?{|_,v| nil == v || ds.send(:auto_param_type, v)}
224
+ s = String.new << "::"
225
+ ds.quote_schema_table_append(s, db_type)
226
+ s
227
+ end
228
+ end
211
229
  end
212
230
 
213
231
  ROW_TYPE_CLASSES = [HashRow, ArrayRow].freeze
@@ -519,19 +537,6 @@ module Sequel
519
537
 
520
538
  private
521
539
 
522
- # Format composite types used in bound variable arrays.
523
- def bound_variable_array(arg)
524
- case arg
525
- when ArrayRow
526
- "\"(#{arg.map{|v| bound_variable_array(v) if v}.join(',').gsub(/("|\\)/, '\\\\\1')})\""
527
- when HashRow
528
- arg.check_columns!
529
- "\"(#{arg.values_at(*arg.columns).map{|v| bound_variable_array(v) if v}.join(',').gsub(/("|\\)/, '\\\\\1')})\""
530
- else
531
- super
532
- end
533
- end
534
-
535
540
  # Make the column type detection handle registered row types.
536
541
  def schema_column_type(db_type)
537
542
  if type = @row_schema_types[db_type]
@@ -276,7 +276,7 @@ module Sequel
276
276
 
277
277
  if eo[:no_results]
278
278
  no_results = true
279
- elsif eo[:eager_block] || eo[:loader] == false
279
+ elsif eo[:eager_block] || eo[:loader] == false || !use_placeholder_loader?
280
280
  ds = eager_loading_dataset(eo)
281
281
 
282
282
  strategy = ds.opts[:eager_limit_strategy] || strategy
@@ -299,6 +299,11 @@ module Sequel
299
299
  strategy = :ruby if strategy == :correlated_subquery
300
300
  strategy = nil if strategy == :ruby && assign_singular?
301
301
  objects = apply_eager_limit_strategy(ds, strategy, eager_limit).all
302
+
303
+ if strategy == :window_function
304
+ delete_rn = ds.row_number_column
305
+ objects.each{|obj| obj.values.delete(delete_rn)}
306
+ end
302
307
  elsif strategy == :union
303
308
  objects = []
304
309
  ds = associated_dataset
@@ -817,7 +822,7 @@ module Sequel
817
822
 
818
823
  # Whether the placeholder loader can be used to load the association.
819
824
  def use_placeholder_loader?
820
- self[:use_placeholder_loader]
825
+ self[:use_placeholder_loader] && _associated_dataset.supports_placeholder_literalizer?
821
826
  end
822
827
  end
823
828
 
@@ -606,6 +606,7 @@ module Sequel
606
606
  @db_schema = get_db_schema
607
607
  end
608
608
 
609
+ @fast_pk_lookup_sql = @fast_instance_delete_sql = nil unless @dataset.supports_placeholder_literalizer?
609
610
  reset_instance_dataset
610
611
  self
611
612
  end
@@ -1141,7 +1142,7 @@ module Sequel
1141
1142
  #
1142
1143
  # Artist[1] === Artist[1] # => true
1143
1144
  # Artist.new === Artist.new # => false
1144
- # Artist[1].set(:name=>'Bob') === Artist[1] # => true
1145
+ # Artist[1].set(name: 'Bob') === Artist[1] # => true
1145
1146
  def ===(obj)
1146
1147
  case pkv = pk
1147
1148
  when nil
@@ -1160,7 +1161,7 @@ module Sequel
1160
1161
  #
1161
1162
  # Artist[1].pk_equal?(Artist[1]) # => true
1162
1163
  # Artist.new.pk_equal?(Artist.new) # => false
1163
- # Artist[1].set(:name=>'Bob').pk_equal?(Artist[1]) # => true
1164
+ # Artist[1].set(name: 'Bob').pk_equal?(Artist[1]) # => true
1164
1165
  alias pk_equal? ===
1165
1166
 
1166
1167
  # class is defined in Object, but it is also a keyword,
@@ -1232,7 +1233,7 @@ module Sequel
1232
1233
  #
1233
1234
  # Artist[1] == Artist[1] # => true
1234
1235
  # Artist.new == Artist.new # => true
1235
- # Artist[1].set(:name=>'Bob') == Artist[1] # => false
1236
+ # Artist[1].set(name: 'Bob') == Artist[1] # => false
1236
1237
  def eql?(obj)
1237
1238
  (obj.class == model) && (obj.values == @values)
1238
1239
  end
@@ -1334,13 +1335,13 @@ module Sequel
1334
1335
  # a = Artist[1]
1335
1336
  # Artist.db.transaction do
1336
1337
  # a.lock!
1337
- # a.update(:name=>'A')
1338
+ # a.update(name: 'A')
1338
1339
  # end
1339
1340
  #
1340
1341
  # a = Artist[2]
1341
1342
  # Artist.db.transaction do
1342
1343
  # a.lock!('FOR NO KEY UPDATE')
1343
- # a.update(:name=>'B')
1344
+ # a.update(name: 'B')
1344
1345
  # end
1345
1346
  def lock!(style=:update)
1346
1347
  _refresh(this.lock_style(style)) unless new?
@@ -9,14 +9,16 @@ module Sequel
9
9
  # 2. not_null validations on NOT NULL columns (optionally, presence validations)
10
10
  # 3. unique validations on columns or sets of columns with unique indexes
11
11
  # 4. max length validations on string columns
12
+ # 5. no null byte validations on string columns
13
+ # 6. minimum and maximum values on columns
12
14
  #
13
- # To determine the columns to use for the type/not_null/max_length validations,
15
+ # To determine the columns to use for the type/not_null/max_length/no_null_byte/max_value/min_value validations,
14
16
  # the plugin looks at the database schema for the model's table. To determine
15
17
  # the unique validations, Sequel looks at the indexes on the table. In order
16
18
  # for this plugin to be fully functional, the underlying database adapter needs
17
19
  # to support both schema and index parsing. Additionally, unique validations are
18
20
  # 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.
21
+ # that select from a subquery.
20
22
  #
21
23
  # This plugin uses the validation_helpers plugin underneath to implement the
22
24
  # validations. It does not allow for any per-column validation message
@@ -50,7 +52,7 @@ module Sequel
50
52
  #
51
53
  # Model.plugin :auto_validations, unique_opts: {only_if_modified: true}
52
54
  #
53
- # This works for unique_opts, max_length_opts, schema_types_opts,
55
+ # This works for unique_opts, max_length_opts, schema_types_opts, max_value_opts, min_value_opts, no_null_byte_opts,
54
56
  # explicit_not_null_opts, and not_null_opts.
55
57
  #
56
58
  # If you only want auto_validations to add validations to columns that do not already
@@ -72,6 +74,19 @@ module Sequel
72
74
  SCHEMA_TYPES_OPTIONS = NOT_NULL_OPTIONS
73
75
  UNIQUE_OPTIONS = NOT_NULL_OPTIONS
74
76
  NO_NULL_BYTE_OPTIONS = MAX_LENGTH_OPTIONS
77
+ MAX_VALUE_OPTIONS = {:from=>:values, :allow_nil=>true, :skip_invalid=>true}.freeze
78
+ MIN_VALUE_OPTIONS = MAX_VALUE_OPTIONS
79
+ AUTO_VALIDATE_OPTIONS = {
80
+ :no_null_byte=>NO_NULL_BYTE_OPTIONS,
81
+ :not_null=>NOT_NULL_OPTIONS,
82
+ :explicit_not_null=>EXPLICIT_NOT_NULL_OPTIONS,
83
+ :max_length=>MAX_LENGTH_OPTIONS,
84
+ :max_value=>MAX_VALUE_OPTIONS,
85
+ :min_value=>MIN_VALUE_OPTIONS,
86
+ :schema_types=>SCHEMA_TYPES_OPTIONS,
87
+ :unique=>UNIQUE_OPTIONS
88
+ }.freeze
89
+
75
90
  EMPTY_ARRAY = [].freeze
76
91
 
77
92
  def self.apply(model, opts=OPTS)
@@ -82,17 +97,11 @@ module Sequel
82
97
  @auto_validate_not_null_columns = []
83
98
  @auto_validate_explicit_not_null_columns = []
84
99
  @auto_validate_max_length_columns = []
100
+ @auto_validate_max_value_columns = []
101
+ @auto_validate_min_value_columns = []
85
102
  @auto_validate_unique_columns = []
86
103
  @auto_validate_types = true
87
-
88
- @auto_validate_options = {
89
- :no_null_byte=>NO_NULL_BYTE_OPTIONS,
90
- :not_null=>NOT_NULL_OPTIONS,
91
- :explicit_not_null=>EXPLICIT_NOT_NULL_OPTIONS,
92
- :max_length=>MAX_LENGTH_OPTIONS,
93
- :schema_types=>SCHEMA_TYPES_OPTIONS,
94
- :unique=>UNIQUE_OPTIONS
95
- }.freeze
104
+ @auto_validate_options = AUTO_VALIDATE_OPTIONS
96
105
  end
97
106
  end
98
107
 
@@ -105,7 +114,7 @@ module Sequel
105
114
  end
106
115
 
107
116
  h = @auto_validate_options.dup
108
- [:not_null, :explicit_not_null, :max_length, :no_null_byte, :schema_types, :unique].each do |type|
117
+ [:not_null, :explicit_not_null, :max_length, :max_value, :min_value, :no_null_byte, :schema_types, :unique].each do |type|
109
118
  if type_opts = opts[:"#{type}_opts"]
110
119
  h[type] = h[type].merge(type_opts).freeze
111
120
  end
@@ -135,6 +144,14 @@ module Sequel
135
144
  # pairs, with the first entry being the column name and second entry being the maximum length.
136
145
  attr_reader :auto_validate_max_length_columns
137
146
 
147
+ # The columns with automatch max value validations, as an array of
148
+ # pairs, with the first entry being the column name and second entry being the maximum value.
149
+ attr_reader :auto_validate_max_value_columns
150
+
151
+ # The columns with automatch min value validations, as an array of
152
+ # pairs, with the first entry being the column name and second entry being the minimum value.
153
+ attr_reader :auto_validate_min_value_columns
154
+
138
155
  # The columns or sets of columns with automatic unique validations
139
156
  attr_reader :auto_validate_unique_columns
140
157
 
@@ -148,6 +165,8 @@ module Sequel
148
165
  :@auto_validate_not_null_columns=>:dup,
149
166
  :@auto_validate_explicit_not_null_columns=>:dup,
150
167
  :@auto_validate_max_length_columns=>:dup,
168
+ :@auto_validate_max_value_columns=>:dup,
169
+ :@auto_validate_min_value_columns=>:dup,
151
170
  :@auto_validate_unique_columns=>:dup,
152
171
  :@auto_validate_options => :dup)
153
172
  Plugins.after_set_dataset(self, :setup_auto_validations)
@@ -168,18 +187,23 @@ module Sequel
168
187
  @auto_validate_not_null_columns.freeze
169
188
  @auto_validate_explicit_not_null_columns.freeze
170
189
  @auto_validate_max_length_columns.freeze
190
+ @auto_validate_max_value_columns.freeze
191
+ @auto_validate_min_value_columns.freeze
171
192
  @auto_validate_unique_columns.freeze
172
193
 
173
194
  super
174
195
  end
175
196
 
176
197
  # Skip automatic validations for the given validation type
177
- # (:not_null, :types, :unique, :max_length, :no_null_byte).
198
+ # (:not_null, :no_null_byte, :types, :unique, :max_length, :max_value, :min_value).
178
199
  # If :all is given as the type, skip all auto validations.
200
+ #
201
+ # Skipping types validation automatically skips max_value and min_value validations,
202
+ # since those validations require valid types.
179
203
  def skip_auto_validations(type)
180
204
  case type
181
205
  when :all
182
- [:not_null, :no_null_byte, :types, :unique, :max_length].each{|v| skip_auto_validations(v)}
206
+ [:not_null, :no_null_byte, :types, :unique, :max_length, :max_value, :min_value].each{|v| skip_auto_validations(v)}
183
207
  when :not_null
184
208
  auto_validate_not_null_columns.clear
185
209
  auto_validate_explicit_not_null_columns.clear
@@ -199,6 +223,8 @@ module Sequel
199
223
  explicit_not_null_cols += Array(primary_key)
200
224
  @auto_validate_explicit_not_null_columns = explicit_not_null_cols.uniq
201
225
  @auto_validate_max_length_columns = db_schema.select{|col, sch| sch[:type] == :string && sch[:max_length].is_a?(Integer)}.map{|col, sch| [col, sch[:max_length]]}
226
+ @auto_validate_max_value_columns = db_schema.select{|col, sch| sch[:max_value]}.map{|col, sch| [col, sch[:max_value]]}
227
+ @auto_validate_min_value_columns = db_schema.select{|col, sch| sch[:min_value]}.map{|col, sch| [col, sch[:min_value]]}
202
228
  @auto_validate_no_null_byte_columns = db_schema.select{|_, sch| sch[:type] == :string}.map{|col, _| col}
203
229
  table = dataset.first_source_table
204
230
  @auto_validate_unique_columns = if db.supports_index_parsing? && [Symbol, SQL::QualifiedIdentifier, SQL::Identifier, String].any?{|c| table.is_a?(c)}
@@ -248,6 +274,18 @@ module Sequel
248
274
 
249
275
  unless skip.include?(:types) || !model.auto_validate_types?
250
276
  validates_schema_types(keys, opts[:schema_types])
277
+
278
+ unless skip.include?(:max_value) || ((max_value_columns = model.auto_validate_max_value_columns).empty?)
279
+ max_value_columns.each do |col, max|
280
+ validates_max_value(max, col, opts[:max_value])
281
+ end
282
+ end
283
+
284
+ unless skip.include?(:min_value) || ((min_value_columns = model.auto_validate_min_value_columns).empty?)
285
+ min_value_columns.each do |col, min|
286
+ validates_min_value(min, col, opts[:min_value])
287
+ end
288
+ end
251
289
  end
252
290
 
253
291
  unless skip.include?(:unique)