sequel 5.19.0 → 5.20.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.
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