sequel 3.3.0 → 3.4.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 (58) hide show
  1. data/CHANGELOG +62 -0
  2. data/README.rdoc +4 -4
  3. data/doc/release_notes/3.3.0.txt +1 -1
  4. data/doc/release_notes/3.4.0.txt +325 -0
  5. data/doc/sharding.rdoc +3 -3
  6. data/lib/sequel/adapters/amalgalite.rb +1 -1
  7. data/lib/sequel/adapters/firebird.rb +4 -9
  8. data/lib/sequel/adapters/jdbc.rb +21 -7
  9. data/lib/sequel/adapters/mysql.rb +2 -1
  10. data/lib/sequel/adapters/odbc.rb +7 -21
  11. data/lib/sequel/adapters/oracle.rb +1 -1
  12. data/lib/sequel/adapters/postgres.rb +6 -1
  13. data/lib/sequel/adapters/shared/mssql.rb +11 -0
  14. data/lib/sequel/adapters/shared/mysql.rb +8 -12
  15. data/lib/sequel/adapters/shared/oracle.rb +13 -0
  16. data/lib/sequel/adapters/shared/postgres.rb +5 -10
  17. data/lib/sequel/adapters/shared/sqlite.rb +21 -1
  18. data/lib/sequel/adapters/sqlite.rb +2 -2
  19. data/lib/sequel/core.rb +147 -11
  20. data/lib/sequel/database.rb +21 -9
  21. data/lib/sequel/dataset.rb +31 -6
  22. data/lib/sequel/dataset/convenience.rb +1 -1
  23. data/lib/sequel/dataset/sql.rb +76 -18
  24. data/lib/sequel/extensions/inflector.rb +2 -51
  25. data/lib/sequel/model.rb +16 -10
  26. data/lib/sequel/model/associations.rb +4 -1
  27. data/lib/sequel/model/base.rb +13 -6
  28. data/lib/sequel/model/default_inflections.rb +46 -0
  29. data/lib/sequel/model/inflections.rb +1 -51
  30. data/lib/sequel/plugins/boolean_readers.rb +52 -0
  31. data/lib/sequel/plugins/instance_hooks.rb +57 -0
  32. data/lib/sequel/plugins/lazy_attributes.rb +13 -1
  33. data/lib/sequel/plugins/nested_attributes.rb +171 -0
  34. data/lib/sequel/plugins/serialization.rb +35 -16
  35. data/lib/sequel/plugins/timestamps.rb +87 -0
  36. data/lib/sequel/plugins/validation_helpers.rb +8 -1
  37. data/lib/sequel/sql.rb +33 -0
  38. data/lib/sequel/version.rb +1 -1
  39. data/spec/adapters/sqlite_spec.rb +11 -6
  40. data/spec/core/core_sql_spec.rb +29 -0
  41. data/spec/core/database_spec.rb +16 -7
  42. data/spec/core/dataset_spec.rb +264 -20
  43. data/spec/extensions/boolean_readers_spec.rb +86 -0
  44. data/spec/extensions/inflector_spec.rb +67 -4
  45. data/spec/extensions/instance_hooks_spec.rb +133 -0
  46. data/spec/extensions/lazy_attributes_spec.rb +45 -5
  47. data/spec/extensions/nested_attributes_spec.rb +272 -0
  48. data/spec/extensions/serialization_spec.rb +64 -1
  49. data/spec/extensions/timestamps_spec.rb +150 -0
  50. data/spec/extensions/validation_helpers_spec.rb +18 -0
  51. data/spec/integration/dataset_test.rb +79 -2
  52. data/spec/integration/schema_test.rb +17 -0
  53. data/spec/integration/timezone_test.rb +55 -0
  54. data/spec/model/associations_spec.rb +19 -7
  55. data/spec/model/model_spec.rb +29 -0
  56. data/spec/model/record_spec.rb +36 -0
  57. data/spec/spec_config.rb +1 -1
  58. metadata +14 -2
