sequel 2.1.0 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +37 -1
- data/Rakefile +2 -2
- data/doc/advanced_associations.rdoc +624 -0
- data/lib/sequel_model.rb +7 -2
- data/lib/sequel_model/association_reflection.rb +112 -2
- data/lib/sequel_model/associations.rb +171 -222
- data/lib/sequel_model/base.rb +28 -7
- data/lib/sequel_model/eager_loading.rb +12 -71
- data/lib/sequel_model/record.rb +143 -14
- data/lib/sequel_model/validations.rb +108 -17
- data/spec/association_reflection_spec.rb +10 -2
- data/spec/associations_spec.rb +449 -94
- data/spec/caching_spec.rb +72 -11
- data/spec/eager_loading_spec.rb +168 -21
- data/spec/hooks_spec.rb +53 -15
- data/spec/model_spec.rb +12 -11
- data/spec/spec_helper.rb +0 -7
- data/spec/validations_spec.rb +39 -16
- metadata +6 -4
data/lib/sequel_model/base.rb
CHANGED
@@ -8,6 +8,7 @@ module Sequel
|
|
8
8
|
@allowed_columns = nil
|
9
9
|
@dataset_methods = {}
|
10
10
|
@primary_key = :id
|
11
|
+
@raise_on_save_failure = true
|
11
12
|
@restrict_primary_key = true
|
12
13
|
@restricted_columns = nil
|
13
14
|
@sti_dataset = nil
|
@@ -26,6 +27,10 @@ module Sequel
|
|
26
27
|
# The default primary key for classes (default: :id)
|
27
28
|
metaattr_reader :primary_key
|
28
29
|
|
30
|
+
# Whether to raise an error instead of returning nil on a failure
|
31
|
+
# to save/create/save_changes/etc.
|
32
|
+
metaattr_accessor :raise_on_save_failure
|
33
|
+
|
29
34
|
# Which columns should not be update in a call to set
|
30
35
|
# (default: no columns).
|
31
36
|
metaattr_reader :restricted_columns
|
@@ -52,12 +57,13 @@ module Sequel
|
|
52
57
|
insert_multiple intersect interval invert_order join join_table last
|
53
58
|
left_outer_join limit map multi_insert naked order order_by order_more
|
54
59
|
paginate print query range reverse_order right_outer_join select
|
55
|
-
select_all select_more set set_graph_aliases single_value size to_csv
|
56
|
-
transform union uniq unordered update where'.map{|x| x.to_sym}
|
60
|
+
select_all select_more set set_graph_aliases single_value size to_csv to_hash
|
61
|
+
transform union uniq unfiltered unordered update where'.map{|x| x.to_sym}
|
57
62
|
|
58
63
|
# Instance variables that are inherited in subclasses
|
59
|
-
INHERITED_INSTANCE_VARIABLES = {:@allowed_columns=>:dup, :@
|
60
|
-
:@
|
64
|
+
INHERITED_INSTANCE_VARIABLES = {:@allowed_columns=>:dup, :@cache_store=>nil,
|
65
|
+
:@cache_ttl=>nil, :@dataset_methods=>:dup, :@primary_key=>nil,
|
66
|
+
:@raise_on_save_failure=>nil, :@restricted_columns=>:dup, :@restrict_primary_key=>nil,
|
61
67
|
:@sti_dataset=>nil, :@sti_key=>nil, :@strict_param_setting=>nil,
|
62
68
|
:@typecast_on_assignment=>nil}
|
63
69
|
|
@@ -83,7 +89,7 @@ module Sequel
|
|
83
89
|
# dataset) it will use Dataset#columns to find the columns, which
|
84
90
|
# may be empty if the Dataset has no records.
|
85
91
|
def self.columns
|
86
|
-
@columns || set_columns(dataset.naked.columns
|
92
|
+
@columns || set_columns(dataset.naked.columns)
|
87
93
|
end
|
88
94
|
|
89
95
|
# Creates new instance with values set to passed-in Hash, saves it
|
@@ -92,7 +98,7 @@ module Sequel
|
|
92
98
|
# returns false.
|
93
99
|
def self.create(values = {}, &block)
|
94
100
|
obj = new(values, &block)
|
95
|
-
return
|
101
|
+
return unless obj.save
|
96
102
|
obj
|
97
103
|
end
|
98
104
|
|
@@ -155,6 +161,17 @@ module Sequel
|
|
155
161
|
db.fetch(*args).set_model(self)
|
156
162
|
end
|
157
163
|
|
164
|
+
# Modify and return eager loading dataset based on association options
|
165
|
+
def self.eager_loading_dataset(opts, ds, select, associations)
|
166
|
+
ds = ds.select(*select) if select
|
167
|
+
ds = ds.order(*opts[:order]) if opts[:order]
|
168
|
+
ds = ds.eager(opts[:eager]) if opts[:eager]
|
169
|
+
ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph]
|
170
|
+
ds = ds.eager(associations) unless associations.blank?
|
171
|
+
ds = opts[:eager_block].call(ds) if opts[:eager_block]
|
172
|
+
ds
|
173
|
+
end
|
174
|
+
|
158
175
|
# Finds a single record according to the supplied filter, e.g.:
|
159
176
|
#
|
160
177
|
# Ticket.find :author => 'Sharon' # => record
|
@@ -441,7 +458,11 @@ module Sequel
|
|
441
458
|
# Dataset is for a single table with all columns,
|
442
459
|
# so set the columns based on the order they were
|
443
460
|
# returned by the schema.
|
444
|
-
|
461
|
+
cols = schema_array.collect{|k,v| k}
|
462
|
+
set_columns(cols)
|
463
|
+
# Also set the columns for the dataset, so the dataset
|
464
|
+
# doesn't have to do a query to get them.
|
465
|
+
dataset.instance_variable_set(:@columns, cols)
|
445
466
|
end
|
446
467
|
else
|
447
468
|
# If the dataset uses multiple tables or custom sql or getting
|
@@ -56,8 +56,8 @@ module Sequel::Model::Associations::EagerLoading
|
|
56
56
|
# or join the tables you need to filter on manually.
|
57
57
|
#
|
58
58
|
# Each association's order, if definied, is respected. Eager also works
|
59
|
-
# on a limited dataset
|
60
|
-
# argument, it is used.
|
59
|
+
# on a limited dataset, but does not use any :limit options for associations.
|
60
|
+
# If the association uses a block or has an :eager_block argument, it is used.
|
61
61
|
def eager(*associations)
|
62
62
|
model = check_model
|
63
63
|
opt = @opts[:eager]
|
@@ -90,7 +90,7 @@ module Sequel::Model::Associations::EagerLoading
|
|
90
90
|
# This does not respect each association's order, as all associations are loaded in
|
91
91
|
# a single query. If you want to order the results, you must manually call .order.
|
92
92
|
#
|
93
|
-
# #eager_graph probably won't work
|
93
|
+
# #eager_graph probably won't work correctly on a limited dataset, unless you are
|
94
94
|
# only graphing many_to_one associations.
|
95
95
|
#
|
96
96
|
# Does not use the block defined for the association, since it does a single query for
|
@@ -132,18 +132,22 @@ module Sequel::Model::Associations::EagerLoading
|
|
132
132
|
assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name)
|
133
133
|
join_type = r[:graph_join_type]
|
134
134
|
conditions = r[:graph_conditions]
|
135
|
+
use_only_conditions = r.include?(:graph_only_conditions)
|
136
|
+
only_conditions = r[:graph_only_conditions]
|
135
137
|
select = r[:graph_select]
|
138
|
+
graph_block = r[:graph_block]
|
136
139
|
ds = case assoc_type = r[:type]
|
137
140
|
when :many_to_one
|
138
|
-
ds.graph(klass, [[klass.primary_key,
|
141
|
+
ds.graph(klass, use_only_conditions ? only_conditions : [[klass.primary_key, r[:key].qualify(ta)]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type, &graph_block)
|
139
142
|
when :one_to_many
|
140
|
-
ds = ds.graph(klass, [[r[:key],
|
143
|
+
ds = ds.graph(klass, use_only_conditions ? only_conditions : [[r[:key], model.primary_key.qualify(ta)]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type, &graph_block)
|
141
144
|
# We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
|
142
145
|
ds.opts[:eager_graph][:reciprocals][assoc_table_alias] = r.reciprocal
|
143
146
|
ds
|
144
147
|
when :many_to_many
|
145
|
-
|
146
|
-
ds.graph(
|
148
|
+
use_jt_only_conditions = r.include?(:graph_join_table_only_conditions)
|
149
|
+
ds = ds.graph(r[:join_table], use_jt_only_conditions ? r[:graph_join_table_only_conditions] : [[r[:left_key], model.primary_key.qualify(ta)]] + r[:graph_join_table_conditions], :select=>false, :table_alias=>ds.eager_unique_table_alias(ds, r[:join_table]), :join_type=>r[:graph_join_table_join_type], &r[:graph_join_table_block])
|
150
|
+
ds.graph(klass, use_only_conditions ? only_conditions : [[klass.primary_key, r[:right_key]]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type, &graph_block)
|
147
151
|
end
|
148
152
|
eager_graph = ds.opts[:eager_graph]
|
149
153
|
eager_graph[:requirements][assoc_table_alias] = requirements.dup
|
@@ -358,70 +362,7 @@ module Sequel::Model::Associations::EagerLoading
|
|
358
362
|
end
|
359
363
|
end
|
360
364
|
|
361
|
-
|
362
|
-
# for the association for all model objects
|
363
|
-
reflections.each do |reflection|
|
364
|
-
assoc_class = reflection.associated_class
|
365
|
-
assoc_name = reflection[:name]
|
366
|
-
# Proc for setting cascaded eager loading
|
367
|
-
assoc_block = Proc.new do |d|
|
368
|
-
if order = reflection[:order]
|
369
|
-
d = d.order(*order)
|
370
|
-
end
|
371
|
-
if c = eager_assoc[assoc_name]
|
372
|
-
d = d.eager(c)
|
373
|
-
end
|
374
|
-
if c = reflection[:eager]
|
375
|
-
d = d.eager(c)
|
376
|
-
end
|
377
|
-
if b = reflection[:eager_block]
|
378
|
-
d = b.call(d)
|
379
|
-
end
|
380
|
-
d
|
381
|
-
end
|
382
|
-
case rtype = reflection[:type]
|
383
|
-
when :many_to_one
|
384
|
-
key = reflection[:key]
|
385
|
-
h = key_hash[key]
|
386
|
-
keys = h.keys
|
387
|
-
# No records have the foreign key set for this association, so skip it
|
388
|
-
next unless keys.length > 0
|
389
|
-
# Set the instance variable to null by default, so records that
|
390
|
-
# don't have a associated records will cache the negative lookup.
|
391
|
-
a.each do |object|
|
392
|
-
object.associations[assoc_name] = nil
|
393
|
-
end
|
394
|
-
assoc_block.call(assoc_class.select(*reflection.select).filter(assoc_class.primary_key=>keys)).all do |assoc_object|
|
395
|
-
next unless objects = h[assoc_object.pk]
|
396
|
-
objects.each{|object| object.associations[assoc_name] = assoc_object}
|
397
|
-
end
|
398
|
-
when :one_to_many, :many_to_many
|
399
|
-
h = key_hash[model.primary_key]
|
400
|
-
ds = if rtype == :one_to_many
|
401
|
-
fkey = reflection[:key]
|
402
|
-
reciprocal = reflection.reciprocal
|
403
|
-
assoc_class.select(*reflection.select).filter(fkey=>h.keys)
|
404
|
-
else
|
405
|
-
fkey = reflection[:left_key_alias]
|
406
|
-
assoc_class.select(*(Array(reflection.select)+Array(reflection[:left_key_select]))).inner_join(reflection[:join_table], [[reflection[:right_key], reflection.associated_primary_key], [reflection[:left_key], h.keys]])
|
407
|
-
end
|
408
|
-
h.values.each do |object_array|
|
409
|
-
object_array.each{|object| object.associations[assoc_name] = []}
|
410
|
-
end
|
411
|
-
assoc_block.call(ds).all do |assoc_object|
|
412
|
-
fk = if rtype == :many_to_many
|
413
|
-
assoc_object.values.delete(fkey)
|
414
|
-
else
|
415
|
-
assoc_object[fkey]
|
416
|
-
end
|
417
|
-
next unless objects = h[fk]
|
418
|
-
objects.each do |object|
|
419
|
-
object.associations[assoc_name].push(assoc_object)
|
420
|
-
assoc_object.associations[reciprocal] = object if reciprocal
|
421
|
-
end
|
422
|
-
end
|
423
|
-
end
|
424
|
-
end
|
365
|
+
reflections.each{|r| r[:eager_loader].call(key_hash, a, eager_assoc[r[:name]])}
|
425
366
|
end
|
426
367
|
|
427
368
|
# Build associations from the graph if #eager_graph was used,
|
data/lib/sequel_model/record.rb
CHANGED
@@ -2,7 +2,7 @@ module Sequel
|
|
2
2
|
class Model
|
3
3
|
# The setter methods (methods ending with =) that are never allowed
|
4
4
|
# to be called automatically via set.
|
5
|
-
RESTRICTED_SETTER_METHODS = %w"== === []= taguri= typecast_on_assignment="
|
5
|
+
RESTRICTED_SETTER_METHODS = %w"== === []= taguri= typecast_on_assignment= strict_param_setting= raise_on_save_failure="
|
6
6
|
|
7
7
|
# The current cached associations. A hash with the keys being the
|
8
8
|
# association name symbols and the values being the associated object
|
@@ -13,6 +13,10 @@ module Sequel
|
|
13
13
|
# see Model#[]=.
|
14
14
|
attr_reader :changed_columns
|
15
15
|
|
16
|
+
# Whether this model instance should raise an exception instead of
|
17
|
+
# returning nil on a failure to save/save_changes/etc.
|
18
|
+
attr_writer :raise_on_save_failure
|
19
|
+
|
16
20
|
# Whether this model instance should raise an error if attempting
|
17
21
|
# to call a method through set/update and their variants that either
|
18
22
|
# doesn't exist or access to it is denied.
|
@@ -42,6 +46,7 @@ module Sequel
|
|
42
46
|
@associations = {}
|
43
47
|
@db_schema = model.db_schema
|
44
48
|
@changed_columns = []
|
49
|
+
@raise_on_save_failure = model.raise_on_save_failure
|
45
50
|
@strict_param_setting = model.strict_param_setting
|
46
51
|
@typecast_on_assignment = model.typecast_on_assignment
|
47
52
|
if from_db
|
@@ -108,7 +113,7 @@ module Sequel
|
|
108
113
|
# the item from the database and returns self.
|
109
114
|
def destroy
|
110
115
|
db.transaction do
|
111
|
-
return
|
116
|
+
return save_failure(:destroy) if before_destroy == false
|
112
117
|
delete
|
113
118
|
after_destroy
|
114
119
|
end
|
@@ -187,12 +192,13 @@ module Sequel
|
|
187
192
|
alias_method :reload, :refresh
|
188
193
|
|
189
194
|
# Creates or updates the record, after making sure the record
|
190
|
-
# is valid. If the record is not valid,
|
191
|
-
#
|
192
|
-
#
|
193
|
-
# returns self.
|
195
|
+
# is valid. If the record is not valid, or before_save,
|
196
|
+
# before_create (if new?), or before_update (if !new?) return
|
197
|
+
# false, returns nil unless raise_on_save_failure is true.
|
198
|
+
# Otherwise, returns self. You can provide an optional list of
|
199
|
+
# columns to update, in which case it only updates those columns.
|
194
200
|
def save(*columns)
|
195
|
-
return
|
201
|
+
return save_failure(:save) unless valid?
|
196
202
|
save!(*columns)
|
197
203
|
end
|
198
204
|
|
@@ -200,12 +206,12 @@ module Sequel
|
|
200
206
|
# it first. You can provide an optional list of columns to update,
|
201
207
|
# in which case it only updates those columns.
|
202
208
|
# If before_save, before_create (if new?), or before_update
|
203
|
-
# (if !new?) return false, returns
|
204
|
-
# returns self.
|
209
|
+
# (if !new?) return false, returns nil unless raise_on_save_failure
|
210
|
+
# is true. Otherwise, returns self.
|
205
211
|
def save!(*columns)
|
206
|
-
return
|
212
|
+
return save_failure(:save) if before_save == false
|
207
213
|
if @new
|
208
|
-
return
|
214
|
+
return save_failure(:create) if before_create == false
|
209
215
|
iid = model.dataset.insert(@values)
|
210
216
|
# if we have a regular primary key and it's not set in @values,
|
211
217
|
# we assume it's the last inserted id
|
@@ -219,7 +225,7 @@ module Sequel
|
|
219
225
|
@new = false
|
220
226
|
after_create
|
221
227
|
else
|
222
|
-
return
|
228
|
+
return save_failure(:update) if before_update == false
|
223
229
|
if columns.empty?
|
224
230
|
this.update(@values)
|
225
231
|
@changed_columns = []
|
@@ -234,9 +240,10 @@ module Sequel
|
|
234
240
|
end
|
235
241
|
|
236
242
|
# Saves only changed columns or does nothing if no columns are marked as
|
237
|
-
# chanaged.
|
243
|
+
# chanaged. If no columns have been changed, returns nil. If unable to
|
244
|
+
# save, returns false unless raise_on_save_failure is true.
|
238
245
|
def save_changes
|
239
|
-
save(*@changed_columns) unless @changed_columns.empty?
|
246
|
+
save(*@changed_columns) || false unless @changed_columns.empty?
|
240
247
|
end
|
241
248
|
|
242
249
|
# Updates the instance with the supplied values with support for virtual
|
@@ -335,6 +342,128 @@ module Sequel
|
|
335
342
|
|
336
343
|
private
|
337
344
|
|
345
|
+
# Backbone behind association_dataset
|
346
|
+
def _dataset(opts)
|
347
|
+
raise(Sequel::Error, 'model object does not have a primary key') if opts.dataset_need_primary_key? && !pk
|
348
|
+
ds = send(opts._dataset_method)
|
349
|
+
opts[:extend].each{|m| ds.extend(m)}
|
350
|
+
ds = ds.select(*opts.select) if opts.select
|
351
|
+
ds = ds.order(*opts[:order]) if opts[:order]
|
352
|
+
ds = ds.limit(*opts[:limit]) if opts[:limit]
|
353
|
+
ds = ds.eager(*opts[:eager]) if opts[:eager]
|
354
|
+
ds = ds.eager_graph(opts[:eager_graph]) if opts[:eager_graph] && opts.eager_graph_lazy_dataset?
|
355
|
+
ds = send(opts.dataset_helper_method, ds) if opts[:block]
|
356
|
+
ds
|
357
|
+
end
|
358
|
+
|
359
|
+
# Add the given associated object to the given association
|
360
|
+
def add_associated_object(opts, o)
|
361
|
+
raise(Sequel::Error, 'model object does not have a primary key') unless pk
|
362
|
+
raise(Sequel::Error, 'associated object does not have a primary key') if opts.need_associated_primary_key? && !o.pk
|
363
|
+
return if run_association_callbacks(opts, :before_add, o) == false
|
364
|
+
send(opts._add_method, o)
|
365
|
+
@associations[opts[:name]].push(o) if @associations.include?(opts[:name])
|
366
|
+
add_reciprocal_object(opts, o)
|
367
|
+
run_association_callbacks(opts, :after_add, o)
|
368
|
+
o
|
369
|
+
end
|
370
|
+
|
371
|
+
# Add/Set the current object to/as the given object's reciprocal association.
|
372
|
+
def add_reciprocal_object(opts, o)
|
373
|
+
return unless reciprocal = opts.reciprocal
|
374
|
+
case opts[:type]
|
375
|
+
when :many_to_many, :many_to_one
|
376
|
+
if array = o.associations[reciprocal] and !array.include?(self)
|
377
|
+
array.push(self)
|
378
|
+
end
|
379
|
+
when :one_to_many
|
380
|
+
o.associations[reciprocal] = self
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
# Load the associated objects using the dataset
|
385
|
+
def load_associated_objects(opts, reload=false)
|
386
|
+
name = opts[:name]
|
387
|
+
if @associations.include?(name) and !reload
|
388
|
+
@associations[name]
|
389
|
+
else
|
390
|
+
objs = if opts.single_associated_object?
|
391
|
+
if !opts[:key]
|
392
|
+
send(opts.dataset_method).all.first
|
393
|
+
elsif send(opts[:key])
|
394
|
+
send(opts.dataset_method).first
|
395
|
+
end
|
396
|
+
else
|
397
|
+
objs = send(opts.dataset_method).all
|
398
|
+
end
|
399
|
+
run_association_callbacks(opts, :after_load, objs)
|
400
|
+
# Only one_to_many associations should set the reciprocal object
|
401
|
+
objs.each{|o| add_reciprocal_object(opts, o)} if opts.set_reciprocal_to_self?
|
402
|
+
@associations[name] = objs
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
# Remove all associated objects from the given association
|
407
|
+
def remove_all_associated_objects(opts)
|
408
|
+
raise(Sequel::Error, 'model object does not have a primary key') unless pk
|
409
|
+
send(opts._remove_all_method)
|
410
|
+
ret = @associations[opts[:name]].each{|o| remove_reciprocal_object(opts, o)} if @associations.include?(opts[:name])
|
411
|
+
@associations[opts[:name]] = []
|
412
|
+
ret
|
413
|
+
end
|
414
|
+
|
415
|
+
# Remove the given associated object from the given association
|
416
|
+
def remove_associated_object(opts, o)
|
417
|
+
raise(Sequel::Error, 'model object does not have a primary key') unless pk
|
418
|
+
raise(Sequel::Error, 'associated object does not have a primary key') if opts.need_associated_primary_key? && !o.pk
|
419
|
+
return if run_association_callbacks(opts, :before_remove, o) == false
|
420
|
+
send(opts._remove_method, o)
|
421
|
+
@associations[opts[:name]].delete_if{|x| o === x} if @associations.include?(opts[:name])
|
422
|
+
remove_reciprocal_object(opts, o)
|
423
|
+
run_association_callbacks(opts, :after_remove, o)
|
424
|
+
o
|
425
|
+
end
|
426
|
+
|
427
|
+
# Remove/unset the current object from/as the given object's reciprocal association.
|
428
|
+
def remove_reciprocal_object(opts, o)
|
429
|
+
return unless reciprocal = opts.reciprocal
|
430
|
+
case opts[:type]
|
431
|
+
when :many_to_many, :many_to_one
|
432
|
+
if array = o.associations[reciprocal]
|
433
|
+
array.delete_if{|x| self === x}
|
434
|
+
end
|
435
|
+
when :one_to_many
|
436
|
+
o.associations[reciprocal] = nil
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
# Run the callback for the association with the object.
|
441
|
+
def run_association_callbacks(reflection, callback_type, object)
|
442
|
+
raise_error = @raise_on_save_failure
|
443
|
+
raise_error = true if reflection[:type] == :many_to_one
|
444
|
+
stop_on_false = true if [:before_add, :before_remove].include?(callback_type)
|
445
|
+
reflection[callback_type].each do |cb|
|
446
|
+
res = case cb
|
447
|
+
when Symbol
|
448
|
+
send(cb, object)
|
449
|
+
when Proc
|
450
|
+
cb.call(self, object)
|
451
|
+
else
|
452
|
+
raise Error, "callbacks should either be Procs or Symbols"
|
453
|
+
end
|
454
|
+
if res == false and stop_on_false
|
455
|
+
save_failure("modify association for", raise_error)
|
456
|
+
return false
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# Raise an error if raise_on_save_failure is true
|
462
|
+
def save_failure(action, raise_error = nil)
|
463
|
+
raise_error = @raise_on_save_failure if raise_error.nil?
|
464
|
+
raise(Error, "unable to #{action} record") if raise_error
|
465
|
+
end
|
466
|
+
|
338
467
|
# Set the columns, filtered by the only and except arrays.
|
339
468
|
def set_restricted(hash, only, except)
|
340
469
|
columns_not_set = model.instance_variable_get(:@columns).blank?
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Sequel
|
2
2
|
class Model
|
3
|
-
# The Validation module houses a couple of
|
3
|
+
# The Validation module houses a couple of classes used by Sequel's
|
4
4
|
# validation code.
|
5
5
|
module Validation
|
6
6
|
# Validation::Errors represents validation errors, a simple hash subclass
|
@@ -85,11 +85,21 @@ module Sequel
|
|
85
85
|
end
|
86
86
|
validations.each do |att, procs|
|
87
87
|
v = o.send(att)
|
88
|
-
procs.each {|p| p
|
88
|
+
procs.each {|tag, p| p.call(o, att, v)}
|
89
89
|
end
|
90
90
|
end
|
91
91
|
|
92
|
-
# Validates acceptance of an attribute.
|
92
|
+
# Validates acceptance of an attribute. Just checks that the value
|
93
|
+
# is equal to the :accept option.
|
94
|
+
#
|
95
|
+
# Possible Options:
|
96
|
+
# * :accept - The value required for the object to be valid (default: '1')
|
97
|
+
# * :allow_blank - Whether to skip the validation if the value is blank (default: false)
|
98
|
+
# * :allow_nil - Whether to skip the validation if the value is nil (default: true)
|
99
|
+
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
100
|
+
# skipping this validation if it returns nil or false.
|
101
|
+
# * :message - The message to use (default: 'is not accepted')
|
102
|
+
# * :tag - The tag to use for this validation (default: :acceptance)
|
93
103
|
def self.validates_acceptance_of(*atts)
|
94
104
|
opts = {
|
95
105
|
:message => 'is not accepted',
|
@@ -97,21 +107,35 @@ module Sequel
|
|
97
107
|
:accept => '1'
|
98
108
|
}.merge!(atts.extract_options!)
|
99
109
|
|
110
|
+
atts << {:if=>opts[:if], :tag=>opts[:tag]||:acceptance}
|
100
111
|
validates_each(*atts) do |o, a, v|
|
101
|
-
next unless o.instance_eval(&if_proc(opts))
|
102
112
|
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
103
113
|
o.errors[a] << opts[:message] unless v == opts[:accept]
|
104
114
|
end
|
105
115
|
end
|
106
116
|
|
107
|
-
# Validates confirmation of an attribute.
|
117
|
+
# Validates confirmation of an attribute. Checks that the object has
|
118
|
+
# a _confirmation value matching the current value. For example:
|
119
|
+
#
|
120
|
+
# validates_confirmation_of :blah
|
121
|
+
#
|
122
|
+
# Just makes sure that object.blah = object.blah_confirmation. Often used for passwords
|
123
|
+
# or email addresses on web forms.
|
124
|
+
#
|
125
|
+
# Possible Options:
|
126
|
+
# * :allow_false - Whether to skip the validation if the value is blank (default: false)
|
127
|
+
# * :allow_nil - Whether to skip the validation if the value is nil (default: false)
|
128
|
+
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
129
|
+
# skipping this validation if it returns nil or false.
|
130
|
+
# * :message - The message to use (default: 'is not confirmed')
|
131
|
+
# * :tag - The tag to use for this validation (default: :confirmation)
|
108
132
|
def self.validates_confirmation_of(*atts)
|
109
133
|
opts = {
|
110
134
|
:message => 'is not confirmed',
|
111
135
|
}.merge!(atts.extract_options!)
|
112
|
-
|
136
|
+
|
137
|
+
atts << {:if=>opts[:if], :tag=>opts[:tag]||:confirmation}
|
113
138
|
validates_each(*atts) do |o, a, v|
|
114
|
-
next unless o.instance_eval(&if_proc(opts))
|
115
139
|
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
116
140
|
c = o.send(:"#{a}_confirmation")
|
117
141
|
o.errors[a] << opts[:message] unless v == c
|
@@ -125,11 +149,40 @@ module Sequel
|
|
125
149
|
# validates_each :name, :password do |object, attribute, value|
|
126
150
|
# object.errors[attribute] << 'is not nice' unless value.nice?
|
127
151
|
# end
|
152
|
+
#
|
153
|
+
# Possible Options:
|
154
|
+
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
155
|
+
# skipping this validation if it returns nil or false.
|
156
|
+
# * :tag - The tag to use for this validation (default: nil)
|
128
157
|
def self.validates_each(*atts, &block)
|
129
|
-
atts.
|
158
|
+
opts = atts.extract_options!
|
159
|
+
blk = if opts[:if]
|
160
|
+
proc{|o,a,v| block.call(o,a,v) if o.instance_eval(&if_proc(opts))}
|
161
|
+
else
|
162
|
+
block
|
163
|
+
end
|
164
|
+
tag = opts[:tag]
|
165
|
+
atts.each do |a|
|
166
|
+
a_vals = validations[a]
|
167
|
+
if tag && (old = a_vals.find{|x| x[0] == tag})
|
168
|
+
old[1] = blk
|
169
|
+
else
|
170
|
+
a_vals << [tag, blk]
|
171
|
+
end
|
172
|
+
end
|
130
173
|
end
|
131
174
|
|
132
|
-
# Validates the format of an attribute
|
175
|
+
# Validates the format of an attribute, checking the string representation of the
|
176
|
+
# value against the regular expression provided by the :with option.
|
177
|
+
#
|
178
|
+
# Possible Options:
|
179
|
+
# * :allow_blank - Whether to skip the validation if the value is blank (default: false)
|
180
|
+
# * :allow_nil - Whether to skip the validation if the value is nil (default: false)
|
181
|
+
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
182
|
+
# skipping this validation if it returns nil or false.
|
183
|
+
# * :message - The message to use (default: 'is invalid')
|
184
|
+
# * :tag - The tag to use for this validation (default: :format)
|
185
|
+
# * :with - The regular expression to validate the value with (required).
|
133
186
|
def self.validates_format_of(*atts)
|
134
187
|
opts = {
|
135
188
|
:message => 'is invalid',
|
@@ -139,14 +192,30 @@ module Sequel
|
|
139
192
|
raise ArgumentError, "A regular expression must be supplied as the :with option of the options hash"
|
140
193
|
end
|
141
194
|
|
195
|
+
atts << {:if=>opts[:if], :tag=>opts[:tag]||:format}
|
142
196
|
validates_each(*atts) do |o, a, v|
|
143
|
-
next unless o.instance_eval(&if_proc(opts))
|
144
197
|
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
145
198
|
o.errors[a] << opts[:message] unless v.to_s =~ opts[:with]
|
146
199
|
end
|
147
200
|
end
|
148
201
|
|
149
202
|
# Validates the length of an attribute.
|
203
|
+
#
|
204
|
+
# Possible Options:
|
205
|
+
# * :allow_blank - Whether to skip the validation if the value is blank (default: false)
|
206
|
+
# * :allow_nil - Whether to skip the validation if the value is nil (default: false)
|
207
|
+
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
208
|
+
# skipping this validation if it returns nil or false.
|
209
|
+
# * :is - The exact size required for the value to be valid (no default)
|
210
|
+
# * :maximum - The maximum size allowed for the value (no default)
|
211
|
+
# * :message - The message to use (no default, overrides :too_long, :too_short, and :wrong_length
|
212
|
+
# options if present)
|
213
|
+
# * :minimum - The minimum size allowed for the value (no default)
|
214
|
+
# * :tag - The tag to use for this validation (default: :length)
|
215
|
+
# * :too_long - The message to use use if it the value is too long (default: 'is too long')
|
216
|
+
# * :too_short - The message to use use if it the value is too short (default: 'is too short')
|
217
|
+
# * :with - The array/range that must include the size of the value for it to be valid (no default)
|
218
|
+
# * :wrong_length - The message to use use if it the value is not valid (default: 'is the wrong length')
|
150
219
|
def self.validates_length_of(*atts)
|
151
220
|
opts = {
|
152
221
|
:too_long => 'is too long',
|
@@ -154,8 +223,8 @@ module Sequel
|
|
154
223
|
:wrong_length => 'is the wrong length'
|
155
224
|
}.merge!(atts.extract_options!)
|
156
225
|
|
226
|
+
atts << {:if=>opts[:if], :tag=>opts[:tag]||:length}
|
157
227
|
validates_each(*atts) do |o, a, v|
|
158
|
-
next unless o.instance_eval(&if_proc(opts))
|
159
228
|
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
160
229
|
if m = opts[:maximum]
|
161
230
|
o.errors[a] << (opts[:message] || opts[:too_long]) unless v && v.size <= m
|
@@ -173,13 +242,22 @@ module Sequel
|
|
173
242
|
end
|
174
243
|
|
175
244
|
# Validates whether an attribute is a number.
|
245
|
+
#
|
246
|
+
# Possible Options:
|
247
|
+
# * :allow_blank - Whether to skip the validation if the value is blank (default: false)
|
248
|
+
# * :allow_nil - Whether to skip the validation if the value is nil (default: false)
|
249
|
+
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
250
|
+
# skipping this validation if it returns nil or false.
|
251
|
+
# * :message - The message to use (default: 'is not a number')
|
252
|
+
# * :tag - The tag to use for this validation (default: :numericality)
|
253
|
+
# * :only_integer - Whether only integers are valid values (default: false)
|
176
254
|
def self.validates_numericality_of(*atts)
|
177
255
|
opts = {
|
178
256
|
:message => 'is not a number',
|
179
257
|
}.merge!(atts.extract_options!)
|
180
258
|
|
259
|
+
atts << {:if=>opts[:if], :tag=>opts[:tag]||:numericality}
|
181
260
|
validates_each(*atts) do |o, a, v|
|
182
|
-
next unless o.instance_eval(&if_proc(opts))
|
183
261
|
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
184
262
|
begin
|
185
263
|
if opts[:only_integer]
|
@@ -193,28 +271,41 @@ module Sequel
|
|
193
271
|
end
|
194
272
|
end
|
195
273
|
|
196
|
-
# Validates the presence of an attribute.
|
274
|
+
# Validates the presence of an attribute. Requires the value not be blank,
|
275
|
+
# with false considered present instead of absent.
|
276
|
+
#
|
277
|
+
# Possible Options:
|
278
|
+
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
279
|
+
# skipping this validation if it returns nil or false.
|
280
|
+
# * :message - The message to use (default: 'is not present')
|
281
|
+
# * :tag - The tag to use for this validation (default: :presence)
|
197
282
|
def self.validates_presence_of(*atts)
|
198
283
|
opts = {
|
199
284
|
:message => 'is not present',
|
200
285
|
}.merge!(atts.extract_options!)
|
201
286
|
|
287
|
+
atts << {:if=>opts[:if], :tag=>opts[:tag]||:presence}
|
202
288
|
validates_each(*atts) do |o, a, v|
|
203
|
-
|
204
|
-
o.errors[a] << opts[:message] unless v && !v.blank?
|
289
|
+
o.errors[a] << opts[:message] if v.blank? && v != false
|
205
290
|
end
|
206
291
|
end
|
207
292
|
|
208
293
|
# Validates only if the fields in the model (specified by atts) are
|
209
294
|
# unique in the database. You should also add a unique index in the
|
210
295
|
# database, as this suffers from a fairly obvious race condition.
|
296
|
+
#
|
297
|
+
# Possible Options:
|
298
|
+
# * :if - A symbol (indicating an instance_method) or proc (which is instance_evaled)
|
299
|
+
# skipping this validation if it returns nil or false.
|
300
|
+
# * :message - The message to use (default: 'is already taken')
|
301
|
+
# * :tag - The tag to use for this validation (default: :uniqueness)
|
211
302
|
def self.validates_uniqueness_of(*atts)
|
212
303
|
opts = {
|
213
304
|
:message => 'is already taken',
|
214
305
|
}.merge!(atts.extract_options!)
|
215
306
|
|
307
|
+
atts << {:if=>opts[:if], :tag=>opts[:tag]||:uniqueness}
|
216
308
|
validates_each(*atts) do |o, a, v|
|
217
|
-
next unless o.instance_eval(&if_proc(opts))
|
218
309
|
next if v.blank?
|
219
310
|
num_dups = o.class.filter(a => v).count
|
220
311
|
allow = if num_dups == 0
|
@@ -244,7 +335,7 @@ module Sequel
|
|
244
335
|
|
245
336
|
### Private Class Methods ###
|
246
337
|
|
247
|
-
def self.if_proc(opts)
|
338
|
+
def self.if_proc(opts) # :nodoc:
|
248
339
|
case opts[:if]
|
249
340
|
when Symbol then proc{send opts[:if]}
|
250
341
|
when Proc then opts[:if]
|