sequel 3.44.0 → 3.45.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 (97) hide show
  1. data/CHANGELOG +44 -0
  2. data/Rakefile +12 -4
  3. data/doc/reflection.rdoc +3 -3
  4. data/doc/release_notes/3.45.0.txt +179 -0
  5. data/doc/schema_modification.rdoc +1 -1
  6. data/doc/transactions.rdoc +23 -0
  7. data/lib/sequel/adapters/db2.rb +1 -0
  8. data/lib/sequel/adapters/ibmdb.rb +19 -3
  9. data/lib/sequel/adapters/jdbc.rb +15 -0
  10. data/lib/sequel/adapters/jdbc/derby.rb +1 -5
  11. data/lib/sequel/adapters/jdbc/h2.rb +1 -0
  12. data/lib/sequel/adapters/jdbc/hsqldb.rb +2 -1
  13. data/lib/sequel/adapters/jdbc/jtds.rb +5 -0
  14. data/lib/sequel/adapters/jdbc/mysql.rb +5 -0
  15. data/lib/sequel/adapters/jdbc/oracle.rb +7 -1
  16. data/lib/sequel/adapters/jdbc/sqlite.rb +1 -1
  17. data/lib/sequel/adapters/jdbc/transactions.rb +28 -1
  18. data/lib/sequel/adapters/mysql.rb +4 -0
  19. data/lib/sequel/adapters/mysql2.rb +5 -1
  20. data/lib/sequel/adapters/oracle.rb +18 -0
  21. data/lib/sequel/adapters/postgres.rb +11 -1
  22. data/lib/sequel/adapters/shared/access.rb +14 -2
  23. data/lib/sequel/adapters/shared/cubrid.rb +1 -11
  24. data/lib/sequel/adapters/shared/db2.rb +11 -6
  25. data/lib/sequel/adapters/shared/mssql.rb +10 -10
  26. data/lib/sequel/adapters/shared/mysql.rb +11 -1
  27. data/lib/sequel/adapters/shared/mysql_prepared_statements.rb +17 -1
  28. data/lib/sequel/adapters/shared/oracle.rb +16 -15
  29. data/lib/sequel/adapters/shared/postgres.rb +91 -59
  30. data/lib/sequel/adapters/shared/sqlite.rb +1 -4
  31. data/lib/sequel/adapters/tinytds.rb +15 -0
  32. data/lib/sequel/connection_pool/threaded.rb +1 -1
  33. data/lib/sequel/core.rb +10 -0
  34. data/lib/sequel/database/connecting.rb +2 -0
  35. data/lib/sequel/database/misc.rb +46 -4
  36. data/lib/sequel/database/query.rb +33 -14
  37. data/lib/sequel/database/schema_methods.rb +0 -5
  38. data/lib/sequel/dataset/misc.rb +9 -0
  39. data/lib/sequel/dataset/mutation.rb +9 -7
  40. data/lib/sequel/dataset/sql.rb +13 -0
  41. data/lib/sequel/exceptions.rb +3 -0
  42. data/lib/sequel/extensions/connection_validator.rb +1 -1
  43. data/lib/sequel/extensions/date_arithmetic.rb +0 -8
  44. data/lib/sequel/extensions/eval_inspect.rb +2 -0
  45. data/lib/sequel/extensions/named_timezones.rb +18 -2
  46. data/lib/sequel/extensions/pg_array.rb +5 -1
  47. data/lib/sequel/extensions/pg_array_ops.rb +2 -0
  48. data/lib/sequel/extensions/pg_hstore.rb +2 -0
  49. data/lib/sequel/extensions/pg_hstore_ops.rb +2 -0
  50. data/lib/sequel/extensions/pg_json.rb +3 -1
  51. data/lib/sequel/extensions/pg_range.rb +2 -0
  52. data/lib/sequel/extensions/pg_range_ops.rb +2 -0
  53. data/lib/sequel/extensions/pg_row.rb +2 -0
  54. data/lib/sequel/extensions/pg_row_ops.rb +2 -0
  55. data/lib/sequel/extensions/query.rb +18 -22
  56. data/lib/sequel/model/associations.rb +3 -4
  57. data/lib/sequel/model/base.rb +2 -0
  58. data/lib/sequel/plugins/force_encoding.rb +2 -0
  59. data/lib/sequel/plugins/json_serializer.rb +155 -39
  60. data/lib/sequel/plugins/serialization.rb +14 -2
  61. data/lib/sequel/plugins/unlimited_update.rb +31 -0
  62. data/lib/sequel/plugins/validation_class_methods.rb +6 -4
  63. data/lib/sequel/plugins/xml_serializer.rb +133 -30
  64. data/lib/sequel/sql.rb +2 -0
  65. data/lib/sequel/timezones.rb +4 -0
  66. data/lib/sequel/version.rb +1 -1
  67. data/spec/adapters/mysql_spec.rb +0 -11
  68. data/spec/adapters/postgres_spec.rb +86 -54
  69. data/spec/adapters/spec_helper.rb +6 -0
  70. data/spec/core/connection_pool_spec.rb +16 -0
  71. data/spec/core/database_spec.rb +77 -1
  72. data/spec/core/dataset_spec.rb +30 -15
  73. data/spec/core/expression_filters_spec.rb +55 -13
  74. data/spec/core/mock_adapter_spec.rb +4 -0
  75. data/spec/core/schema_spec.rb +0 -2
  76. data/spec/core/spec_helper.rb +5 -0
  77. data/spec/core_extensions_spec.rb +33 -28
  78. data/spec/extensions/constraint_validations_spec.rb +2 -2
  79. data/spec/extensions/core_refinements_spec.rb +12 -12
  80. data/spec/extensions/json_serializer_spec.rb +137 -31
  81. data/spec/extensions/named_timezones_spec.rb +10 -0
  82. data/spec/extensions/pg_auto_parameterize_spec.rb +5 -0
  83. data/spec/extensions/pg_json_spec.rb +14 -0
  84. data/spec/extensions/pg_row_spec.rb +11 -0
  85. data/spec/extensions/pretty_table_spec.rb +2 -2
  86. data/spec/extensions/query_spec.rb +11 -8
  87. data/spec/extensions/serialization_spec.rb +20 -0
  88. data/spec/extensions/spec_helper.rb +8 -2
  89. data/spec/extensions/sql_expr_spec.rb +1 -1
  90. data/spec/extensions/unlimited_update_spec.rb +20 -0
  91. data/spec/extensions/xml_serializer_spec.rb +68 -16
  92. data/spec/integration/dataset_test.rb +28 -0
  93. data/spec/integration/spec_helper.rb +6 -0
  94. data/spec/integration/transaction_test.rb +39 -0
  95. data/spec/model/model_spec.rb +1 -1
  96. data/spec/sequel_coverage.rb +15 -0
  97. metadata +8 -3
