sequel 4.18.0 → 4.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +18 -0
  3. data/MIT-LICENSE +1 -1
  4. data/doc/advanced_associations.rdoc +1 -1
  5. data/doc/association_basics.rdoc +55 -34
  6. data/doc/model_hooks.rdoc +7 -5
  7. data/doc/release_notes/4.19.0.txt +45 -0
  8. data/doc/validations.rdoc +4 -0
  9. data/lib/sequel/adapters/shared/mysql.rb +5 -3
  10. data/lib/sequel/adapters/shared/sqlanywhere.rb +3 -2
  11. data/lib/sequel/dataset/sql.rb +1 -1
  12. data/lib/sequel/extensions/migration.rb +12 -8
  13. data/lib/sequel/model/associations.rb +24 -24
  14. data/lib/sequel/model/base.rb +39 -8
  15. data/lib/sequel/plugins/accessed_columns.rb +61 -0
  16. data/lib/sequel/plugins/association_pks.rb +4 -4
  17. data/lib/sequel/plugins/boolean_readers.rb +1 -1
  18. data/lib/sequel/plugins/class_table_inheritance.rb +1 -1
  19. data/lib/sequel/plugins/column_conflicts.rb +93 -0
  20. data/lib/sequel/plugins/composition.rb +3 -3
  21. data/lib/sequel/plugins/dirty.rb +6 -6
  22. data/lib/sequel/plugins/json_serializer.rb +1 -1
  23. data/lib/sequel/plugins/list.rb +4 -4
  24. data/lib/sequel/plugins/mssql_optimistic_locking.rb +1 -1
  25. data/lib/sequel/plugins/nested_attributes.rb +2 -2
  26. data/lib/sequel/plugins/optimistic_locking.rb +3 -3
  27. data/lib/sequel/plugins/pg_array_associations.rb +23 -23
  28. data/lib/sequel/plugins/prepared_statements_associations.rb +1 -1
  29. data/lib/sequel/plugins/rcte_tree.rb +2 -2
  30. data/lib/sequel/plugins/serialization.rb +1 -1
  31. data/lib/sequel/plugins/single_table_inheritance.rb +1 -1
  32. data/lib/sequel/plugins/timestamps.rb +2 -2
  33. data/lib/sequel/plugins/typecast_on_load.rb +1 -1
  34. data/lib/sequel/plugins/validation_class_methods.rb +4 -4
  35. data/lib/sequel/plugins/validation_helpers.rb +2 -2
  36. data/lib/sequel/version.rb +1 -1
  37. data/spec/core/dataset_spec.rb +7 -0
  38. data/spec/extensions/accessed_columns_spec.rb +51 -0
  39. data/spec/extensions/column_conflicts_spec.rb +55 -0
  40. data/spec/extensions/hook_class_methods_spec.rb +18 -5
  41. data/spec/extensions/migration_spec.rb +4 -1
  42. data/spec/extensions/static_cache_spec.rb +3 -3
  43. data/spec/model/hooks_spec.rb +76 -9
  44. data/spec/model/record_spec.rb +43 -2
  45. metadata +8 -2
@@ -1143,6 +1143,20 @@ module Sequel
1143
1143
  attr_reader :values
1144
1144
  alias to_hash values
1145
1145
 
1146
+ # Get the value of the column. Takes a single symbol or string argument.
1147
+ # By default it calls send with the argument to get the value. This can
1148
+ # be overridden if you have columns that conflict with existing
1149
+ # method names.
1150
+ alias get_column_value send
1151
+
1152
+ # Set the value of the column. Takes two argument. The first is a
1153
+ # symbol or string argument for the column name, suffixed with =. The
1154
+ # second is the value to set for the column. By default it calls send
1155
+ # with the argument to set the value. This can be overridden if you have
1156
+ # columns that conflict with existing method names (unlikely for setter
1157
+ # methods, but possible).
1158
+ alias set_column_value send
1159
+
1146
1160
  # Creates new instance and passes the given values to set.
1147
1161
  # If a block is given, yield the instance to the block.
1148
1162
  #
@@ -1219,6 +1233,14 @@ module Sequel
1219
1233
  def autoincrementing_primary_key
1220
1234
  primary_key
1221
1235
  end
1236
+
1237
+ # Cancel the current action. Should be called in before hooks to halt
1238
+ # the processing of the action. If a +msg+ argument is given and
1239
+ # the model instance is configured to raise exceptions on failure,
1240
+ # sets the message to use for the raised HookFailed exception.
1241
+ def cancel_action(msg=nil)
1242
+ raise_hook_failure(msg)
1243
+ end
1222
1244
 
