sequel 3.3.0 → 3.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +62 -0
- data/README.rdoc +4 -4
- data/doc/release_notes/3.3.0.txt +1 -1
- data/doc/release_notes/3.4.0.txt +325 -0
- data/doc/sharding.rdoc +3 -3
- data/lib/sequel/adapters/amalgalite.rb +1 -1
- data/lib/sequel/adapters/firebird.rb +4 -9
- data/lib/sequel/adapters/jdbc.rb +21 -7
- data/lib/sequel/adapters/mysql.rb +2 -1
- data/lib/sequel/adapters/odbc.rb +7 -21
- data/lib/sequel/adapters/oracle.rb +1 -1
- data/lib/sequel/adapters/postgres.rb +6 -1
- data/lib/sequel/adapters/shared/mssql.rb +11 -0
- data/lib/sequel/adapters/shared/mysql.rb +8 -12
- data/lib/sequel/adapters/shared/oracle.rb +13 -0
- data/lib/sequel/adapters/shared/postgres.rb +5 -10
- data/lib/sequel/adapters/shared/sqlite.rb +21 -1
- data/lib/sequel/adapters/sqlite.rb +2 -2
- data/lib/sequel/core.rb +147 -11
- data/lib/sequel/database.rb +21 -9
- data/lib/sequel/dataset.rb +31 -6
- data/lib/sequel/dataset/convenience.rb +1 -1
- data/lib/sequel/dataset/sql.rb +76 -18
- data/lib/sequel/extensions/inflector.rb +2 -51
- data/lib/sequel/model.rb +16 -10
- data/lib/sequel/model/associations.rb +4 -1
- data/lib/sequel/model/base.rb +13 -6
- data/lib/sequel/model/default_inflections.rb +46 -0
- data/lib/sequel/model/inflections.rb +1 -51
- data/lib/sequel/plugins/boolean_readers.rb +52 -0
- data/lib/sequel/plugins/instance_hooks.rb +57 -0
- data/lib/sequel/plugins/lazy_attributes.rb +13 -1
- data/lib/sequel/plugins/nested_attributes.rb +171 -0
- data/lib/sequel/plugins/serialization.rb +35 -16
- data/lib/sequel/plugins/timestamps.rb +87 -0
- data/lib/sequel/plugins/validation_helpers.rb +8 -1
- data/lib/sequel/sql.rb +33 -0
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/sqlite_spec.rb +11 -6
- data/spec/core/core_sql_spec.rb +29 -0
- data/spec/core/database_spec.rb +16 -7
- data/spec/core/dataset_spec.rb +264 -20
- data/spec/extensions/boolean_readers_spec.rb +86 -0
- data/spec/extensions/inflector_spec.rb +67 -4
- data/spec/extensions/instance_hooks_spec.rb +133 -0
- data/spec/extensions/lazy_attributes_spec.rb +45 -5
- data/spec/extensions/nested_attributes_spec.rb +272 -0
- data/spec/extensions/serialization_spec.rb +64 -1
- data/spec/extensions/timestamps_spec.rb +150 -0
- data/spec/extensions/validation_helpers_spec.rb +18 -0
- data/spec/integration/dataset_test.rb +79 -2
- data/spec/integration/schema_test.rb +17 -0
- data/spec/integration/timezone_test.rb +55 -0
- data/spec/model/associations_spec.rb +19 -7
- data/spec/model/model_spec.rb +29 -0
- data/spec/model/record_spec.rb +36 -0
- data/spec/spec_config.rb +1 -1
- 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 :
|
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 :
|
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
|
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
|
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
|
data/lib/sequel/version.rb
CHANGED
@@ -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
|
361
|
-
|
362
|
-
sqls.any?{|x| x =~ /
|
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
|
372
|
-
|
373
|
-
sqls.any?{|x| x =~ /
|
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
|
data/spec/core/core_sql_spec.rb
CHANGED
@@ -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
|
data/spec/core/database_spec.rb
CHANGED
@@ -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(:
|
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 #
|
298
|
-
(@db << "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
|
302
|
-
|
303
|
-
|
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
|
|
data/spec/core/dataset_spec.rb
CHANGED
@@ -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-%
|
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-%
|
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)
|
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,
|
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
|