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
@@ -31,9 +31,13 @@ module Sequel
31
31
 
32
32
  module ClassMethods
33
33
  # A map of the serialized columns for this model. Keys are column
34
- # symbols, values are serialization formats (:marshal or :yaml).
34
+ # symbols, values are serialization formats (:marshal, :yaml, or :json).
35
35
  attr_reader :serialization_map
36
36
 
37
+ # Module to store the serialized column accessor methods, so they can
38
+ # call be overridden and call super to get the serialization behavior
39
+ attr_accessor :serialization_module
40
+
37
41
  # Copy the serialization format and columns to serialize into the subclass.
38
42
  def inherited(subclass)
39
43
  super
@@ -51,22 +55,9 @@ module Sequel
51
55
  # and instance level writer that stores new deserialized value in deserialized
52
56
  # columns
53
57
  def serialize_attributes(format, *columns)
54
- raise(Error, "Unsupported serialization format (#{format}), should be :marshal or :yaml") unless [:marshal, :yaml].include?(format)
58
+ raise(Error, "Unsupported serialization format (#{format}), should be :marshal, :yaml, or :json") unless [:marshal, :yaml, :json].include?(format)
55
59
  raise(Error, "No columns given. The serialization plugin requires you specify which columns to serialize") if columns.empty?
56
- columns.each do |column|
57
- serialization_map[column] = format
58
- define_method(column) do
59
- if deserialized_values.has_key?(column)
60
- deserialized_values[column]
61
- else
62
- deserialized_values[column] = deserialize_value(column, @values[column])
63
- end
64
- end
65
- define_method("#{column}=") do |v|
66
- changed_columns << column unless changed_columns.include?(column)
67
- deserialized_values[column] = v
68
- end
69
- end
60
+ define_serialized_attribute_accessor(format, *columns)
70
61
  end
71
62
 
72
63
  # The columns that will be serialized. This is only for
@@ -74,6 +65,30 @@ module Sequel
74
65
  def serialized_columns
75
66
  serialization_map.keys
76
67
  end
68
+
69
+ private
70
+
71
+ # Add serializated attribute acessor methods to the serialization_module
72
+ def define_serialized_attribute_accessor(format, *columns)
73
+ m = self
74
+ include(self.serialization_module ||= Module.new) unless serialization_module
75
+ serialization_module.class_eval do
76
+ columns.each do |column|
77
+ m.serialization_map[column] = format
78
+ define_method(column) do
79
+ if deserialized_values.has_key?(column)
80
+ deserialized_values[column]
81
+ else
82
+ deserialized_values[column] = deserialize_value(column, super())
83
+ end
84
+ end
85
+ define_method("#{column}=") do |v|
86
+ changed_columns << column unless changed_columns.include?(column)
87
+ deserialized_values[column] = v
88
+ end
89
+ end
90
+ end
91
+ end
77
92
  end
78
93
 
79
94
  module InstanceMethods
@@ -110,6 +125,8 @@ module Sequel
110
125
  Marshal.load(v.unpack('m')[0]) rescue Marshal.load(v)
111
126
  when :yaml
112
127
  YAML.load v if v
128
+ when :json
129
+ JSON.parse v if v
113
130
  else
114
131
  raise Error, "Bad serialization format (#{model.serialization_map[column].inspect}) for column #{column.inspect}"
115
132
  end
@@ -123,6 +140,8 @@ module Sequel
123
140
  [Marshal.dump(v)].pack('m')
124
141
  when :yaml
125
142
  v.to_yaml
143
+ when :json
144
+ JSON.generate v
126
145
  else
127
146
  raise Error, "Bad serialization format (#{model.serialization_map[column].inspect}) for column #{column.inspect}"
128
147
  end