1223
1245
  # The columns that have been updated. This isn't completely accurate,
1224
1246
  # as it could contain columns whose values have not changed.
@@ -1597,23 +1619,23 @@ module Sequel
1597
1619
  when :skip
1598
1620
  fields.each do |f|
1599
1621
  if hash.has_key?(f)
1600
- send("#{f}=", hash[f])
1622
+ set_column_value("#{f}=", hash[f])
1601
1623
  elsif f.is_a?(Symbol) && hash.has_key?(sf = f.to_s)
1602
- send("#{sf}=", hash[sf])
1624
+ set_column_value("#{sf}=", hash[sf])
1603
1625
  end
1604
1626
  end
1605
1627
  when :raise
1606
1628
  fields.each do |f|
1607
1629
  if hash.has_key?(f)
1608
- send("#{f}=", hash[f])
1630
+ set_column_value("#{f}=", hash[f])
1609
1631
  elsif f.is_a?(Symbol) && hash.has_key?(sf = f.to_s)
1610
- send("#{sf}=", hash[sf])
1632
+ set_column_value("#{sf}=", hash[sf])
1611
1633
  else
1612
1634
  raise(Sequel::Error, "missing field in hash: #{f.inspect} not in #{hash.inspect}")
1613
1635
  end
1614
1636
  end
1615
1637
  else
1616
- fields.each{|f| send("#{f}=", hash[f])}
1638
+ fields.each{|f| set_column_value("#{f}=", hash[f])}
1617
1639
  end
1618
1640
  self
1619
1641
  end
@@ -2073,8 +2095,17 @@ module Sequel
2073
2095
 
2074
2096
  # Raise an error appropriate to the hook type. May be swallowed by
2075
2097
  # checked_save_failure depending on the raise_on_failure? setting.
2076
- def raise_hook_failure(type)
2077
- raise HookFailed.new("the #{type} hook failed", self)
2098
+ def raise_hook_failure(type=nil)
2099
+ msg = case type
2100
+ when String
2101
+ type
2102
+ when Symbol
2103
+ "the #{type} hook failed"
2104
+ else
2105
+ "a hook failed"
2106
+ end
2107
+
2108
+ raise HookFailed.new(msg, self)
2078
2109
  end
2079
2110
 
2080
2111
  # Get the ruby class or classes related to the given column's type.
@@ -2093,7 +2124,7 @@ module Sequel
2093
2124
  hash.each do |k,v|
2094
2125
  m = "#{k}="
2095
2126
  if meths.include?(m)
2096
- send(m, v)
2127
+ set_column_value(m, v)
2097
2128
  elsif strict
2098
2129
  # Avoid using respond_to? or creating symbols from user input
2099
2130
  if public_methods.map{|s| s.to_s}.include?(m)
@@ -0,0 +1,61 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The accessed_columns plugin records which columns have been
4
+ # accessed for a model instance. This is useful if you are
5
+ # looking to remove other columns from being SELECTed by the
6
+ # dataset that retrieved the instance, which can significantly
7
+ # improve performance:
8
+ #
9
+ # a = Album[1]
10
+ # a.accessed_columns # []
11
+ # a.name
12
+ # a.accessed_columns # [:name]
13
+ # a.artist_id
14
+ # a.accessed_columns # [:name, :artist_id]
15
+ #
16
+ # Note that this plugin should probably not be used in production,
17
+ # as it causes a performance hit.
18
+ #
19
+ # Usage:
20
+ #
21
+ # # Make all model subclass instances record accessed columns (called before loading subclasses)
22
+ # Sequel::Model.plugin :accessed_columns
23
+ #
24
+ # # Make the Album instances record accessed columns
25
+ # Album.plugin :accessed_columns
26
+ module AccessedColumns
27
+ module InstanceMethods
28
+ # Record the column access before retrieving the value.
29
+ def [](c)
30
+ (@accessed_columns ||= {})[c] = true unless frozen?
31
+ super
32
+ end
33
+
34
+ # Clear the accessed columns when saving.
35
+ def after_save
36
+ super
37
+ @accessed_columns = nil
38
+ end
39
+
40
+ # The columns that have been accessed.
41
+ def accessed_columns
42
+ @accessed_columns ? @accessed_columns.keys : []
43
+ end
44
+
45
+ # Copy the accessed columns when duping and cloning.
46
+ def initialize_copy(other)
47
+ other.instance_variable_set(:@accessed_columns, @accessed_columns.dup) if @accessed_columns
48
+ super
49
+ end
50
+
51
+ private
52
+
53
+ # Clear the accessed columns when refreshing.
54
+ def _refresh(_)
55
+ @accessed_columns = nil
56
+ super
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -61,12 +61,12 @@ module Sequel
61
61
  if clpk