@@ -32,10 +32,10 @@ module Sequel
32
32
  db.busy_timeout(opts.fetch(:timeout, 5000))
33
33
  db.type_translation = true
34
34
 
35
- # Handle datetime's with Sequel.datetime_class
35
+ # Handle datetimes with Sequel.datetime_class
36
36
  prok = proc do |t,v|
37
37
  v = Time.at(v.to_i).iso8601 if UNIX_EPOCH_TIME_FORMAT.match(v)
38
- Sequel.string_to_datetime(v)
38
+ Sequel.database_to_application_timestamp(v)
39
39
  end
40
40
  db.translator.add_translator("timestamp", &prok)
41
41
  db.translator.add_translator("datetime", &prok)
data/lib/sequel/core.rb CHANGED
@@ -29,15 +29,58 @@
29
29
  #
30
30
  # Sequel.datetime_class = DateTime
31
31
  #
32
+ # Sequel doesn't pay much attention to timezones by default, but you can set it
33
+ # handle timezones if you want. There are three separate timezone settings:
34
+ #
35
+ # * application_timezone - The timezone you want the application to use. This is
36
+ # the timezone that incoming times from the database and typecasting are converted
37
+ # to.
38
+ # * database_timezone - The timezone for storage in the database. This is the
39
+ # timezone to which Sequel will convert timestamps before literalizing them
40
+ # for storage in the database. It is also the timezone that Sequel will assume
41
+ # database timestamp values are already in (if they don't include an offset).
42
+ # * typecast_timezone - The timezone that incoming data that Sequel needs to typecast
43
+ # is assumed to be already in (if they don't include an offset).
44
+ #
45
+ # You can set also set all three timezones to the same value at once via
46
+ # Sequel.default_timezone=.
47
+ #
48
+ # Sequel.application_timezone = :utc # or :local or nil
49
+ # Sequel.database_timezone = :utc # or :local or nil
50
+ # Sequel.typecast_timezone = :utc # or :local or nil
51
+ # Sequel.default_timezone = :utc # or :local or nil
52
+ #
53
+ # The only timezone values that are supported by default are :utc (convert to UTC),
54
+ # :local (convert to local time), and nil (don't convert). If you need to
55
+ # convert to a specific timezone, or need the timezones being used to change based
56
+ # on the environment (e.g. current user), you need to use an extension (and use
57
+ # DateTime as the datetime_class).
58
+ #
32
59
  # You can set the SEQUEL_NO_CORE_EXTENSIONS constant or environment variable to have
33
60
  # Sequel not extend the core classes.
34
61
  module Sequel
62
+ # The offset of the current time zone from UTC, in seconds.
63
+ LOCAL_DATETIME_OFFSET_SECS = Time.now.utc_offset
64
+
65
+ # The offset of the current time zone from UTC, as a fraction of a day.
66
+ LOCAL_DATETIME_OFFSET = respond_to?(:Rational, true) ? Rational(LOCAL_DATETIME_OFFSET_SECS, 60*60*24) : LOCAL_DATETIME_OFFSET_SECS/60/60/24.0
67
+
68
+ @application_timezone = nil
35
69
  @convert_two_digit_years = true
70
+ @database_timezone = nil
36
71
  @datetime_class = Time
72
+ @typecast_timezone = nil
37
73
  @virtual_row_instance_eval = true
38
74
 
39
75
  class << self
40
76
  attr_accessor :convert_two_digit_years, :datetime_class, :virtual_row_instance_eval
77
+ attr_accessor :application_timezone, :database_timezone, :typecast_timezone
78
+ end
79
+
80
+ # Convert the given Time/DateTime object into the database timezone, used when
81
+ # literalizing objects in an SQL string.
82
+ def self.application_to_database_timestamp(v)
83
+ convert_output_timestamp(v, Sequel.database_timezone)
41
84
  end
42
85
 
43
86
  # Returns true if the passed object could be a specifier of conditions, false otherwise.