@@ -201,6 +201,13 @@ module Sequel
201
201
  Sequel.synchronize{prepared_statements[name]}
202
202
  end
203
203
 
204
+ # Proxy the quote_identifier method to the dataset,
205
+ # useful for quoting unqualified identifiers for use
206
+ # outside of datasets.
207
+ def quote_identifier(v)
208
+ schema_utility_dataset.quote_identifier(v)
209
+ end
210
+
204
211
  # Default serial primary key options, used by the table creation
205
212
  # code.
206
213
  def serial_primary_key_options
@@ -326,9 +333,9 @@ module Sequel
326
333
  []
327
334
  end
328
335
 
329
- # A hash with regexp keys and exception class values, used
336
+ # An enumerable yielding pairs of regexps and exception classes, used
330
337
  # to match against underlying driver exception messages in
331
- # order to raise a more specific Sequel::Error subclass.
338
+ # order to raise a more specific Sequel::DatabaseError subclass.
332
339
  def database_error_regexps
333
340
  DEFAULT_DATABASE_ERROR_REGEXPS
334
341
  end
@@ -339,19 +346,52 @@ module Sequel
339
346
  database_specific_error_class(exception, opts) || DatabaseError
340
347
  end
341
348
 
349
+ # Return the SQLState for the given exception, if one can be
350
+ # determined
351
+ def database_exception_sqlstate(exception, opts)
352
+ nil
353
+ end
354
+
342
355
  # Return a specific Sequel::DatabaseError exception class if
343
356
  # one is appropriate for the underlying exception,
344
357
  # or nil if there is no specific exception class.
345
358
  def database_specific_error_class(exception, opts)