@@ -0,0 +1,87 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The timestamps plugin creates hooks that automatically set create and
4
+ # update timestamp fields. Both field names used are configurable, and you
5
+ # can also set whether to overwrite existing create timestamps (false
6
+ # by default), or whether to set the update timestamp when creating (also
7
+ # false by default).
8
+ module Timestamps
9
+ # Configure the plugin by setting the avialable options. Note that
10
+ # if this method is run more than once, previous settings are ignored,
11
+ # and it will just use the settings given or the default settings. Options:
12
+ # * :create - The field to hold the create timestamp (default: :created_at)
13
+ # * :force - Whether to overwrite an existing create timestamp (default: false)
14
+ # * :update - The field to hold the update timestamp (default: :updated_at)
15
+ # * :update_on_create - Whether to set the update timestamp to the create timestamp when creating (default: false)
16
+ def self.configure(model, opts={})
17
+ model.instance_eval do
18
+ @create_timestamp_field = opts[:create]||:created_at
19
+ @update_timestamp_field = opts[:update]||:updated_at
20
+ @create_timestamp_overwrite = opts[:force]||false
21
+ @set_update_timestamp_on_create = opts[:update_on_create]||false
22
+ end
23
+ end
24
+
25
+ module ClassMethods
26
+ # The field to store the create timestamp
27
+ attr_reader :create_timestamp_field
28
+
29
+ # The field to store the update timestamp
30
+ attr_reader :update_timestamp_field
31
+
32
+ # Whether to overwrite the create timestamp if it already exists
33
+ def create_timestamp_overwrite?
34
+ @create_timestamp_overwrite
35
+ end
36
+
37
+ # Copy the class instance variables used from the superclass to the subclass
38
+ def inherited(subclass)
39
+ super
40
+ [:@create_timestamp_field, :@update_timestamp_field, :@create_timestamp_overwrite, :@set_update_timestamp_on_create].each do |iv|
41
+ subclass.instance_variable_set(iv, instance_variable_get(iv))
42
+ end
43
+ end
44
+
45
+ # Whether to set the update timestamp to the create timestamp when creating
46
+ def set_update_timestamp_on_create?
47
+ @set_update_timestamp_on_create
48
+ end
49
+ end
50
+
51
+ module InstanceMethods
52
+ # Set the create timestamp when creating
53
+ def before_create
54
+ super
55
+ set_create_timestamp
56
+ end
57
+
58
+ # Set the update timestamp when updating
59
+ def before_update
60
+ super
61
+ set_update_timestamp
62
+ end
63
+
64
+ private
65
+
66
+ # If the object has accessor methods for the create timestamp field, and
67
+ # the create timestamp value is nil or overwriting it is allowed, set the
68
+ # create timestamp field to the time given or the current time. If setting
69
+ # the update timestamp on creation is configured, set the update timestamp
70
+ # as well.
71
+ def set_create_timestamp(time=nil)
72
+ field = model.create_timestamp_field
73
+ meth = :"#{field}="
74
+ self.send(meth, time||=Sequel.datetime_class.now) if respond_to?(field) && respond_to?(meth) && (model.create_timestamp_overwrite? || send(field).nil?)
75
+ set_update_timestamp(time) if model.set_update_timestamp_on_create?
76
+ end
77
+
78
+ # Set the update timestamp to the time given or the current time if the
79
+ # object has a setter method for the update timestamp field.
80
+ def set_update_timestamp(time=nil)
81
+ meth = :"#{model.update_timestamp_field}="
82
+ self.send(meth, time||Sequel.datetime_class.now) if respond_to?(meth)
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -117,11 +117,17 @@ module Sequel
117
117
  # validates_unique(:column1, :column2)
118
118
  # validates them separately.
119
119
  #
120
+ # You can pass a block, which is yielded the dataset in which the columns
121
+ # must be unique. So if you are doing a soft delete of records, in which
122
+ # the name must be unique, but only for active records:
123
+ #
124
+ # validates_unique(:name){|ds| ds.filter(:active)}
125
+ #
120
126
  # You should also add a unique index in the
121
127
  # database, as this suffers from a fairly obvious race condition.
122
128
  #
123
129
  # This validation does not respect the :allow_* options that the other validations accept,
124
- # since it can deals with multiple attributes at once.
130
+ # since it can deal with a grouping of multiple attributes.
125
131
  #
126
132
  # Possible Options:
127
133
  # * :message - The message to use (default: 'is already taken')
@@ -129,6 +135,7 @@ module Sequel
129
135
  message = (atts.pop[:message] if atts.last.is_a?(Hash)) || 'is already taken'
130
136
  atts.each do |a|
131
137
  ds = model.filter(Array(a).map{|x| [x, send(x)]})
138
+ ds = yield(ds) if block_given?
132
139
  errors.add(a, message) unless (new? ? ds : ds.exclude(pk_hash)).count == 0
133
140
  end
134
141
  end
data/lib/sequel/sql.rb CHANGED
@@ -540,6 +540,22 @@ module Sequel
540
540
 
541
541
  to_s_method :column_all_sql
542
542
  end
543
+
544
+ # Represents constants or psuedo-constants (e.g. CURRENT_DATE) in SQL
545
+ class Constant < GenericExpression
546
+ # Create an object with the given table
547
+ def initialize(constant)
548
+ @constant = constant
549
+ end
550
+
551
+ to_s_method :constant_sql, '@constant'
552
+ end
553
+
554
+ module Constants
555
+ CURRENT_DATE = Constant.new(:CURRENT_DATE)
556
+ CURRENT_TIME = Constant.new(:CURRENT_TIME)
557
+ CURRENT_TIMESTAMP = Constant.new(:CURRENT_TIMESTAMP)
558
+ end
543
559
 
544
560
  # Represents an SQL function call.
545
561
  class Function < GenericExpression
@@ -675,6 +691,21 @@ module Sequel
675
691
  @expression, @descending = expression, descending
676
692
  end
677
693
 