@@ -73,6 +116,29 @@ module Sequel
73
116
  Database.connect(*args, &block)
74
117
  end
75
118
 
119
+ # Convert the given object into an object of Sequel.datetime_class in the
120
+ # application_timezone. Used when coverting datetime/timestamp columns
121
+ # returned by the database.
122
+ def self.database_to_application_timestamp(v)
123
+ convert_timestamp(v, Sequel.database_timezone)
124
+ end
125
+
126
+ # Sets the database, application, and typecasting timezones to the given timezone.
127
+ def self.default_timezone=(tz)
128
+ self.database_timezone = tz
129
+ self.application_timezone = tz
130
+ self.typecast_timezone = tz
131
+ end
132
+
133
+ # Load all Sequel extensions given. Only loads extensions included in this
134
+ # release of Sequel, doesn't load external extensions.
135
+ #
136
+ # Sequel.extension(:schema_dumper)
137
+ # Sequel.extension(:pagination, :query)
138
+ def self.extension(*extensions)
139
+ require(extensions, 'extensions')
140
+ end
141
+
76
142
  # Set the method to call on identifiers going into the database. This affects
77
143
  # the literalization of identifiers by calling this method on them before they are input.
78
144
  # Sequel upcases identifiers in all SQL strings for most databases, so to turn that off:
@@ -112,15 +178,6 @@ module Sequel
112
178
  Database.quote_identifiers = value
113
179
  end
114
180
 
115
- # Load all Sequel extensions given. Only loads extensions included in this
116
- # release of Sequel, doesn't load external extensions.
117
- #
118
- # Sequel.extension(:schema_dumper)
119
- # Sequel.extension(:pagination, :query)
120
- def self.extension(*extensions)
121
- require(extensions, 'extensions')
122
- end
123
-
124
181
  # Require all given files which should be in the same or a subdirectory of
125
182
  # this file. If a subdir is given, assume all files are in that subdir.
126
183
  def self.require(files, subdir=nil)
@@ -168,7 +225,14 @@ module Sequel
168
225
  raise InvalidValue, "Invalid Time value #{s.inspect} (#{e.message})"
169
226
  end
170
227
  end
171
-
228
+
229
+ # Convert the given object into an object of Sequel.datetime_class in the
230
+ # application_timezone. Used when typecasting values when assigning them
231
+ # to model datetime attributes.
232
+ def self.typecast_to_application_timestamp(v)
233
+ convert_timestamp(v, Sequel.typecast_timezone)
234
+ end
235
+
172
236
  ### Private Class Methods ###
173
237
 
174
238
  # Helper method that the database adapter class methods that are added to Sequel via
@@ -184,6 +248,78 @@ module Sequel
184
248
  end
185
249
  connect(opts, &block)
186
250
  end
