sequel 5.22.0 → 5.26.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +66 -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.23.0.txt +56 -0
  8. data/doc/release_notes/5.24.0.txt +56 -0
  9. data/doc/release_notes/5.25.0.txt +32 -0
  10. data/doc/release_notes/5.26.0.txt +35 -0
  11. data/doc/testing.rdoc +1 -0
  12. data/lib/sequel/adapters/jdbc.rb +7 -1
  13. data/lib/sequel/adapters/jdbc/postgresql.rb +1 -13
  14. data/lib/sequel/adapters/jdbc/sqlite.rb +29 -0
  15. data/lib/sequel/adapters/mysql2.rb +0 -1
  16. data/lib/sequel/adapters/shared/mssql.rb +9 -8
  17. data/lib/sequel/adapters/shared/postgres.rb +25 -7
  18. data/lib/sequel/adapters/shared/sqlite.rb +16 -2
  19. data/lib/sequel/adapters/tinytds.rb +12 -0
  20. data/lib/sequel/adapters/utils/mysql_mysql2.rb +1 -1
  21. data/lib/sequel/database/logging.rb +7 -1
  22. data/lib/sequel/database/schema_generator.rb +11 -2
  23. data/lib/sequel/database/schema_methods.rb +2 -0
  24. data/lib/sequel/dataset/actions.rb +3 -2
  25. data/lib/sequel/extensions/named_timezones.rb +51 -9
  26. data/lib/sequel/extensions/pg_array.rb +4 -0
  27. data/lib/sequel/extensions/pg_json.rb +88 -17
  28. data/lib/sequel/extensions/pg_json_ops.rb +124 -0
  29. data/lib/sequel/extensions/pg_range.rb +9 -0
  30. data/lib/sequel/extensions/pg_row.rb +3 -1
  31. data/lib/sequel/extensions/sql_comments.rb +2 -2
  32. data/lib/sequel/model/base.rb +12 -5
  33. data/lib/sequel/plugins/association_multi_add_remove.rb +83 -0
  34. data/lib/sequel/plugins/association_proxies.rb +3 -2
  35. data/lib/sequel/plugins/caching.rb +3 -0
  36. data/lib/sequel/plugins/class_table_inheritance.rb +10 -0
  37. data/lib/sequel/plugins/csv_serializer.rb +26 -9
  38. data/lib/sequel/plugins/dirty.rb +3 -9
  39. data/lib/sequel/plugins/insert_conflict.rb +72 -0
  40. data/lib/sequel/plugins/nested_attributes.rb +7 -0
  41. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +89 -30
  42. data/lib/sequel/plugins/sharding.rb +11 -5
  43. data/lib/sequel/plugins/static_cache.rb +8 -3
  44. data/lib/sequel/plugins/static_cache_cache.rb +53 -0
  45. data/lib/sequel/plugins/typecast_on_load.rb +3 -2
  46. data/lib/sequel/sql.rb +3 -1
  47. data/lib/sequel/timezones.rb +50 -11
  48. data/lib/sequel/version.rb +1 -1
  49. data/spec/adapters/postgres_spec.rb +130 -0
  50. data/spec/bin_spec.rb +2 -2
  51. data/spec/core/database_spec.rb +50 -0
  52. data/spec/core/dataset_spec.rb +23 -1
  53. data/spec/core/expression_filters_spec.rb +7 -2
  54. data/spec/core/schema_spec.rb +18 -0
  55. data/spec/core/spec_helper.rb +1 -1
  56. data/spec/core_extensions_spec.rb +1 -1
  57. data/spec/extensions/association_multi_add_remove_spec.rb +1041 -0
  58. data/spec/extensions/dirty_spec.rb +33 -0
  59. data/spec/extensions/insert_conflict_spec.rb +103 -0
  60. data/spec/extensions/named_timezones_spec.rb +109 -2
  61. data/spec/extensions/nested_attributes_spec.rb +48 -0
  62. data/spec/extensions/pg_auto_constraint_validations_spec.rb +37 -0
  63. data/spec/extensions/pg_json_ops_spec.rb +67 -0
  64. data/spec/extensions/pg_json_spec.rb +12 -0
  65. data/spec/extensions/pg_range_spec.rb +19 -2
  66. data/spec/extensions/sharding_spec.rb +8 -0
  67. data/spec/extensions/spec_helper.rb +9 -2
  68. data/spec/extensions/static_cache_cache_spec.rb +35 -0
  69. data/spec/guards_helper.rb +1 -1
  70. data/spec/integration/plugin_test.rb +27 -0
  71. data/spec/integration/schema_test.rb +16 -2
  72. data/spec/model/spec_helper.rb +1 -1
  73. metadata +30 -2