346
359
  return DatabaseDisconnectError if disconnect_error?(exception, opts)
347
360
 
348
- database_error_regexps.each do |regexp, klass|
349
- return klass if exception.message =~ regexp
361
+ if sqlstate = database_exception_sqlstate(exception, opts)
362
+ if klass = database_specific_error_class_from_sqlstate(sqlstate)
363
+ return klass
364
+ end
365
+ else
366
+ database_error_regexps.each do |regexp, klass|
367
+ return klass if exception.message =~ regexp
368
+ end
350
369
  end
351
370
 
352
371
  nil
353
372
  end
354
373
 
374
+ NOT_NULL_CONSTRAINT_SQLSTATES = %w'23502'.freeze.each{|s| s.freeze}
375
+ FOREIGN_KEY_CONSTRAINT_SQLSTATES = %w'23503 23506 23504'.freeze.each{|s| s.freeze}
376
+ UNIQUE_CONSTRAINT_SQLSTATES = %w'23505'.freeze.each{|s| s.freeze}
377
+ CHECK_CONSTRAINT_SQLSTATES = %w'23513 23514'.freeze.each{|s| s.freeze}
378
+ SERIALIZATION_CONSTRAINT_SQLSTATES = %w'40001'.freeze.each{|s| s.freeze}
379
+ # Given the SQLState, return the appropriate DatabaseError subclass.
380
+ def database_specific_error_class_from_sqlstate(sqlstate)
381
+ case sqlstate
382
+ when *NOT_NULL_CONSTRAINT_SQLSTATES
383
+ NotNullConstraintViolation
384
+ when *FOREIGN_KEY_CONSTRAINT_SQLSTATES
385
+ ForeignKeyConstraintViolation
386
+ when *UNIQUE_CONSTRAINT_SQLSTATES
387
+ UniqueConstraintViolation
388
+ when *CHECK_CONSTRAINT_SQLSTATES
389
+ CheckConstraintViolation
390
+ when *SERIALIZATION_CONSTRAINT_SQLSTATES
391
+ SerializationFailure
392
+ end
393
+ end
394
+
355
395
  # Return true if exception represents a disconnect error, false otherwise.
356
396
  def disconnect_error?(exception, opts)
357
397
  opts[:disconnect]
@@ -435,12 +475,14 @@ module Sequel
435
475
  (value.is_a?(String) && value =~ LEADING_ZERO_RE) ? Integer(value, 10) : Integer(value)
436
476
  end
437
477
  else
478
+ # :nocov:
438
479
  # Replacement string when replacing leading zeroes.
439
480
  LEADING_ZERO_REP = "\\1".freeze
440
481
  # Typecast the value to an Integer
441
482
  def typecast_value_integer(value)
442
483
  Integer(value.is_a?(String) ? value.sub(LEADING_ZERO_RE, LEADING_ZERO_REP) : value)
443
484
  end
485
+ # :nocov:
444
486
  end
445
487
 
446
488
  # Typecast the value to a String
@@ -251,21 +251,26 @@ module Sequel
251
251
  #
252
252
  # The following general options are respected:
253
253
  #
254
- # :disconnect :: Can be set to :retry to automatically retry the transaction with
255
- # a new connection object if it detects a disconnect on the connection.
256
- # Note that this should not be used unless the entire transaction
257
- # block is idempotent, as otherwise it can cause non-idempotent
258
- # behavior to execute multiple times. This does no checking for
259
- # infinite loops, so if your transaction will repeatedly raise a
260
- # disconnection error, this will cause the transaction block to loop
261
- # indefinitely.
254
+ # :disconnect :: If set to :retry, automatically sets the :retry_on option
255
+ # with a Sequel::DatabaseDisconnectError. This option is only
256
+ # present for backwards compatibility, please use the :retry_on
257
+ # option instead.
262
258
  # :isolation :: The transaction isolation level to use for this transaction,
263
259
  # should be :uncommitted, :committed, :repeatable, or :serializable,
264
260
  # used if given and the database/adapter supports customizable
265
261
  # transaction isolation levels.
262
+ # :num_retries :: The number of times to retry if the :retry_on option is used.
263
+ # The default is 5 times. Can be set to nil to retry indefinitely,
264
+ # but that is not recommended.
266
265
  # :prepare :: A string to use as the transaction identifier for a
