sequel 1.4.0 → 1.5.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 +257 -154
- data/Rakefile +8 -16
- data/lib/sequel_model.rb +15 -257
- data/lib/sequel_model/associations.rb +70 -33
- data/lib/sequel_model/base.rb +80 -35
- data/lib/sequel_model/caching.rb +3 -3
- data/lib/sequel_model/deprecated.rb +81 -0
- data/lib/sequel_model/eager_loading.rb +303 -43
- data/lib/sequel_model/hooks.rb +29 -25
- data/lib/sequel_model/inflections.rb +112 -0
- data/lib/sequel_model/inflector.rb +279 -0
- data/lib/sequel_model/plugins.rb +15 -14
- data/lib/sequel_model/record.rb +87 -75
- data/lib/sequel_model/schema.rb +2 -0
- data/lib/sequel_model/validations.rb +300 -2
- data/spec/associations_spec.rb +175 -9
- data/spec/base_spec.rb +37 -18
- data/spec/caching_spec.rb +7 -4
- data/spec/deprecated_relations_spec.rb +3 -43
- data/spec/eager_loading_spec.rb +295 -7
- data/spec/hooks_spec.rb +7 -4
- data/spec/inflector_spec.rb +34 -0
- data/spec/model_spec.rb +30 -53
- data/spec/record_spec.rb +191 -33
- data/spec/spec_helper.rb +17 -2
- data/spec/validations_spec.rb +414 -15
- metadata +7 -22
- data/lib/sequel_model/pretty_table.rb +0 -73
data/lib/sequel_model/plugins.rb
CHANGED
@@ -28,20 +28,21 @@ module Sequel
|
|
28
28
|
end
|
29
29
|
alias_method :is_a, :is
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
31
|
+
private
|
32
|
+
# Returns the module for the specified plugin. If the module is not
|
33
|
+
# defined, the corresponding plugin gem is automatically loaded.
|
34
|
+
def plugin_module(plugin)
|
35
|
+
module_name = plugin.to_s.gsub(/(^|_)(.)/) {$2.upcase}
|
36
|
+
if not Sequel::Plugins.const_defined?(module_name)
|
37
|
+
require plugin_gem(plugin)
|
38
|
+
end
|
39
|
+
Sequel::Plugins.const_get(module_name)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns the gem name for the given plugin.
|
43
|
+
def plugin_gem(plugin)
|
44
|
+
"sequel_#{plugin}"
|
37
45
|
end
|
38
|
-
Sequel::Plugins.const_get(module_name)
|
39
|
-
end
|
40
|
-
|
41
|
-
# Returns the gem name for the given plugin.
|
42
|
-
def plugin_gem(plugin)
|
43
|
-
"sequel_#{plugin}"
|
44
|
-
end
|
45
46
|
end
|
46
47
|
end
|
47
|
-
end
|
48
|
+
end
|
data/lib/sequel_model/record.rb
CHANGED
@@ -40,10 +40,18 @@ module Sequel
|
|
40
40
|
def ==(obj)
|
41
41
|
(obj.class == model) && (obj.values == @values)
|
42
42
|
end
|
43
|
+
alias_method :eql?, :"=="
|
43
44
|
|
44
|
-
#
|
45
|
+
# If pk is not nil, true only if the objects have the same class and pk.
|
46
|
+
# If pk is nil, false.
|
45
47
|
def ===(obj)
|
46
|
-
(obj.class == model) && (obj.pk == pk)
|
48
|
+
pk.nil? ? false : (obj.class == model) && (obj.pk == pk)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Unique for objects with the same class and pk (if pk is not nil), or
|
52
|
+
# the same class and values (if pk is nil).
|
53
|
+
def hash
|
54
|
+
[model, pk.nil? ? @values.sort_by{|k,v| k.to_s} : pk].hash
|
47
55
|
end
|
48
56
|
|
49
57
|
# Returns key for primary key.
|
@@ -131,36 +139,39 @@ module Sequel
|
|
131
139
|
class_def(:cache_key) {raise Error, "No primary key is associated with this model"}
|
132
140
|
end
|
133
141
|
|
134
|
-
# Creates new instance with values set to passed-in Hash
|
135
|
-
#
|
142
|
+
# Creates new instance with values set to passed-in Hash, saves it
|
143
|
+
# (running any callbacks), and returns the instance.
|
136
144
|
def self.create(values = {}, &block)
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
obj
|
141
|
-
end
|
145
|
+
obj = new(values, &block)
|
146
|
+
obj.save
|
147
|
+
obj
|
142
148
|
end
|
143
149
|
|
144
150
|
# Updates the instance with the supplied values with support for virtual
|
145
151
|
# attributes, ignoring any values for which no setter method is available.
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
152
|
+
# Does not save the record.
|
153
|
+
#
|
154
|
+
# If no columns have been set for this model (very unlikely), assume symbol
|
155
|
+
# keys are valid column names, and assign the column value based on that.
|
156
|
+
def set_with_params(hash)
|
157
|
+
columns_not_set = !model.instance_variable_get(:@columns)
|
158
|
+
meths = setter_methods
|
159
|
+
hash.each do |k,v|
|
160
|
+
m = "#{k}="
|
161
|
+
if meths.include?(m)
|
162
|
+
send(m, v)
|
163
|
+
elsif columns_not_set && (Symbol === k)
|
164
|
+
self[k] = v
|
165
|
+
end
|
150
166
|
end
|
151
|
-
save_changes
|
152
167
|
end
|
153
|
-
alias_method :update_with, :update_with_params
|
154
168
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
record
|
160
|
-
end
|
161
|
-
alias_method :create_with, :create_with_params
|
169
|
+
# Runs set_with_params and saves the changes (which runs any callback methods).
|
170
|
+
def update_with_params(values)
|
171
|
+
set_with_params(values)
|
172
|
+
save_changes
|
162
173
|
end
|
163
|
-
|
174
|
+
|
164
175
|
# Returns (naked) dataset bound to current instance.
|
165
176
|
def this
|
166
177
|
@this ||= self.class.dataset.filter(:id => @values[:id]).limit(1).naked
|
@@ -194,27 +205,19 @@ module Sequel
|
|
194
205
|
# This method guesses whether the record exists when
|
195
206
|
# <tt>new_record</tt> is set to false.
|
196
207
|
def initialize(values = nil, from_db = false, &block)
|
208
|
+
values ||= {}
|
197
209
|
@changed_columns = []
|
198
|
-
|
199
|
-
@
|
200
|
-
|
201
|
-
values.each do |k, v| m = :"#{k}="
|
202
|
-
if respond_to?(m)
|
203
|
-
send(m, v)
|
204
|
-
values.delete(k)
|
205
|
-
end
|
206
|
-
end
|
207
|
-
values.inject(@values) {|m, kv| m[kv[0].to_sym] = kv[1]; m}
|
208
|
-
# @values.merge!(values)
|
209
|
-
end
|
210
|
+
if from_db
|
211
|
+
@new = false
|
212
|
+
@values = values
|
210
213
|
else
|
211
|
-
@values =
|
214
|
+
@values = {}
|
215
|
+
@new = true
|
216
|
+
set_with_params(values)
|
212
217
|
end
|
213
|
-
|
214
|
-
k = primary_key
|
215
|
-
@new = !from_db
|
218
|
+
@changed_columns.clear
|
216
219
|
|
217
|
-
|
220
|
+
yield self if block
|
218
221
|
after_initialize
|
219
222
|
end
|
220
223
|
|
@@ -228,7 +231,6 @@ module Sequel
|
|
228
231
|
def new?
|
229
232
|
@new
|
230
233
|
end
|
231
|
-
alias :new_record? :new?
|
232
234
|
|
233
235
|
# Returns true when current instance exists, false otherwise.
|
234
236
|
def exists?
|
@@ -274,17 +276,47 @@ module Sequel
|
|
274
276
|
save(*@changed_columns) unless @changed_columns.empty?
|
275
277
|
end
|
276
278
|
|
277
|
-
#
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
279
|
+
# Sets the value attributes without saving the record. Returns
|
280
|
+
# the values changed. Raises an error if the keys are not symbols
|
281
|
+
# or strings or a string key was passed that was not a valid column.
|
282
|
+
# This is a low level method that does not respect virtual attributes. It
|
283
|
+
# should probably be avoided. Look into using set_with_params instead.
|
284
|
+
def set_values(values)
|
285
|
+
s = str_columns
|
286
|
+
vals = values.inject({}) do |m, kv|
|
287
|
+
k, v = kv
|
288
|
+
k = case k
|
289
|
+
when Symbol
|
290
|
+
k
|
291
|
+
when String
|
292
|
+
# Prevent denial of service via memory exhaustion by only
|
293
|
+
# calling to_sym if the symbol already exists.
|
294
|
+
raise(::Sequel::Error, "all string keys must be a valid columns") unless s.include?(k)
|
295
|
+
k.to_sym
|
296
|
+
else
|
297
|
+
raise(::Sequel::Error, "Only symbols and strings allows as keys")
|
298
|
+
end
|
299
|
+
m[k] = v
|
300
|
+
m
|
301
|
+
end
|
302
|
+
vals.each {|k, v| @values[k] = v}
|
303
|
+
vals
|
304
|
+
end
|
305
|
+
|
306
|
+
# Sets the values attributes with set_values and then updates
|
307
|
+
# the record in the database using those values. This is a
|
308
|
+
# low level method that does not run the usual save callbacks.
|
309
|
+
# It should probably be avoided. Look into using update_with_params instead.
|
310
|
+
def update_values(values)
|
311
|
+
this.update(set_values(values))
|
282
312
|
end
|
283
|
-
alias_method :update, :set
|
284
313
|
|
285
314
|
# Reloads values from database and returns self.
|
286
315
|
def refresh
|
287
316
|
@values = this.first || raise(Error, "Record not found")
|
317
|
+
model.all_association_reflections.each do |r|
|
318
|
+
instance_variable_set("@#{r[:name]}", nil)
|
319
|
+
end
|
288
320
|
self
|
289
321
|
end
|
290
322
|
alias_method :reload, :refresh
|
@@ -296,41 +328,21 @@ module Sequel
|
|
296
328
|
delete
|
297
329
|
after_destroy
|
298
330
|
end
|
331
|
+
self
|
299
332
|
end
|
300
333
|
|
301
|
-
# Deletes and returns self.
|
334
|
+
# Deletes and returns self. Does not run callbacks.
|
335
|
+
# Look into using destroy instead.
|
302
336
|
def delete
|
303
337
|
this.delete
|
304
338
|
self
|
305
339
|
end
|
306
340
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
att = $1.to_sym
|
313
|
-
write = $2 == EQUAL_SIGN
|
314
|
-
|
315
|
-
# check whether the column is legal
|
316
|
-
unless @values.has_key?(att) || columns.include?(att)
|
317
|
-
raise Error, "Invalid column (#{att.inspect}) for #{self}"
|
318
|
-
end
|
319
|
-
|
320
|
-
# define the column accessor
|
321
|
-
Thread.exclusive do
|
322
|
-
if write
|
323
|
-
model.class_def(m) {|v| self[att] = v}
|
324
|
-
else
|
325
|
-
model.class_def(m) {self[att]}
|
326
|
-
end
|
327
|
-
end
|
328
|
-
|
329
|
-
# call the accessor
|
330
|
-
respond_to?(m) ? send(m, *args) : super(m, *args)
|
331
|
-
else
|
332
|
-
super(m, *args)
|
341
|
+
private
|
342
|
+
# Returns all methods that can be used for attribute
|
343
|
+
# assignment (those that end with =)
|
344
|
+
def setter_methods
|
345
|
+
methods.grep(/=\z/)
|
333
346
|
end
|
334
|
-
end
|
335
347
|
end
|
336
348
|
end
|
data/lib/sequel_model/schema.rb
CHANGED
@@ -1,6 +1,304 @@
|
|
1
|
-
|
1
|
+
class Array
|
2
|
+
# Removes and returns the last member of the array if it is a hash. Otherwise,
|
3
|
+
# an empty hash is returned This method is useful when writing methods that
|
4
|
+
# take an options hash as the last parameter. For example:
|
5
|
+
#
|
6
|
+
# def validate_each(*args, &block)
|
7
|
+
# opts = args.extract_options!
|
8
|
+
# ...
|
9
|
+
# end
|
10
|
+
def extract_options!
|
11
|
+
last.is_a?(Hash) ? pop : {}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# The Validations module provides validation capabilities as a mixin. When
|
16
|
+
# included into a class, it enhances the class with class and instance
|
17
|
+
# methods for defining validations and validating class instances.
|
18
|
+
#
|
19
|
+
# The Validation emulates the validation capabilities of ActiveRecord, and
|
20
|
+
# provides methods for validating acceptance, confirmation, presence, format,
|
21
|
+
# length and numericality of attributes.
|
22
|
+
#
|
23
|
+
# To use validations, you need to include the Validation module in your
|
24
|
+
# class:
|
25
|
+
#
|
26
|
+
# class MyClass
|
27
|
+
# include Validation
|
28
|
+
# validates_length_of :password, :minimum => 6
|
29
|
+
# end
|
30
|
+
module Validation
|
31
|
+
# Includes the Validation class methods into the including class.
|
32
|
+
def self.included(c)
|
33
|
+
c.extend ClassMethods
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the validation errors associated with the object.
|
37
|
+
def errors
|
38
|
+
@errors ||= Errors.new
|
39
|
+
end
|
40
|
+
|
41
|
+
# Validates the object.
|
42
|
+
def validate
|
43
|
+
errors.clear
|
44
|
+
self.class.validate(self)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Validates the object and returns true if no errors are reported.
|
48
|
+
def valid?
|
49
|
+
validate
|
50
|
+
errors.empty?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Validation::Errors represents validation errors.
|
54
|
+
class Errors
|
55
|
+
# Initializes a new instance of validation errors.
|
56
|
+
def initialize
|
57
|
+
@errors = Hash.new {|h, k| h[k] = []}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Returns true if no errors are stored.
|
61
|
+
def empty?
|
62
|
+
@errors.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
# Clears all errors.
|
66
|
+
def clear
|
67
|
+
@errors.clear
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns size of errors array
|
71
|
+
def size
|
72
|
+
@errors.size
|
73
|
+
end
|
74
|
+
|
75
|
+
# Iterates over errors
|
76
|
+
def each(&block)
|
77
|
+
@errors.each(&block)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the errors for the given attribute.
|
81
|
+
def on(att)
|
82
|
+
@errors[att]
|
83
|
+
end
|
84
|
+
alias_method :[], :on
|
85
|
+
|
86
|
+
# Adds an error for the given attribute.
|
87
|
+
def add(att, msg)
|
88
|
+
@errors[att] << msg
|
89
|
+
end
|
90
|
+
|
91
|
+
# Returns an array of fully-formatted error messages.
|
92
|
+
def full_messages
|
93
|
+
@errors.inject([]) do |m, kv| att, errors = *kv
|
94
|
+
errors.each {|e| m << "#{att} #{e}"}
|
95
|
+
m
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# The Generator class is used to generate validation definitions using
|
101
|
+
# the validates {} idiom.
|
102
|
+
class Generator
|
103
|
+
# Initializes a new generator.
|
104
|
+
def initialize(receiver ,&block)
|
105
|
+
@receiver = receiver
|
106
|
+
instance_eval(&block)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Delegates method calls to the receiver by calling receiver.validates_xxx.
|
110
|
+
def method_missing(m, *args, &block)
|
111
|
+
@receiver.send(:"validates_#{m}", *args, &block)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Validation class methods.
|
116
|
+
module ClassMethods
|
117
|
+
# Defines validations by converting a longhand block into a series of
|
118
|
+
# shorthand definitions. For example:
|
119
|
+
#
|
120
|
+
# class MyClass
|
121
|
+
# include Validation
|
122
|
+
# validates do
|
123
|
+
# length_of :name, :minimum => 6
|
124
|
+
# length_of :password, :minimum => 8
|
125
|
+
# end
|
126
|
+
# end
|
127
|
+
#
|
128
|
+
# is equivalent to:
|
129
|
+
# class MyClass
|
130
|
+
# include Validation
|
131
|
+
# validates_length_of :name, :minimum => 6
|
132
|
+
# validates_length_of :password, :minimum => 8
|
133
|
+
# end
|
134
|
+
def validates(&block)
|
135
|
+
Generator.new(self, &block)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns the validations hash for the class.
|
139
|
+
def validations
|
140
|
+
@validations ||= Hash.new {|h, k| h[k] = []}
|
141
|
+
end
|
2
142
|
|
3
|
-
|
143
|
+
# Returns true if validations are defined.
|
144
|
+
def has_validations?
|
145
|
+
!validations.empty?
|
146
|
+
end
|
147
|
+
|
148
|
+
# Validates the given instance.
|
149
|
+
def validate(o)
|
150
|
+
if superclass.respond_to?(:validate) && !@skip_superclass_validations
|
151
|
+
superclass.validate(o)
|
152
|
+
end
|
153
|
+
validations.each do |att, procs|
|
154
|
+
v = o.send(att)
|
155
|
+
procs.each {|p| p[o, att, v]}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def skip_superclass_validations
|
160
|
+
@skip_superclass_validations = true
|
161
|
+
end
|
162
|
+
|
163
|
+
# Adds a validation for each of the given attributes using the supplied
|
164
|
+
# block. The block must accept three arguments: instance, attribute and
|
165
|
+
# value, e.g.:
|
166
|
+
#
|
167
|
+
# validates_each :name, :password do |object, attribute, value|
|
168
|
+
# object.errors[attribute] << 'is not nice' unless value.nice?
|
169
|
+
# end
|
170
|
+
def validates_each(*atts, &block)
|
171
|
+
atts.each {|a| validations[a] << block}
|
172
|
+
end
|
173
|
+
|
174
|
+
# Validates acceptance of an attribute.
|
175
|
+
def validates_acceptance_of(*atts)
|
176
|
+
opts = {
|
177
|
+
:message => 'is not accepted',
|
178
|
+
:allow_nil => true,
|
179
|
+
:accept => '1'
|
180
|
+
}.merge!(atts.extract_options!)
|
181
|
+
|
182
|
+
validates_each(*atts) do |o, a, v|
|
183
|
+
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
184
|
+
o.errors[a] << opts[:message] unless v == opts[:accept]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Validates confirmation of an attribute.
|
189
|
+
def validates_confirmation_of(*atts)
|
190
|
+
opts = {
|
191
|
+
:message => 'is not confirmed',
|
192
|
+
}.merge!(atts.extract_options!)
|
193
|
+
|
194
|
+
validates_each(*atts) do |o, a, v|
|
195
|
+
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
196
|
+
c = o.send(:"#{a}_confirmation")
|
197
|
+
o.errors[a] << opts[:message] unless v == c
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# Validates the format of an attribute.
|
202
|
+
def validates_format_of(*atts)
|
203
|
+
opts = {
|
204
|
+
:message => 'is invalid',
|
205
|
+
}.merge!(atts.extract_options!)
|
206
|
+
|
207
|
+
unless opts[:with].is_a?(Regexp)
|
208
|
+
raise ArgumentError, "A regular expression must be supplied as the :with option of the options hash"
|
209
|
+
end
|
210
|
+
|
211
|
+
validates_each(*atts) do |o, a, v|
|
212
|
+
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
213
|
+
o.errors[a] << opts[:message] unless v.to_s =~ opts[:with]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Validates the length of an attribute.
|
218
|
+
def validates_length_of(*atts)
|
219
|
+
opts = {
|
220
|
+
:too_long => 'is too long',
|
221
|
+
:too_short => 'is too short',
|
222
|
+
:wrong_length => 'is the wrong length'
|
223
|
+
}.merge!(atts.extract_options!)
|
224
|
+
|
225
|
+
validates_each(*atts) do |o, a, v|
|
226
|
+
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
227
|
+
if m = opts[:maximum]
|
228
|
+
o.errors[a] << (opts[:message] || opts[:too_long]) unless v && v.size <= m
|
229
|
+
end
|
230
|
+
if m = opts[:minimum]
|
231
|
+
o.errors[a] << (opts[:message] || opts[:too_short]) unless v && v.size >= m
|
232
|
+
end
|
233
|
+
if i = opts[:is]
|
234
|
+
o.errors[a] << (opts[:message] || opts[:wrong_length]) unless v && v.size == i
|
235
|
+
end
|
236
|
+
if w = opts[:within]
|
237
|
+
o.errors[a] << (opts[:message] || opts[:wrong_length]) unless v && w.include?(v.size)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
NUMBER_RE = /^\d*\.{0,1}\d+$/
|
243
|
+
INTEGER_RE = /\A[+-]?\d+\Z/
|
244
|
+
|
245
|
+
# Validates whether an attribute is a number.
|
246
|
+
def validates_numericality_of(*atts)
|
247
|
+
opts = {
|
248
|
+
:message => 'is not a number',
|
249
|
+
}.merge!(atts.extract_options!)
|
250
|
+
|
251
|
+
re = opts[:only_integer] ? INTEGER_RE : NUMBER_RE
|
252
|
+
|
253
|
+
validates_each(*atts) do |o, a, v|
|
254
|
+
next if (v.nil? && opts[:allow_nil]) || (v.blank? && opts[:allow_blank])
|
255
|
+
o.errors[a] << opts[:message] unless v.to_s =~ re
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# Validates the presence of an attribute.
|
260
|
+
def validates_presence_of(*atts)
|
261
|
+
opts = {
|
262
|
+
:message => 'is not present',
|
263
|
+
}.merge!(atts.extract_options!)
|
264
|
+
|
265
|
+
validates_each(*atts) do |o, a, v|
|
266
|
+
o.errors[a] << opts[:message] unless v && !v.blank?
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Validates only if the fields in the model (specified by atts) are
|
271
|
+
# unique in the database. You should also add a unique index in the
|
272
|
+
# database, as this suffers from a fairly obvious race condition.
|
273
|
+
def validates_uniqueness_of(*atts)
|
274
|
+
opts = {
|
275
|
+
:message => 'is already taken',
|
276
|
+
}.merge!(atts.extract_options!)
|
277
|
+
|
278
|
+
validates_each(*atts) do |o, a, v|
|
279
|
+
next if v.blank?
|
280
|
+
num_dups = o.class.filter(a => v).count
|
281
|
+
allow = if num_dups == 0
|
282
|
+
# No unique value in the database
|
283
|
+
true
|
284
|
+
elsif num_dups > 1
|
285
|
+
# Multiple "unique" values in the database!!
|
286
|
+
# Someone didn't add a unique index
|
287
|
+
false
|
288
|
+
elsif o.new?
|
289
|
+
# New record, but unique value already exists in the database
|
290
|
+
false
|
291
|
+
elsif o.class[a => v].pk == o.pk
|
292
|
+
# Unique value exists in database, but for the same record, so the update won't cause a duplicate record
|
293
|
+
true
|
294
|
+
else
|
295
|
+
false
|
296
|
+
end
|
297
|
+
o.errors[a] << opts[:message] unless allow
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
4
302
|
|
5
303
|
module Sequel
|
6
304
|
class Model
|