@@ -159,9 +159,9 @@ module Sequel
159
159
 
160
160
  private
161
161
 
162
- # Reset the initial values when setting values.
163
- def _refresh_set_values(hash)
164
- reset_initial_values
162
+ # Reset initial values when clearing changed columns
163
+ def _clear_changed_columns(reason)
164
+ reset_initial_values if reason == :initialize || reason == :refresh
165
165
  super
166
166
  end
167
167
 
@@ -214,12 +214,6 @@ module Sequel
214
214
  self
215
215
  end
216
216
 
217
- # Reset the initial values when initializing.
218
- def initialize_set(h)
219
- super
220
- reset_initial_values
221
- end
222
-
223
217
  # Array holding column symbols that were not present initially. This is necessary
224
218
  # to differentiate between values that were not present and values that were
225
219
  # present but equal to nil.
@@ -0,0 +1,72 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The insert_conflict plugin allows handling conflicts due to unique
6
+ # constraints when saving new model instance, using the INSERT ON CONFLICT
7
+ # support in PostgreSQL 9.5+ and SQLite 3.24.0+. Example:
8
+ #
9
+ # class Album < Sequel::Model
10
+ # plugin :insert_conflict
11
+ # end
12
+ #
13
+ # Album.new(name: 'Foo', copies_sold: 1000).
14
+ # insert_conflict(
15
+ # target: :name,
16
+ # update: {copies_sold: Sequel[:excluded][:b]}
17
+ # ).
18
+ # save
19
+ #
20
+ # This example will try to insert the album, but if there is an existing
21
+ # album with the name 'Foo', this will update the copies_sold attribute
22
+ # for that album. See the PostgreSQL and SQLite adapter documention for
23
+ # the options you can pass to the insert_conflict method.
24
+ #
25
+ # Usage:
26
+ #
27
+ # # Make all model subclasses support insert_conflict
28
+ # Sequel::Model.plugin :insert_conflict
29
+ #
30
+ # # Make the Album class support insert_conflict
31
+ # Album.plugin :insert_conflict
32
+ module InsertConflict
33
+ def self.configure(model)
34
+ model.instance_exec do
35
+ if @dataset && !@dataset.respond_to?(:insert_conflict)
36
+ raise Error, "#{self}'s dataset does not support insert_conflict"
37
+ end
38
+ end
39
+ end
40
+
41
+ module InstanceMethods
42
+ # Set the insert_conflict options to pass to the dataset when inserting.
43
+ def insert_conflict(opts=OPTS)
44
+ raise Error, "Model#insert_conflict is only supported on new model instances" unless new?
45
+ @insert_conflict_opts = opts
46
+ self
47
+ end
48
+
49
+ private
50
+
51
+ # Set the dataset used for inserting to use INSERT ON CONFLICT
52
+ # Model#insert_conflict has been called on the instance previously.
53
+ def _insert_dataset
54
+ ds = super
55
+
56
+ if @insert_conflict_opts
57
+ ds = ds.insert_conflict(@insert_conflict_opts)
58
+ end
59
+
60
+ ds
61
+ end
62
+
63
+ # Disable the use of prepared insert statements, as they are not compatible
64
+ # with this plugin.
65
+ def use_prepared_statements_for?(type)
66
+ return false if type == :insert || type == :insert_select
67
+ super if defined?(super)
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -113,6 +113,10 @@ module Sequel
113
113
  # value, the attribute hash is ignored.