267
266
  # prepared transaction (two-phase commit), if the database/adapter
268
267
  # supports prepared transactions.
268
+ # :retry_on :: An exception class or array of exception classes for which to
269
+ # automatically retry the transaction. Can only be set if not inside
270
+ # an existing transaction.
271
+ # Note that this should not be used unless the entire transaction
272
+ # block is idempotent, as otherwise it can cause non-idempotent
273
+ # behavior to execute multiple times.
269
274
  # :rollback :: Can the set to :reraise to reraise any Sequel::Rollback exceptions
270
275
  # raised, or :always to always rollback even if no exceptions occur
271
276
  # (useful for testing).
@@ -284,16 +289,28 @@ module Sequel
284
289
  # and :remote_write (9.2+).
285
290
  def transaction(opts={}, &block)
286
291
  if opts[:disconnect] == :retry
292
+ raise(Error, 'cannot specify both :disconnect=>:retry and :retry_on') if opts[:retry_on]
293
+ return transaction(opts.merge(:retry_on=>Sequel::DatabaseDisconnectError, :disconnect=>nil), &block)
294
+ end
295
+
296
+ if retry_on = opts[:retry_on]
297
+ num_retries = opts.fetch(:num_retries, 5)
287
298
  begin
288
- transaction(opts.merge(:disconnect=>:disallow), &block)
289
- rescue Sequel::DatabaseDisconnectError
290
- retry
299
+ transaction(opts.merge(:retry_on=>nil, :retrying=>true), &block)
300
+ rescue *retry_on
301
+ if num_retries
302
+ num_retries -= 1
303
+ retry if num_retries >= 0
304
+ else
305
+ retry
306
+ end
307
+ raise
291
308
  end
292
309
  else
293
310
  synchronize(opts[:server]) do |conn|
294
311
  if already_in_transaction?(conn, opts)
295
- if opts[:disconnect] == :disallow
296
- raise Sequel::Error, "cannot set :disconnect=>:retry if you are already inside a transaction"
312
+ if opts[:retrying]
313
+ raise Sequel::Error, "cannot set :disconnect=>:retry or :retry_on options if you are already inside a transaction"
297
314
  end
298
315
  return yield(conn)
299
316
  end
@@ -501,6 +518,7 @@ module Sequel
501
518
  end
502
519
 
503
520
  if (! defined?(RUBY_ENGINE) or RUBY_ENGINE == 'ruby' or RUBY_ENGINE == 'rbx') and RUBY_VERSION < '1.9'
521
+ # :nocov:
504
522
  # Whether to commit the current transaction. On ruby 1.8 and rubinius,
505
523
  # Thread.current.status is checked because Thread#kill skips rescue
506
524
  # blocks (so exception would be nil), but the transaction should
@@ -518,6 +536,7 @@ module Sequel
518
536
  end
519
537
  end
520
538
  end
539
+ # :nocov:
521
540
  else
522
541
  # Whether to commit the current transaction. On ruby 1.9 and JRuby,
523
542
  # transactions will be committed if Thread#kill is used on an thread
@@ -642,7 +661,7 @@ module Sequel
642
661
  :boolean
643
662
  when /\A(real|float|double( precision)?|double\(\d+,\d+\)( unsigned)?)\z/io
644
663
  :float
645
- when /\A(?:(?:(?:num(?:ber|eric)?|decimal)(?:\(\d+,\s*(\d+|false|true)\))?)|(?:small)?money)\z/io
664
+ when /\A(?:(?:(?:num(?:ber|eric)?|decimal)(?:\(\d+,\s*(\d+|false|true)\))?))\z/io
646
665
  $1 && ['0', 'false'].include?($1) ? :integer : :decimal
647
666
  when /bytea|blob|image|(var)?binary/io
648
667
  :blob
@@ -705,11 +705,6 @@ module Sequel
705
705
  schema_utility_dataset.quote_schema_table(table)
706
706
  end
707
707
 
708
- # Proxy the quote_identifier method to the dataset, used for quoting tables and columns.
709
- def quote_identifier(v)
710
- schema_utility_dataset.quote_identifier(v)
711
- end
712
-
713
708
  # SQL DDL statement for renaming a table.
714
709
  def rename_table_sql(name, new_name)
