sequel 5.21.0 → 5.26.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +80 -0
  3. data/README.rdoc +1 -1
  4. data/doc/dataset_filtering.rdoc +15 -0
  5. data/doc/opening_databases.rdoc +3 -0
  6. data/doc/postgresql.rdoc +2 -2
  7. data/doc/release_notes/5.22.0.txt +48 -0
  8. data/doc/release_notes/5.23.0.txt +56 -0
  9. data/doc/release_notes/5.24.0.txt +56 -0
  10. data/doc/release_notes/5.25.0.txt +32 -0
  11. data/doc/release_notes/5.26.0.txt +35 -0
  12. data/doc/testing.rdoc +1 -0
  13. data/lib/sequel/adapters/jdbc.rb +7 -1
  14. data/lib/sequel/adapters/jdbc/postgresql.rb +1 -13
  15. data/lib/sequel/adapters/jdbc/sqlite.rb +29 -0
  16. data/lib/sequel/adapters/mysql2.rb +0 -1
  17. data/lib/sequel/adapters/shared/mssql.rb +11 -9
  18. data/lib/sequel/adapters/shared/postgres.rb +42 -12
  19. data/lib/sequel/adapters/shared/sqlite.rb +16 -2
  20. data/lib/sequel/adapters/tinytds.rb +12 -0
  21. data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -1
  22. data/lib/sequel/database/logging.rb +7 -1
  23. data/lib/sequel/database/schema_generator.rb +11 -2
  24. data/lib/sequel/database/schema_methods.rb +2 -0
  25. data/lib/sequel/dataset.rb +4 -2
  26. data/lib/sequel/dataset/actions.rb +3 -2
  27. data/lib/sequel/dataset/query.rb +4 -0
  28. data/lib/sequel/dataset/sql.rb +11 -7
  29. data/lib/sequel/extensions/named_timezones.rb +51 -9
  30. data/lib/sequel/extensions/pg_array.rb +4 -0
  31. data/lib/sequel/extensions/pg_json.rb +88 -17
  32. data/lib/sequel/extensions/pg_json_ops.rb +124 -0
  33. data/lib/sequel/extensions/pg_range.rb +12 -2
  34. data/lib/sequel/extensions/pg_row.rb +3 -1
  35. data/lib/sequel/extensions/sql_comments.rb +2 -2
  36. data/lib/sequel/model/base.rb +12 -5
  37. data/lib/sequel/plugins/association_multi_add_remove.rb +83 -0
  38. data/lib/sequel/plugins/association_proxies.rb +3 -2
  39. data/lib/sequel/plugins/caching.rb +3 -0
  40. data/lib/sequel/plugins/class_table_inheritance.rb +10 -0
  41. data/lib/sequel/plugins/csv_serializer.rb +26 -9
  42. data/lib/sequel/plugins/dirty.rb +3 -9
  43. data/lib/sequel/plugins/insert_conflict.rb +72 -0
  44. data/lib/sequel/plugins/nested_attributes.rb +7 -0
  45. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +89 -30
  46. data/lib/sequel/plugins/sharding.rb +11 -5
  47. data/lib/sequel/plugins/static_cache.rb +8 -3
  48. data/lib/sequel/plugins/static_cache_cache.rb +53 -0
  49. data/lib/sequel/plugins/typecast_on_load.rb +3 -2
  50. data/lib/sequel/sql.rb +18 -4
  51. data/lib/sequel/timezones.rb +50 -11
  52. data/lib/sequel/version.rb +1 -1
  53. data/spec/adapters/postgres_spec.rb +174 -0
  54. data/spec/bin_spec.rb +2 -2
  55. data/spec/core/database_spec.rb +50 -0
  56. data/spec/core/dataset_spec.rb +33 -1
  57. data/spec/core/expression_filters_spec.rb +32 -3
  58. data/spec/core/schema_spec.rb +18 -0
  59. data/spec/core/spec_helper.rb +1 -1
  60. data/spec/core_extensions_spec.rb +1 -1
  61. data/spec/extensions/association_multi_add_remove_spec.rb +1041 -0
  62. data/spec/extensions/dirty_spec.rb +33 -0
  63. data/spec/extensions/insert_conflict_spec.rb +103 -0
  64. data/spec/extensions/named_timezones_spec.rb +109 -2
  65. data/spec/extensions/nested_attributes_spec.rb +48 -0
  66. data/spec/extensions/pg_auto_constraint_validations_spec.rb +37 -0
  67. data/spec/extensions/pg_json_ops_spec.rb +67 -0
  68. data/spec/extensions/pg_json_spec.rb +12 -0
  69. data/spec/extensions/pg_range_spec.rb +90 -9
  70. data/spec/extensions/sharding_spec.rb +8 -0
  71. data/spec/extensions/spec_helper.rb +9 -2
  72. data/spec/extensions/static_cache_cache_spec.rb +35 -0
  73. data/spec/guards_helper.rb +1 -1
  74. data/spec/integration/dataset_test.rb +24 -8
  75. data/spec/integration/plugin_test.rb +27 -0
  76. data/spec/integration/schema_test.rb +16 -2
  77. data/spec/model/spec_helper.rb +1 -1
  78. metadata +32 -2