114
114
  # :remove :: Allow disassociation of nested records (can remove the associated
115
115
  # object from the parent object, but not destroy the associated object).
116
+ # :require_modification :: Whether to require modification of nested objects when
117
+ # updating or deleting them (checking that a single row was
118
+ # updated). By default, uses the default require_modification
119
+ # setting for the nested object.
116
120
  # :transform :: A proc to transform attribute hashes before they are
117
121
  # passed to associated object. Takes two arguments, the parent object and
118
122
  # the attribute hash. Uses the return value as the new attribute hash.
@@ -282,6 +286,9 @@ module Sequel
282
286
  obj = Array(public_send(reflection[:name])).find{|x| Array(x.pk).map(&:to_s) == pk}
283
287
  end
284
288
  if obj
289
+ unless (require_modification = meta[:require_modification]).nil?
290
+ obj.require_modification = require_modification
291
+ end
285
292
  attributes = attributes.dup.delete_if{|k,v| str_keys.include? k.to_s}
286
293
  if meta[:destroy] && klass.db.send(:typecast_value_boolean, attributes.delete(:_delete) || attributes.delete('_delete'))
287
294
  nested_attributes_remove(meta, obj, :destroy=>true)
@@ -45,6 +45,31 @@ module Sequel
45
45
  # to be associated to particular column(s), and use a specific error message:
46
46
  #
47
47
  # Album.pg_auto_constraint_validation_override(:constraint_name, [:column1], "validation error message")
48
+ #
49
+ # Using the pg_auto_constraint_validations plugin requires 5 queries per
50
+ # model at load time in order to gather the necessary metadata. For applications
51
+ # with a large number of models, this can result in a noticeable delay during model
52
+ # initialization. To mitigate this issue, you can cache the necessary metadata in
53
+ # a file with the :cache_file option:
54
+ #
55
+ # Sequel::Model.plugin :pg_auto_constraint_validations, cache_file: 'db/pgacv.cache'
56
+ #
57
+ # The file does not have to exist when loading the plugin. If it exists, the plugin
58
+ # will load the cache and use the cached results instead of issuing queries if there
59
+ # is an entry in the cache. If there is no entry in the cache, it will update the
60
+ # in-memory cache with the metadata results. To save the in in-memory cache back to
61
+ # the cache file, run:
62
+ #
63
+ # Sequel::Model.dump_pg_auto_constraint_validations_cache
64
+ #
65
+ # Note that when using the :cache_file option, it is up to the application to ensure
66
+ # that the dumped cached metadata reflects the current state of the database. Sequel
67
+ # does no checking to ensure this, as checking would take time and the
68
+ # purpose of this code is to take a shortcut.
69
+ #
70
+ # The cached schema is dumped in Marshal format, since it is the fastest
71
+ # and it handles all ruby objects used in the metadata. Because of this,
72
+ # you should not attempt to load the metadata from a untrusted file.
48
73
  #
49
74
  # Usage:
50
75
  #
@@ -67,13 +92,28 @@ module Sequel
67
92
  }.freeze).each_value(&:freeze)
68
93
 
69
94
  # Setup the constraint violation metadata. Options:
95
+ # :cache_file :: File storing cached metadata, to avoid queries for each model
70
96
  # :messages :: Override the default error messages for each constraint
71
97
  # violation type (:not_null, :check, :unique, :foreign_key, :referenced_by)
72
98
  def self.configure(model, opts=OPTS)
73
99
  model.instance_exec do
100
+ if @pg_auto_constraint_validations_cache_file = opts[:cache_file]
101
+ @pg_auto_constraint_validations_cache = if ::File.file?(@pg_auto_constraint_validations_cache_file)
102
+ cache = Marshal.load(File.read(@pg_auto_constraint_validations_cache_file))
103
+ cache.each_value do |hash|
104
+ hash.freeze.each_value(&:freeze)
105
+ end
106
+ else
107
+ {}
108
+ end
109
+ else
110
+ @pg_auto_constraint_validations_cache = nil
111
+ end
112
+
74
113
  setup_pg_auto_constraint_validations