694
+ # Return a copy that is ASC
695
+ def asc
696
+ OrderedExpression.new(@expression, false)
697
+ end
698
+
699
+ # Return a copy that is DESC
700
+ def desc
701
+ OrderedExpression.new(@expression)
702
+ end
703
+
704
+ # Return an inverted expression, changing ASC to DESC and vice versa
705
+ def invert
706
+ OrderedExpression.new(@expression, !@descending)
707
+ end
708
+
678
709
  to_s_method :ordered_expression_sql
679
710
  end
680
711
 
@@ -912,4 +943,6 @@ module Sequel
912
943
  include SQL::StringMethods
913
944
  include SQL::InequalityMethods
914
945
  end
946
+
947
+ include SQL::Constants
915
948
  end
@@ -1,6 +1,6 @@
1
1
  module Sequel
2
2
  MAJOR = 3
3
- MINOR = 3
3
+ MINOR = 4
4
4
  TINY = 0
5
5
 
6
6
  VERSION = [MAJOR, MINOR, TINY].join('.')
@@ -343,6 +343,8 @@ context "A SQLite database" do
343
343
  end
344
344
 
345
345
  specify "should choose a temporary table name that isn't already used when dropping or renaming columns" do
346
+ sqls = []
347
+ @db.loggers << (l=Class.new{define_method(:info){|sql| sqls << sql}}.new)
346
348
  @db.create_table! :test3 do
347
349
  Integer :h
348
350
  Integer :i
@@ -357,9 +359,10 @@ context "A SQLite database" do
357
359
  @db[:test3].columns.should == [:h, :i]
358
360
  @db[:test3_backup0].columns.should == [:j]
359
361
  @db[:test3_backup1].columns.should == [:k]
360
- sqls = @db.drop_column(:test3, :i)
361
- sqls.any?{|x| x =~ /test3_backup2/}.should == true
362
- sqls.any?{|x| x =~ /test3_backup[01]/}.should == false
362
+ sqls.clear
363
+ @db.drop_column(:test3, :i)
364
+ sqls.any?{|x| x =~ /\ACREATE TABLE.*test3_backup2/}.should == true
365
+ sqls.any?{|x| x =~ /\ACREATE TABLE.*test3_backup[01]/}.should == false
363
366
  @db[:test3].columns.should == [:h]
364
367
  @db[:test3_backup0].columns.should == [:j]
365
368
  @db[:test3_backup1].columns.should == [:k]
@@ -368,13 +371,15 @@ context "A SQLite database" do
368
371
  Integer :l
369
372
  end
370
373
 
371
- sqls = @db.rename_column(:test3, :h, :i)
372
- sqls.any?{|x| x =~ /test3_backup3/}.should == true
373
- sqls.any?{|x| x =~ /test3_backup[012]/}.should == false
374
+ sqls.clear
375
+ @db.rename_column(:test3, :h, :i)
376
+ sqls.any?{|x| x =~ /\ACREATE TABLE.*test3_backup3/}.should == true
377
+ sqls.any?{|x| x =~ /\ACREATE TABLE.*test3_backup[012]/}.should == false
374
378
  @db[:test3].columns.should == [:i]
375
379
  @db[:test3_backup0].columns.should == [:j]
376
380
  @db[:test3_backup1].columns.should == [:k]
377
381
  @db[:test3_backup2].columns.should == [:l]
382
+ @db.loggers.delete(l)
378
383
  end
379
384
 
380
385
  specify "should support add_index" do
@@ -249,6 +249,15 @@ context "Symbol" do
249
249
  @ds.literal(:column.qualify(:table.qualify(:schema))).should == '"SCHEMA"."TABLE"."COLUMN"'
250
250
  @ds.literal(:column.qualify(:table__name.identifier.qualify(:schema))).should == '"SCHEMA"."TABLE__NAME"."COLUMN"'
251
251
  end
252
+
253
+ specify "should be able to specify order" do
254
+ @oe = :xyz.desc
255
+ @oe.class.should == Sequel::SQL::OrderedExpression
256
+ @oe.descending.should == true
257
+ @oe = :xyz.asc
258
+ @oe.class.should == Sequel::SQL::OrderedExpression
259
+ @oe.descending.should == false
260
+ end
252
261
  end
253
262
 
254
263
  context "Dataset#literal" do
@@ -370,3 +379,23 @@ context "Sequel::SQL::Function#==" do
370
379
  (c == d).should == false
371
380
  end
372
381
  end
382
+
383
+ context "Sequel::SQL::OrderedExpression" do
384
+ specify "should #desc" do
385
+ @oe = :column.asc
386
+ @oe.descending.should == false
387
+ @oe.desc.descending.should == true
388
+ end
389
+
390
+ specify "should #asc" do
391
+ @oe = :column.desc
392
+ @oe.descending.should == true
393
+ @oe.asc.descending.should == false
394
+ end
395
+
396
+ specify "should #invert" do
397
+ @oe = :column.desc
398
+ @oe.invert.descending.should == false
399
+ @oe.invert.invert.descending.should == true
400
+ end
401
+ end
@@ -286,21 +286,30 @@ context "Database#execute" do
286
286
  end