@@ -340,14 +340,18 @@ module Sequel
340
340
  raise Sequel::Error, "invalid array, empty string" if eos?
341
341
  raise Sequel::Error, "invalid array, doesn't start with {" unless scan(/((\[\d+:\d+\])+=)?\{/)
342
342
 
343
+ # :nocov:
343
344
  while !eos?
345
+ # :nocov:
344
346
  char = scan(/[{}",]|[^{}",]+/)
345
347
  if char == ','
346
348
  # Comma outside quoted string indicates end of current entry
347
349
  new_entry
348
350
  elsif char == '"'
349
351
  raise Sequel::Error, "invalid array, opening quote with existing recorded data" unless @recorded.empty?
352
+ # :nocov:
350
353
  while true
354
+ # :nocov:
351
355
  char = scan(/["\\]|[^"\\]+/)
352
356
  if char == '\\'
353
357
  @recorded << getch
@@ -12,7 +12,7 @@
12
12
  #
13
13
  # Note that wrapping JSON primitives changes the behavior for
14
14
  # JSON false and null values. Because only +false+ and +nil+
15
- # in Ruby are considered falesy, wrapping these objects results
15
+ # in Ruby are considered falsey, wrapping these objects results
16
16
  # in unexpected behavior if you use the values directly in
17
17
  # conditionals:
18
18
  #
@@ -208,11 +208,19 @@ module Sequel
208
208
  JSONB_PRIMITIVE_WRAPPER_MAPPING.freeze
209
209
 
210
210
  JSON_COMBINED_WRAPPER_MAPPING =JSON_WRAPPER_MAPPING.merge(JSON_PRIMITIVE_WRAPPER_MAPPING).freeze
211
- JSON_WRAP_CLASSES = JSON_COMBINED_WRAPPER_MAPPING.keys.freeze
212
-
213
211
  JSONB_COMBINED_WRAPPER_MAPPING =JSONB_WRAPPER_MAPPING.merge(JSONB_PRIMITIVE_WRAPPER_MAPPING).freeze
214
212
  JSONB_WRAP_CLASSES = JSONB_COMBINED_WRAPPER_MAPPING.keys.freeze
215
213
 
214
+ Sequel::Deprecation.deprecate_constant(self, :JSON_WRAPPER_MAPPING)
215
+ Sequel::Deprecation.deprecate_constant(self, :JSONB_WRAPPER_MAPPING)
216
+ Sequel::Deprecation.deprecate_constant(self, :JSON_PRIMITIVE_WRAPPER_MAPPING)
217
+ Sequel::Deprecation.deprecate_constant(self, :JSONB_PRIMITIVE_WRAPPER_MAPPING)
218
+ Sequel::Deprecation.deprecate_constant(self, :JSON_COMBINED_WRAPPER_MAPPING)
219
+ Sequel::Deprecation.deprecate_constant(self, :JSONB_COMBINED_WRAPPER_MAPPING)
220
+ Sequel::Deprecation.deprecate_constant(self, :JSONB_WRAP_CLASSES)
221
+
222
+ JSON_WRAP_CLASSES = [Hash, Array, String, Integer, Float, NilClass, TrueClass, FalseClass].freeze
223
+
216
224
  # Methods enabling Database object integration with the json type.
217
225
  module JSONDatabaseMethods
218
226
  def self.extended(db)
@@ -228,6 +236,69 @@ module Sequel
228
236
  end
229
237
  end
230
238
 
239
+ # Return the wrapper class for the json type if value is Hash or Array.
240
+ def self.json_wrapper(value)
241
+ case value
242
+ when ::Hash
243
+ JSONHash
244
+ when ::Array
245
+ JSONArray
246
+ end
247
+ end
248
+
249
+ # Return the wrapper class for the jsonb type if value is Hash or Array.
250
+ def self.jsonb_wrapper(value)
251
+ case value
252
+ when ::Hash
253
+ JSONBHash
254
+ when ::Array
255
+ JSONBArray
256
+ end
257
+ end
258
+
259
+ # Return the wrapper class for the json type if value is a supported type.
260
+ def self.json_primitive_wrapper(value)
261
+ case value
262
+ when ::Hash
263
+ JSONHash
264
+ when ::Array
265
+ JSONArray
266
+ when ::String
267
+ JSONString
268
+ when ::Integer
269
+ JSONInteger
270
+ when ::Float
271
+ JSONFloat
272
+ when ::NilClass
273
+ JSONNull
274
+ when ::TrueClass
275
+ JSONTrue
276
+ when ::FalseClass
277
+ JSONFalse
278
+ end
279
+ end
280
+
281
+ # Return the wrapper class for the jsonb type if value is a supported type.
282
+ def self.jsonb_primitive_wrapper(value)
283
+ case value
284
+ when ::Hash
285
+ JSONBHash
286
+ when ::Array
287
+ JSONBArray
288
+ when ::String
289
+ JSONBString
290
+ when ::Integer
291
+ JSONBInteger
292
+ when ::Float
293
+ JSONBFloat
294
+ when ::NilClass
295
+ JSONBNull
296
+ when ::TrueClass
297
+ JSONBTrue
298
+ when ::FalseClass
299
+ JSONBFalse
300
+ end
301
+ end
231
302
 
232
303
  # Deprecated
233
304
  def self.db_parse_json(s)
@@ -326,9 +397,9 @@ module Sequel
326
397
  # Wrap the parsed JSON value in the appropriate JSON wrapper class.
327
398
  # Only wrap primitive values if wrap_json_primitives is set.
328
399
  def _wrap_json(value)
329
- if klass = JSON_WRAPPER_MAPPING[value.class]
400
+ if klass = JSONDatabaseMethods.json_wrapper(value)
330
401
  klass.new(value)
331
- elsif klass = JSON_PRIMITIVE_WRAPPER_MAPPING[value.class]
402
+ elsif klass = JSONDatabaseMethods.json_primitive_wrapper(value)
332
403
  if wrap_json_primitives
333
404
  klass.new(value)
334
405
  else
@@ -342,9 +413,9 @@ module Sequel
342
413
  # Wrap the parsed JSON value in the appropriate JSONB wrapper class.
343
414
  # Only wrap primitive values if wrap_json_primitives is set.
344
415
  def _wrap_jsonb(value)
345
- if klass = JSONB_WRAPPER_MAPPING[value.class]
416
+ if klass = JSONDatabaseMethods.jsonb_wrapper(value)
346
417
  klass.new(value)
347
- elsif klass = JSONB_PRIMITIVE_WRAPPER_MAPPING[value.class]
418
+ elsif klass = JSONDatabaseMethods.jsonb_primitive_wrapper(value)
348
419
  if wrap_json_primitives
349
420
  klass.new(value)
350
421
  else
@@ -413,10 +484,10 @@ module Sequel
413
484
  _wrap_json(_parse_json(value))
414
485
  end
415
486
  when *JSON_WRAP_CLASSES
416
- JSON_COMBINED_WRAPPER_MAPPING[value.class].new(value)
487
+ JSONDatabaseMethods.json_primitive_wrapper(value).new(value)
417
488
  when JSONBObject
418
489
  value = value.__getobj__
419
- JSON_COMBINED_WRAPPER_MAPPING[value.class].new(value)
490
+ JSONDatabaseMethods.json_primitive_wrapper(value).new(value)
420
491
  else
421
492
  raise Sequel::InvalidValue, "invalid value for json: #{value.inspect}"
422
493
  end
@@ -433,11 +504,11 @@ module Sequel
433
504
  else
434
505
  _wrap_jsonb(_parse_json(value))
435
506
  end
436
- when *JSONB_WRAP_CLASSES
437
- JSONB_COMBINED_WRAPPER_MAPPING[value.class].new(value)
507
+ when *JSON_WRAP_CLASSES
508
+ JSONDatabaseMethods.jsonb_primitive_wrapper(value).new(value)
438
509
  when JSONObject
439
510
  value = value.__getobj__
440
- JSONB_COMBINED_WRAPPER_MAPPING[value.class].new(value)
511
+ JSONDatabaseMethods.jsonb_primitive_wrapper(value).new(value)
441
512
  else
442
513
  raise Sequel::InvalidValue, "invalid value for jsonb: #{value.inspect}"
443
514
  end
@@ -460,7 +531,7 @@ module Sequel
460
531
  Postgres::JSONHash.new(v)
461
532
  when Postgres::JSONBObject
462
533
  v = v.__getobj__
463
- Postgres::JSON_COMBINED_WRAPPER_MAPPING[v.class].new(v)
534
+ Postgres::JSONDatabaseMethods.json_primitive_wrapper(v).new(v)
464
535
  else
465
536
  Sequel.pg_json_op(v)
466
537
  end
@@ -472,7 +543,7 @@ module Sequel
472
543
  def pg_json_wrap(v)
473
544
  case v
474
545
  when *Postgres::JSON_WRAP_CLASSES
475
- Postgres::JSON_COMBINED_WRAPPER_MAPPING[v.class].new(v)
546
+ Postgres::JSONDatabaseMethods.json_primitive_wrapper(v).new(v)
476
547
  else
477
548
  raise Error, "invalid value passed to Sequel.pg_json_wrap: #{v.inspect}"
478
549
  end
@@ -492,7 +563,7 @@ module Sequel
492
563
  Postgres::JSONBHash.new(v)
493
564
  when Postgres::JSONObject
494
565
  v = v.__getobj__
495
- Postgres::JSONB_COMBINED_WRAPPER_MAPPING[v.class].new(v)
566
+ Postgres::JSONDatabaseMethods.jsonb_primitive_wrapper(v).new(v)
496
567
  else
497
568
  Sequel.pg_jsonb_op(v)
498
569
  end
@@ -503,8 +574,8 @@ module Sequel
503
574
  # other types.
504
575
  def pg_jsonb_wrap(v)
505
576
  case v
506
- when *Postgres::JSONB_WRAP_CLASSES
507
- Postgres::JSONB_COMBINED_WRAPPER_MAPPING[v.class].new(v)
577
+ when *Postgres::JSON_WRAP_CLASSES
578
+ Postgres::JSONDatabaseMethods.jsonb_primitive_wrapper(v).new(v)
508
579
  else
509
580
  raise Error, "invalid value passed to Sequel.pg_jsonb_wrap: #{v.inspect}"
510
581
  end
@@ -73,6 +73,23 @@
73
73
  # j.pretty # jsonb_pretty(jsonb_column)
74
74
  # j.set(%w'0 a', :h) # jsonb_set(jsonb_column, ARRAY['0','a'], h, true)
75
75
  #
76
+ # On PostgreSQL 12+ SQL/JSON functions and operators are supported:
77
+ #
78
+ # j.path_exists('$.foo') # (jsonb_column @? '$.foo')
79
+ # j.path_match('$.foo') # (jsonb_column @@ '$.foo')
80
+ #
81
+ # j.path_exists!('$.foo') # jsonb_path_exists(jsonb_column, '$.foo')
82
+ # j.path_match!('$.foo') # jsonb_path_match(jsonb_column, '$.foo')
83
+ # j.path_query('$.foo') # jsonb_path_query(jsonb_column, '$.foo')
84
+ # j.path_query_array('$.foo') # jsonb_path_query_array(jsonb_column, '$.foo')
85
+ # j.path_query_first('$.foo') # jsonb_path_query_first(jsonb_column, '$.foo')
86
+ #
87
+ # For the PostgreSQL 12+ SQL/JSON functions, one argument is required (+path+) and
88
+ # two more arguments are optional (+vars+ and +silent+). +path+ specifies the JSON path.
89
+ # +vars+ specifies a hash or a string in JSON format of named variables to be
90
+ # substituted in +path+. +silent+ specifies whether errors are suppressed. By default,
91
+ # errors are not suppressed.
92
+ #
76
93
  # If you are also using the pg_json extension, you should load it before
77
94
  # loading this extension. Doing so will allow you to use the #op method on
78
95
  # JSONHash, JSONHarray, JSONBHash, and JSONBArray, allowing you to perform json/jsonb operations
@@ -292,6 +309,8 @@ module Sequel
292
309
  CONTAINED_BY = ["(".freeze, " <@ ".freeze, ")".freeze].freeze
293
310
  DELETE_PATH = ["(".freeze, " #- ".freeze, ")".freeze].freeze
294
311
  HAS_KEY = ["(".freeze, " ? ".freeze, ")".freeze].freeze
312
+ PATH_EXISTS = ["(".freeze, " @? ".freeze, ")".freeze].freeze
313
+ PATH_MATCH = ["(".freeze, " @@ ".freeze, ")".freeze].freeze
295
314
 
296
315
  # jsonb expression for deletion of the given argument from the
297
316
  # current jsonb.
@@ -362,6 +381,95 @@ module Sequel
362
381
  self.class.new(function(:insert, wrap_input_array(path), wrap_input_jsonb(other), insert_after))
363
382
  end
364
383
 
384
+ # Returns whether the JSON path returns any item for the json object.
385
+ #
386
+ # json_op.path_exists("$.foo") # (json @? '$.foo')
387
+ def path_exists(path)
388
+ bool_op(PATH_EXISTS, path)
389
+ end
390
+
391
+ # Returns whether the JSON path returns any item for the json object.
392
+ #
393
+ # json_op.path_exists!("$.foo")
394
+ # # jsonb_path_exists(json, '$.foo')
395
+ #
396
+ # json_op.path_exists!("$.foo ? ($ > $x)", x: 2)
397
+ # # jsonb_path_exists(json, '$.foo ? ($ > $x)', '{"x":2}')
398
+ #
399
+ # json_op.path_exists!("$.foo ? ($ > $x)", {x: 2}, true)
400
+ # # jsonb_path_exists(json, '$.foo ? ($ > $x)', '{"x":2}', true)
401
+ def path_exists!(path, vars=nil, silent=nil)
402
+ Sequel::SQL::BooleanExpression.new(:NOOP, _path_function(:jsonb_path_exists, path, vars, silent))
403
+ end
404
+
405
+ # Returns the first item of the result of JSON path predicate check for the json object.
406
+ # Returns nil if the first item is not true or false.
407
+ #
408
+ # json_op.path_match("$.foo") # (json @@ '$.foo')
409
+ def path_match(path)
410
+ bool_op(PATH_MATCH, path)
411
+ end
412
+
413
+ # Returns the first item of the result of JSON path predicate check for the json object.
414
+ # Returns nil if the first item is not true or false and silent is true.
415
+ #
416
+ # json_op.path_match!("$.foo")
417
+ # # jsonb_path_match(json, '$.foo')
418
+ #
419
+ # json_op.path_match!("$.foo ? ($ > $x)", x: 2)
420
+ # # jsonb_path_match(json, '$.foo ? ($ > $x)', '{"x":2}')
421
+ #
422
+ # json_op.path_match!("$.foo ? ($ > $x)", {x: 2}, true)
423
+ # # jsonb_path_match(json, '$.foo ? ($ > $x)', '{"x":2}', true)
424
+ def path_match!(path, vars=nil, silent=nil)
425
+ Sequel::SQL::BooleanExpression.new(:NOOP, _path_function(:jsonb_path_match, path, vars, silent))
426
+ end
427
+
428
+ # Returns a set of all jsonb values specified by the JSON path
429
+ # for the json object.
430
+ #
431
+ # json_op.path_query("$.foo")
432
+ # # jsonb_path_query(json, '$.foo')
433
+ #
434
+ # json_op.path_query("$.foo ? ($ > $x)", x: 2)
435
+ # # jsonb_path_query(json, '$.foo ? ($ > $x)', '{"x":2}')
436
+ #
437
+ # json_op.path_query("$.foo ? ($ > $x)", {x: 2}, true)
438
+ # # jsonb_path_query(json, '$.foo ? ($ > $x)', '{"x":2}', true)
439
+ def path_query(path, vars=nil, silent=nil)
440
+ _path_function(:jsonb_path_query, path, vars, silent)
441
+ end
442
+
443
+ # Returns a jsonb array of all values specified by the JSON path
444
+ # for the json object.
445
+ #
446
+ # json_op.path_query_array("$.foo")
447
+ # # jsonb_path_query_array(json, '$.foo')
448
+ #
449
+ # json_op.path_query_array("$.foo ? ($ > $x)", x: 2)
450
+ # # jsonb_path_query_array(json, '$.foo ? ($ > $x)', '{"x":2}')
451
+ #
452
+ # json_op.path_query_array("$.foo ? ($ > $x)", {x: 2}, true)
453
+ # # jsonb_path_query_array(json, '$.foo ? ($ > $x)', '{"x":2}', true)
454
+ def path_query_array(path, vars=nil, silent=nil)
455
+ JSONBOp.new(_path_function(:jsonb_path_query_array, path, vars, silent))
456
+ end
457
+
458
+ # Returns the first item of the result specified by the JSON path
459
+ # for the json object.
460
+ #
461
+ # json_op.path_query_first("$.foo")
462
+ # # jsonb_path_query_first(json, '$.foo')
463
+ #
464
+ # json_op.path_query_first("$.foo ? ($ > $x)", x: 2)
465
+ # # jsonb_path_query_first(json, '$.foo ? ($ > $x)', '{"x":2}')
466
+ #
467
+ # json_op.path_query_first("$.foo ? ($ > $x)", {x: 2}, true)
468
+ # # jsonb_path_query_first(json, '$.foo ? ($ > $x)', '{"x":2}', true)
469
+ def path_query_first(path, vars=nil, silent=nil)
470
+ JSONBOp.new(_path_function(:jsonb_path_query_first, path, vars, silent))
471
+ end
472
+
365
473
  # Return the receiver, since it is already a JSONBOp.
366
474
  def pg_jsonb
367
475
  self
@@ -386,6 +494,22 @@ module Sequel
386
494
 
387
495
  private
388
496
 
497
+ # Internals of the jsonb SQL/JSON path functions.
498
+ def _path_function(func, path, vars, silent)
499
+ args = []
500
+ if vars
501
+ if vars.is_a?(Hash)
502
+ vars = vars.to_json
503
+ end
504
+ args << vars
505
+
506
+ unless silent.nil?
507
+ args << silent
508
+ end
509
+ end
510
+ SQL::Function.new(func, self, path, *args)
511
+ end
512
+
389
513
  # Return a placeholder literal with the given str and args, wrapped
390
514
  # in a boolean expression, used by operators that return booleans.
391
515
  def bool_op(str, other)
@@ -402,6 +402,15 @@ module Sequel
402
402
  end
403
403
  alias == eql?
404
404
 
405
+ # Make sure equal ranges have the same hash.
406
+ def hash
407
+ if @empty
408
+ @db_type.hash
409
+ else
410
+ [@begin, @end, @exclude_begin, @exclude_end, @db_type].hash
411
+ end
412
+ end
413
+
405
414
  # Allow PGRange values in case statements, where they return true if they
406
415
  # are equal to each other using eql?, or if this PGRange can be converted
407
416
  # to a Range, delegating to that range.
@@ -453,13 +462,14 @@ module Sequel
453
462
  end
454
463
 
455
464
  ENDLESS_RANGE_NOT_SUPPORTED = RUBY_VERSION < '2.6'
465
+ STARTLESS_RANGE_NOT_SUPPORTED = RUBY_VERSION < '2.7'
456
466
 
457
467
  # Return a ruby Range object for this instance, if one can be created.
458
468
  def to_range
459
469
  return @range if @range
460
470
  raise(Error, "cannot create ruby range for an empty PostgreSQL range") if empty?
461
471
  raise(Error, "cannot create ruby range when PostgreSQL range excludes beginning element") if exclude_begin?
462
- raise(Error, "cannot create ruby range when PostgreSQL range has unbounded beginning") unless self.begin
472
+ raise(Error, "cannot create ruby range when PostgreSQL range has unbounded beginning") if STARTLESS_RANGE_NOT_SUPPORTED && !self.begin
463
473
  raise(Error, "cannot create ruby range when PostgreSQL range has unbounded ending") if ENDLESS_RANGE_NOT_SUPPORTED && !self.end
464
474
  @range = Range.new(self.begin, self.end, exclude_end?)
465
475
  end
@@ -468,7 +478,7 @@ module Sequel
468
478
  # it must have a beginning and an ending (no unbounded ranges), and it cannot exclude
469
479
  # the beginning element.
470
480
  def valid_ruby_range?
471
- !(empty? || exclude_begin? || !self.begin || (ENDLESS_RANGE_NOT_SUPPORTED && !self.end))
481
+ !(empty? || exclude_begin? || (STARTLESS_RANGE_NOT_SUPPORTED && !self.begin) || (ENDLESS_RANGE_NOT_SUPPORTED && !self.end))
472
482
  end
473
483
 
474
484
  # Whether the beginning of the range is unbounded.
@@ -7,7 +7,7 @@
7
7
  # that when composite fields are retrieved, they are parsed and returned
8
8
  # as instances of Sequel::Postgres::PGRow::(HashRow|ArrayRow), or
9
9
  # optionally a custom type. HashRow and ArrayRow are DelegateClasses of
10
- # of Hash and Array, so they mostly act like a hash or array, but not
10
+ # Hash and Array, so they mostly act like a hash or array, but not
11
11
  # completely (is_a?(Hash) and is_a?(Array) are false). If you want the
12
12
  # actual hash for a HashRow, call HashRow#to_hash, and if you want the
13
13
  # actual array for an ArrayRow, call ArrayRow#to_a. This is done so
@@ -228,7 +228,9 @@ module Sequel
228
228
  if skip(/\)/)