75
114
  @pg_auto_constraint_validations_messages = (@pg_auto_constraint_validations_messages || DEFAULT_ERROR_MESSAGES).merge(opts[:messages] || OPTS).freeze
76
115
  end
116
+ nil
77
117
  end
78
118
 
79
119
  module ClassMethods
@@ -85,9 +125,16 @@ module Sequel
85
125
  # generated validation failures.
86
126
  attr_reader :pg_auto_constraint_validations_messages
87
127
 
88
- Plugins.inherited_instance_variables(self, :@pg_auto_constraint_validations=>nil, :@pg_auto_constraint_validations_messages=>nil)
128
+ Plugins.inherited_instance_variables(self, :@pg_auto_constraint_validations=>nil, :@pg_auto_constraint_validations_messages=>nil, :@pg_auto_constraint_validations_cache=>nil, :@pg_auto_constraint_validations_cache_file=>nil)
89
129
  Plugins.after_set_dataset(self, :setup_pg_auto_constraint_validations)
90
130
 
131
+ # Dump the in-memory cached metadata to the cache file.
132
+ def dump_pg_auto_constraint_validations_cache
133
+ raise Error, "No pg_auto_constraint_validations setup" unless file = @pg_auto_constraint_validations_cache_file
134
+ File.open(file, 'wb'){|f| f.write(Marshal.dump(@pg_auto_constraint_validations_cache))}
135
+ nil
136
+ end
137
+
91
138
  # Override the constraint validation columns and message for a given constraint
92
139
  def pg_auto_constraint_validation_override(constraint, columns, message)
93
140
  pgacv = Hash[@pg_auto_constraint_validations]
@@ -122,39 +169,51 @@ module Sequel
122
169
  return
123
170
  end
124
171
 
125
- checks = {}
126
- indexes = {}
127
- foreign_keys = {}
128
- referenced_by = {}
172
+ cache = @pg_auto_constraint_validations_cache
173
+ literal_table_name = dataset.literal(table_name)
174
+ unless cache && (metadata = cache[literal_table_name])
175
+ checks = {}
176
+ indexes = {}
177
+ foreign_keys = {}
178
+ referenced_by = {}
129
179
 
130
- db.check_constraints(table_name).each do |k, v|
131
- checks[k] = v[:columns].dup.freeze unless v[:columns].empty?
132
- end
133
- db.indexes(table_name, :include_partial=>true).each do |k, v|
134
- if v[:unique]
135
- indexes[k] = v[:columns].dup.freeze
180
+ db.check_constraints(table_name).each do |k, v|
181
+ checks[k] = v[:columns].dup.freeze unless v[:columns].empty?
182
+ end
183
+ db.indexes(table_name, :include_partial=>true).each do |k, v|
184
+ if v[:unique]
185
+ indexes[k] = v[:columns].dup.freeze
186
+ end
187
+ end
188
+ db.foreign_key_list(table_name, :schema=>false).each do |fk|
189
+ foreign_keys[fk[:name]] = fk[:columns].dup.freeze
190
+ end
191
+ db.foreign_key_list(table_name, :reverse=>true, :schema=>false).each do |fk|
192
+ referenced_by[[fk[:schema], fk[:table], fk[:name]].freeze] = fk[:key].dup.freeze
193
+ end
194
+
195
+ schema, table = db[:pg_class].
196
+ join(:pg_namespace, :oid=>:relnamespace, db.send(:regclass_oid, table_name)=>:oid).
197
+ get([:nspname, :relname])
198
+
199
+ metadata = {
200
+ :schema=>schema,
201
+ :table=>table,
202
+ :check=>checks,
203
+ :unique=>indexes,
204
+ :foreign_key=>foreign_keys,
205
+ :referenced_by=>referenced_by,
206
+ :overrides=>OPTS
207
+ }.freeze
208
+ metadata.each_value(&:freeze)
209
+
210
+ if cache
211
+ cache[literal_table_name] = metadata
136
212
  end