715
710
  "ALTER TABLE #{quote_schema_table(name)} RENAME TO #{quote_schema_table(new_name)}"
@@ -49,6 +49,15 @@ module Sequel
49
49
  def each_server
50
50
  db.servers.each{|s| yield server(s)}
51
51
  end
52
+
53
+ # Returns the string with the LIKE metacharacters (% and _) escaped.
54
+ # Useful for when the LIKE term is a user-provided string where metacharacters should not
55
+ # be recognized. Example:
56
+ #
57
+ # ds.escape_like("foo\\%_") # 'foo\\\%\_'
58
+ def escape_like(string)
59
+ string.gsub(/[\\%_]/){|m| "\\#{m}"}
60
+ end
52
61
 
53
62
  # Alias of +first_source_alias+
54
63
  def first_source
@@ -6,14 +6,17 @@ module Sequel
6
6
  # ---------------------
7
7
 
8
8
  # All methods that should have a ! method added that modifies the receiver.
9
- MUTATION_METHODS = QUERY_METHODS - [:paginate, :naked]
9
+ MUTATION_METHODS = QUERY_METHODS - [:paginate, :naked, :from_self]
10
10
 
11
11
  # Setup mutation (e.g. filter!) methods. These operate the same as the
12
12
  # non-! methods, but replace the options of the current dataset with the
13
13
  # options of the resulting dataset.
14
14
  def self.def_mutation_method(*meths)
15
+ options = meths.pop if meths.last.is_a?(Hash)
16
+ mod = options[:module] if options
17
+ mod ||= self
15
18
  meths.each do |meth|
16
- class_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end", __FILE__, __LINE__)
19
+ mod.class_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end", __FILE__, __LINE__)
17
20
  end
18
21
  end
19
22
 
@@ -33,11 +36,10 @@ module Sequel
33
36
  # a single hash argument and returns the object you want #each to return.
34
37
  attr_accessor :row_proc
35
38
 
36
- # Add a mutation method to this dataset instance.
37
- def def_mutation_method(*meths)
38
- meths.each do |meth|
39
- instance_eval("def #{meth}!(*args, &block); mutation_method(:#{meth}, *args, &block) end", __FILE__, __LINE__)
40
- end
39
+ # Avoid self-referential dataset by cloning.
40
+ def from_self!(*args, &block)
41
+ @opts.merge!(clone.from_self(*args, &block).opts)
42
+ self
41
43
  end
42
44
 
43
45
  # Remove the row_proc from the current dataset.
@@ -189,6 +189,7 @@ module Sequel
189
189
  ARRAY_EMPTY = '(NULL)'.freeze
190
190
  AS = ' AS '.freeze
191
191
  ASC = ' ASC'.freeze
192
+ BACKSLASH = "\\".freeze
192
193
  BOOL_FALSE = "'f'".freeze
193
194
  BOOL_TRUE = "'t'".freeze
194
195
  BRACKET_CLOSE = ']'.freeze
@@ -219,6 +220,7 @@ module Sequel
219
220
  DOUBLE_APOS = "''".freeze
220
221
  DOUBLE_QUOTE = '""'.freeze
221
222
  EQUAL = ' = '.freeze
223
+ ESCAPE = " ESCAPE ".freeze
222
224
  EXTRACT = 'extract('.freeze
223
225
  EXISTS = ['EXISTS '.freeze].freeze
224
226
  FOR_UPDATE = ' FOR UPDATE'.freeze
@@ -239,6 +241,7 @@ module Sequel
239
241
  INTO = " INTO ".freeze
240
242
  IS_LITERALS = {nil=>'NULL'.freeze, true=>'TRUE'.freeze, false=>'FALSE'.freeze}.freeze
241
243
  IS_OPERATORS = ::Sequel::SQL::ComplexExpression::IS_OPERATORS
244
+ LIKE_OPERATORS = ::Sequel::SQL::ComplexExpression::LIKE_OPERATORS
242
245
  LIMIT = " LIMIT ".freeze
243
246
  N_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::N_ARITY_OPERATORS
244
247
  NOT_SPACE = 'NOT '.freeze
@@ -455,6 +458,16 @@ module Sequel
455
458
  literal_append(sql, vals)
456
459
  sql << PAREN_CLOSE
457
460
  end
