sequel 3.3.0 → 3.4.0

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