137
- end
138
- db.foreign_key_list(table_name, :schema=>false).each do |fk|
139
- foreign_keys[fk[:name]] = fk[:columns].dup.freeze
140
- end
141
- db.foreign_key_list(table_name, :reverse=>true, :schema=>false).each do |fk|
142
- referenced_by[[fk[:schema], fk[:table], fk[:name]].freeze] = fk[:key].dup.freeze
143
213
  end
144
214
 
145
- schema, table = db[:pg_class].
146
- join(:pg_namespace, :oid=>:relnamespace, db.send(:regclass_oid, table_name)=>:oid).
147
- get([:nspname, :relname])
148
-
149
- (@pg_auto_constraint_validations = {
150
- :schema=>schema,
151
- :table=>table,
152
- :check=>checks,
153
- :unique=>indexes,
154
- :foreign_key=>foreign_keys,
155
- :referenced_by=>referenced_by,
156
- :overrides=>OPTS
157
- }.freeze).each_value(&:freeze)
215
+ @pg_auto_constraint_validations = metadata
216
+ nil
158
217
  end
159
218
  end
160
219
 
@@ -107,12 +107,18 @@ module Sequel
107
107
  # previous row_proc, but calls set_server on the output of that row_proc,
108
108
  # ensuring that objects retrieved by a specific shard know which shard they
109
109
  # are tied to.
110
- def server(s)
111
- ds = super
112
- if rp = row_proc
113
- ds = ds.with_row_proc(proc{|r| rp.call(r).set_server(s)})
110
+ def row_proc
111
+ rp = super
112
+ if rp
113
+ case server = db.pool.send(:pick_server, opts[:server])
114
+ when nil, :default, :read_only
115
+ # nothing
116
+ else
117
+ old_rp = rp
118
+ rp = proc{|r| old_rp.call(r).set_server(server)}
119
+ end
114
120
  end
115
- ds
121
+ rp
116
122
  end
117
123
  end
118
124
  end
@@ -211,18 +211,23 @@ module Sequel
211
211
  # Reload the cache for this model by retrieving all of the instances in the dataset
212
212
  # freezing them, and populating the cached array and hash.
213
213
  def load_cache
214
- a = dataset.all
214
+ @all = load_static_cache_rows
215
215
  h = {}
216
- a.each do |o|
216
+ @all.each do |o|
217
217
  o.errors.freeze
218
218
  h[o.pk.freeze] = o.freeze
219
219
  end
220
- @all = a.freeze
221
220
  @cache = h.freeze
222
221
  end
223
222
 
224
223
  private
225
224
 
225
+ # Load the static cache rows from the database.
226
+ def load_static_cache_rows
227
+ ret = super if defined?(super)
228
+ ret || dataset.all.freeze
229
+ end
230
+
226
231
  # Return the frozen object with the given pk, or nil if no such object exists
227
232
  # in the cache, without issuing a database query.
228
233
  def primary_key_lookup(pk)