62
62
  def_association_pks_getter(opts) do
63
63
  h = {}
64
- lk.zip(lpk).each{|k, pk| h[k] = send(pk)}
64
+ lk.zip(lpk).each{|k, pk| h[k] = get_column_value(pk)}
65
65
  _join_table_dataset(opts).filter(h).select_map(rk)
66
66
  end
67
67
  else
68
68
  def_association_pks_getter(opts) do
69
- _join_table_dataset(opts).filter(lk=>send(lpk)).select_map(rk)
69
+ _join_table_dataset(opts).filter(lk=>get_column_value(lpk)).select_map(rk)
70
70
  end
71
71
  end
72
72
 
@@ -74,10 +74,10 @@ module Sequel
74
74
  pks = send(crk ? :convert_cpk_array : :convert_pk_array, opts, pks)
75
75
  checked_transaction do
76
76
  if clpk
77
- lpkv = lpk.map{|k| send(k)}
77
+ lpkv = lpk.map{|k| get_column_value(k)}
78
78
  cond = lk.zip(lpkv)
79
79
  else
80
- lpkv = send(lpk)
80
+ lpkv = get_column_value(lpk)
81
81
  cond = {lk=>lpkv}
82
82
  end
83
83
  ds = _join_table_dataset(opts).filter(cond)
@@ -40,7 +40,7 @@ module Sequel
40
40
  # Add a attribute? method for the column to a module included in the class.
41
41
  def create_boolean_reader(column)
42
42
  overridable_methods_module.module_eval do
43
- define_method("#{column}?"){model.db.typecast_value(:boolean, send(column))}
43
+ define_method("#{column}?"){model.db.typecast_value(:boolean, get_column_value(column))}
44
44
  end
45
45
  end
46
46
 
@@ -255,7 +255,7 @@ module Sequel
255
255
  # Set the cti_key column to the name of the model.
256
256
  def _before_validation
257
257
  if new? && model.cti_key && !model.cti_model_map
258
- send("#{model.cti_key}=", model.name.to_s)
258
+ set_column_value("#{model.cti_key}=", model.name.to_s)
259
259
  end
260
260
  super
261
261
  end