229
229
  values << nil
230
230
  else
231
+ # :nocov:
231
232
  until eos?
233
+ # :nocov:
232
234
  if skip(/"/)
233
235
  values << scan(/(\\.|""|[^"])*/).gsub(/\\(.)|"(")/, '\1\2')
234
236
  skip(/"[,)]/)
@@ -9,8 +9,8 @@
9
9
  # #
10
10
  #
11
11
  # As you can see, this uses single line SQL comments (--) suffixed
12
- # by a newline. This # plugin transforms all consecutive
13
- # whitespace in the comment to # a single string:
12
+ # by a newline. This plugin transforms all consecutive whitespace
13
+ # in the comment to a single string:
14
14
  #
15
15
  # ds = DB[:table].comment("Some\r\nComment Here").all
16
16
  # # SELECT * FROM table -- Some Comment Here
@@ -1069,7 +1069,7 @@ module Sequel
1069
1069
  @new = true
1070
1070
  @modified = true
1071
1071
  initialize_set(values)
1072
- _changed_columns.clear
1072
+ _clear_changed_columns(:initialize)
1073
1073
  yield self if block_given?
1074
1074
  end
1075
1075
 
@@ -1626,6 +1626,13 @@ module Sequel
1626
1626
  def _changed_columns
1627
1627
  @changed_columns ||= []
1628
1628
  end
1629
+
1630
+ # Clear the changed columns. Reason is the reason for clearing
1631
+ # the columns, and should be one of: :initialize, :refresh, :create
1632
+ # or :update.
1633
+ def _clear_changed_columns(_reason)
1634
+ _changed_columns.clear
1635
+ end
1629
1636
 
1630
1637
  # Do the deletion of the object's dataset, and check that the row
1631
1638
  # was actually deleted.
@@ -1716,7 +1723,7 @@ module Sequel
1716
1723
  # is used for reading newly inserted values from the database
1717
1724
  def _refresh(dataset)
1718
1725
  _refresh_set_values(_refresh_get(dataset) || raise(NoExistingObject, "Record not found"))
1719
- _changed_columns.clear
1726
+ _clear_changed_columns(:refresh)
1720
1727
  end
1721
1728
 
1722
1729
  # Get the row of column data from the database.
@@ -1754,7 +1761,7 @@ module Sequel
1754
1761
  @this = nil
1755
1762
  @new = false
1756
1763
  @modified = false
1757
- pk ? _save_refresh : _changed_columns.clear
1764
+ pk ? _save_refresh : _clear_changed_columns(:create)
1758
1765
  after_create
1759
1766
  true
1760
1767
  end
@@ -1771,7 +1778,7 @@ module Sequel
1771
1778
  cc.clear
1772
1779
  else
1773
1780
  columns_updated = _save_update_all_columns_hash
1774
- _changed_columns.clear
1781
+ _clear_changed_columns(:update)
1775
1782
  end
1776
1783
  else # update only the specified columns
1777
1784
  columns = Array(columns)
@@ -1798,7 +1805,7 @@ module Sequel
1798
1805
  # can be overridden to avoid the refresh.
1799
1806
  def _save_refresh
1800
1807
  _save_set_values(_refresh_get(this.server?(:default)) || raise(NoExistingObject, "Record not found"))
1801
- _changed_columns.clear
1808
+ _clear_changed_columns(:create)
1802
1809
  end
1803
1810
 
1804
1811
  # Set values to the provided hash. Called after a create,