@@ -0,0 +1,53 @@
1
+ # frozen-string-literal: true
2
+
3
+ module Sequel
4
+ module Plugins
5
+ # The static_cache_cache plugin allows for caching the row content for subclasses
6
+ # that use the static cache plugin (or just the current class). Using this plugin
7
+ # can avoid the need to query the database every time loading the plugin into a
8
+ # model, which can save time when you have a lot of models using the static_cache
9
+ # plugin.
10
+ #
11
+ # Usage:
12
+ #
13
+ # # Make all model subclasses that use the static_cache plugin use
14
+ # # the cached values in the given file
15
+ # Sequel::Model.plugin :static_cache_cache, "static_cache.cache"
16
+ #
17
+ # # Make the AlbumType model the cached values in the given file,
18
+ # # should be loaded before the static_cache plugin
19
+ # AlbumType.plugin :static_cache_cache, "static_cache.cache"
20
+ module StaticCacheCache
21
+ def self.configure(model, file)
22
+ model.instance_variable_set(:@static_cache_cache_file, file)
23
+ model.instance_variable_set(:@static_cache_cache, File.exist?(file) ? Marshal.load(File.read(file)) : {})
24
+ end
25
+
26
+ module ClassMethods
27
+ # Dump the in-memory cached rows to the cache file.
28
+ def dump_static_cache_cache
29
+ File.open(@static_cache_cache_file, 'wb'){|f| f.write(Marshal.dump(@static_cache_cache))}
30
+ nil
31
+ end
32
+
33
+ Plugins.inherited_instance_variables(self, :@static_cache_cache_file=>nil, :@static_cache_cache=>nil)
34
+
35
+ private
36
+
37
+ # Load the rows for the model from the cache if available.
38
+ # If not available, load the rows from the database, and
39
+ # then update the cache with the raw rows.
40
+ def load_static_cache_rows
41
+ if rows = Sequel.synchronize{@static_cache_cache[name]}
42
+ rows.map{|row| call(row)}.freeze
43
+ else
44
+ rows = dataset.all.freeze
45
+ raw_rows = rows.map(&:values)
46
+ Sequel.synchronize{@static_cache_cache[name] = raw_rows}
47
+ rows
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -41,7 +41,9 @@ module Sequel
41
41
  # Typecast values using #load_typecast when the values are retrieved
42
42
  # from the database.
43
43
  def call(values)
44
- super.load_typecast
44
+ o = super.load_typecast
45
+ o.send(:_clear_changed_columns, :initialize)
46
+ o
45
47
  end
46
48
 
47
49
  # Freeze typecast on load columns when freezing model class.
@@ -63,7 +65,6 @@ module Sequel
63
65
  set_column_value("#{c}=", v)
64
66
  end
65
67
  end
66
- _changed_columns.clear
67
68
  self
68
69
  end
69
70
 
@@ -788,8 +788,10 @@ module Sequel
788
788
  def coerce(other)
789
789
  if other.is_a?(Numeric)
790
790
  [SQL::NumericExpression.new(:NOOP, other), self]
791
- else
791
+ elsif defined?(super)
792
792
  super
793
+ else
794
+ [self, other]
793
795
  end
794
796
  end
795
797
 
@@ -54,7 +54,14 @@ module Sequel
54
54
  convert_output_datetime_other(v, output_timezone)
55
55
  end
56
56
  else
57
- v.public_send(output_timezone == :utc ? :getutc : :getlocal)
57
+ case output_timezone
58
+ when :utc
59
+ v.getutc
60
+ when :local
61
+ v.getlocal
62
+ else
63
+ convert_output_time_other(v, output_timezone)
64
+ end
58
65
  end
59
66
  else
60
67
  v
@@ -110,7 +117,7 @@ module Sequel
110
117
  # same time and just modifying the timezone.
111
118
  def convert_input_datetime_no_offset(v, input_timezone)
112
119
  case input_timezone
113
- when :utc, nil
120
+ when nil, :utc
114
121
  v # DateTime assumes UTC if no offset is given
115
122
  when :local
116
123
  offset = local_offset_for_datetime(v)
@@ -119,7 +126,7 @@ module Sequel
119
126
  convert_input_datetime_other(v, input_timezone)
120
127
  end
121
128
  end
122
-
129
+
123
130
  # Convert the given +DateTime+ to the given input_timezone that is not supported
124
131
  # by default (i.e. one other than +nil+, <tt>:local</tt>, or <tt>:utc</tt>). Raises an +InvalidValue+ by default.
125
132
  # Can be overridden in extensions.
@@ -127,6 +134,13 @@ module Sequel
127
134
  raise InvalidValue, "Invalid input_timezone: #{input_timezone.inspect}"
128
135
  end
129
136
 
137
+ # Convert the given +Time+ to the given input_timezone that is not supported
138
+ # by default (i.e. one other than +nil+, <tt>:local</tt>, or <tt>:utc</tt>). Raises an +InvalidValue+ by default.
139
+ # Can be overridden in extensions.
140
+ def convert_input_time_other(v, input_timezone)
141
+ raise InvalidValue, "Invalid input_timezone: #{input_timezone.inspect}"
142
+ end
143
+
130
144
  # Converts the object from a +String+, +Array+, +Date+, +DateTime+, or +Time+ into an