251
+
252
+ # Converts the object from a String, Array, Date, DateTime, or Time into an
253
+ # instance of Sequel.datetime_class. If a string and an offset is not given,
254
+ # assume that the string is already in the given input_timezone.
255
+ def self.convert_input_timestamp(v, input_timezone) # :nodoc:
256
+ case v
257
+ when String
258
+ v2 = Sequel.string_to_datetime(v)
259
+ if !input_timezone || Date._parse(v).has_key?(:offset)
260
+ v2
261
+ else
262
+ # Correct for potentially wrong offset if offset is given
263
+ if v2.is_a?(DateTime)
264
+ # DateTime assumes UTC if no offset is given
265
+ v2 = v2.new_offset(LOCAL_DATETIME_OFFSET) - LOCAL_DATETIME_OFFSET if input_timezone == :local
266
+ else
267
+ # Time assumes local time if no offset is given
268
+ v2 = v2.getutc + LOCAL_DATETIME_OFFSET_SECS if input_timezone == :utc
269
+ end
270
+ v2
271
+ end
272
+ when Array
273
+ y, mo, d, h, mi, s = v
274
+ if datetime_class == DateTime
275
+ DateTime.civil(y, mo, d, h, mi, s, input_timezone == :utc ? 0 : LOCAL_DATETIME_OFFSET)
276
+ else
277
+ Time.send(input_timezone == :utc ? :utc : :local, y, mo, d, h, mi, s)
278
+ end
279
+ when Time
280
+ if datetime_class == DateTime
281
+ v.respond_to?(:to_datetime) ? v.to_datetime : string_to_datetime(v.iso8601)
282
+ else
283
+ v
284
+ end
285
+ when DateTime
286
+ if datetime_class == DateTime
287
+ v
288
+ else
289
+ v.respond_to?(:to_time) ? v.to_time : string_to_datetime(v.to_s)
290
+ end
291
+ when Date
292
+ convert_input_timestamp(v.to_s, input_timezone)
293
+ else
294
+ raise InvalidValue, "Invalid convert_input_timestamp type: #{v.inspect}"
295
+ end
296
+ end
297
+
298
+ # Converts the object to the given output_timezone.
299
+ def self.convert_output_timestamp(v, output_timezone) # :nodoc:
300
+ if output_timezone
301
+ if v.is_a?(DateTime)
302
+ v.new_offset(output_timezone == :utc ? 0 : LOCAL_DATETIME_OFFSET)
303
+ else
304
+ v.send(output_timezone == :utc ? :getutc : :getlocal)
305
+ end
306
+ else
307
+ v
308
+ end
309
+ end
310
+
311
+ # Converts the given object from the given input timezone to the
312
+ # application timezone using convert_input_timestamp and
313
+ # convert_output_timestamp.
314
+ def self.convert_timestamp(v, input_timezone) # :nodoc:
315
+ begin
316
+ convert_output_timestamp(convert_input_timestamp(v, input_timezone), Sequel.application_timezone)
317
+ rescue InvalidValue
318
+ raise
319
+ rescue
320
+ raise InvalidValue, "Invalid #{datetime_class} value: #{v.inspect}"
321
+ end
322
+ end
187
323
 
188
324
  # Method that adds a database adapter class method to Sequel that calls
189
325
  # Sequel.adapter_method.
@@ -193,7 +329,7 @@ module Sequel
193
329
  end
194
330
  end
195
331
 
196
- private_class_method :adapter_method, :def_adapter_method
332
+ private_class_method :adapter_method, :convert_input_timestamp, :convert_output_timestamp, :convert_timestamp, :def_adapter_method
197
333
 
198
334
  require(%w"metaprogramming sql connection_pool exceptions dataset database version")
199
335
  require(%w"schema_generator schema_methods schema_sql", 'database')
@@ -234,9 +234,10 @@ module Sequel
234
234
 
235
235
  ### Instance Methods ###
236
236
 
237
- # Executes the supplied SQL statement string.
237
+ # Runs the supplied SQL statement string on the database server.
238
+ # Alias for run.
238
239
  def <<(sql)
239
- execute_ddl(sql)
240
+ run(sql)
240
241
  end
241
242
 
242
243
  # Returns a dataset from the database. If the first argument is a string,
@@ -429,6 +430,14 @@ module Sequel
429
430
  @quote_identifiers = @opts.include?(:quote_identifiers) ? @opts[:quote_identifiers] : (@@quote_identifiers.nil? ? quote_identifiers_default : @@quote_identifiers)
430
431
  end
431
432
 
433
+ # Runs the supplied SQL statement string on the database server. Returns nil.
434
+ # Options:
435
+ # * :server - The server to run the SQL on.
436
+ def run(sql, opts={})
437
+ execute_ddl(sql, opts)
438
+ nil
439
+ end
440
+
432
441
  # Returns a new dataset with the select method invoked.
433
442
  def select(*args, &block)
434
443
  dataset.select(*args, &block)
@@ -924,6 +933,8 @@ module Sequel
924
933
  Date.new(value.year, value.month, value.day)
925
934
  when String
926
935
  Sequel.string_to_date(value)
