sequel 2.9.0 → 2.10.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 +56 -0
- data/{README → README.rdoc} +85 -57
- data/Rakefile +10 -5
- data/bin/sequel +7 -16
- data/doc/advanced_associations.rdoc +5 -17
- data/doc/cheat_sheet.rdoc +18 -20
- data/doc/dataset_filtering.rdoc +8 -32
- data/doc/schema.rdoc +20 -0
- data/lib/sequel_core.rb +35 -1
- data/lib/sequel_core/adapters/ado.rb +1 -1
- data/lib/sequel_core/adapters/db2.rb +2 -2
- data/lib/sequel_core/adapters/dbi.rb +2 -11
- data/lib/sequel_core/adapters/do.rb +205 -0
- data/lib/sequel_core/adapters/do/mysql.rb +38 -0
- data/lib/sequel_core/adapters/do/postgres.rb +92 -0
- data/lib/sequel_core/adapters/do/sqlite.rb +31 -0
- data/lib/sequel_core/adapters/firebird.rb +298 -0
- data/lib/sequel_core/adapters/informix.rb +10 -1
- data/lib/sequel_core/adapters/jdbc.rb +78 -19
- data/lib/sequel_core/adapters/jdbc/h2.rb +69 -0
- data/lib/sequel_core/adapters/jdbc/mysql.rb +10 -0
- data/lib/sequel_core/adapters/jdbc/postgresql.rb +7 -3
- data/lib/sequel_core/adapters/mysql.rb +53 -77
- data/lib/sequel_core/adapters/odbc.rb +1 -1
- data/lib/sequel_core/adapters/openbase.rb +1 -1
- data/lib/sequel_core/adapters/oracle.rb +2 -2
- data/lib/sequel_core/adapters/postgres.rb +16 -14
- data/lib/sequel_core/adapters/shared/mysql.rb +44 -9
- data/lib/sequel_core/adapters/shared/oracle.rb +4 -5
- data/lib/sequel_core/adapters/shared/postgres.rb +86 -82
- data/lib/sequel_core/adapters/shared/sqlite.rb +39 -24
- data/lib/sequel_core/adapters/sqlite.rb +11 -1
- data/lib/sequel_core/connection_pool.rb +10 -2
- data/lib/sequel_core/core_sql.rb +13 -3
- data/lib/sequel_core/database.rb +131 -30
- data/lib/sequel_core/database/schema.rb +5 -5
- data/lib/sequel_core/dataset.rb +31 -6
- data/lib/sequel_core/dataset/convenience.rb +11 -11
- data/lib/sequel_core/dataset/query.rb +2 -2
- data/lib/sequel_core/dataset/sql.rb +6 -6
- data/lib/sequel_core/exceptions.rb +4 -0
- data/lib/sequel_core/migration.rb +4 -4
- data/lib/sequel_core/schema/generator.rb +19 -3
- data/lib/sequel_core/schema/sql.rb +24 -20
- data/lib/sequel_core/sql.rb +13 -16
- data/lib/sequel_core/version.rb +11 -0
- data/lib/sequel_model.rb +2 -0
- data/lib/sequel_model/base.rb +2 -2
- data/lib/sequel_model/hooks.rb +46 -7
- data/lib/sequel_model/record.rb +11 -9
- data/lib/sequel_model/schema.rb +1 -1
- data/lib/sequel_model/validations.rb +72 -61
- data/spec/adapters/firebird_spec.rb +371 -0
- data/spec/adapters/mysql_spec.rb +118 -62
- data/spec/adapters/oracle_spec.rb +5 -5
- data/spec/adapters/postgres_spec.rb +33 -18
- data/spec/adapters/sqlite_spec.rb +2 -2
- data/spec/integration/dataset_test.rb +3 -3
- data/spec/integration/schema_test.rb +55 -5
- data/spec/integration/spec_helper.rb +11 -0
- data/spec/integration/type_test.rb +59 -16
- data/spec/sequel_core/connection_pool_spec.rb +14 -0
- data/spec/sequel_core/core_sql_spec.rb +24 -14
- data/spec/sequel_core/database_spec.rb +96 -11
- data/spec/sequel_core/dataset_spec.rb +97 -37
- data/spec/sequel_core/expression_filters_spec.rb +51 -40
- data/spec/sequel_core/object_graph_spec.rb +2 -2
- data/spec/sequel_core/schema_generator_spec.rb +31 -6
- data/spec/sequel_core/schema_spec.rb +25 -9
- data/spec/sequel_core/spec_helper.rb +4 -1
- data/spec/sequel_core/version_spec.rb +7 -0
- data/spec/sequel_model/associations_spec.rb +1 -1
- data/spec/sequel_model/hooks_spec.rb +68 -13
- data/spec/sequel_model/model_spec.rb +4 -4
- data/spec/sequel_model/record_spec.rb +22 -0
- data/spec/sequel_model/spec_helper.rb +2 -1
- data/spec/sequel_model/validations_spec.rb +107 -7
- metadata +15 -5
data/lib/sequel_model.rb
CHANGED
@@ -50,6 +50,8 @@ module Sequel
|
|
50
50
|
# cache_store, cache_ttl, dataset_methods, primary_key, restricted_columns,
|
51
51
|
# sti_dataset, and sti_key. You should not usually need to
|
52
52
|
# access these directly.
|
53
|
+
# * All validation methods also accept the options specified in #validates_each,
|
54
|
+
# in addition to the options specified in the RDoc for that method.
|
53
55
|
# * The following class level attr_accessors are created: raise_on_typecast_failure,
|
54
56
|
# raise_on_save_failure, strict_param_setting, typecast_empty_string_to_nil,
|
55
57
|
# and typecast_on_assignment:
|
data/lib/sequel_model/base.rb
CHANGED
@@ -402,8 +402,8 @@ module Sequel
|
|
402
402
|
# create dataset methods, so they can be chained for scoping.
|
403
403
|
# For example:
|
404
404
|
#
|
405
|
-
# Topic.subset(:popular, :num_posts > 100)
|
406
|
-
# Topic.subset(:recent, :created_on > Date.today
|
405
|
+
# Topic.subset(:popular, :num_posts.sql_number > 100)
|
406
|
+
# Topic.subset(:recent, :created_on + 7 > Date.today)
|
407
407
|
#
|
408
408
|
# Allows you to do:
|
409
409
|
#
|
data/lib/sequel_model/hooks.rb
CHANGED
@@ -8,6 +8,45 @@ module Sequel
|
|
8
8
|
# Hooks that are only for internal use
|
9
9
|
PRIVATE_HOOKS = [:before_update_values, :before_delete]
|
10
10
|
|
11
|
+
# This adds a new hook type. It will define both a class
|
12
|
+
# method that you can use to add hooks, as well as an instance method
|
13
|
+
# that you can use to call all hooks of that type. The class method
|
14
|
+
# can be called with a symbol or a block or both. If a block is given and
|
15
|
+
# and symbol is not, it adds the hook block to the hook type. If a block
|
16
|
+
# and symbol are both given, it replaces the hook block associated with
|
17
|
+
# that symbol for a given hook type, or adds it if there is no hook block
|
18
|
+
# with that symbol for that hook type. If no block is given, it assumes
|
19
|
+
# the symbol specifies an instance method to call and adds it to the hook
|
20
|
+
# type.
|
21
|
+
#
|
22
|
+
# If any hook block returns false, the instance method will return false
|
23
|
+
# immediately without running the rest of the hooks of that type.
|
24
|
+
#
|
25
|
+
# It is recommended that you always provide a symbol to this method,
|
26
|
+
# for descriptive purposes. It's only necessary to do so when you
|
27
|
+
# are using a system that reloads code.
|
28
|
+
#
|
29
|
+
# All of Sequel's standard hook types are also implemented using this
|
30
|
+
# method.
|
31
|
+
#
|
32
|
+
# Example of usage:
|
33
|
+
#
|
34
|
+
# class MyModel
|
35
|
+
# define_hook :before_move_to
|
36
|
+
# before_move_to(:check_move_allowed){|o| o.allow_move?}
|
37
|
+
# def move_to(there)
|
38
|
+
# return if before_move_to == false
|
39
|
+
# # move MyModel object to there
|
40
|
+
# end
|
41
|
+
# end
|
42
|
+
def self.add_hook_type(*hooks)
|
43
|
+
hooks.each do |hook|
|
44
|
+
@hooks[hook] = []
|
45
|
+
instance_eval("def #{hook}(method = nil, &block); define_hook_instance_method(:#{hook}); add_hook(:#{hook}, method, &block) end")
|
46
|
+
class_eval("def #{hook}; end")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
11
50
|
# Returns true if there are any hook blocks for the given hook.
|
12
51
|
def self.has_hooks?(hook)
|
13
52
|
!@hooks[hook].empty?
|
@@ -32,7 +71,11 @@ module Sequel
|
|
32
71
|
if tag && (old = h.find{|x| x[0] == tag})
|
33
72
|
old[1] = block
|
34
73
|
else
|
35
|
-
|
74
|
+
if hook.to_s =~ /^before/
|
75
|
+
h.unshift([tag,block])
|
76
|
+
else
|
77
|
+
h << [tag, block]
|
78
|
+
end
|
36
79
|
end
|
37
80
|
end
|
38
81
|
|
@@ -43,7 +86,7 @@ module Sequel
|
|
43
86
|
|
44
87
|
private_class_method :add_hook, :define_hook_instance_method
|
45
88
|
|
46
|
-
private
|
89
|
+
private
|
47
90
|
|
48
91
|
# Runs all hook blocks of given hook type on this object.
|
49
92
|
# Stops running hook blocks and returns false if any hook block returns false.
|
@@ -53,10 +96,6 @@ module Sequel
|
|
53
96
|
|
54
97
|
# For performance reasons, we define empty hook instance methods, which are
|
55
98
|
# overwritten with real hook instance methods whenever the hook class method is called.
|
56
|
-
(HOOKS + PRIVATE_HOOKS)
|
57
|
-
@hooks[hook] = []
|
58
|
-
instance_eval("def #{hook}(method = nil, &block); define_hook_instance_method(:#{hook}); add_hook(:#{hook}, method, &block) end")
|
59
|
-
class_eval("def #{hook}; end")
|
60
|
-
end
|
99
|
+
add_hook_type(*(HOOKS + PRIVATE_HOOKS))
|
61
100
|
end
|
62
101
|
end
|
data/lib/sequel_model/record.rb
CHANGED
@@ -199,7 +199,7 @@ module Sequel
|
|
199
199
|
def save!(*columns)
|
200
200
|
opts = columns.extract_options!
|
201
201
|
return save_failure(:save) if before_save == false
|
202
|
-
if
|
202
|
+
if new?
|
203
203
|
return save_failure(:create) if before_create == false
|
204
204
|
ds = model.dataset
|
205
205
|
if ds.respond_to?(:insert_select) and h = ds.insert_select(@values)
|
@@ -212,26 +212,28 @@ module Sequel
|
|
212
212
|
if (pk = primary_key) && !(Array === pk) && !@values[pk]
|
213
213
|
@values[pk] = iid
|
214
214
|
end
|
215
|
-
if pk
|
216
|
-
@this = nil # remove memoized this dataset
|
217
|
-
refresh
|
218
|
-
end
|
215
|
+
@this = nil if pk
|
219
216
|
end
|
220
|
-
@new = false
|
221
217
|
after_create
|
218
|
+
after_save
|
219
|
+
@new = false
|
220
|
+
refresh if pk
|
222
221
|
else
|
223
222
|
return save_failure(:update) if before_update == false
|
224
223
|
if columns.empty?
|
225
224
|
vals = opts[:changed] ? @values.reject{|k,v| !changed_columns.include?(k)} : @values
|
226
225
|
this.update(vals)
|
227
|
-
changed_columns.clear
|
228
226
|
else # update only the specified columns
|
229
227
|
this.update(@values.reject{|k, v| !columns.include?(k)})
|
230
|
-
changed_columns.reject!{|c| columns.include?(c)}
|
231
228
|
end
|
232
229
|
after_update
|
230
|
+
after_save
|
231
|
+
if columns.empty?
|
232
|
+
changed_columns.clear
|
233
|
+
else
|
234
|
+
changed_columns.reject!{|c| columns.include?(c)}
|
235
|
+
end
|
233
236
|
end
|
234
|
-
after_save
|
235
237
|
self
|
236
238
|
end
|
237
239
|
|
data/lib/sequel_model/schema.rb
CHANGED
@@ -104,26 +104,21 @@ module Sequel
|
|
104
104
|
end
|
105
105
|
|
106
106
|
# Validates acceptance of an attribute. Just checks that the value
|
107
|
-
# is equal to the :accept option.
|
107
|
+
# is equal to the :accept option. This method is unique in that
|
108
|
+
# :allow_nil is assumed to be true instead of false.
|
108
109
|
#
|
109
110
|
# Possible Options:
|
110
111
|
# * :accept - The value required for the object to be valid (default: '1')
|
111
|
-
# * :allow_blank - Whether to skip the validation if the value is blank (default: false)
|
112
|
-
# * :allow_nil - Whether to skip the validation if the value is nil (default: true)
|
113
|
-
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
114
|
-
# skipping this validation if it returns nil or false.
|
115
112
|
# * :message - The message to use (default: 'is not accepted')
|
116
|
-
# * :tag - The tag to use for this validation (default: :acceptance)
|
117
113
|
def self.validates_acceptance_of(*atts)
|
118
114
|
opts = {
|
119
115
|
:message => 'is not accepted',
|
120
116
|
:allow_nil => true,
|
121
|
-
:accept => '1'
|
117
|
+
:accept => '1',
|
118
|
+
:tag => :acceptance,
|
122
119
|
}.merge!(atts.extract_options!)
|
123
|
-
|
124
|
-
atts << {:if=>opts[:if], :tag=>opts[:tag]||:acceptance}
|
120
|
+
atts << opts
|
125
121
|
validates_each(*atts) do |o, a, v|
|
126
|
-
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
127
122
|
o.errors[a] << opts[:message] unless v == opts[:accept]
|
128
123
|
end
|
129
124
|
end
|
@@ -137,22 +132,15 @@ module Sequel
|
|
137
132
|
# or email addresses on web forms.
|
138
133
|
#
|
139
134
|
# Possible Options:
|
140
|
-
# * :allow_false - Whether to skip the validation if the value is blank (default: false)
|
141
|
-
# * :allow_nil - Whether to skip the validation if the value is nil (default: false)
|
142
|
-
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
143
|
-
# skipping this validation if it returns nil or false.
|
144
135
|
# * :message - The message to use (default: 'is not confirmed')
|
145
|
-
# * :tag - The tag to use for this validation (default: :confirmation)
|
146
136
|
def self.validates_confirmation_of(*atts)
|
147
137
|
opts = {
|
148
138
|
:message => 'is not confirmed',
|
139
|
+
:tag => :confirmation,
|
149
140
|
}.merge!(atts.extract_options!)
|
150
|
-
|
151
|
-
atts << {:if=>opts[:if], :tag=>opts[:tag]||:confirmation}
|
141
|
+
atts << opts
|
152
142
|
validates_each(*atts) do |o, a, v|
|
153
|
-
|
154
|
-
c = o.send(:"#{a}_confirmation")
|
155
|
-
o.errors[a] << opts[:message] unless v == c
|
143
|
+
o.errors[a] << opts[:message] unless v == o.send(:"#{a}_confirmation")
|
156
144
|
end
|
157
145
|
end
|
158
146
|
|
@@ -165,13 +153,30 @@ module Sequel
|
|
165
153
|
# end
|
166
154
|
#
|
167
155
|
# Possible Options:
|
156
|
+
# * :allow_blank - Whether to skip the validation if the value is blank.
|
157
|
+
# * :allow_missing - Whether to skip the validation if the attribute isn't a key in the
|
158
|
+
# values hash. This is different from allow_nil, because Sequel only sends the attributes
|
159
|
+
# in the values when doing an insert or update. If the attribute is not present, Sequel
|
160
|
+
# doesn't specify it, so the database will use the table's default value. This is different
|
161
|
+
# from having an attribute in values with a value of nil, which Sequel will send as NULL.
|
162
|
+
# If your database table has a non NULL default, this may be a good option to use. You
|
163
|
+
# don't want to use allow_nil, because if the attribute is in values but has a value nil,
|
164
|
+
# Sequel will attempt to insert a NULL value into the database, instead of using the
|
165
|
+
# database's default.
|
166
|
+
# * :allow_nil - Whether to skip the validation if the value is nil.
|
168
167
|
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
169
168
|
# skipping this validation if it returns nil or false.
|
170
|
-
# * :tag - The tag to use for this validation
|
169
|
+
# * :tag - The tag to use for this validation.
|
171
170
|
def self.validates_each(*atts, &block)
|
172
171
|
opts = atts.extract_options!
|
173
|
-
blk = if opts[:if]
|
174
|
-
proc
|
172
|
+
blk = if (i = opts[:if]) || (am = opts[:allow_missing]) || (an = opts[:allow_nil]) || (ab = opts[:allow_blank])
|
173
|
+
proc do |o,a,v|
|
174
|
+
next if i && !o.instance_eval(&if_proc(opts))
|
175
|
+
next if an && Array(v).all?{|x| x.nil?}
|
176
|
+
next if ab && Array(v).all?{|x| x.blank?}
|
177
|
+
next if am && Array(a).all?{|x| !o.values.has_key?(x)}
|
178
|
+
block.call(o,a,v)
|
179
|
+
end
|
175
180
|
else
|
176
181
|
block
|
177
182
|
end
|
@@ -190,25 +195,20 @@ module Sequel
|
|
190
195
|
# value against the regular expression provided by the :with option.
|
191
196
|
#
|
192
197
|
# Possible Options:
|
193
|
-
# * :allow_blank - Whether to skip the validation if the value is blank (default: false)
|
194
|
-
# * :allow_nil - Whether to skip the validation if the value is nil (default: false)
|
195
|
-
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
196
|
-
# skipping this validation if it returns nil or false.
|
197
198
|
# * :message - The message to use (default: 'is invalid')
|
198
|
-
# * :tag - The tag to use for this validation (default: :format)
|
199
199
|
# * :with - The regular expression to validate the value with (required).
|
200
200
|
def self.validates_format_of(*atts)
|
201
201
|
opts = {
|
202
202
|
:message => 'is invalid',
|
203
|
+
:tag => :format,
|
203
204
|
}.merge!(atts.extract_options!)
|
204
205
|
|
205
206
|
unless opts[:with].is_a?(Regexp)
|
206
207
|
raise ArgumentError, "A regular expression must be supplied as the :with option of the options hash"
|
207
208
|
end
|
208
209
|
|
209
|
-
atts <<
|
210
|
+
atts << opts
|
210
211
|
validates_each(*atts) do |o, a, v|
|
211
|
-
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
212
212
|
o.errors[a] << opts[:message] unless v.to_s =~ opts[:with]
|
213
213
|
end
|
214
214
|
end
|
@@ -216,16 +216,11 @@ module Sequel
|
|
216
216
|
# Validates the length of an attribute.
|
217
217
|
#
|
218
218
|
# Possible Options:
|
219
|
-
# * :allow_blank - Whether to skip the validation if the value is blank (default: false)
|
220
|
-
# * :allow_nil - Whether to skip the validation if the value is nil (default: false)
|
221
|
-
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
222
|
-
# skipping this validation if it returns nil or false.
|
223
219
|
# * :is - The exact size required for the value to be valid (no default)
|
224
220
|
# * :maximum - The maximum size allowed for the value (no default)
|
225
221
|
# * :message - The message to use (no default, overrides :too_long, :too_short, and :wrong_length
|
226
222
|
# options if present)
|
227
223
|
# * :minimum - The minimum size allowed for the value (no default)
|
228
|
-
# * :tag - The tag to use for this validation (default: :length)
|
229
224
|
# * :too_long - The message to use use if it the value is too long (default: 'is too long')
|
230
225
|
# * :too_short - The message to use use if it the value is too short (default: 'is too short')
|
231
226
|
# * :with - The array/range that must include the size of the value for it to be valid (no default)
|
@@ -237,14 +232,9 @@ module Sequel
|
|
237
232
|
:wrong_length => 'is the wrong length'
|
238
233
|
}.merge!(atts.extract_options!)
|
239
234
|
|
240
|
-
tag
|
241
|
-
|
242
|
-
else
|
243
|
-
([:length] + [:maximum, :minimum, :is, :within].reject{|x| !opts.include?(x)}).join('-').to_sym
|
244
|
-
end
|
245
|
-
atts << {:if=>opts[:if], :tag=>tag}
|
235
|
+
opts[:tag] ||= ([:length] + [:maximum, :minimum, :is, :within].reject{|x| !opts.include?(x)}).join('-').to_sym
|
236
|
+
atts << opts
|
246
237
|
validates_each(*atts) do |o, a, v|
|
247
|
-
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
248
238
|
if m = opts[:maximum]
|
249
239
|
o.errors[a] << (opts[:message] || opts[:too_long]) unless v && v.size <= m
|
250
240
|
end
|
@@ -260,24 +250,46 @@ module Sequel
|
|
260
250
|
end
|
261
251
|
end
|
262
252
|
|
253
|
+
# Validates whether an attribute is not a string. This is generally useful
|
254
|
+
# in conjunction with raise_on_typecast_failure = false, where you are
|
255
|
+
# passing in string values for non-string attributes (such as numbers and dates).
|
256
|
+
# If typecasting fails (invalid number or date), the value of the attribute will
|
257
|
+
# be a string in an invalid format, and if typecasting succeeds, the value will
|
258
|
+
# not be a string.
|
259
|
+
#
|
260
|
+
# Possible Options:
|
261
|
+
# * :message - The message to use (default: 'is a string' or 'is not a valid (integer|datetime|etc.)' if the type is known)
|
262
|
+
def self.validates_not_string(*atts)
|
263
|
+
opts = {
|
264
|
+
:tag => :not_string,
|
265
|
+
}.merge!(atts.extract_options!)
|
266
|
+
atts << opts
|
267
|
+
validates_each(*atts) do |o, a, v|
|
268
|
+
if v.is_a?(String)
|
269
|
+
unless message = opts[:message]
|
270
|
+
message = if sch = o.db_schema[a] and typ = sch[:type]
|
271
|
+
"is not a valid #{typ}"
|
272
|
+
else
|
273
|
+
"is a string"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
o.errors[a] << message
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
263
281
|
# Validates whether an attribute is a number.
|
264
282
|
#
|
265
283
|
# Possible Options:
|
266
|
-
# * :allow_blank - Whether to skip the validation if the value is blank (default: false)
|
267
|
-
# * :allow_nil - Whether to skip the validation if the value is nil (default: false)
|
268
|
-
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
269
|
-
# skipping this validation if it returns nil or false.
|
270
284
|
# * :message - The message to use (default: 'is not a number')
|
271
|
-
# * :tag - The tag to use for this validation (default: :numericality)
|
272
285
|
# * :only_integer - Whether only integers are valid values (default: false)
|
273
286
|
def self.validates_numericality_of(*atts)
|
274
287
|
opts = {
|
275
288
|
:message => 'is not a number',
|
289
|
+
:tag => :numericality,
|
276
290
|
}.merge!(atts.extract_options!)
|
277
|
-
|
278
|
-
atts << {:if=>opts[:if], :tag=>opts[:tag]||:numericality}
|
291
|
+
atts << opts
|
279
292
|
validates_each(*atts) do |o, a, v|
|
280
|
-
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
281
293
|
begin
|
282
294
|
if opts[:only_integer]
|
283
295
|
Kernel.Integer(v.to_s)
|
@@ -294,16 +306,13 @@ module Sequel
|
|
294
306
|
# with false considered present instead of absent.
|
295
307
|
#
|
296
308
|
# Possible Options:
|
297
|
-
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
298
|
-
# skipping this validation if it returns nil or false.
|
299
309
|
# * :message - The message to use (default: 'is not present')
|
300
|
-
# * :tag - The tag to use for this validation (default: :presence)
|
301
310
|
def self.validates_presence_of(*atts)
|
302
311
|
opts = {
|
303
312
|
:message => 'is not present',
|
313
|
+
:tag => :presence,
|
304
314
|
}.merge!(atts.extract_options!)
|
305
|
-
|
306
|
-
atts << {:if=>opts[:if], :tag=>opts[:tag]||:presence}
|
315
|
+
atts << opts
|
307
316
|
validates_each(*atts) do |o, a, v|
|
308
317
|
o.errors[a] << opts[:message] if v.blank? && v != false
|
309
318
|
end
|
@@ -314,26 +323,28 @@ module Sequel
|
|
314
323
|
# fields to specify that the combination of fields must be unique,
|
315
324
|
# instead of that each field should have a unique value.
|
316
325
|
#
|
326
|
+
# This means that the code:
|
327
|
+
# validates_uniqueness_of([:column1, :column2])
|
328
|
+
# validates the grouping of column1 and column2 while
|
329
|
+
# validates_uniqueness_of(:column1, :column2)
|
330
|
+
# validates them separately.
|
331
|
+
#
|
317
332
|
# You should also add a unique index in the
|
318
333
|
# database, as this suffers from a fairly obvious race condition.
|
319
334
|
#
|
320
335
|
# Possible Options:
|
321
|
-
# * :allow_nil - Whether to skip the validation if the value(s) is/are nil (default: false)
|
322
|
-
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
323
|
-
# skipping this validation if it returns nil or false.
|
324
336
|
# * :message - The message to use (default: 'is already taken')
|
325
|
-
# * :tag - The tag to use for this validation (default: :uniqueness)
|
326
337
|
def self.validates_uniqueness_of(*atts)
|
327
338
|
opts = {
|
328
339
|
:message => 'is already taken',
|
340
|
+
:tag => :uniqueness,
|
329
341
|
}.merge!(atts.extract_options!)
|
330
342
|
|
331
|
-
atts <<
|
343
|
+
atts << opts
|
332
344
|
validates_each(*atts) do |o, a, v|
|
333
345
|
error_field = a
|
334
346
|
a = Array(a)
|
335
347
|
v = Array(v)
|
336
|
-
next unless v.any? or opts[:allow_nil] == false
|
337
348
|
ds = o.class.filter(a.zip(v))
|
338
349
|
num_dups = ds.count
|
339
350
|
allow = if num_dups == 0
|
@@ -0,0 +1,371 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper.rb')
|
2
|
+
|
3
|
+
unless defined?(FIREBIRD_DB)
|
4
|
+
FIREBIRD_URL = 'firebird://sysdba:masterkey@localhost/reality_spec' unless defined? FIREBIRD_URL
|
5
|
+
FIREBIRD_DB = Sequel.connect(ENV['SEQUEL_FB_SPEC_DB']||FIREBIRD_URL)
|
6
|
+
end
|
7
|
+
|
8
|
+
FIREBIRD_DB.create_table! :test do
|
9
|
+
varchar :name, :size => 50
|
10
|
+
integer :val, :index => true
|
11
|
+
end
|
12
|
+
|
13
|
+
FIREBIRD_DB.create_table! :test2 do
|
14
|
+
integer :val
|
15
|
+
timestamp :time_stamp
|
16
|
+
end
|
17
|
+
|
18
|
+
FIREBIRD_DB.create_table! :test3 do
|
19
|
+
integer :val
|
20
|
+
timestamp :time_stamp
|
21
|
+
end
|
22
|
+
|
23
|
+
FIREBIRD_DB.create_table! :test5 do
|
24
|
+
primary_key :xid
|
25
|
+
integer :val
|
26
|
+
end
|
27
|
+
|
28
|
+
context "A Firebird database" do
|
29
|
+
setup do
|
30
|
+
@db = FIREBIRD_DB
|
31
|
+
end
|
32
|
+
|
33
|
+
specify "should provide disconnect functionality" do
|
34
|
+
@db.tables
|
35
|
+
@db.pool.size.should == 1
|
36
|
+
@db.disconnect
|
37
|
+
@db.pool.size.should == 0
|
38
|
+
end
|
39
|
+
|
40
|
+
specify "should raise Sequel::Error on error" do
|
41
|
+
proc{@db << "SELECT 1 + 'a'"}.should raise_error(Sequel::Error)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
context "A Firebird dataset" do
|
46
|
+
setup do
|
47
|
+
@d = FIREBIRD_DB[:test]
|
48
|
+
@d.delete # remove all records
|
49
|
+
end
|
50
|
+
|
51
|
+
specify "should return the correct record count" do
|
52
|
+
@d.count.should == 0
|
53
|
+
@d << {:name => 'abc', :val => 123}
|
54
|
+
@d << {:name => 'abc', :val => 456}
|
55
|
+
@d << {:name => 'def', :val => 789}
|
56
|
+
@d.count.should == 3
|
57
|
+
end
|
58
|
+
|
59
|
+
specify "should return the correct records" do
|
60
|
+
@d.to_a.should == []
|
61
|
+
@d << {:name => 'abc', :val => 123}
|
62
|
+
@d << {:name => 'abc', :val => 456}
|
63
|
+
@d << {:name => 'def', :val => 789}
|
64
|
+
|
65
|
+
@d.order(:val).to_a.should == [
|
66
|
+
{:name => 'abc', :val => 123},
|
67
|
+
{:name => 'abc', :val => 456},
|
68
|
+
{:name => 'def', :val => 789}
|
69
|
+
]
|
70
|
+
end
|
71
|
+
|
72
|
+
specify "should update records correctly" do
|
73
|
+
@d << {:name => 'abc', :val => 123}
|
74
|
+
@d << {:name => 'abc', :val => 456}
|
75
|
+
@d << {:name => 'def', :val => 789}
|
76
|
+
@d.filter(:name => 'abc').update(:val => 530)
|
77
|
+
|
78
|
+
# the third record should stay the same
|
79
|
+
# floating-point precision bullshit
|
80
|
+
@d[:name => 'def'][:val].should == 789
|
81
|
+
@d.filter(:val => 530).count.should == 2
|
82
|
+
end
|
83
|
+
|
84
|
+
specify "should delete records correctly" do
|
85
|
+
@d << {:name => 'abc', :val => 123}
|
86
|
+
@d << {:name => 'abc', :val => 456}
|
87
|
+
@d << {:name => 'def', :val => 789}
|
88
|
+
@d.filter(:name => 'abc').delete
|
89
|
+
|
90
|
+
@d.count.should == 1
|
91
|
+
@d.first[:name].should == 'def'
|
92
|
+
end
|
93
|
+
|
94
|
+
specify "should be able to literalize booleans" do
|
95
|
+
proc {@d.literal(true)}.should_not raise_error
|
96
|
+
proc {@d.literal(false)}.should_not raise_error
|
97
|
+
end
|
98
|
+
|
99
|
+
specify "should quote columns and tables using double quotes if quoting identifiers" do
|
100
|
+
@d.quote_identifiers = true
|
101
|
+
@d.select(:name).sql.should == \
|
102
|
+
'SELECT "NAME" FROM "TEST"'
|
103
|
+
|
104
|
+
@d.select('COUNT(*)'.lit).sql.should == \
|
105
|
+
'SELECT COUNT(*) FROM "TEST"'
|
106
|
+
|
107
|
+
@d.select(:max[:val]).sql.should == \
|
108
|
+
'SELECT max("VAL") FROM "TEST"'
|
109
|
+
|
110
|
+
@d.select(:now[]).sql.should == \
|
111
|
+
'SELECT now() FROM "TEST"'
|
112
|
+
|
113
|
+
@d.select(:max[:items__val]).sql.should == \
|
114
|
+
'SELECT max("ITEMS"."VAL") FROM "TEST"'
|
115
|
+
|
116
|
+
@d.order(:name.desc).sql.should == \
|
117
|
+
'SELECT * FROM "TEST" ORDER BY "NAME" DESC'
|
118
|
+
|
119
|
+
@d.select('TEST.NAME AS item_:name'.lit).sql.should == \
|
120
|
+
'SELECT TEST.NAME AS item_:name FROM "TEST"'
|
121
|
+
|
122
|
+
@d.select('"NAME"'.lit).sql.should == \
|
123
|
+
'SELECT "NAME" FROM "TEST"'
|
124
|
+
|
125
|
+
@d.select('max(TEST."NAME") AS "max_:name"'.lit).sql.should == \
|
126
|
+
'SELECT max(TEST."NAME") AS "max_:name" FROM "TEST"'
|
127
|
+
|
128
|
+
@d.select(:test[:ABC, 'hello']).sql.should == \
|
129
|
+
"SELECT test(\"ABC\", 'hello') FROM \"TEST\""
|
130
|
+
|
131
|
+
@d.select(:test[:ABC__DEF, 'hello']).sql.should == \
|
132
|
+
"SELECT test(\"ABC\".\"DEF\", 'hello') FROM \"TEST\""
|
133
|
+
|
134
|
+
@d.select(:test[:ABC__DEF, 'hello'].as(:X2)).sql.should == \
|
135
|
+
"SELECT test(\"ABC\".\"DEF\", 'hello') AS \"X2\" FROM \"TEST\""
|
136
|
+
|
137
|
+
@d.insert_sql(:val => 333).should =~ \
|
138
|
+
/\AINSERT INTO "TEST" \("VAL"\) VALUES \(333\)( RETURNING NULL)?\z/
|
139
|
+
|
140
|
+
@d.insert_sql(:X => :Y).should =~ \
|
141
|
+
/\AINSERT INTO "TEST" \("X"\) VALUES \("Y"\)( RETURNING NULL)?\z/
|
142
|
+
end
|
143
|
+
|
144
|
+
specify "should quote fields correctly when reversing the order if quoting identifiers" do
|
145
|
+
@d.quote_identifiers = true
|
146
|
+
@d.reverse_order(:name).sql.should == \
|
147
|
+
'SELECT * FROM "TEST" ORDER BY "NAME" DESC'
|
148
|
+
|
149
|
+
@d.reverse_order(:name.desc).sql.should == \
|
150
|
+
'SELECT * FROM "TEST" ORDER BY "NAME" ASC'
|
151
|
+
|
152
|
+
@d.reverse_order(:name, :test.desc).sql.should == \
|
153
|
+
'SELECT * FROM "TEST" ORDER BY "NAME" DESC, "TEST" ASC'
|
154
|
+
|
155
|
+
@d.reverse_order(:name.desc, :test).sql.should == \
|
156
|
+
'SELECT * FROM "TEST" ORDER BY "NAME" ASC, "TEST" DESC'
|
157
|
+
end
|
158
|
+
|
159
|
+
specify "should support transactions" do
|
160
|
+
FIREBIRD_DB.transaction do
|
161
|
+
@d << {:name => 'abc', :val => 1}
|
162
|
+
end
|
163
|
+
|
164
|
+
@d.count.should == 1
|
165
|
+
end
|
166
|
+
|
167
|
+
specify "should have #transaction yield the connection" do
|
168
|
+
FIREBIRD_DB.transaction do |conn|
|
169
|
+
conn.should_not == nil
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
specify "should correctly rollback transactions" do
|
174
|
+
proc do
|
175
|
+
FIREBIRD_DB.transaction do
|
176
|
+
@d << {:name => 'abc', :val => 1}
|
177
|
+
raise RuntimeError, 'asdf'
|
178
|
+
end
|
179
|
+
end.should raise_error(RuntimeError)
|
180
|
+
|
181
|
+
@d.count.should == 0
|
182
|
+
end
|
183
|
+
|
184
|
+
specify "should handle returning inside of the block by committing" do
|
185
|
+
def FIREBIRD_DB.ret_commit
|
186
|
+
transaction do
|
187
|
+
self[:test] << {:name => 'abc'}
|
188
|
+
return
|
189
|
+
self[:test] << {:name => 'd'}
|
190
|
+
end
|
191
|
+
end
|
192
|
+
@d.count.should == 0
|
193
|
+
FIREBIRD_DB.ret_commit
|
194
|
+
@d.count.should == 1
|
195
|
+
FIREBIRD_DB.ret_commit
|
196
|
+
@d.count.should == 2
|
197
|
+
proc do
|
198
|
+
FIREBIRD_DB.transaction do
|
199
|
+
raise RuntimeError, 'asdf'
|
200
|
+
end
|
201
|
+
end.should raise_error(RuntimeError)
|
202
|
+
|
203
|
+
@d.count.should == 2
|
204
|
+
end
|
205
|
+
|
206
|
+
specify "should quote and upcase reserved keywords" do
|
207
|
+
@d = FIREBIRD_DB[:testing]
|
208
|
+
@d.quote_identifiers = true
|
209
|
+
@d.select(:select).sql.should == \
|
210
|
+
'SELECT "SELECT" FROM "TESTING"'
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
context "A Firebird dataset with a timestamp field" do
|
215
|
+
setup do
|
216
|
+
@d = FIREBIRD_DB[:test3]
|
217
|
+
@d.delete
|
218
|
+
end
|
219
|
+
|
220
|
+
specify "should store milliseconds in time fields" do
|
221
|
+
t = Time.now
|
222
|
+
@d << {:val=>1, :time_stamp=>t}
|
223
|
+
@d.literal(@d[:val =>'1'][:time_stamp]).should == @d.literal(t)
|
224
|
+
@d[:val=>'1'][:time_stamp].usec.should == t.usec - t.usec % 100
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
context "A Firebird database" do
|
229
|
+
setup do
|
230
|
+
@db = FIREBIRD_DB
|
231
|
+
end
|
232
|
+
|
233
|
+
specify "should allow us to name the sequences" do
|
234
|
+
g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
|
235
|
+
primary_key :id, :sequence_name => "seq_test"
|
236
|
+
end
|
237
|
+
FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
|
238
|
+
"CREATE TABLE POSTS (ID integer PRIMARY KEY )",
|
239
|
+
"CREATE SEQUENCE SEQ_TEST",
|
240
|
+
" CREATE TRIGGER BI_POSTS_ID for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_test;\n end\n end\n\n"
|
241
|
+
], "DROP SEQUENCE SEQ_TEST" ]
|
242
|
+
end
|
243
|
+
|
244
|
+
specify "should allow us to set the starting position for the sequences" do
|
245
|
+
g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
|
246
|
+
primary_key :id, :sequence_start_position => 999
|
247
|
+
end
|
248
|
+
FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
|
249
|
+
"CREATE TABLE POSTS (ID integer PRIMARY KEY )",
|
250
|
+
"CREATE SEQUENCE SEQ_POSTS_ID",
|
251
|
+
"ALTER SEQUENCE SEQ_POSTS_ID RESTART WITH 999",
|
252
|
+
" CREATE TRIGGER BI_POSTS_ID for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_posts_id;\n end\n end\n\n"
|
253
|
+
], "DROP SEQUENCE SEQ_POSTS_ID" ]
|
254
|
+
end
|
255
|
+
|
256
|
+
specify "should allow us to name and set the starting position for the sequences" do
|
257
|
+
g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
|
258
|
+
primary_key :id, :sequence_name => "seq_test", :sequence_start_position => 999
|
259
|
+
end
|
260
|
+
FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
|
261
|
+
"CREATE TABLE POSTS (ID integer PRIMARY KEY )",
|
262
|
+
"CREATE SEQUENCE SEQ_TEST",
|
263
|
+
"ALTER SEQUENCE SEQ_TEST RESTART WITH 999",
|
264
|
+
" CREATE TRIGGER BI_POSTS_ID for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_test;\n end\n end\n\n"
|
265
|
+
], "DROP SEQUENCE SEQ_TEST" ]
|
266
|
+
end
|
267
|
+
|
268
|
+
specify "should allow us to name the triggers" do
|
269
|
+
g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
|
270
|
+
primary_key :id, :trigger_name => "trig_test"
|
271
|
+
end
|
272
|
+
FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
|
273
|
+
"CREATE TABLE POSTS (ID integer PRIMARY KEY )",
|
274
|
+
"CREATE SEQUENCE SEQ_POSTS_ID",
|
275
|
+
" CREATE TRIGGER TRIG_TEST for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_posts_id;\n end\n end\n\n"
|
276
|
+
], "DROP SEQUENCE SEQ_POSTS_ID" ]
|
277
|
+
end
|
278
|
+
|
279
|
+
specify "should allow us to not create the sequence" do
|
280
|
+
g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
|
281
|
+
primary_key :id, :create_sequence => false
|
282
|
+
end
|
283
|
+
FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
|
284
|
+
"CREATE TABLE POSTS (ID integer PRIMARY KEY )",
|
285
|
+
" CREATE TRIGGER BI_POSTS_ID for POSTS\n ACTIVE BEFORE INSERT position 0\n as begin\n if ((new.ID is null) or (new.ID = 0)) then\n begin\n new.ID = next value for seq_posts_id;\n end\n end\n\n"
|
286
|
+
], nil]
|
287
|
+
end
|
288
|
+
|
289
|
+
specify "should allow us to not create the trigger" do
|
290
|
+
g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
|
291
|
+
primary_key :id, :create_trigger => false
|
292
|
+
end
|
293
|
+
FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
|
294
|
+
"CREATE TABLE POSTS (ID integer PRIMARY KEY )",
|
295
|
+
"CREATE SEQUENCE SEQ_POSTS_ID",
|
296
|
+
], "DROP SEQUENCE SEQ_POSTS_ID"]
|
297
|
+
end
|
298
|
+
|
299
|
+
specify "should allow us to not create either the sequence nor the trigger" do
|
300
|
+
g = Sequel::Schema::Generator.new(FIREBIRD_DB) do
|
301
|
+
primary_key :id, :create_sequence => false, :create_trigger => false
|
302
|
+
end
|
303
|
+
FIREBIRD_DB.create_table_sql_list(:posts, *g.create_info).should == [[
|
304
|
+
"CREATE TABLE POSTS (ID integer PRIMARY KEY )"
|
305
|
+
], nil]
|
306
|
+
end
|
307
|
+
|
308
|
+
specify "should support column operations" do
|
309
|
+
@db.create_table!(:test2){varchar :name, :size => 50; integer :val}
|
310
|
+
@db[:test2] << {}
|
311
|
+
@db[:test2].columns.should == [:name, :val]
|
312
|
+
|
313
|
+
@db.add_column :test2, :xyz, :varchar, :size => 50
|
314
|
+
@db[:test2].columns.should == [:name, :val, :xyz]
|
315
|
+
|
316
|
+
@db[:test2].columns.should == [:name, :val, :xyz]
|
317
|
+
@db.drop_column :test2, :xyz
|
318
|
+
|
319
|
+
@db[:test2].columns.should == [:name, :val]
|
320
|
+
|
321
|
+
@db[:test2].delete
|
322
|
+
@db.add_column :test2, :xyz, :varchar, :default => '000', :size => 50#, :create_domain => 'xyz_varchar'
|
323
|
+
@db[:test2] << {:name => 'mmm', :val => 111, :xyz => 'qqqq'}
|
324
|
+
|
325
|
+
@db[:test2].columns.should == [:name, :val, :xyz]
|
326
|
+
@db.rename_column :test2, :xyz, :zyx
|
327
|
+
@db[:test2].columns.should == [:name, :val, :zyx]
|
328
|
+
@db[:test2].first[:zyx].should == 'qqqq'
|
329
|
+
|
330
|
+
@db.add_column :test2, :xyz, :decimal, :elements => [12, 2]
|
331
|
+
@db[:test2].delete
|
332
|
+
@db[:test2] << {:name => 'mmm', :val => 111, :xyz => 56.4}
|
333
|
+
@db.set_column_type :test2, :xyz, :varchar, :size => 50
|
334
|
+
|
335
|
+
@db[:test2].first[:xyz].should == "56.40"
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
context "Postgres::Dataset#insert" do
|
340
|
+
setup do
|
341
|
+
@ds = FIREBIRD_DB[:test5]
|
342
|
+
@ds.delete
|
343
|
+
end
|
344
|
+
|
345
|
+
specify "should using call insert_returning_sql" do
|
346
|
+
@ds.should_receive(:single_value).once.with(:sql=>'INSERT INTO TEST5 (VAL) VALUES (10) RETURNING XID')
|
347
|
+
@ds.insert(:val=>10)
|
348
|
+
end
|
349
|
+
|
350
|
+
specify "should have insert_returning_sql use the RETURNING keyword" do
|
351
|
+
@ds.insert_returning_sql(:XID, :val=>10).should == "INSERT INTO TEST5 (VAL) VALUES (10) RETURNING XID"
|
352
|
+
@ds.insert_returning_sql('*'.lit, :val=>10).should == "INSERT INTO TEST5 (VAL) VALUES (10) RETURNING *"
|
353
|
+
@ds.insert_returning_sql('NULL'.lit, :val=>10).should == "INSERT INTO TEST5 (VAL) VALUES (10) RETURNING NULL"
|
354
|
+
end
|
355
|
+
|
356
|
+
specify "should correctly return the inserted record's primary key value" do
|
357
|
+
value1 = 10
|
358
|
+
id1 = @ds.insert(:val=>value1)
|
359
|
+
@ds.first(:XID=>id1)[:val].should == value1
|
360
|
+
value2 = 20
|
361
|
+
id2 = @ds.insert(:val=>value2)
|
362
|
+
@ds.first(:XID=>id2)[:val].should == value2
|
363
|
+
end
|
364
|
+
|
365
|
+
specify "should return nil if the table has no primary key" do
|
366
|
+
ds = FIREBIRD_DB[:test]
|
367
|
+
ds.delete
|
368
|
+
ds.insert(:name=>'a').should == nil
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|