131
145
  # instance of <tt>Sequel.datetime_class</tt>. If given an array or a string that doesn't
132
146
  # contain an offset, assume that the array/string is already in the given +input_timezone+.
@@ -139,12 +153,17 @@ module Sequel
139
153
  else
140
154
  # Correct for potentially wrong offset if string doesn't include offset
141
155
  if v2.is_a?(DateTime)
142
- v2 = convert_input_datetime_no_offset(v2, input_timezone)
156
+ convert_input_datetime_no_offset(v2, input_timezone)
143
157
  else
144
- # Time assumes local time if no offset is given
145
- v2 = v2.getutc + v2.utc_offset if input_timezone == :utc
158
+ case input_timezone
159
+ when nil, :local
160
+ v2
161
+ when :utc
162
+ (v2 + v2.utc_offset).utc
163
+ else
164
+ convert_input_time_other((v2 + v2.utc_offset).utc, input_timezone)
165
+ end
146
166
  end
147
- v2
148
167
  end
149
168
  when Array
150
169
  y, mo, d, h, mi, s, ns, off = v
@@ -155,8 +174,18 @@ module Sequel
155
174
  else
156
175
  convert_input_datetime_no_offset(DateTime.civil(y, mo, d, h, mi, s), input_timezone)
157
176
  end
177
+ elsif off
178
+ s += Rational(ns, 1000000000) if ns
179
+ Time.new(y, mo, d, h, mi, s, (off*86400).to_i)
158
180
  else
159
- Time.public_send(input_timezone == :utc ? :utc : :local, y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0))
181
+ case input_timezone
182
+ when nil, :local
183
+ Time.local(y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0))
184
+ when :utc
185
+ Time.utc(y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0))
186
+ else
187
+ convert_input_time_other(Time.utc(y, mo, d, h, mi, s, (ns ? ns / 1000.0 : 0)), input_timezone)
188
+ end
160
189
  end
161
190
  when Hash
162
191
  ary = [:year, :month, :day, :hour, :minute, :second, :nanos].map{|x| (v[x] || v[x.to_s]).to_i}
@@ -188,23 +217,33 @@ module Sequel
188
217
  raise InvalidValue, "Invalid output_timezone: #{output_timezone.inspect}"
189
218
  end
190
219
 
220
+ # Convert the given +Time+ to the given output_timezone that is not supported
221
+ # by default (i.e. one other than +nil+, <tt>:local</tt>, or <tt>:utc</tt>). Raises an +InvalidValue+ by default.
222
+ # Can be overridden in extensions.
223
+ def convert_output_time_other(v, output_timezone)
224
+ raise InvalidValue, "Invalid output_timezone: #{output_timezone.inspect}"
225
+ end
226
+
191
227
  # Convert the timezone setter argument. Returns argument given by default,
192
228
  # exists for easier overriding in extensions.
193
229
  def convert_timezone_setter_arg(tz)
194
230
  tz
195
231
  end
196
232
 
197
- # Takes a DateTime dt, and returns the correct local offset for that dt, daylight savings included.
233
+ # Takes a DateTime dt, and returns the correct local offset for that dt, daylight savings included, in fraction of a day.
198
234
  def local_offset_for_datetime(dt)
199
235
  time_offset_to_datetime_offset Time.local(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec).utc_offset
200
236
  end
201
237
 
202
238
  # Caches offset conversions to avoid excess Rational math.
203
239
  def time_offset_to_datetime_offset(offset_secs)
204
- @local_offsets ||= {}
205
- @local_offsets[offset_secs] ||= Rational(offset_secs, 86400)
240
+ if offset = Sequel.synchronize{@local_offsets[offset_secs]}
241
+ return offset
242
+ end
243
+ Sequel.synchronize{@local_offsets[offset_secs] = Rational(offset_secs, 86400)}
206
244
  end
207
245
  end
208
246
 
247
+ @local_offsets = {}
209
248
  extend Timezones
210
249
  end