287
287
  end
288
288
 
289
- context "Database#<<" do
289
+ context "Database#<< and run" do
290
290
  before do
291
+ sqls = @sqls = []
291
292
  @c = Class.new(Sequel::Database) do
292
- define_method(:execute) {|sql, opts| sql}
293
+ define_method(:execute_ddl){|sql, *opts| sqls.clear; sqls << sql; sqls.concat(opts)}
293
294
  end
294
295
  @db = @c.new({})
295
296
  end
296
297
 
297
- specify "should pass the supplied sql to #execute" do
298
- (@db << "DELETE FROM items").should == "DELETE FROM items"
298
+ specify "should pass the supplied sql to #execute_ddl" do
299
+ (@db << "DELETE FROM items")
300
+ @sqls.should == ["DELETE FROM items", {}]
301
+ @db.run("DELETE FROM items2")
302
+ @sqls.should == ["DELETE FROM items2", {}]
299
303
  end
300
304
 
301
- specify "should not remove comments and whitespace from strings" do
302
- s = "INSERT INTO items VALUES ('---abc')"
303
- (@db << s).should == s
305
+ specify "should return nil" do
306
+ (@db << "DELETE FROM items").should == nil
307
+ @db.run("DELETE FROM items").should == nil
308
+ end
309
+
310
+ specify "should accept options passed to execute_ddl" do
311
+ @db.run("DELETE FROM items", :server=>:s1)
312
+ @sqls.should == ["DELETE FROM items", {:server=>:s1}]
304
313
  end
305
314
  end
306
315
 
@@ -172,6 +172,10 @@ context "A simple dataset" do
172
172
  @dataset.delete_sql.should == 'DELETE FROM test'
173
173
  end
174
174
 
175
+ specify "should format a truncate statement" do
176
+ @dataset.truncate_sql.should == 'TRUNCATE TABLE test'
177
+ end
178
+
175
179
  specify "should format an insert statement with default values" do
176
180
  @dataset.insert_sql.should == 'INSERT INTO test DEFAULT VALUES'
177
181
  end
@@ -233,6 +237,7 @@ context "A simple dataset" do
233
237
  ds.insert_sql.should == sql
234
238
  ds.delete_sql.should == sql
235
239
  ds.update_sql.should == sql
240
+ ds.truncate_sql.should == sql
236
241
  end
237
242
  end
238
243
 
@@ -248,6 +253,14 @@ context "A dataset with multiple tables in its FROM clause" do
248
253
  specify "should raise on #delete_sql" do
249
254
  proc {@dataset.delete_sql}.should raise_error(Sequel::InvalidOperation)
250
255
  end
256
+
257
+ specify "should raise on #truncate_sql" do
258
+ proc {@dataset.truncate_sql}.should raise_error(Sequel::InvalidOperation)
259
+ end
260
+
261
+ specify "should raise on #insert_sql" do
262
+ proc {@dataset.insert_sql}.should raise_error(Sequel::InvalidOperation)
263
+ end
251
264
 
252
265
  specify "should generate a select query FROM all specified tables" do
253
266
  @dataset.select_sql.should == "SELECT * FROM t1, t2"
@@ -357,12 +370,6 @@ context "Dataset#where" do
357
370
  "SELECT * FROM test WHERE ((a = 1) AND (e < 5))"
358
371
  end
359
372
 
360
- specify "should raise if the dataset is grouped" do
361
- proc {@dataset.group(:t).where(:a => 1)}.should_not raise_error
362
- @dataset.group(:t).where(:a => 1).sql.should ==
363
- "SELECT * FROM test WHERE (a = 1) GROUP BY t"
364
- end
365
-
366
373
  specify "should accept ranges" do
367
374
  @dataset.filter(:id => 4..7).sql.should ==
368
375
  'SELECT * FROM test WHERE ((id >= 4) AND (id <= 7))'
@@ -651,6 +658,14 @@ context "a grouped dataset" do
651
658
  specify "should raise when trying to generate a delete statement" do
652
659
  proc {@dataset.delete_sql}.should raise_error
653
660
  end
661
+
662
+ specify "should raise when trying to generate a truncate statement" do
663
+ proc {@dataset.truncate_sql}.should raise_error
664
+ end
665
+
666
+ specify "should raise when trying to generate an insert statement" do
667
+ proc {@dataset.insert_sql}.should raise_error
668
+ end
654
669
 
655
670
  specify "should specify the grouping in generated select statement" do
656
671
  @dataset.select_sql.should ==
@@ -682,13 +697,24 @@ context "Dataset#group_by" do
682
697
  "SELECT * FROM test GROUP BY type_id"
683
698
  @dataset.group_by(:a, :b).select_sql.should ==
684
699
  "SELECT * FROM test GROUP BY a, b"
685
- end
686
-
687
- specify "should specify the grouping in generated select statement" do
688
700
  @dataset.group_by(:type_id=>nil).select_sql.should ==
