sequel 5.19.0 → 5.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +24 -0
  3. data/doc/release_notes/5.20.0.txt +89 -0
  4. data/doc/transactions.rdoc +38 -0
  5. data/lib/sequel/adapters/mysql2.rb +2 -2
  6. data/lib/sequel/adapters/shared/postgres.rb +5 -7
  7. data/lib/sequel/database/query.rb +1 -1
  8. data/lib/sequel/database/schema_generator.rb +1 -1
  9. data/lib/sequel/database/transactions.rb +57 -5
  10. data/lib/sequel/dataset/placeholder_literalizer.rb +4 -1
  11. data/lib/sequel/dataset/query.rb +1 -1
  12. data/lib/sequel/extensions/schema_dumper.rb +1 -1
  13. data/lib/sequel/model/associations.rb +35 -9
  14. data/lib/sequel/model/plugins.rb +104 -0
  15. data/lib/sequel/plugins/association_dependencies.rb +3 -3
  16. data/lib/sequel/plugins/association_pks.rb +14 -4
  17. data/lib/sequel/plugins/class_table_inheritance.rb +1 -0
  18. data/lib/sequel/plugins/composition.rb +13 -9
  19. data/lib/sequel/plugins/finder.rb +2 -2
  20. data/lib/sequel/plugins/hook_class_methods.rb +17 -5
  21. data/lib/sequel/plugins/inverted_subsets.rb +2 -2
  22. data/lib/sequel/plugins/pg_auto_constraint_validations.rb +61 -32
  23. data/lib/sequel/plugins/subset_conditions.rb +2 -2
  24. data/lib/sequel/plugins/validation_class_methods.rb +5 -3
  25. data/lib/sequel/version.rb +1 -1
  26. data/spec/adapters/postgres_spec.rb +32 -0
  27. data/spec/core/database_spec.rb +73 -2
  28. data/spec/core/schema_spec.rb +6 -0
  29. data/spec/extensions/class_table_inheritance_spec.rb +30 -8
  30. data/spec/extensions/core_refinements_spec.rb +1 -1
  31. data/spec/extensions/hook_class_methods_spec.rb +22 -0
  32. data/spec/extensions/migration_spec.rb +13 -0
  33. data/spec/extensions/pg_auto_constraint_validations_spec.rb +8 -0
  34. data/spec/extensions/s_spec.rb +1 -1
  35. data/spec/extensions/schema_dumper_spec.rb +4 -2
  36. data/spec/integration/plugin_test.rb +15 -0
  37. data/spec/integration/transaction_test.rb +50 -0
  38. data/spec/model/associations_spec.rb +84 -4
  39. data/spec/model/plugins_spec.rb +111 -0
  40. metadata +4 -2
@@ -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
- proc{public_send(r[:remove_all_method])}
63
+ [r[:remove_all_method]]
64
64
  when :one_to_one
65
- proc{public_send(r[:setter_method], nil)}
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{|p| instance_exec(&p)}
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
- association_module_def(:"#{singularize(opts[:name])}_pks=", opts){|pks| _association_pks_setter(opts, pks)} if opts[:pks_setter]
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
- instance_exec(pks, &model.association_reflection(name)[:pks_setter])
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
- instance_exec(&opts[:pks_getter])
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
- instance_exec(pks, &opts[:pks_setter])
243
+ # pks_setter_method is private
244
+ send(opts[:pks_setter_method], pks)
235
245
  end
236
246
  end
237
247
 
@@ -326,6 +326,7 @@ module Sequel
326
326
  cti_tables.reverse_each do |ct|
327
327
  db.schema(ct).each{|sk,v| db_schema[sk] = v}
328
328
  end
329
+ setup_auto_validations if respond_to?(:setup_auto_validations, true)
329
330
  end
330
331
  end
331
332
 
@@ -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 proc will be instance_execed
36
- # the first time the getter is called, and the :decomposer proc
37
- # will be instance_execed before saving. The above example could
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 that is instance_execed when the composition getter method is called
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 that is instance_execed before saving the model object,
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
- composer = opts[:composer]
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
- instance_exec(&composer)
140
+ # composer_meth is private
141
+ send(composer_meth)
139
142
  else
140
- compositions[name] = instance_exec(&composer)
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
- @compositions.keys.each{|n| instance_exec(&model.compositions[n][:decomposer])} if @compositions
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
 
@@ -35,8 +35,8 @@ module Sequel
35
35
  module Finder
36
36
  FINDER_TYPES = [:first, :all, :each, :get].freeze
37
37
 
38
- def self.apply(mod)
39
- mod.instance_exec do
38
+ def self.apply(model)
39
+ model.instance_exec do
40
40
  @finders ||= {}
41
41
  @finder_loaders ||= {}
42
42
  end
@@ -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
- @hooks[hook].each{|k,v| yield v}
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
- [:before_create, :before_update, :before_validation, :before_save, :before_destroy].each{|h| class_eval("def #{h}; model.hook_blocks(:#{h}){|b| instance_exec(&b)}; super end", __FILE__, __LINE__)}
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.hook_blocks(:#{h}){|b| instance_exec(&b)}; end", __FILE__, __LINE__)}
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(mod, &block)
33
- mod.instance_exec do
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
@@ -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 using the :messages plugin
26
- # option, but there is only a single message used per constraint type.
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
- case e
167
- when Sequel::NotNullConstraintViolation
168
- if column = info[:column]
169
- add_pg_constraint_validation_error([m.call(column)], messages[:not_null])
170
- end
171
- when Sequel::CheckConstraintViolation
172
- if columns = cv_info[:check][constraint]
173
- add_pg_constraint_validation_error(columns, messages[:check])
174
- end
175
- when Sequel::UniqueConstraintViolation
176
- if columns = cv_info[:unique][constraint]
177
- add_pg_constraint_validation_error(columns, messages[:unique])
178
- end
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
- elsif message_primary.start_with?('insert')
193
- if columns = cv_info[:foreign_key][constraint]
194
- add_pg_constraint_validation_error(columns, messages[:foreign_key])
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