sequel 5.18.0 → 5.20.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +40 -0
- data/doc/opening_databases.rdoc +5 -2
- data/doc/release_notes/5.19.0.txt +28 -0
- data/doc/release_notes/5.20.0.txt +89 -0
- data/doc/sharding.rdoc +12 -0
- data/doc/transactions.rdoc +38 -0
- data/lib/sequel/adapters/jdbc.rb +7 -2
- data/lib/sequel/adapters/mysql2.rb +2 -2
- data/lib/sequel/adapters/shared/postgres.rb +8 -8
- data/lib/sequel/adapters/shared/sqlite.rb +3 -1
- data/lib/sequel/adapters/sqlanywhere.rb +33 -17
- data/lib/sequel/adapters/sqlite.rb +20 -13
- data/lib/sequel/connection_pool.rb +0 -5
- data/lib/sequel/database/misc.rb +10 -9
- data/lib/sequel/database/query.rb +1 -1
- data/lib/sequel/database/schema_generator.rb +1 -1
- data/lib/sequel/database/transactions.rb +57 -5
- data/lib/sequel/dataset/actions.rb +6 -5
- data/lib/sequel/dataset/graph.rb +2 -2
- data/lib/sequel/dataset/placeholder_literalizer.rb +4 -1
- data/lib/sequel/dataset/prepared_statements.rb +1 -1
- data/lib/sequel/dataset/query.rb +1 -1
- data/lib/sequel/extensions/constraint_validations.rb +14 -0
- data/lib/sequel/extensions/pg_enum.rb +23 -15
- data/lib/sequel/extensions/schema_dumper.rb +1 -1
- data/lib/sequel/model/associations.rb +38 -12
- data/lib/sequel/model/base.rb +1 -1
- data/lib/sequel/model/plugins.rb +104 -0
- data/lib/sequel/plugins/association_dependencies.rb +3 -3
- data/lib/sequel/plugins/association_pks.rb +14 -4
- data/lib/sequel/plugins/class_table_inheritance.rb +1 -0
- data/lib/sequel/plugins/composition.rb +13 -9
- data/lib/sequel/plugins/finder.rb +2 -2
- data/lib/sequel/plugins/hook_class_methods.rb +17 -5
- data/lib/sequel/plugins/inverted_subsets.rb +2 -2
- data/lib/sequel/plugins/json_serializer.rb +3 -3
- data/lib/sequel/plugins/nested_attributes.rb +1 -1
- data/lib/sequel/plugins/pg_array_associations.rb +8 -4
- data/lib/sequel/plugins/pg_auto_constraint_validations.rb +61 -32
- data/lib/sequel/plugins/prepared_statements.rb +1 -1
- data/lib/sequel/plugins/prepared_statements_safe.rb +1 -1
- data/lib/sequel/plugins/subset_conditions.rb +2 -2
- data/lib/sequel/plugins/validation_class_methods.rb +5 -3
- data/lib/sequel/plugins/validation_helpers.rb +2 -2
- data/lib/sequel/sql.rb +1 -1
- data/lib/sequel/version.rb +1 -1
- data/spec/adapters/postgres_spec.rb +40 -0
- data/spec/core/database_spec.rb +73 -2
- data/spec/core/schema_spec.rb +7 -1
- data/spec/extensions/class_table_inheritance_spec.rb +30 -8
- data/spec/extensions/constraint_validations_spec.rb +20 -2
- data/spec/extensions/core_refinements_spec.rb +1 -1
- data/spec/extensions/hook_class_methods_spec.rb +22 -0
- data/spec/extensions/migration_spec.rb +13 -0
- data/spec/extensions/pg_auto_constraint_validations_spec.rb +8 -0
- data/spec/extensions/pg_enum_spec.rb +5 -0
- data/spec/extensions/s_spec.rb +1 -1
- data/spec/extensions/schema_dumper_spec.rb +4 -2
- data/spec/integration/plugin_test.rb +15 -0
- data/spec/integration/transaction_test.rb +50 -0
- data/spec/model/associations_spec.rb +84 -4
- data/spec/model/plugins_spec.rb +111 -0
- metadata +7 -3
data/lib/sequel/model/base.rb
CHANGED
@@ -1517,7 +1517,7 @@ module Sequel
|
|
1517
1517
|
# # Sequel::Error raised
|
1518
1518
|
def set_fields(hash, fields, opts=nil)
|
1519
1519
|
opts = if opts
|
1520
|
-
|
1520
|
+
model.default_set_fields_options.merge(opts)
|
1521
1521
|
else
|
1522
1522
|
model.default_set_fields_options
|
1523
1523
|
end
|
data/lib/sequel/model/plugins.rb
CHANGED
@@ -51,5 +51,109 @@ module Sequel
|
|
51
51
|
r
|
52
52
|
end
|
53
53
|
end
|
54
|
+
|
55
|
+
method_num = 0
|
56
|
+
method_num_mutex = Mutex.new
|
57
|
+
# Return a unique method name symbol for the given suffix.
|
58
|
+
SEQUEL_METHOD_NAME = lambda do |suffix|
|
59
|
+
:"_sequel_#{suffix}_#{method_num_mutex.synchronize{method_num += 1}}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Define a private instance method using the block with the provided name and
|
63
|
+
# expected arity. If the name is given as a Symbol, it is used directly.
|
64
|
+
# If the name is given as a String, a unique name will be generated using
|
65
|
+
# that string. The expected_arity should be either 0 (no arguments) or
|
66
|
+
# 1 (single argument).
|
67
|
+
#
|
68
|
+
# If a block with an arity that does not match the expected arity is used,
|
69
|
+
# a deprecation warning will be issued. The method defined should still
|
70
|
+
# work, though it will be slower than a method with the expected arity.
|
71
|
+
#
|
72
|
+
# Sequel only checks arity for regular blocks, not lambdas. Lambdas were
|
73
|
+
# already strict in regards to arity, so there is no need to try to fix
|
74
|
+
# arity to keep backwards compatibility for lambdas.
|
75
|
+
#
|
76
|
+
# Blocks with required keyword arguments are not supported by this method.
|
77
|
+
def self.def_sequel_method(model, meth, expected_arity, &block)
|
78
|
+
if meth.is_a?(String)
|
79
|
+
meth = SEQUEL_METHOD_NAME.call(meth)
|
80
|
+
end
|
81
|
+
call_meth = meth
|
82
|
+
|
83
|
+
unless block.lambda?
|
84
|
+
required_args, optional_args, rest, keyword = _define_sequel_method_arg_numbers(block)
|
85
|
+
|
86
|
+
if keyword == :required
|
87
|
+
raise Error, "cannot use block with required keyword arguments when calling define_sequel_method with expected arity #{expected_arity}"
|
88
|
+
end
|
89
|
+
|
90
|
+
case expected_arity
|
91
|
+
when 0
|
92
|
+
unless required_args == 0
|
93
|
+
# SEQUEL6: remove
|
94
|
+
Sequel::Deprecation.deprecate("Arity mismatch in block passed to define_sequel_method. Expected Arity 0, but arguments required for #{block.inspect}. Support for this will be removed in Sequel 6.")
|
95
|
+
b = block
|
96
|
+
block = lambda{instance_exec(&b)} # Fallback
|
97
|
+
end
|
98
|
+
when 1
|
99
|
+
if required_args == 0 && optional_args == 0 && !rest
|
100
|
+
# SEQUEL6: remove
|
101
|
+
Sequel::Deprecation.deprecate("Arity mismatch in block passed to define_sequel_method. Expected Arity 1, but no arguments accepted for #{block.inspect}. Support for this will be removed in Sequel 6.")
|
102
|
+
temp_method = SEQUEL_METHOD_NAME.call("temp")
|
103
|
+
model.class_eval("def #{temp_method}(_) #{meth =~ /\A\w+\z/ ? "#{meth}_arity" : "send(:\"#{meth}_arity\")"} end", __FILE__, __LINE__)
|
104
|
+
model.send(:alias_method, meth, temp_method)
|
105
|
+
model.send(:undef_method, temp_method)
|
106
|
+
model.send(:private, meth)
|
107
|
+
meth = :"#{meth}_arity"
|
108
|
+
elsif required_args > 1
|
109
|
+
# SEQUEL6: remove
|
110
|
+
Sequel::Deprecation.deprecate("Arity mismatch in block passed to define_sequel_method. Expected Arity 1, but more arguments required for #{block.inspect}. Support for this will be removed in Sequel 6.")
|
111
|
+
b = block
|
112
|
+
block = lambda{|r| instance_exec(r, &b)} # Fallback
|
113
|
+
end
|
114
|
+
else
|
115
|
+
raise Error, "unexpected arity passed to define_sequel_method: #{expected_arity.inspect}"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
model.send(:define_method, meth, &block)
|
120
|
+
model.send(:private, meth)
|
121
|
+
call_meth
|
122
|
+
end
|
123
|
+
|
124
|
+
# Return the number of required argument, optional arguments,
|
125
|
+
# whether the callable accepts any additional arguments,
|
126
|
+
# and whether the callable accepts keyword arguments (true, false
|
127
|
+
# or :required).
|
128
|
+
def self._define_sequel_method_arg_numbers(callable)
|
129
|
+
optional_args = 0
|
130
|
+
rest = false
|
131
|
+
keyword = false
|
132
|
+
callable.parameters.map(&:first).each do |arg_type, _|
|
133
|
+
case arg_type
|
134
|
+
when :opt
|
135
|
+
optional_args += 1
|
136
|
+
when :rest
|
137
|
+
rest = true
|
138
|
+
when :keyreq
|
139
|
+
keyword = :required
|
140
|
+
when :key, :keyrest
|
141
|
+
keyword ||= true
|
142
|
+
end
|
143
|
+
end
|
144
|
+
arity = callable.arity
|
145
|
+
if arity < 0
|
146
|
+
arity = arity.abs - 1
|
147
|
+
end
|
148
|
+
required_args = arity
|
149
|
+
arity -= 1 if keyword == :required
|
150
|
+
|
151
|
+
if callable.is_a?(Proc) && !callable.lambda?
|
152
|
+
optional_args -= arity
|
153
|
+
end
|
154
|
+
|
155
|
+
[required_args, optional_args, rest, keyword]
|
156
|
+
end
|
157
|
+
private_class_method :_define_sequel_method_arg_numbers
|
54
158
|
end
|
55
159
|
end
|
@@ -60,9 +60,9 @@ module Sequel
|
|
60
60
|
association_dependencies[:"#{time}_#{action}"] << if action == :nullify
|
61
61
|
case type
|
62
62
|
when :one_to_many , :many_to_many
|
63
|
-
|
63
|
+
[r[:remove_all_method]]
|
64
64
|
when :one_to_one
|
65
|
-
|
65
|
+
[r[:setter_method], nil]
|
66
66
|
else
|
67
67
|
raise(Error, "Can't nullify many_to_one associated objects: association: #{association}")
|
68
68
|
end
|
@@ -97,7 +97,7 @@ module Sequel
|
|
97
97
|
def before_destroy
|
98
98
|
model.association_dependencies[:before_delete].each{|m| public_send(m).delete}
|
99
99
|
model.association_dependencies[:before_destroy].each{|m| public_send(m).destroy}
|
100
|
-
model.association_dependencies[:before_nullify].each{|
|
100
|
+
model.association_dependencies[:before_nullify].each{|args| public_send(*args)}
|
101
101
|
super
|
102
102
|
end
|
103
103
|
end
|
@@ -60,8 +60,15 @@ module Sequel
|
|
60
60
|
|
61
61
|
# Define a association_pks method using the block for the association reflection
|
62
62
|
def def_association_pks_methods(opts)
|
63
|
+
opts[:pks_getter_method] = :"#{singularize(opts[:name])}_pks_getter"
|
64
|
+
association_module_def(opts[:pks_getter_method], &opts[:pks_getter])
|
63
65
|
association_module_def(:"#{singularize(opts[:name])}_pks", opts){_association_pks_getter(opts)}
|
64
|
-
|
66
|
+
|
67
|
+
if opts[:pks_setter]
|
68
|
+
opts[:pks_setter_method] = :"#{singularize(opts[:name])}_pks_setter"
|
69
|
+
association_module_def(opts[:pks_setter_method], &opts[:pks_setter])
|
70
|
+
association_module_def(:"#{singularize(opts[:name])}_pks=", opts){|pks| _association_pks_setter(opts, pks)}
|
71
|
+
end
|
65
72
|
end
|
66
73
|
|
67
74
|
# Add a getter that checks the join table for matching records and
|
@@ -181,7 +188,8 @@ module Sequel
|
|
181
188
|
def after_save
|
182
189
|
if assoc_pks = @_association_pks
|
183
190
|
assoc_pks.each do |name, pks|
|
184
|
-
|
191
|
+
# pks_setter_method is private
|
192
|
+
send(model.association_reflection(name)[:pks_setter_method], pks)
|
185
193
|
end
|
186
194
|
@_association_pks = nil
|
187
195
|
end
|
@@ -206,7 +214,8 @@ module Sequel
|
|
206
214
|
elsif delay && @_association_pks && (objs = @_association_pks[opts[:name]])
|
207
215
|
objs
|
208
216
|
else
|
209
|
-
|
217
|
+
# pks_getter_method is private
|
218
|
+
send(opts[:pks_getter_method])
|
210
219
|
end
|
211
220
|
end
|
212
221
|
|
@@ -231,7 +240,8 @@ module Sequel
|
|
231
240
|
modified!
|
232
241
|
(@_association_pks ||= {})[opts[:name]] = pks
|
233
242
|
else
|
234
|
-
|
243
|
+
# pks_setter_method is private
|
244
|
+
send(opts[:pks_setter_method], pks)
|
235
245
|
end
|
236
246
|
end
|
237
247
|
|
@@ -32,9 +32,10 @@ module Sequel
|
|
32
32
|
#
|
33
33
|
# The :mapping option is just a shortcut that works in particular
|
34
34
|
# cases. To handle any case, you can define a custom :composer
|
35
|
-
# and :decomposer procs. The :composer
|
36
|
-
#
|
37
|
-
#
|
35
|
+
# and :decomposer procs. The :composer and :decomposer procs will
|
36
|
+
# be used to define instance methods. The :composer will be called
|
37
|
+
# the first time the getter is called, and the :decomposer
|
38
|
+
# will be called before saving. The above example could
|
38
39
|
# also be implemented as:
|
39
40
|
#
|
40
41
|
# Album.composition :date,
|
@@ -74,9 +75,9 @@ module Sequel
|
|
74
75
|
#
|
75
76
|
# Options:
|
76
77
|
# :class :: if using the :mapping option, the class to use, as a Class, String or Symbol.
|
77
|
-
# :composer :: A proc
|
78
|
+
# :composer :: A proc used to define the method that the composition getter method will call
|
78
79
|
# to create the composition.
|
79
|
-
# :decomposer :: A proc
|
80
|
+
# :decomposer :: A proc used to define the method called before saving the model object,
|
80
81
|
# if the composition object exists, which sets the columns in the model object
|
81
82
|
# based on the value of the composition object.
|
82
83
|
# :mapping :: An array where each element is either a symbol or an array of two symbols.
|
@@ -129,15 +130,17 @@ module Sequel
|
|
129
130
|
|
130
131
|
# Define getter and setter methods for the composition object.
|
131
132
|
def define_composition_accessor(name, opts=OPTS)
|
132
|
-
|
133
|
+
composer_meth = opts[:composer_method] = Plugins.def_sequel_method(@composition_module, "#{name}_composer", 0, &opts[:composer])
|
134
|
+
opts[:decomposer_method] = Plugins.def_sequel_method(@composition_module, "#{name}_decomposer", 0, &opts[:decomposer])
|
133
135
|
@composition_module.class_eval do
|
134
136
|
define_method(name) do
|
135
137
|
if compositions.has_key?(name)
|
136
138
|
compositions[name]
|
137
139
|
elsif frozen?
|
138
|
-
|
140
|
+
# composer_meth is private
|
141
|
+
send(composer_meth)
|
139
142
|
else
|
140
|
-
compositions[name] =
|
143
|
+
compositions[name] = send(composer_meth)
|
141
144
|
end
|
142
145
|
end
|
143
146
|
define_method("#{name}=") do |v|
|
@@ -171,7 +174,8 @@ module Sequel
|
|
171
174
|
# For each composition, set the columns in the model class based
|
172
175
|
# on the composition object.
|
173
176
|
def before_validation
|
174
|
-
|
177
|
+
# decomposer_method is private
|
178
|
+
@compositions.keys.each{|n| send(model.compositions[n][:decomposer_method])} if @compositions
|
175
179
|
super
|
176
180
|
end
|
177
181
|
|
@@ -59,7 +59,14 @@ module Sequel
|
|
59
59
|
|
60
60
|
# Yield every block related to the given hook.
|
61
61
|
def hook_blocks(hook)
|
62
|
-
|
62
|
+
# SEQUEL6: Remove
|
63
|
+
Sequel::Deprecation.deprecate("The hook_blocks class method in the hook_class_methods plugin is deprecated and will be removed in Sequel 6.")
|
64
|
+
@hooks[hook].each{|_,v,_| yield v}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Yield every method related to the given hook.
|
68
|
+
def hook_methods_for(hook)
|
69
|
+
@hooks[hook].each{|_,_,m| yield m}
|
63
70
|
end
|
64
71
|
|
65
72
|
Plugins.inherited_instance_variables(self, :@hooks=>:hash_dup)
|
@@ -75,23 +82,28 @@ module Sequel
|
|
75
82
|
# Allow calling private hook methods
|
76
83
|
block = proc {send(tag)}
|
77
84
|
end
|
85
|
+
|
78
86
|
h = @hooks[hook]
|
87
|
+
|
79
88
|
if tag && (old = h.find{|x| x[0] == tag})
|
80
89
|
old[1] = block
|
90
|
+
Plugins.def_sequel_method(self, old[2], 0, &block)
|
81
91
|
else
|
92
|
+
meth = Plugins.def_sequel_method(self, "validation_class_methods_#{hook}", 0, &block)
|
82
93
|
if hook.to_s =~ /^before/
|
83
|
-
h.unshift([tag,block])
|
94
|
+
h.unshift([tag, block, meth])
|
84
95
|
else
|
85
|
-
h << [tag, block]
|
96
|
+
h << [tag, block, meth]
|
86
97
|
end
|
87
98
|
end
|
88
99
|
end
|
89
100
|
end
|
90
101
|
|
91
102
|
module InstanceMethods
|
92
|
-
|
103
|
+
# hook methods are private
|
104
|
+
[:before_create, :before_update, :before_validation, :before_save, :before_destroy].each{|h| class_eval("def #{h}; model.hook_methods_for(:#{h}){|m| send(m)}; super end", __FILE__, __LINE__)}
|
93
105
|
|
94
|
-
[:after_create, :after_update, :after_validation, :after_save, :after_destroy].each{|h| class_eval("def #{h}; super; model.
|
106
|
+
[:after_create, :after_update, :after_validation, :after_save, :after_destroy].each{|h| class_eval("def #{h}; super; model.hook_methods_for(:#{h}){|m| send(m)}; end", __FILE__, __LINE__)}
|
95
107
|
end
|
96
108
|
end
|
97
109
|
end
|
@@ -29,8 +29,8 @@ module Sequel
|
|
29
29
|
# # SELECT * FROM albums WHERE (published IS NOT TRUE)
|
30
30
|
#
|
31
31
|
module InvertedSubsets
|
32
|
-
def self.apply(
|
33
|
-
|
32
|
+
def self.apply(model, &block)
|
33
|
+
model.instance_exec do
|
34
34
|
@dataset_module_class = Class.new(@dataset_module_class) do
|
35
35
|
include DatasetModuleMethods
|
36
36
|
if block
|
@@ -279,7 +279,7 @@ module Sequel
|
|
279
279
|
# obj.json_serializer_opts(only: :name)
|
280
280
|
# [obj].to_json # => '[{"name":"..."}]'
|
281
281
|
def json_serializer_opts(opts=OPTS)
|
282
|
-
@json_serializer_opts =
|
282
|
+
@json_serializer_opts = (@json_serializer_opts||OPTS).merge(opts)
|
283
283
|
end
|
284
284
|
|
285
285
|
# Return a string in JSON format. Accepts the following
|
@@ -301,9 +301,9 @@ module Sequel
|
|
301
301
|
# use an underscored version of the model's name.
|
302
302
|
def to_json(*a)
|
303
303
|
opts = model.json_serializer_opts
|
304
|
-
opts =
|
304
|
+
opts = opts.merge(@json_serializer_opts) if @json_serializer_opts
|
305
305
|
if (arg_opts = a.first).is_a?(Hash)
|
306
|
-
opts =
|
306
|
+
opts = opts.merge(arg_opts)
|
307
307
|
a = []
|
308
308
|
end
|
309
309
|
|
@@ -155,7 +155,7 @@ module Sequel
|
|
155
155
|
def set_nested_attributes(assoc, obj, opts=OPTS)
|
156
156
|
raise(Error, "no association named #{assoc} for #{model.inspect}") unless ref = model.association_reflection(assoc)
|
157
157
|
raise(Error, "nested attributes are not enabled for association #{assoc} for #{model.inspect}") unless meta = ref[:nested_attributes]
|
158
|
-
meta =
|
158
|
+
meta = meta.merge(opts)
|
159
159
|
meta[:reflection] = ref
|
160
160
|
if ref.returns_array?
|
161
161
|
nested_attributes_list_setter(meta, obj)
|
@@ -337,8 +337,10 @@ module Sequel
|
|
337
337
|
end
|
338
338
|
opts[:eager_loader] ||= proc do |eo|
|
339
339
|
id_map = eo[:id_map]
|
340
|
+
eo = Hash[eo]
|
341
|
+
eo[:loader] = false
|
340
342
|
|
341
|
-
eager_load_results(opts,
|
343
|
+
eager_load_results(opts, eo) do |assoc_record|
|
342
344
|
if pks ||= assoc_record.get_column_value(key)
|
343
345
|
pks.each do |pkv|
|
344
346
|
next unless objects = id_map[pkv]
|
@@ -374,7 +376,7 @@ module Sequel
|
|
374
376
|
|
375
377
|
opts[:eager_grapher] ||= proc do |eo|
|
376
378
|
ds = eo[:self]
|
377
|
-
ds = ds.graph(eager_graph_dataset(opts, eo), conditions,
|
379
|
+
ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep), &graph_block)
|
378
380
|
ds
|
379
381
|
end
|
380
382
|
|
@@ -433,7 +435,9 @@ module Sequel
|
|
433
435
|
end
|
434
436
|
end
|
435
437
|
|
436
|
-
|
438
|
+
eo = Hash[eo]
|
439
|
+
eo[:id_map] = id_map
|
440
|
+
eager_load_results(opts, eo) do |assoc_record|
|
437
441
|
if objects = id_map[assoc_record.get_column_value(pkm)]
|
438
442
|
objects.each do |object|
|
439
443
|
object.associations[name].push(assoc_record)
|
@@ -466,7 +470,7 @@ module Sequel
|
|
466
470
|
|
467
471
|
opts[:eager_grapher] ||= proc do |eo|
|
468
472
|
ds = eo[:self]
|
469
|
-
ds = ds.graph(eager_graph_dataset(opts, eo), conditions,
|
473
|
+
ds = ds.graph(eager_graph_dataset(opts, eo), conditions, eo.merge(:select=>select, :join_type=>eo[:join_type]||join_type, :qualify=>:deep), &graph_block)
|
470
474
|
ds
|
471
475
|
end
|
472
476
|
|
@@ -22,8 +22,9 @@ module Sequel
|
|
22
22
|
# This plugin is not intended as a replacement for other validations,
|
23
23
|
# it is intended as a last resort. The purpose of validations is to provide nice
|
24
24
|
# error messages for the user, and the error messages generated by this plugin are
|
25
|
-
# fairly generic. The error messages can be customized
|
26
|
-
#
|
25
|
+
# fairly generic by default. The error messages can be customized per constraint type
|
26
|
+
# using the :messages plugin option, and individually per constraint using
|
27
|
+
# +pg_auto_constraint_validation_override+ (see below).
|
27
28
|
#
|
28
29
|
# This plugin only works on the postgres adapter when using the pg 0.16+ driver,
|
29
30
|
# PostgreSQL 9.3+ server, and PostgreSQL 9.3+ client library (libpq). In other cases
|
@@ -37,6 +38,13 @@ module Sequel
|
|
37
38
|
# rescue Sequel::ValidationFailed
|
38
39
|
# album.errors.on(:artist_id) # ['is invalid']
|
39
40
|
# end
|
41
|
+
#
|
42
|
+
# While the database usually provides enough information to correctly associated
|
43
|
+
# constraint violations with model columns, there are cases where it does not.
|
44
|
+
# In those cases, you can override the handling of specific constraint violations
|
45
|
+
# to be associated to particular column(s), and use a specific error message:
|
46
|
+
#
|
47
|
+
# Album.pg_auto_constraint_validation_override(:constraint_name, [:column1], "validation error message")
|
40
48
|
#
|
41
49
|
# Usage:
|
42
50
|
#
|
@@ -80,6 +88,16 @@ module Sequel
|
|
80
88
|
Plugins.inherited_instance_variables(self, :@pg_auto_constraint_validations=>nil, :@pg_auto_constraint_validations_messages=>nil)
|
81
89
|
Plugins.after_set_dataset(self, :setup_pg_auto_constraint_validations)
|
82
90
|
|
91
|
+
# Override the constraint validation columns and message for a given constraint
|
92
|
+
def pg_auto_constraint_validation_override(constraint, columns, message)
|
93
|
+
pgacv = Hash[@pg_auto_constraint_validations]
|
94
|
+
overrides = pgacv[:overrides] = Hash[pgacv[:overrides]]
|
95
|
+
overrides[constraint] = [Array(columns), message].freeze
|
96
|
+
overrides.freeze
|
97
|
+
@pg_auto_constraint_validations = pgacv.freeze
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
|
83
101
|
private
|
84
102
|
|
85
103
|
# Get the list of constraints, unique indexes, foreign keys in the current
|
@@ -110,7 +128,7 @@ module Sequel
|
|
110
128
|
referenced_by = {}
|
111
129
|
|
112
130
|
db.check_constraints(table_name).each do |k, v|
|
113
|
-
checks[k] = v[:columns].dup.freeze
|
131
|
+
checks[k] = v[:columns].dup.freeze unless v[:columns].empty?
|
114
132
|
end
|
115
133
|
db.indexes(table_name, :include_partial=>true).each do |k, v|
|
116
134
|
if v[:unique]
|
@@ -134,7 +152,8 @@ module Sequel
|
|
134
152
|
:check=>checks,
|
135
153
|
:unique=>indexes,
|
136
154
|
:foreign_key=>foreign_keys,
|
137
|
-
:referenced_by=>referenced_by
|
155
|
+
:referenced_by=>referenced_by,
|
156
|
+
:overrides=>OPTS
|
138
157
|
}.freeze).each_value(&:freeze)
|
139
158
|
end
|
140
159
|
end
|
@@ -158,40 +177,50 @@ module Sequel
|
|
158
177
|
m = ds.method(:output_identifier)
|
159
178
|
schema = info[:schema]
|
160
179
|
table = info[:table]
|
180
|
+
|
161
181
|
if constraint = info[:constraint]
|
162
182
|
constraint = m.call(constraint)
|
183
|
+
|
184
|
+
columns, message = cv_info[:overrides][constraint]
|
185
|
+
if columns
|
186
|
+
override = true
|
187
|
+
add_pg_constraint_validation_error(columns, message)
|
188
|
+
end
|
163
189
|
end
|
190
|
+
|
164
191
|
messages = model.pg_auto_constraint_validations_messages
|
165
192
|
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
when Sequel::ForeignKeyConstraintViolation
|
180
|
-
message_primary = info[:message_primary]
|
181
|
-
if message_primary.start_with?('update')
|
182
|
-
# This constraint violation is different from the others, because the constraint
|
183
|
-
# referenced is a constraint for a different table, not for this table. This
|
184
|
-
# happens when another table references the current table, and the referenced
|
185
|
-
# column in the current update is modified such that referential integrity
|
186
|
-
# would be broken. Use the reverse foreign key information to figure out
|
187
|
-
# which column is affected in that case.
|
188
|
-
skip_schema_table_check = true
|
189
|
-
if columns = cv_info[:referenced_by][[m.call(schema), m.call(table), constraint]]
|
190
|
-
add_pg_constraint_validation_error(columns, messages[:referenced_by])
|
193
|
+
unless override
|
194
|
+
case e
|
195
|
+
when Sequel::NotNullConstraintViolation
|
196
|
+
if column = info[:column]
|
197
|
+
add_pg_constraint_validation_error([m.call(column)], messages[:not_null])
|
198
|
+
end
|
199
|
+
when Sequel::CheckConstraintViolation
|
200
|
+
if columns = cv_info[:check][constraint]
|
201
|
+
add_pg_constraint_validation_error(columns, messages[:check])
|
202
|
+
end
|
203
|
+
when Sequel::UniqueConstraintViolation
|
204
|
+
if columns = cv_info[:unique][constraint]
|
205
|
+
add_pg_constraint_validation_error(columns, messages[:unique])
|
191
206
|
end
|
192
|
-
|
193
|
-
|
194
|
-
|
207
|
+
when Sequel::ForeignKeyConstraintViolation
|
208
|
+
message_primary = info[:message_primary]
|
209
|
+
if message_primary.start_with?('update')
|
210
|
+
# This constraint violation is different from the others, because the constraint
|
211
|
+
# referenced is a constraint for a different table, not for this table. This
|
212
|
+
# happens when another table references the current table, and the referenced
|
213
|
+
# column in the current update is modified such that referential integrity
|
214
|
+
# would be broken. Use the reverse foreign key information to figure out
|
215
|
+
# which column is affected in that case.
|
216
|
+
skip_schema_table_check = true
|
217
|
+
if columns = cv_info[:referenced_by][[m.call(schema), m.call(table), constraint]]
|
218
|
+
add_pg_constraint_validation_error(columns, messages[:referenced_by])
|
219
|
+
end
|
220
|
+
elsif message_primary.start_with?('insert')
|
221
|
+
if columns = cv_info[:foreign_key][constraint]
|
222
|
+
add_pg_constraint_validation_error(columns, messages[:foreign_key])
|
223
|
+
end
|
195
224
|
end
|
196
225
|
end
|
197
226
|
end
|