@@ -0,0 +1,93 @@
1
+ module Sequel
2
+ module Plugins
3
+ # The column_conflicts plugin overrides Model#get_column_value and #set_column_value
4
+ # to automatically handle column names that conflict with Ruby/Sequel method names.
5
+ #
6
+ # By default, Model#get_column_value and #set_column_value just call send, this
7
+ # plugin overrides the methods and gets/sets the value directly in the values
8
+ # hash if the column name conflicts with an existing Sequel::Model instance
9
+ # method name.
10
+ #
11
+ # Checking for column conflicts causes a performance hit, which is why Sequel
12
+ # does not enable such checks by default.
13
+ #
14
+ # When using this plugin, you can manually update the columns used. This may be useful if
15
+ # the columns conflict with one of your custom methods, instead of a method defined in
16
+ # Sequel::Model:
17
+ #
18
+ # Album.plugin :column_conflicts
19
+ # Album.get_column_conflict!(:column)
20
+ # Album.set_column_conflict!(:other_column)
21
+ #
22
+ # Usage:
23
+ #
24
+ # # Make all model's handle column conflicts automatically (called before loading subclasses)
25
+ # Sequel::Model.plugin :column_conflicts
26
+ #
27
+ # # Make the Album class handle column conflicts automatically
28
+ # Album.plugin :column_conflicts
29
+ module ColumnConflicts
30
+ # Check for column conflicts on the current model if the model has a dataset.
31
+ def self.configure(model)
32
+ model.instance_eval do
33
+ @get_column_conflicts = {}
34
+ @set_column_conflicts = {}
35
+ check_column_conflicts if @dataset
36
+ end
37
+ end
38
+
39
+ module ClassMethods
40
+ Plugins.after_set_dataset(self, :check_column_conflicts)
41
+ Plugins.inherited_instance_variables(self, :@get_column_conflicts=>:dup, :@set_column_conflicts=>:dup)
42
+
43
+ # Hash for columns where the getter method already exists. keys are column symbols/strings that
44
+ # conflict with method names and should be looked up directly instead of calling a method,
45
+ # values are the column symbol to lookup in the values hash.
46
+ attr_reader :get_column_conflicts
47
+
48
+ # Hash for columns where the setter method already exists. keys are column symbols/strings suffixed
49
+ # with = that conflict with method names and should be set directly in the values hash,
50
+ # values are the column symbol to set in the values hash.
51
+ attr_reader :set_column_conflicts
52
+
53
+ # Compare the column names for the model with the methods defined on Sequel::Model, and automatically
54
+ # setup the column conflicts.
55
+ def check_column_conflicts
56
+ mod = Sequel::Model
57
+ columns.find_all{|c| mod.method_defined?(c)}.each{|c| get_column_conflict!(c)}
58
+ columns.find_all{|c| mod.method_defined?("#{c}=")}.each{|c| set_column_conflict!(c)}
59
+ end
60
+
61
+ # Set the given column as one with a getter method conflict.
62
+ def get_column_conflict!(column)
63
+ @get_column_conflicts[column.to_sym] = @get_column_conflicts[column.to_s] = column.to_sym
64
+ end
65
+
66
+ # Set the given column as one with a setter method conflict.
67
+ def set_column_conflict!(column)
68
+ @set_column_conflicts[:"#{column}="] = @set_column_conflicts["#{column}="] = column.to_sym
69
+ end
70
+ end
71
+
72
+ module InstanceMethods
73
+ # If the given column has a getter method conflict, lookup the value directly in the values hash.
74
+ def get_column_value(c)
75
+ if col = model.get_column_conflicts[c]
76
+ self[col]
77
+ else
78
+ super
79
+ end
80
+ end
81
+
82
+ # If the given column has a setter method conflict, set the value directly in the values hash.
83
+ def set_column_value(c, v)
84
+ if col = model.set_column_conflicts[c]
85
+ self[col] = v
86
+ else
87
+ super
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -100,7 +100,7 @@ module Sequel
100
100
  klass = opts[:class]
101
101
  class_proc = proc{klass || constantize(opts[:class_name])}
102
102
  opts[:composer] = proc do
103
- if values = keys.map{|k| send(k)} and values.any?{|v| !v.nil?}
103
+ if values = keys.map{|k| get_column_value(k)} and values.any?{|v| !v.nil?}
104
104
  class_proc.call.new(*values)
105
105
  else
106
106
  nil
@@ -113,9 +113,9 @@ module Sequel
113
113
  setters = setter_meths.zip(cov_methods)
114
114
  opts[:decomposer] = proc do
115
115
  if (o = compositions[name]).nil?
116
- setter_meths.each{|sm| send(sm, nil)}
116
+ setter_meths.each{|sm| get_column_value(sm, nil)}
117
117
  else
118
- setters.each{|sm, cm| send(sm, o.send(cm))}
118
+ setters.each{|sm, cm| get_column_value(sm, o.send(cm))}
119
119
  end
120
120
  end
121
121
  end
@@ -60,7 +60,7 @@ module Sequel
60
60
  #
61
61
  # column_change(:name) # => ['Initial', 'Current']
62
62
  def column_change(column)
63
- [initial_value(column), send(column)] if column_changed?(column)
63
+ [initial_value(column), get_column_value(column)] if column_changed?(column)
64
64
  end
65
65
 
66
66
  # A hash with column symbol keys and pairs of initial and
@@ -70,7 +70,7 @@ module Sequel
70
70
  def column_changes
71
71
  h = {}
72
72
  initial_values.each do |column, value|
73
- h[column] = [value, send(column)]
73
+ h[column] = [value, get_column_value(column)]
74
74
  end
75
75
  h
76
76
  end
@@ -99,7 +99,7 @@ module Sequel
99
99
  #
100
100
  # initial_value(:name) # => 'Initial'
101
101
  def initial_value(column)
102
- initial_values.fetch(column){send(column)}
102
+ initial_values.fetch(column){get_column_value(column)}
103
103
  end
104
104
 
105
105
  # A hash with column symbol keys and initial values.
@@ -116,7 +116,7 @@ module Sequel
116
116
  # name # => 'Initial'
117
117
  def reset_column(column)
118
118
  if initial_values.has_key?(column)
119
- send(:"#{column}=", initial_values[column])
119
+ set_column_value(:"#{column}=", initial_values[column])
120
120
  end