936
+ when Hash
937
+ Date.new(*[:year, :month, :day].map{|x| (value[x] || value[x.to_s]).to_i})
927
938
  else
928
939
  raise InvalidValue, "invalid value for Date: #{value.inspect}"
929
940
  end
@@ -931,14 +942,12 @@ module Sequel
931
942
 
932
943
  # Typecast the value to a DateTime or Time depending on Sequel.datetime_class
933
944
  def typecast_value_datetime(value)
934
- raise(Sequel::InvalidValue, "invalid value for Datetime: #{value.inspect}") unless [DateTime, Date, Time, String].any?{|c| value.is_a?(c)}
935
- if Sequel.datetime_class === value
936
- # Already the correct class, no need to convert
937
- value
945
+ raise(Sequel::InvalidValue, "invalid value for Datetime: #{value.inspect}") unless [DateTime, Date, Time, String, Hash].any?{|c| value.is_a?(c)}
946
+ klass = Sequel.datetime_class
947
+ if value.is_a?(Hash)
948
+ klass.send(klass == Time ? :mktime : :new, *[:year, :month, :day, :hour, :minute, :second].map{|x| (value[x] || value[x.to_s]).to_i})
938
949
  else
939
- # First convert it to standard ISO 8601 time, then
940
- # parse that string using the time class.
941
- Sequel.string_to_datetime(Time === value ? value.iso8601 : value.to_s)
950
+ Sequel.typecast_to_application_timestamp(value)
942
951
  end
943
952
  end
944
953
 
@@ -976,6 +985,9 @@ module Sequel
976
985
  value
977
986
  when String
978
987
  Sequel.string_to_time(value)
988
+ when Hash
989
+ t = Time.now
990
+ Time.mktime(t.year, t.month, t.day, *[:hour, :minute, :second].map{|x| (value[x] || value[x.to_s]).to_i})
979
991
  else
980
992
  raise Sequel::InvalidValue, "invalid value for Time: #{value.inspect}"
981
993
  end
@@ -42,13 +42,13 @@ module Sequel
42
42
 
43
43
  # All methods that should have a ! method added that modifies
44
44
  # the receiver.
45
- MUTATION_METHODS = %w'add_graph_aliases and distinct exclude exists
45
+ MUTATION_METHODS = %w'add_graph_aliases and distinct except exclude
46
46
  filter from from_self full_outer_join graph
47
- group group_and_count group_by having inner_join intersect invert join
48
- left_outer_join limit naked or order order_by order_more paginate qualify query reject
49
- reverse reverse_order right_outer_join select select_all select_more
50
- set_defaults set_graph_aliases set_overrides sort sort_by
51
- unfiltered ungraphed union unordered where with with_sql'.collect{|x| x.to_sym}
47
+ group group_and_count group_by having inner_join intersect invert join join_table
48
+ left_outer_join limit naked or order order_by order_more paginate qualify query
49
+ reverse reverse_order right_outer_join select select_all select_more server
50
+ set_defaults set_graph_aliases set_overrides unfiltered ungraphed ungrouped union
51
+ unlimited unordered where with with_recursive with_sql'.collect{|x| x.to_sym}
52
52
 
53
53
  NOTIMPL_MSG = "This method must be overridden in Sequel adapters".freeze
54
54
  WITH_SUPPORTED='with'.freeze
@@ -175,6 +175,10 @@ module Sequel
175
175
 
176
176
  # Iterates over the records in the dataset as they are yielded from the
177
177
  # database adapter, and returns self.
178
+ #
179
+ # Note that this method is not safe to use on many adapters if you are
180
+ # running additional queries inside the provided block. If you are
181
+ # running queries inside the block, you use should all instead of each.
178
182
  def each(&block)
179
183
  if @opts[:graph]
180
184
  graph_each(&block)
@@ -275,10 +279,25 @@ module Sequel
275
279
  true
276
280
  end
277
281
 
