sequel 4.18.0 → 4.19.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 (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