461
+ when :LIKE, :'NOT LIKE'
462
+ sql << PAREN_OPEN
463
+ literal_append(sql, args.at(0))
464
+ sql << SPACE << op.to_s << SPACE
465
+ literal_append(sql, args.at(1))
466
+ sql << ESCAPE
467
+ literal_append(sql, BACKSLASH)
468
+ sql << PAREN_CLOSE
469
+ when :ILIKE, :'NOT ILIKE'
470
+ complex_expression_sql_append(sql, (op == :ILIKE ? :LIKE : :"NOT LIKE"), args.map{|v| Sequel.function(:UPPER, v)})
458
471
  when *TWO_ARITY_OPERATORS
459
472
  if REGEXP_OPERATORS.include?(op) && !supports_regexp?
460
473
  raise InvalidOperation, "Pattern matching via regular expressions is not supported on #{db.database_type}"
@@ -40,6 +40,9 @@ module Sequel
40
40
  # Error raised when Sequel determines a database unique constraint has been violated.
41
41
  class UniqueConstraintViolation < ConstraintViolation; end
42
42
 
43
+ # Error raised when Sequel determines a serialization failure/deadlock in the database.
44
+ class SerializationFailure < DatabaseError; end
45
+
43
46
  # Error raised on an invalid operation, such as trying to update or delete
44
47
  # a joined or grouped dataset.
45
48
  class InvalidOperation < Error; end
@@ -21,7 +21,7 @@
21
21
  # Note that if you set the timeout to validate connections
22
22
  # on every checkout, you should probably manually control
23
23
  # connection checkouts on a coarse basis, using
24
- # Database#synchonrize. In a web application, the optimal
24
+ # Database#synchronize. In a web application, the optimal
25
25
  # place for that would be a rack middleware. Validating
26
26
  # connections on every checkout without setting up coarse
27
27
  # connection checkouts will hurt performance, in some cases
@@ -62,14 +62,6 @@ module Sequel
62
62
  ACCESS_DURATION_UNITS = DURATION_UNITS.zip(%w'yyyy m d h n s'.map{|s| s.freeze}).freeze
63
63
  DB2_DURATION_UNITS = DURATION_UNITS.zip(DURATION_UNITS.map{|s| Sequel.lit(s.to_s).freeze}).freeze
64
64
 
65
- # Return an SQL fragment for the literalized version of the
66
- # DateAdd expression.
67
- def date_add_sql(da)
68
- sql = ''
69
- date_arith_sql_append(sql, da)
70
- sql
71
- end
72
-
73
65
  # Append the SQL fragment for the DateAdd expression to the SQL query.
74
66
  def date_add_sql_append(sql, da)
75
67
  h = da.interval
@@ -30,10 +30,12 @@ module Sequel
30
30
  when Time
31
31
  datepart = "%Y-%m-%dT" unless obj.is_a?(Sequel::SQLTime)
32
32
  if RUBY_VERSION < '1.9'
33
+ # :nocov:
33
34
  # Time on 1.8 doesn't handle %N (or %z on Windows), manually set the usec value in the string
34
35
  hours, mins = obj.utc_offset.divmod(3600)
35
36
  mins /= 60
36
37
  "#{obj.class}.parse(#{obj.strftime("#{datepart}%H:%M:%S.#{sprintf('%06i%+03i%02i', obj.usec, hours, mins)}").inspect})#{'.utc' if obj.utc?}"
38
+ # :nocov:
37
39
  else
38
40
  "#{obj.class}.parse(#{obj.strftime("#{datepart}%T.%N%z").inspect})#{'.utc' if obj.utc?}"
39
41
  end
@@ -37,6 +37,13 @@ module Sequel
37
37
  self.datetime_class = DateTime
38
38
 
39
39
  module NamedTimezones
40
+ # Handles TZInfo::AmbiguousTime exceptions automatically by providing a
41
+ # proc called with both the datetime value being converted as well as
42
+ # the array of TZInfo::TimezonePeriod results. Example:
43
+ #
44
+ # Sequel.tzinfo_disambiguator = proc{|datetime, periods| periods.first}
45
+ attr_accessor :tzinfo_disambiguator
46
+
40
47
  private
41
48
 
42
49
  # Assume the given DateTime has a correct time but a wrong timezone. It is
