sequel 3.44.0 → 3.45.0

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