282
+ # Whether the dataset supports timezones in literal timestamps
283
+ def supports_timestamp_timezones?
284
+ false
285
+ end
286
+
287
+ # Whether the dataset supports fractional seconds in literal timestamps
288
+ def supports_timestamp_usecs?
289
+ true
290
+ end
291
+
278
292
  # Whether the dataset supports window functions.
279
293
  def supports_window_functions?
280
294
  false
281
295
  end
296
+
297
+ # Truncates the dataset. Returns nil.
298
+ def truncate
299
+ execute_ddl(truncate_sql)
300
+ end
282
301
 
283
302
  # Updates values for the dataset. The returned value is generally the
284
303
  # number of rows updated, but that is adapter dependent.
@@ -314,6 +333,12 @@ module Sequel
314
333
  @db.execute(sql, {:server=>@opts[:server] || :read_only}.merge(opts), &block)
315
334
  end
316
335
 
336
+ # Execute the given SQL on the database using execute_ddl.
337
+ def execute_ddl(sql, opts={}, &block)
338
+ @db.execute_ddl(sql, default_server_opts(opts), &block)
339
+ nil
340
+ end
341
+
317
342
  # Execute the given SQL on the database using execute_dui.
318
343
  def execute_dui(sql, opts={}, &block)
319
344
  @db.execute_dui(sql, default_server_opts(opts), &block)
@@ -109,7 +109,7 @@ module Sequel
109
109
  # # this will commit every 50 records
110
110
  # dataset.import([:x, :y], [[1, 2], [3, 4], ...], :slice => 50)
111
111
  def import(columns, values, opts={})
112
- return @db.transaction{execute_dui("#{insert_sql_base}#{quote_schema_table(@opts[:from].first)} (#{identifier_list(columns)}) VALUES #{literal(values)}")} if values.is_a?(Dataset)
112
+ return @db.transaction{execute_dui("#{insert_sql_base}#{quote_schema_table(@opts[:from].first)} (#{identifier_list(columns)}) #{subselect_sql(values)}")} if values.is_a?(Dataset)
113
113
 
114
114
  return if values.empty?
115
115
  raise(Error, IMPORT_ERROR_MSG) if columns.empty?
@@ -17,6 +17,8 @@ module Sequel
17
17
  QUESTION_MARK = '?'.freeze
18
18
  STOCK_COUNT_OPTS = {:select => [SQL::AliasedExpression.new(LiteralString.new("COUNT(*)").freeze, :count)], :order => nil}.freeze
19
19
  SELECT_CLAUSE_ORDER = %w'with distinct columns from join where group having compounds order limit'.freeze
20
+ TIMESTAMP_FORMAT = "'%Y-%m-%d %H:%M:%S%N%z'".freeze
21
+ STANDARD_TIMESTAMP_FORMAT = "TIMESTAMP #{TIMESTAMP_FORMAT}".freeze
20
22
  TWO_ARITY_OPERATORS = ::Sequel::SQL::ComplexExpression::TWO_ARITY_OPERATORS
21
23
  WILDCARD = '*'.freeze
22
24
  SQL_WITH = "WITH ".freeze
@@ -88,6 +90,11 @@ module Sequel
88
90
  raise(InvalidOperation, "invalid operator #{op}")
89
91
  end
90
92
  end
93
+
94
+ # SQL fragment for constants
95
+ def constant_sql(constant)
96
+ constant.to_s
97
+ end
91
98
 
92
99
  # Returns the number of records in the dataset.
93
100
  def count
@@ -103,11 +110,7 @@ module Sequel
103
110
 
104
111
  return static_sql(opts[:sql]) if opts[:sql]
105
112
 
106
- if opts[:group]
107
- raise InvalidOperation, "Grouped datasets cannot be deleted from"
108
- elsif opts[:from].is_a?(Array) && opts[:from].size > 1
109
- raise InvalidOperation, "Joined datasets cannot be deleted from"
110
- end
113
+ check_modification_allowed!
111
114
 
112
115
  sql = "DELETE FROM #{source_list(opts[:from])}"