689
701
  "SELECT * FROM test GROUP BY (type_id IS NULL)"
690
702
  end
691
703
 
704
+ specify "should ungroup when passed nil, empty, or no arguments" do
705
+ @dataset.group_by.select_sql.should ==
706
+ "SELECT * FROM test"
707
+ @dataset.group_by(nil).select_sql.should ==
708
+ "SELECT * FROM test"
709
+ end
710
+
711
+ specify "should undo previous grouping" do
712
+ @dataset.group_by(:a).group_by(:b).select_sql.should ==
713
+ "SELECT * FROM test GROUP BY b"
714
+ @dataset.group_by(:a, :b).group_by.select_sql.should ==
715
+ "SELECT * FROM test"
716
+ end
717
+
692
718
  specify "should be aliased as #group" do
693
719
  @dataset.group(:type_id=>nil).select_sql.should ==
694
720
  "SELECT * FROM test GROUP BY (type_id IS NULL)"
@@ -753,14 +779,14 @@ context "Dataset#literal" do
753
779
 
754
780
  specify "should literalize Time properly" do
755
781
  t = Time.now
756
- s = t.strftime("'%Y-%m-%dT%H:%M:%S%z'").gsub(/(\d\d')\z/, ':\1')
757
- @dataset.literal(t).should == s
782
+ s = t.strftime("'%Y-%m-%d %H:%M:%S")
783
+ @dataset.literal(t).should == "#{s}.#{sprintf('%06i', t.usec)}'"
758
784
  end
759
785
 
760
786
  specify "should literalize DateTime properly" do
761
787
  t = DateTime.now
762
- s = t.strftime("'%Y-%m-%dT%H:%M:%S%z'").gsub(/(\d\d')\z/, ':\1')
763
- @dataset.literal(t).should == s
788
+ s = t.strftime("'%Y-%m-%d %H:%M:%S")
789
+ @dataset.literal(t).should == "#{s}.#{sprintf('%06i', t.sec_fraction* 86400000000)}'"
764
790
  end
765
791
 
766
792
  specify "should literalize Date properly" do
@@ -773,18 +799,52 @@ context "Dataset#literal" do
773
799
  @dataset.meta_def(:requires_sql_standard_datetimes?){true}
774
800
 
775
801
  t = Time.now
776
- s = t.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S'")
777
- @dataset.literal(t).should == s
802
+ s = t.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S")
803
+ @dataset.literal(t).should == "#{s}.#{sprintf('%06i', t.usec)}'"
778
804
 
779
805
  t = DateTime.now
780
- s = t.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S'")
781
- @dataset.literal(t).should == s
806
+ s = t.strftime("TIMESTAMP '%Y-%m-%d %H:%M:%S")
807
+ @dataset.literal(t).should == "#{s}.#{sprintf('%06i', t.sec_fraction* 86400000000)}'"
782
808
 
783
809
  d = Date.today
784
810
  s = d.strftime("DATE '%Y-%m-%d'")
785
811
  @dataset.literal(d).should == s
786
812
  end
787
813
 
814
+ specify "should literalize Time and DateTime properly if the database support timezones in timestamps" do
815
+ @dataset.meta_def(:supports_timestamp_timezones?){true}
816
+
817
+ t = Time.now.utc
818
+ s = t.strftime("'%Y-%m-%d %H:%M:%S")
819
+ @dataset.literal(t).should == "#{s}.#{sprintf('%06i', t.usec)}+0000'"
820
+
821
+ t = DateTime.now.new_offset(0)
822
+ s = t.strftime("'%Y-%m-%d %H:%M:%S")
823
+ @dataset.literal(t).should == "#{s}.#{sprintf('%06i', t.sec_fraction* 86400000000)}+0000'"
824
+ end
825
+
826
+ specify "should literalize Time and DateTime properly if the database doesn't support usecs in timestamps" do
827
+ @dataset.meta_def(:supports_timestamp_usecs?){false}
828
+
829
+ t = Time.now.utc
830
+ s = t.strftime("'%Y-%m-%d %H:%M:%S")
831
+ @dataset.literal(t).should == "#{s}'"
832
+
833
+ t = DateTime.now.new_offset(0)
834
+ s = t.strftime("'%Y-%m-%d %H:%M:%S")
835
+ @dataset.literal(t).should == "#{s}'"
836
+
837
+ @dataset.meta_def(:supports_timestamp_timezones?){true}
838
+
839
+ t = Time.now.utc
840
+ s = t.strftime("'%Y-%m-%d %H:%M:%S")
841
+ @dataset.literal(t).should == "#{s}+0000'"
842
+
843
+ t = DateTime.now.new_offset(0)
844
+ s = t.strftime("'%Y-%m-%d %H:%M:%S")
845
+ @dataset.literal(t).should == "#{s}+0000'"
846
+ end
847
+
788
848
  specify "should not modify literal strings" do
789
849
  @dataset.literal('col1 + 2'.lit).should == 'col1 + 2'
790
850
 
@@ -1029,6 +1089,12 @@ context "Dataset#unlimited" do
1029
1089
  end
1030
1090
  end
1031
1091
 
1092
+ context "Dataset#ungrouped" do
1093
+ specify "should remove group and having clauses from the dataset" do
1094
+ Sequel::Dataset.new(nil).from(:test).group(:a).having(:b).ungrouped.sql.should == 'SELECT * FROM test'
1095
+ end
1096
+ end
1097
+
1032
1098
  context "Dataset#unordered" do
1033
1099
  specify "should remove ordering from the dataset" do
1034
1100
  Sequel::Dataset.new(nil).from(:test).order(:name).unordered.sql.should == 'SELECT * FROM test'
@@ -1680,6 +1746,13 @@ context "Dataset#join_table" do
1680
1746
  @d.join(:categories, :a=>:d){|j,lj,js| :b.qualify(j) > :c.qualify(lj)}.sql.should ==
1681
1747
  'SELECT * FROM "items" INNER JOIN "categories" ON (("categories"."a" = "items"."d") AND ("categories"."b" > "items"."c"))'
1682
1748
  end
1749
+
1750
+ specify "should not allow insert, update, delete, or truncate" do
1751
+ proc{@d.join(:categories, :a=>:d).insert_sql}.should raise_error(Sequel::InvalidOperation)
1752
+ proc{@d.join(:categories, :a=>:d).update_sql(:a=>1)}.should raise_error(Sequel::InvalidOperation)
1753
+ proc{@d.join(:categories, :a=>:d).delete_sql}.should raise_error(Sequel::InvalidOperation)
1754
+ proc{@d.join(:categories, :a=>:d).truncate_sql}.should raise_error(Sequel::InvalidOperation)
1755
+ end
1683
1756
  end
1684
1757
 
1685
1758
  context "Dataset#[]=" do
@@ -2254,7 +2327,7 @@ context "Dataset#import" do
2254
2327
  @ds.import([:x, :y], @ds2)
2255
2328
  @db.sqls.should == [
2256
2329
  'BEGIN',
2257
- "INSERT INTO items (x, y) VALUES (SELECT a, b FROM cats WHERE (purr IS TRUE))",
2330
+ "INSERT INTO items (x, y) SELECT a, b FROM cats WHERE (purr IS TRUE)",
2258
2331
  'COMMIT'
2259
2332
  ]
2260
2333
  end
@@ -2617,7 +2690,7 @@ context "Dataset#grep" do
2617
2690
  end
2618
2691
  end
2619
2692
 
2620
- context "Dataset default #fetch_rows, #insert, #update, and #delete, #execute" do
2693
+ context "Dataset default #fetch_rows, #insert, #update, #delete, #truncate, #execute" do
2621
2694
  before do
2622
2695
  @db = Sequel::Database.new
2623
2696
  @ds = @db[:items]
@@ -2630,16 +2703,33 @@ context "Dataset default #fetch_rows, #insert, #update, and #delete, #execute" d
2630
2703
  specify "#delete should execute delete SQL" do
2631
2704
  @db.should_receive(:execute).once.with('DELETE FROM items', :server=>:default)
2632
2705
  @ds.delete
2706
+ @db.should_receive(:execute_dui).once.with('DELETE FROM items', :server=>:default)
2707
+ @ds.delete
2633
2708
  end
2634
2709
 
2635
2710
  specify "#insert should execute insert SQL" do
2636
2711
  @db.should_receive(:execute).once.with('INSERT INTO items DEFAULT VALUES', :server=>:default)
2637
2712
  @ds.insert([])
2713
+ @db.should_receive(:execute_insert).once.with('INSERT INTO items DEFAULT VALUES', :server=>:default)
2714
+ @ds.insert([])
2638
2715
  end
2639
2716
 
2640
2717
  specify "#update should execute update SQL" do
2641
2718
  @db.should_receive(:execute).once.with('UPDATE items SET number = 1', :server=>:default)
2642
2719
  @ds.update(:number=>1)
2720
+ @db.should_receive(:execute_dui).once.with('UPDATE items SET number = 1', :server=>:default)
2721
+ @ds.update(:number=>1)
2722
+ end
2723
+
2724
+ specify "#truncate should execute truncate SQL" do
2725
+ @db.should_receive(:execute).once.with('TRUNCATE TABLE items', :server=>:default)
2726
+ @ds.truncate.should == nil
2727
+ @db.should_receive(:execute_ddl).once.with('TRUNCATE TABLE items', :server=>:default)
2728
+ @ds.truncate.should == nil
2729
+ end
2730
+
2731
+ specify "#truncate should raise an InvalidOperation exception if the dataset is filtered" do
2732
+ proc{@ds.filter(:a=>1).truncate}.should raise_error(Sequel::InvalidOperation)
2643
2733
  end
2644
2734
 
2645
2735
  specify "#execute should execute the SQL on the database" do
@@ -2985,4 +3075,158 @@ context "Sequel::Dataset #with and #with_recursive" do
2985
3075
  proc{@ds.with(:t, @db[:x], :args=>[:b])}.should raise_error(Sequel::Error)
2986
3076
  proc{@ds.with_recursive(:t, @db[:x], @db[:t], :args=>[:b, :c])}.should raise_error(Sequel::Error)
2987
3077
  end
2988
- end
3078
+ end
3079
+
3080
+ describe Sequel::SQL::Constants do
3081
+ before do
3082
+ @db = MockDatabase.new
3083
+ end
3084
+
3085
+ it "should have CURRENT_DATE" do
3086
+ @db.literal(Sequel::SQL::Constants::CURRENT_DATE) == 'CURRENT_DATE'
3087
+ @db.literal(Sequel::CURRENT_DATE) == 'CURRENT_DATE'
3088
+ end
3089
+
3090
+ it "should have CURRENT_TIME" do
3091
+ @db.literal(Sequel::SQL::Constants::CURRENT_TIME) == 'CURRENT_TIME'
3092
+ @db.literal(Sequel::CURRENT_TIME) == 'CURRENT_TIME'
3093
+ end
3094
+
3095
+ it "should have CURRENT_TIMESTAMP" do
3096
+ @db.literal(Sequel::SQL::Constants::CURRENT_TIMESTAMP) == 'CURRENT_TIMESTAMP'
3097
+ @db.literal(Sequel::CURRENT_TIMESTAMP) == 'CURRENT_TIMESTAMP'
3098
+ end
3099
+ end
3100
+
3101
+ describe "Sequel timezone support" do
3102
+ before do
3103
+ @db = MockDatabase.new
3104
+ @dataset = @db.dataset
3105
+ @dataset.meta_def(:supports_timestamp_timezones?){true}
3106
+ @dataset.meta_def(:supports_timestamp_usecs?){false}
3107
+ @offset = sprintf("%+03i%02i", *(Time.now.utc_offset/60).divmod(60))
3108
+ end
3109
+ after do
3110
+ Sequel.default_timezone = nil
3111
+ Sequel.datetime_class = Time
3112
+ end
3113
+
3114
+ specify "should handle an database timezone of :utc when literalizing values" do
3115
+ Sequel.database_timezone = :utc
3116
+
3117
+ t = Time.now
3118
+ s = t.getutc.strftime("'%Y-%m-%d %H:%M:%S")
3119
+ @dataset.literal(t).should == "#{s}+0000'"
3120
+
3121
+ t = DateTime.now
3122
+ s = t.new_offset(0).strftime("'%Y-%m-%d %H:%M:%S")
3123
+ @dataset.literal(t).should == "#{s}+0000'"
3124
+ end
3125
+
3126
+ specify "should handle an database timezone of :local when literalizing values" do
3127
+ Sequel.database_timezone = :local
3128
+
3129
+ t = Time.now.utc
3130
+ s = t.getlocal.strftime("'%Y-%m-%d %H:%M:%S")
3131
+ @dataset.literal(t).should == "#{s}#{@offset}'"
3132
+
3133
+ t = DateTime.now.new_offset(0)
3134
+ s = t.new_offset(Sequel::LOCAL_DATETIME_OFFSET).strftime("'%Y-%m-%d %H:%M:%S")
3135
+ @dataset.literal(t).should == "#{s}#{@offset}'"
3136
+ end
3137
+
3138
+ specify "should handle converting database timestamps into application timestamps" do
3139
+ Sequel.database_timezone = :utc
3140
+ Sequel.application_timezone = :local
3141
+ t = Time.now.utc
3142
+ Sequel.database_to_application_timestamp(t).to_s.should == t.getlocal.to_s
3143
+ Sequel.database_to_application_timestamp(t.to_s).to_s.should == t.getlocal.to_s
3144
+ Sequel.database_to_application_timestamp(t.strftime('%Y-%m-%d %H:%M:%S')).to_s.should == t.getlocal.to_s
3145
+
3146
+ Sequel.datetime_class = DateTime
3147
+ dt = DateTime.now
3148
+ dt2 = dt.new_offset(0)
3149
+ Sequel.database_to_application_timestamp(dt2).to_s.should == dt.to_s
3150
+ Sequel.database_to_application_timestamp(dt2.to_s).to_s.should == dt.to_s
3151
+ Sequel.database_to_application_timestamp(dt2.strftime('%Y-%m-%d %H:%M:%S')).to_s.should == dt.to_s
3152
+
3153
+ Sequel.datetime_class = Time
3154
+ Sequel.database_timezone = :local
3155
+ Sequel.application_timezone = :utc
3156
+ Sequel.database_to_application_timestamp(t.getlocal).to_s.should == t.to_s
3157
+ Sequel.database_to_application_timestamp(t.getlocal.to_s).to_s.should == t.to_s
3158
+ Sequel.database_to_application_timestamp(t.getlocal.strftime('%Y-%m-%d %H:%M:%S')).to_s.should == t.to_s
3159
+
3160
+ Sequel.datetime_class = DateTime
3161
+ Sequel.database_to_application_timestamp(dt).to_s.should == dt2.to_s
3162
+ Sequel.database_to_application_timestamp(dt.to_s).to_s.should == dt2.to_s
3163
+ Sequel.database_to_application_timestamp(dt.strftime('%Y-%m-%d %H:%M:%S')).to_s.should == dt2.to_s
3164
+ end
3165
+
3166
+ specify "should handle typecasting timestamp columns" do
3167
+ Sequel.typecast_timezone = :utc
3168
+ Sequel.application_timezone = :local
3169
+ t = Time.now.utc
3170
+ @db.typecast_value(:datetime, t).to_s.should == t.getlocal.to_s
3171
+ @db.typecast_value(:datetime, t.to_s).to_s.should == t.getlocal.to_s
3172
+ @db.typecast_value(:datetime, t.strftime('%Y-%m-%d %H:%M:%S')).to_s.should == t.getlocal.to_s
3173
+
3174
+ Sequel.datetime_class = DateTime
3175
+ dt = DateTime.now
3176
+ dt2 = dt.new_offset(0)
3177
+ @db.typecast_value(:datetime, dt2).to_s.should == dt.to_s
3178
+ @db.typecast_value(:datetime, dt2.to_s).to_s.should == dt.to_s
3179
+ @db.typecast_value(:datetime, dt2.strftime('%Y-%m-%d %H:%M:%S')).to_s.should == dt.to_s
3180
+
3181
+ Sequel.datetime_class = Time
3182
+ Sequel.typecast_timezone = :local
3183
+ Sequel.application_timezone = :utc
3184
+ @db.typecast_value(:datetime, t.getlocal).to_s.should == t.to_s
3185
+ @db.typecast_value(:datetime, t.getlocal.to_s).to_s.should == t.to_s
3186
+ @db.typecast_value(:datetime, t.getlocal.strftime('%Y-%m-%d %H:%M:%S')).to_s.should == t.to_s
3187
+
3188
+ Sequel.datetime_class = DateTime
3189
+ @db.typecast_value(:datetime, dt).to_s.should == dt2.to_s
3190
+ @db.typecast_value(:datetime, dt.to_s).to_s.should == dt2.to_s
3191
+ @db.typecast_value(:datetime, dt.strftime('%Y-%m-%d %H:%M:%S')).to_s.should == dt2.to_s
3192
+ end
3193
+
3194
+ specify "should handle converting database timestamp columns from an array of values" do
3195
+ Sequel.database_timezone = :utc
3196
+ Sequel.application_timezone = :local
3197
+ t = Time.now.utc
3198
+ Sequel.database_to_application_timestamp([t.year, t.mon, t.day, t.hour, t.min, t.sec]).to_s.should == t.getlocal.to_s
3199
+
3200
+ Sequel.datetime_class = DateTime
3201
+ dt = DateTime.now
3202
+ dt2 = dt.new_offset(0)
3203
+ Sequel.database_to_application_timestamp([dt2.year, dt2.mon, dt2.day, dt2.hour, dt2.min, dt2.sec]).to_s.should == dt.to_s
3204
+
3205
+ Sequel.datetime_class = Time
3206
+ Sequel.database_timezone = :local
3207
+ Sequel.application_timezone = :utc
3208
+ t = t.getlocal
3209
+ Sequel.database_to_application_timestamp([t.year, t.mon, t.day, t.hour, t.min, t.sec]).to_s.should == t.getutc.to_s
3210
+
3211
+ Sequel.datetime_class = DateTime
3212
+ Sequel.database_to_application_timestamp([dt.year, dt.mon, dt.day, dt.hour, dt.min, dt.sec]).to_s.should == dt2.to_s
3213
+ end
3214
+
3215
+ specify "should raise an InvalidValue error when an error occurs while converting a timestamp" do
3216
+ proc{Sequel.database_to_application_timestamp([0, 0, 0, 0, 0, 0])}.should raise_error(Sequel::InvalidValue)
3217
+ end
3218
+
3219
+ specify "should raise an error when attempting to typecast to a timestamp from an unsupported type" do
3220
+ proc{Sequel.database_to_application_timestamp(Object.new)}.should raise_error(Sequel::InvalidValue)
3221
+ end
3222
+
3223
+ specify "should have Sequel.default_timezone= should set all other timezones" do
3224
+ Sequel.database_timezone.should == nil
3225
+ Sequel.application_timezone.should == nil
3226
+ Sequel.typecast_timezone.should == nil
3227
+ Sequel.default_timezone = :utc
3228
+ Sequel.database_timezone.should == :utc
3229
+ Sequel.application_timezone.should == :utc
3230
+ Sequel.typecast_timezone.should == :utc
3231
+ end
3232
+ end