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
@@ -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