113
116
 
@@ -308,7 +311,7 @@ module Sequel
308
311
  # dataset.group(:id) # SELECT * FROM items GROUP BY id
309
312
  # dataset.group(:id, :name) # SELECT * FROM items GROUP BY id, name
310
313
  def group(*columns)
311
- clone(:group => columns)
314
+ clone(:group => (columns.compact.empty? ? nil : columns))
312
315
  end
313
316
  alias group_by group
314
317
 
@@ -344,6 +347,8 @@ module Sequel
344
347
  def insert_sql(*values)
345
348
  return static_sql(@opts[:sql]) if @opts[:sql]
346
349
 
350
+ check_modification_allowed!
351
+
347
352
  from = source_list(@opts[:from])
348
353
  case values.size
349
354
  when 0
@@ -778,14 +783,32 @@ module Sequel
778
783
  def subscript_sql(s)
779
784
  "#{literal(s.f)}[#{expression_list(s.sub)}]"
780
785
  end
786
+
787
+ # SQL query to truncate the table
788
+ def truncate_sql
789
+ if opts[:sql]
790
+ static_sql(opts[:sql])
791
+ else
792
+ check_modification_allowed!
793
+ raise(InvalidOperation, "Can't truncate filtered datasets") if opts[:where]
794
+ _truncate_sql(source_list(opts[:from]))
795
+ end
796
+ end
781
797
 
782
798
  # Returns a copy of the dataset with no filters (HAVING or WHERE clause) applied.
783
799
  #
784
- # dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items
800
+ # dataset.group(:a).having(:a=>1).where(:b).unfiltered # SELECT * FROM items GROUP BY a
785
801
  def unfiltered
786
802
  clone(:where => nil, :having => nil)
787
803
  end
788
804
 
805
+ # Returns a copy of the dataset with no grouping (GROUP or HAVING clause) applied.
806
+ #
807
+ # dataset.group(:a).having(:a=>1).where(:b).ungrouped # SELECT * FROM items WHERE b
808
+ def ungrouped
809
+ clone(:group => nil, :having => nil)
810
+ end
811
+
789
812
  # Adds a UNION clause using a second dataset object.
790
813
  # A UNION compound dataset returns all rows in either the current dataset
791
814
  # or the given dataset.
@@ -826,11 +849,7 @@ module Sequel
826
849
 
827
850
  return static_sql(opts[:sql]) if opts[:sql]
828
851
 
829
- if opts[:group]
830
- raise InvalidOperation, "A grouped dataset cannot be updated"
831
- elsif (opts[:from].size > 1) or opts[:join]
832
- raise InvalidOperation, "A joined dataset cannot be updated"
833
- end
852
+ check_modification_allowed!
834
853
 
835
854
  sql = "UPDATE #{source_list(@opts[:from])} SET "
836
855
  set = if values.is_a?(Hash)
@@ -945,6 +964,13 @@ module Sequel
945
964
  def as_sql(expression, aliaz)
946
965
  "#{expression} AS #{quote_identifier(aliaz)}"
947
966
  end
967
+
968
+ # Raise an InvalidOperation exception if deletion is not allowed
969
+ # for this dataset
970
+ def check_modification_allowed!
971
+ raise(InvalidOperation, "Grouped datasets cannot be modified") if opts[:group]
972
+ raise(InvalidOperation, "Joined datasets cannot be modified") if (opts[:from].is_a?(Array) && opts[:from].size > 1) || opts[:join]
973
+ end
948
974
 
949
975
  # Converts an array of column names into a comma seperated string of
950
976
  # column names. If the array is empty, a wildcard (*) is returned.
@@ -969,6 +995,11 @@ module Sequel
969
995
  columns.map{|i| literal(i)}.join(COMMA_SEPARATOR)
970
996
  end
971
997
 