121
121
  if missing_initial_values.include?(column)
122
122
  values.delete(column)
@@ -136,7 +136,7 @@ module Sequel
136
136
  value = if initial_values.has_key?(column)
137
137
  initial_values[column]
138
138
  else
139
- send(column)
139
+ get_column_value(column)
140
140
  end
141
141
 
142
142
  initial_values[column] = if value && value != true && value.respond_to?(:clone)
@@ -184,7 +184,7 @@ module Sequel
184
184
  end
185
185
  else
186
186
  check_missing_initial_value(column)
187
- iv[column] = send(column)
187
+ iv[column] = get_column_value(column)
188
188
  super
189
189
  end
190
190
  end
@@ -270,7 +270,7 @@ module Sequel
270
270
 
271
271
  h = {}
272
272
 
273
- cols.each{|c| h[c.to_s] = send(c)}
273
+ cols.each{|c| h[c.to_s] = get_column_value(c)}
274
274
  if inc = opts[:include]
275
275
  if inc.is_a?(Hash)
276
276
  inc.each do |k, v|
@@ -63,7 +63,7 @@ module Sequel
63
63
  proc{|obj| obj.model.filter(scope=>obj.send(scope))}
64
64
  when Array
65
65
  model.dataset = model.dataset.order_prepend(*scope)
66
- proc{|obj| obj.model.filter(scope.map{|s| [s, obj.send(s)]})}
66
+ proc{|obj| obj.model.filter(scope.map{|s| [s, obj.get_column_value(s)]})}
67
67
  else
68
68
  scope
69
69
  end
@@ -90,8 +90,8 @@ module Sequel
90
90
  # Set the value of the position_field to the maximum value plus 1 unless the
91
91
  # position field already has a value.
92
92
  def before_create
93
- unless send(position_field)
94
- send("#{position_field}=", list_dataset.max(position_field).to_i+1)
93
+ unless get_column_value(position_field)
94
+ set_column_value("#{position_field}=", list_dataset.max(position_field).to_i+1)
95
95
  end
96
96
  super
97
97
  end
@@ -168,7 +168,7 @@ module Sequel
168
168
 
169
169
  # The value of the model's position field for this instance.
170
170
  def position_value
171
- send(position_field)
171
+ get_column_value(position_field)
172
172
  end
173
173
 
174
174
  # The model instance the given number of places below this model instance
@@ -57,7 +57,7 @@ module Sequel
57
57
  # Add the lock column instance filter to the object.
58
58
  def lock_column_instance_filter
59
59
  lc = model.lock_column
60
- instance_filter(lc=>Sequel.blob(send(lc)))
60
+ instance_filter(lc=>Sequel.blob(get_column_value(lc)))
61
61
  end
62
62
 
63
63
  # Clear the instance filters when refreshing, so that attempting to
@@ -152,9 +152,9 @@ module Sequel
152
152
  # not use an ensure block, so callers should be careful.
153
153
  def nested_attributes_check_key_modifications(meta, obj)
154
154
  reflection = meta[:reflection]
155
- keys = reflection.associated_object_keys.map{|x| obj.send(x)}
155
+ keys = reflection.associated_object_keys.map{|x| obj.get_column_value(x)}
156
156
  yield
157
- unless keys == reflection.associated_object_keys.map{|x| obj.send(x)}
157
+ unless keys == reflection.associated_object_keys.map{|x| obj.get_column_value(x)}
158
158
  raise(Error, "Modifying association dependent key(s) when updating associated objects is not allowed")
159
159
  end
160
160
  end
@@ -57,7 +57,7 @@ module Sequel
57
57
  # Add the lock column instance filter to the object.
58
58
  def lock_column_instance_filter
59
59
  lc = model.lock_column
60
- instance_filter(lc=>send(lc))
60
+ instance_filter(lc=>get_column_value(lc))
61
61
  end
62
62
 
63
63
  # Clear the instance filters when refreshing, so that attempting to
@@ -72,10 +72,10 @@ module Sequel
72
72
  # lock version.
73
73
  def _update_columns(columns)
74
74
  lc = model.lock_column
75
- lcv = send(lc)
75
+ lcv = get_column_value(lc)
76
76
  columns[lc] = lcv + 1
77
77
  super
78
- send("#{lc}=", lcv + 1)
78
+ set_column_value("#{lc}=", lcv + 1)
79
79
  end
80
80
  end
81
81
  end