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