998
+ # The strftime format to use when literalizing the time.
999
+ def default_timestamp_format
1000
+ requires_sql_standard_datetimes? ? STANDARD_TIMESTAMP_FORMAT : TIMESTAMP_FORMAT
1001
+ end
1002
+
972
1003
  # SQL fragment based on the expr type. See #filter.
973
1004
  def filter_expr(expr = nil, &block)
974
1005
  expr = nil if expr == []
@@ -1002,6 +1033,27 @@ module Sequel
1002
1033
  raise(Error, 'Invalid filter argument')
1003
1034
  end
1004
1035
  end
1036
+
1037
+ # Format the timestamp based on the default_timestamp_format, with a couple
1038
+ # of modifiers. First, allow %N to be used for fractions seconds (if the
1039
+ # database supports them), and override %z to always use a numeric offset
1040
+ # of hours and minutes.
1041
+ def format_timestamp(v)
1042
+ v2 = Sequel.application_to_database_timestamp(v)
1043
+ fmt = default_timestamp_format.gsub(/%[Nz]/) do |m|
1044
+ if m == '%N'
1045
+ sprintf(".%06d", v.is_a?(DateTime) ? v.sec_fraction*86400000000 : v.usec) if supports_timestamp_usecs?
1046
+ else
1047
+ if supports_timestamp_timezones?
1048
+ # Would like to just use %z format, but it doesn't appear to work on Windows
1049
+ # Instead, the offset fragment is constructed manually
1050
+ minutes = (v2.is_a?(DateTime) ? v2.offset * 1440 : v2.utc_offset/60).to_i
1051
+ sprintf("%+03i%02i", *minutes.divmod(60))
1052
+ end
1053
+ end
1054
+ end
1055
+ v2.strftime(fmt)
1056
+ end
1005
1057
 
1006
1058
  # SQL fragment specifying a list of identifiers
1007
1059
  def identifier_list(columns)
@@ -1035,7 +1087,7 @@ module Sequel
1035
1087
  order.map do |f|
1036
1088
  case f
1037
1089
  when SQL::OrderedExpression
1038
- SQL::OrderedExpression.new(f.expression, !f.descending)
1090
+ f.invert
1039
1091
  else
1040
1092
  SQL::OrderedExpression.new(f)
1041
1093
  end
@@ -1074,9 +1126,9 @@ module Sequel
1074
1126
  requires_sql_standard_datetimes? ? v.strftime("DATE '%Y-%m-%d'") : "'#{v}'"
1075
1127
  end
1076
1128
 
1077
- # SQL fragment for DateTime, using the ISO8601 format.
1129
+ # SQL fragment for DateTime
1078
1130
  def literal_datetime(v)
1079
- requires_sql_standard_datetimes? ? v.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S'") : "'#{v}'"
1131
+ format_timestamp(v)
1080
1132
  end
1081
1133
 
1082
1134
  # SQL fragment for SQL::Expression, result depends on the specific type of expression.
@@ -1128,12 +1180,12 @@ module Sequel
1128
1180
  c_alias ? as_sql(qc, c_alias) : qc
1129
1181
  end
1130
1182
 
1131
- # SQL fragment for Time, uses the ISO8601 format.
1183
+ # SQL fragment for Time
1132
1184
  def literal_time(v)
1133
- requires_sql_standard_datetimes? ? v.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S'") : "'#{v.iso8601}'"
1185
+ format_timestamp(v)
1134
1186
  end
1135
1187
 
1136
- # SQL fragment for true.
1188
+ # SQL fragment for true
1137
1189
  def literal_true
1138
1190
  BOOL_TRUE
1139
1191
  end
@@ -1323,5 +1375,11 @@ module Sequel
1323
1375
  def table_ref(t)
1324
1376
  t.is_a?(String) ? quote_identifier(t) : literal(t)
1325
1377
  end
1378
+
1379
+ # Formats the truncate statement. Assumes the table given has already been
1380
+ # literalized.
1381
+ def _truncate_sql(table)
1382
+ "TRUNCATE TABLE #{table}"
1383
+ end
1326
1384
  end
1327
1385
  end