@@ -44,7 +51,7 @@ module Sequel
44
51
  # Keep the time the same but convert the timezone to the input_timezone.
45
52
  # Expects the input_timezone to be a TZInfo::Timezone instance.
46
53
  def convert_input_datetime_other(v, input_timezone)
47
- local_offset = input_timezone.period_for_local(v).utc_total_offset_rational
54
+ local_offset = input_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset_rational
48
55
  (v - local_offset).new_offset(local_offset)
49
56
  end
50
57
 
@@ -54,7 +61,7 @@ module Sequel
54
61
  # TZInfo converts times, but expects the given DateTime to have an offset
55
62
  # of 0 and always leaves the timezone offset as 0
56
63
  v = output_timezone.utc_to_local(v.new_offset(0))
57
- local_offset = output_timezone.period_for_local(v).utc_total_offset_rational
64
+ local_offset = output_timezone.period_for_local(v, &tzinfo_disambiguator_for(v)).utc_total_offset_rational
58
65
  # Convert timezone offset from UTC to the offset for the output_timezone
59
66
  (v - local_offset).new_offset(local_offset)
60
67
  end
@@ -63,6 +70,15 @@ module Sequel
63
70
  def convert_timezone_setter_arg(tz)
64
71
  tz.is_a?(String) ? TZInfo::Timezone.get(tz) : super
65
72
  end
73
+
74
+ # Return a disambiguation proc that provides both the datetime value
75
+ # and the periods, in order to allow the choice of period to depend
76
+ # on the datetime value.
77
+ def tzinfo_disambiguator_for(v)
78
+ if pr = @tzinfo_disambiguator
79
+ proc{|periods| pr.call(v, periods)}
80
+ end
81
+ end
66
82
  end
67
83
 
68
84
  extend NamedTimezones
@@ -402,10 +402,12 @@ module Sequel
402
402
  end
403
403
 
404
404
  if Sequel::Postgres.respond_to?(:parse_pg_array)
405
+ # :nocov:
405
406
  # Use sequel_pg's C-based parser if it has already been defined.
406
407
  def call(string)
407
408
  PGArray.new(Sequel::Postgres.parse_pg_array(string, @converter), @type)
408
409
  end
410
+ # :nocov:
409
411
  else
410
412
  # Parse the string using Parser with the appropriate
411
413
  # converter, and return a PGArray with the appropriate database
@@ -431,7 +433,7 @@ module Sequel
431
433
  # recursive map of the output is done to make sure that the entires in the
432
434
  # correct type.
433
435
  def call(string)
434
- array = JSON.parse(string.gsub(SUBST_RE){|m| SUBST[m]})
436
+ array = Sequel.parse_json(string.gsub(SUBST_RE){|m| SUBST[m]})
435
437
  array = Sequel.recursive_map(array, @converter) if @converter
436
438
  PGArray.new(array, @type)
437
439
  end
@@ -527,6 +529,7 @@ module Sequel
527
529
  Database.register_extension(:pg_array, Postgres::PGArray::DatabaseMethods)
528
530
  end
529
531
 
532
+ # :nocov:
530
533
  if Sequel.core_extensions?
531
534
  class Array
532
535
  # Return a PGArray proxy to the receiver, using a
@@ -548,3 +551,4 @@ if defined?(Sequel::CoreRefinements)
548
551
  end
549
552
  end
550
553
  end
554
+ # :nocov:
@@ -257,6 +257,7 @@ module Sequel
257
257
  end
258
258
  end
259
259
 
260
+ # :nocov:
260
261
  if Sequel.core_extensions?
261
262
  class Symbol
262
263
  include Sequel::Postgres::ArrayOpMethods
@@ -270,3 +271,4 @@ if defined?(Sequel::CoreRefinements)
270
271
  end
271
272
  end
272
273
  end
274
+ # :nocov:
@@ -320,6 +320,7 @@ module Sequel
320
320
  Database.register_extension(:pg_hstore, Postgres::HStore::DatabaseMethods)
321
321
  end
322
322
 
323
+ # :nocov:
323
324
  if Sequel.core_extensions?
324
325
  class Hash
325
326
  # Create a new HStore using the receiver as the input
@@ -343,3 +344,4 @@ if defined?(Sequel::CoreRefinements)
343
344
  end
344
345
  end
345
346
  end
347
+ # :nocov: