sequel 5.60.1 → 5.62.0

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