sequel 2.0.1 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,3 @@
1
- # This file holds general class methods for Sequel::Model
2
-
3
1
  module Sequel
4
2
  class Model
5
3
  # Whether to lazily load the schema for future subclasses. Unless turned
@@ -7,15 +5,42 @@ module Sequel
7
5
  # created
8
6
  @@lazy_load_schema = false
9
7
 
10
- # The default primary key for tables, inherited by future subclasses
8
+ @allowed_columns = nil
9
+ @dataset_methods = {}
11
10
  @primary_key = :id
12
-
13
- # Whether to typecast attribute values on assignment, inherited by
14
- # future subclasses.
11
+ @restrict_primary_key = true
12
+ @restricted_columns = nil
13
+ @sti_dataset = nil
14
+ @sti_key = nil
15
+ @strict_param_setting = true
15
16
  @typecast_on_assignment = true
16
17
 
18
+ # Which columns should be the only columns allowed in a call to set
19
+ # (default: all columns).
20
+ metaattr_reader :allowed_columns
21
+
22
+ # Hash of dataset methods to add to this class and subclasses when
23
+ # set_dataset is called.
24
+ metaattr_reader :dataset_methods
25
+
17
26
  # The default primary key for classes (default: :id)
18
- metaattr_accessor :primary_key
27
+ metaattr_reader :primary_key
28
+
29
+ # Which columns should not be update in a call to set
30
+ # (default: no columns).
31
+ metaattr_reader :restricted_columns
32
+
33
+ # The base dataset for STI, to which filters are added to get
34
+ # only the models for the specific STI subclass.
35
+ metaattr_reader :sti_dataset
36
+
37
+ # The column name holding the STI key for this model
38
+ metaattr_reader :sti_key
39
+
40
+ # Whether new/set/update and their variants should raise an error
41
+ # if an invalid key is used (either that doesn't exist or that
42
+ # access is restricted to it).
43
+ metaattr_accessor :strict_param_setting
19
44
 
20
45
  # Whether to typecast attribute values on assignment (default: true)
21
46
  metaattr_accessor :typecast_on_assignment
@@ -28,8 +53,14 @@ module Sequel
28
53
  left_outer_join limit map multi_insert naked order order_by order_more
29
54
  paginate print query range reverse_order right_outer_join select
30
55
  select_all select_more set set_graph_aliases single_value size to_csv
31
- transform union uniq unordered update where'
32
-
56
+ transform union uniq unordered update where'.map{|x| x.to_sym}
57
+
58
+ # Instance variables that are inherited in subclasses
59
+ INHERITED_INSTANCE_VARIABLES = {:@allowed_columns=>:dup, :@dataset_methods=>:dup,
60
+ :@primary_key=>nil, :@restricted_columns=>:dup, :@restrict_primary_key=>nil,
61
+ :@sti_dataset=>nil, :@sti_key=>nil, :@strict_param_setting=>nil,
62
+ :@typecast_on_assignment=>nil}
63
+
33
64
  # Returns the first record from the database matching the conditions.
34
65
  # If a hash is given, it is used as the conditions. If another
35
66
  # object is given, it finds the first record whose primary key(s) match
@@ -102,7 +133,9 @@ module Sequel
102
133
  raise(Error, "No arguments given") if args.empty?
103
134
  if block_given?
104
135
  raise(Error, "Defining a dataset method using a block requires only one argument") if args.length > 1
105
- dataset.meta_def(args.first, &block)
136
+ meth = args.first
137
+ @dataset_methods[meth] = block
138
+ dataset.meta_def(meth, &block)
106
139
  end
107
140
  args.each{|arg| instance_eval("def #{arg}(*args, &block); dataset.#{arg}(*args, &block) end", __FILE__, __LINE__)}
108
141
  end
@@ -136,19 +169,23 @@ module Sequel
136
169
  end
137
170
 
138
171
  # If possible, set the dataset for the model subclass as soon as it
139
- # is created. Also, inherit the typecast_on_assignment and primary_key
140
- # attributes from the parent class.
172
+ # is created. Also, inherit the INHERITED_INSTANCE_VARIABLES
173
+ # from the parent class.
141
174
  def self.inherited(subclass)
142
175
  sup_class = subclass.superclass
143
176
  ivs = subclass.instance_variables
144
- subclass.instance_variable_set(:@typecast_on_assignment, sup_class.typecast_on_assignment) unless ivs.include?("@typecast_on_assignment")
145
- subclass.instance_variable_set(:@primary_key, sup_class.primary_key) unless ivs.include?("@primary_key")
177
+ INHERITED_INSTANCE_VARIABLES.each do |iv, dup|
178
+ next if ivs.include?(iv.to_s)
179
+ sup_class_value = sup_class.instance_variable_get(iv)
180
+ sup_class_value = sup_class_value.dup if dup == :dup && sup_class_value
181
+ subclass.instance_variable_set(iv, sup_class_value)
182
+ end
146
183
  unless ivs.include?("@dataset")
147
184
  begin
148
185
  if sup_class == Model
149
186
  subclass.set_dataset(Model.db[subclass.implicit_table_name]) unless subclass.name.empty?
150
187
  elsif ds = sup_class.instance_variable_get(:@dataset)
151
- subclass.set_dataset(ds.clone)
188
+ subclass.set_dataset(sup_class.sti_key ? sup_class.sti_dataset.filter(sup_class.sti_key=>subclass.name) : ds.clone)
152
189
  end
153
190
  rescue
154
191
  end
@@ -199,6 +236,19 @@ module Sequel
199
236
  end
200
237
  end
201
238
 
239
+ # Restrict the setting of the primary key(s) inside new/set/update. Because
240
+ # this is the default, this only make sense to use in a subclass where the
241
+ # parent class has used unrestrict_primary_key.
242
+ def self.restrict_primary_key
243
+ @restrict_primary_key = true
244
+ end
245
+
246
+ # Whether or not setting the primary key inside new/set/update is
247
+ # restricted, true by default.
248
+ def self.restrict_primary_key?
249
+ @restrict_primary_key
250
+ end
251
+
202
252
  # Serializes column with YAML or through marshalling. Arguments should be
203
253
  # column symbols, with an optional trailing hash with a :format key
204
254
  # set to :yaml or :marshal (:yaml is the default). Setting this adds
@@ -213,6 +263,17 @@ module Sequel
213
263
  @dataset.transform(@transform) if @dataset
214
264
  end
215
265
 
266
+ # Set the columns to allow in new/set/update. Using this means that
267
+ # any columns not listed here will not be modified. If you have any virtual
268
+ # setter methods (methods that end in =) that you want to be used in
269
+ # new/set/update, they need to be listed here as well (without the =).
270
+ #
271
+ # It may be better to use (set|update)_only instead of this in places where
272
+ # only certain columns may be allowed.
273
+ def self.set_allowed_columns(*cols)
274
+ @allowed_columns = cols
275
+ end
276
+
216
277
  # Sets the dataset associated with the Model class. ds can be a Symbol
217
278
  # (specifying a table name in the current database), or a Dataset.
218
279
  # If a dataset is used, the model's database is changed to the given
@@ -236,14 +297,10 @@ module Sequel
236
297
  raise(Error, "Model.set_dataset takes a Symbol or a Sequel::Dataset")
237
298
  end
238
299
  @dataset.set_model(self)
239
- def_dataset_method(:destroy) do
240
- raise(Error, "No model associated with this dataset") unless @opts[:models]
241
- count = 0
242
- @db.transaction {each {|r| count += 1; r.destroy}}
243
- count
244
- end
300
+ @dataset.extend(DatasetMethods)
245
301
  @dataset.extend(Associations::EagerLoading)
246
302
  @dataset.transform(@transform) if @transform
303
+ @dataset_methods.each{|meth, block| @dataset.meta_def(meth, &block)} if @dataset_methods
247
304
  begin
248
305
  (@db_schema = get_db_schema) unless @@lazy_load_schema
249
306
  rescue
@@ -271,6 +328,39 @@ module Sequel
271
328
  @primary_key = (key.length == 1) ? key[0] : key.flatten
272
329
  end
273
330
 
331
+ # Set the columns to restrict in new/set/update. Using this means that
332
+ # any columns listed here will not be modified. If you have any virtual
333
+ # setter methods (methods that end in =) that you want not to be used in
334
+ # new/set/update, they need to be listed here as well (without the =).
335
+ #
336
+ # It may be better to use (set|update)_except instead of this in places where
337
+ # only certain columns may be allowed.
338
+ def self.set_restricted_columns(*cols)
339
+ @restricted_columns = cols
340
+ end
341
+
342
+ # Makes this model a polymorphic model with the given key being a string
343
+ # field in the database holding the name of the class to use. If the
344
+ # key given has a NULL value or there are any problems looking up the
345
+ # class, uses the current class.
346
+ #
347
+ # This should be used to set up single table inheritance for the model,
348
+ # and it only makes sense to use this in the parent class.
349
+ #
350
+ # You should call sti_key after any calls to set_dataset in the model,
351
+ # otherwise subclasses might not have the filters set up correctly.
352
+ #
353
+ # The filters that sti_key sets up in subclasses will not work if
354
+ # those subclasses have further subclasses. For those middle subclasses,
355
+ # you will need to call set_dataset manually with the correct filter set.
356
+ def self.set_sti_key(key)
357
+ m = self
358
+ @sti_key = key
359
+ @sti_dataset = dataset
360
+ dataset.set_model(key, Hash.new{|h,k| h[k] = (k.constantize rescue m)})
361
+ before_create(:set_sti_key){send("#{key}=", model.name)}
362
+ end
363
+
274
364
  # Returns the columns as a list of frozen strings instead
275
365
  # of a list of symbols. This makes it possible to check
276
366
  # whether a column exists without creating a symbol, which
@@ -302,6 +392,11 @@ module Sequel
302
392
  dataset.opts[:from].first
303
393
  end
304
394
 
395
+ # Allow the setting of the primary key(s) inside new/set/update.
396
+ def self.unrestrict_primary_key
397
+ @restrict_primary_key = false
398
+ end
399
+
305
400
  # Add model methods that call dataset methods
306
401
  def_dataset_method(*DATASET_METHODS)
307
402
 
@@ -366,6 +461,6 @@ module Sequel
366
461
  @columns
367
462
  end
368
463
 
369
- metaprivate :def_column_accessor, :get_db_schema, :set_columns
464
+ private_class_method :def_column_accessor, :get_db_schema, :set_columns
370
465
  end
371
466
  end
@@ -54,7 +54,7 @@ module Sequel
54
54
  obj
55
55
  end
56
56
 
57
- metaprivate :cache_delete, :cache_key, :cache_lookup
57
+ private_class_method :cache_delete, :cache_key, :cache_lookup
58
58
 
59
59
  ### Instance Methods ###
60
60
 
@@ -0,0 +1,26 @@
1
+ # Dataset methods are methods that the model class extends its dataset with in
2
+ # the call to set_dataset.
3
+ module Sequel::Model::DatasetMethods
4
+ # Destroy each row in the dataset by instantiating it and then calling
5
+ # destroy on the resulting model object. This isn't as fast as deleting
6
+ # the object, which does a single SQL call, but this runs any destroy
7
+ # hooks.
8
+ def destroy
9
+ raise(Error, "No model associated with this dataset") unless @opts[:models]
10
+ count = 0
11
+ @db.transaction {each {|r| count += 1; r.destroy}}
12
+ count
13
+ end
14
+
15
+ # This allows you to call to_hash without any arguments, which will
16
+ # result in a hash with the primary key value being the key and the
17
+ # model object being the value.
18
+ def to_hash(key_column=nil, value_column=nil)
19
+ if key_column
20
+ super
21
+ else
22
+ raise(Sequel::Error, "No primary key for model") unless pk = @opts[:models][nil].primary_key
23
+ super(pk, value_column)
24
+ end
25
+ end
26
+ end
@@ -132,17 +132,18 @@ module Sequel::Model::Associations::EagerLoading
132
132
  assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name)
133
133
  join_type = r[:graph_join_type]
134
134
  conditions = r[:graph_conditions]
135
+ select = r[:graph_select]
135
136
  ds = case assoc_type = r[:type]
136
137
  when :many_to_one
137
- ds.graph(klass, [[klass.primary_key, :"#{ta}__#{r[:key]}"]] + conditions, :table_alias=>assoc_table_alias, :join_type=>join_type)
138
+ ds.graph(klass, [[klass.primary_key, :"#{ta}__#{r[:key]}"]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type)
138
139
  when :one_to_many
139
- ds = ds.graph(klass, [[r[:key], :"#{ta}__#{model.primary_key}"]] + conditions, :table_alias=>assoc_table_alias, :join_type=>join_type)
140
+ ds = ds.graph(klass, [[r[:key], :"#{ta}__#{model.primary_key}"]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type)
140
141
  # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
141
142
  ds.opts[:eager_graph][:reciprocals][assoc_table_alias] = r.reciprocal
142
143
  ds
143
144
  when :many_to_many
144
145
  ds = ds.graph(r[:join_table], [[r[:left_key], :"#{ta}__#{model.primary_key}"]] + r[:graph_join_table_conditions], :select=>false, :table_alias=>ds.eager_unique_table_alias(ds, r[:join_table]), :join_type=>join_type)
145
- ds.graph(klass, [[klass.primary_key, r[:right_key]]] + conditions, :table_alias=>assoc_table_alias, :join_type=>join_type)
146
+ ds.graph(klass, [[klass.primary_key, r[:right_key]]] + conditions, :select=>select, :table_alias=>assoc_table_alias, :join_type=>join_type)
146
147
  end
147
148
  eager_graph = ds.opts[:eager_graph]
148
149
  eager_graph[:requirements][assoc_table_alias] = requirements.dup
@@ -278,7 +279,8 @@ module Sequel::Model::Associations::EagerLoading
278
279
  return if dependency_map.empty?
279
280
  # Don't clobber the instance variable array for *_to_many associations if it has already been setup
280
281
  dependency_map.keys.each do |ta|
281
- current.instance_variable_set("@#{alias_map[ta]}", type_map[ta] == :many_to_one ? :null : []) unless current.instance_variable_get("@#{alias_map[ta]}")
282
+ assoc_name = alias_map[ta]
283
+ current.associations[assoc_name] = type_map[ta] == :many_to_one ? nil : [] unless current.associations.include?(assoc_name)
282
284
  end
283
285
  dependency_map.each do |ta, deps|
284
286
  next unless rec = record_graph[ta]
@@ -288,15 +290,14 @@ module Sequel::Model::Associations::EagerLoading
288
290
  else
289
291
  records_map[ta][rec.pk] = rec
290
292
  end
291
- ivar = "@#{alias_map[ta]}"
293
+ assoc_name = alias_map[ta]
292
294
  case assoc_type = type_map[ta]
293
295
  when :many_to_one
294
- current.instance_variable_set(ivar, rec)
296
+ current.associations[assoc_name] = rec
295
297
  else
296
- list = current.instance_variable_get(ivar)
297
- list.push(rec)
298
- if (assoc_type == :one_to_many) && (reciprocal = reciprocal_map[ta])
299
- rec.instance_variable_set(reciprocal, current)
298
+ current.associations[assoc_name].push(rec)
299
+ if assoc_type == :one_to_many and reciprocal = reciprocal_map[ta]
300
+ rec.associations[reciprocal] = current
300
301
  end
301
302
  end
302
303
  # Recurse into dependencies of the current object
@@ -362,7 +363,6 @@ module Sequel::Model::Associations::EagerLoading
362
363
  reflections.each do |reflection|
363
364
  assoc_class = reflection.associated_class
364
365
  assoc_name = reflection[:name]
365
- assoc_iv = :"@#{assoc_name}"
366
366
  # Proc for setting cascaded eager loading
367
367
  assoc_block = Proc.new do |d|
368
368
  if order = reflection[:order]
@@ -389,13 +389,11 @@ module Sequel::Model::Associations::EagerLoading
389
389
  # Set the instance variable to null by default, so records that
390
390
  # don't have a associated records will cache the negative lookup.
391
391
  a.each do |object|
392
- object.instance_variable_set(assoc_iv, :null)
392
+ object.associations[assoc_name] = nil
393
393
  end
394
394
  assoc_block.call(assoc_class.select(*reflection.select).filter(assoc_class.primary_key=>keys)).all do |assoc_object|
395
395
  next unless objects = h[assoc_object.pk]
396
- objects.each do |object|
397
- object.instance_variable_set(assoc_iv, assoc_object)
398
- end
396
+ objects.each{|object| object.associations[assoc_name] = assoc_object}
399
397
  end
400
398
  when :one_to_many, :many_to_many
401
399
  h = key_hash[model.primary_key]
@@ -408,9 +406,7 @@ module Sequel::Model::Associations::EagerLoading
408
406
  assoc_class.select(*(Array(reflection.select)+Array(reflection[:left_key_select]))).inner_join(reflection[:join_table], [[reflection[:right_key], reflection.associated_primary_key], [reflection[:left_key], h.keys]])
409
407
  end
410
408
  h.values.each do |object_array|
411
- object_array.each do |object|
412
- object.instance_variable_set(assoc_iv, [])
413
- end
409
+ object_array.each{|object| object.associations[assoc_name] = []}
414
410
  end
415
411
  assoc_block.call(ds).all do |assoc_object|
416
412
  fk = if rtype == :many_to_many
@@ -420,8 +416,8 @@ module Sequel::Model::Associations::EagerLoading
420
416
  end
421
417
  next unless objects = h[fk]
422
418
  objects.each do |object|
423
- object.instance_variable_get(assoc_iv) << assoc_object
424
- assoc_object.instance_variable_set(reciprocal, object) if reciprocal
419
+ object.associations[assoc_name].push(assoc_object)
420
+ assoc_object.associations[reciprocal] = object if reciprocal
425
421
  end
426
422
  end
427
423
  end
@@ -51,7 +51,7 @@ module Sequel
51
51
  all_hooks(hook).each{|b| return false if object.instance_eval(&b) == false}
52
52
  end
53
53
 
54
- metaprivate :add_hook, :all_hooks, :hooks, :run_hooks
54
+ private_class_method :add_hook, :all_hooks, :hooks, :run_hooks
55
55
 
56
56
  (HOOKS + PRIVATE_HOOKS).each do |hook|
57
57
  instance_eval("def #{hook}(method = nil, &block); add_hook(:#{hook}, method, &block) end")
@@ -57,6 +57,6 @@ module Sequel
57
57
  Sequel::Plugins.const_get(module_name)
58
58
  end
59
59
 
60
- metaprivate :plugin_gem, :plugin_module
60
+ private_class_method :plugin_gem, :plugin_module
61
61
  end
62
62
  end
@@ -1,18 +1,30 @@
1
- # This file holds general instance methods for Sequel::Model
2
-
3
1
  module Sequel
4
2
  class Model
3
+ # The setter methods (methods ending with =) that are never allowed
4
+ # to be called automatically via set.
5
+ RESTRICTED_SETTER_METHODS = %w"== === []= taguri= typecast_on_assignment="
6
+
7
+ # The current cached associations. A hash with the keys being the
8
+ # association name symbols and the values being the associated object
9
+ # or nil (many_to_one), or the array of associated objects (*_to_many).
10
+ attr_reader :associations
11
+
5
12
  # The columns that have been updated. This isn't completely accurate,
6
13
  # see Model#[]=.
7
14
  attr_reader :changed_columns
8
15
 
9
- # The hash of attribute values. Keys are symbols with the names of the
10
- # underlying database columns.
11
- attr_reader :values
16
+ # Whether this model instance should raise an error if attempting
17
+ # to call a method through set/update and their variants that either
18
+ # doesn't exist or access to it is denied.
19
+ attr_writer :strict_param_setting
12
20
 
13
21
  # Whether this model instance should typecast on attribute assignment
14
22
  attr_writer :typecast_on_assignment
15
23
 
24
+ # The hash of attribute values. Keys are symbols with the names of the
25
+ # underlying database columns.
26
+ attr_reader :values
27
+
16
28
  class_attr_reader :columns, :dataset, :db, :primary_key, :str_columns
17
29
 
18
30
  # Creates new instance with values set to passed-in Hash.
@@ -27,16 +39,18 @@ module Sequel
27
39
  # exists.
28
40
  def initialize(values = nil, from_db = false, &block)
29
41
  values ||= {}
42
+ @associations = {}
43
+ @db_schema = model.db_schema
30
44
  @changed_columns = []
45
+ @strict_param_setting = model.strict_param_setting
31
46
  @typecast_on_assignment = model.typecast_on_assignment
32
- @db_schema = model.db_schema
33
47
  if from_db
34
48
  @new = false
35
49
  @values = values
36
50
  else
37
51
  @values = {}
38
52
  @new = true
39
- set_with_params(values)
53
+ set(values)
40
54
  end
41
55
  @changed_columns.clear
42
56
 
@@ -167,9 +181,7 @@ module Sequel
167
181
  # exists in the database.
168
182
  def refresh
169
183
  @values = this.first || raise(Error, "Record not found")
170
- model.all_association_reflections.each do |r|
171
- instance_variable_set("@#{r[:name]}", nil)
172
- end
184
+ @associations.clear
173
185
  self
174
186
  end
175
187
  alias_method :reload, :refresh
@@ -227,11 +239,40 @@ module Sequel
227
239
  save(*@changed_columns) unless @changed_columns.empty?
228
240
  end
229
241
 
242
+ # Updates the instance with the supplied values with support for virtual
243
+ # attributes, ignoring any values for which no setter method is available.
244
+ # Does not save the record.
245
+ #
246
+ # If no columns have been set for this model (very unlikely), assume symbol
247
+ # keys are valid column names, and assign the column value based on that.
248
+ def set(hash)
249
+ set_restricted(hash, nil, nil)
250
+ end
251
+ alias_method :set_with_params, :set
252
+
253
+ # Set all values using the entries in the hash, ignoring any setting of
254
+ # allowed_columns or restricted columns in the model.
255
+ def set_all(hash)
256
+ set_restricted(hash, false, false)
257
+ end
258
+
259
+ # Set all values using the entries in the hash, except for the keys
260
+ # given in except.
261
+ def set_except(hash, *except)
262
+ set_restricted(hash, false, except.flatten)
263
+ end
264
+
265
+ # Set the values using the entries in the hash, only if the key
266
+ # is included in only.
267
+ def set_only(hash, *only)
268
+ set_restricted(hash, only.flatten, false)
269
+ end
270
+
230
271
  # Sets the value attributes without saving the record. Returns
231
272
  # the values changed. Raises an error if the keys are not symbols
232
273
  # or strings or a string key was passed that was not a valid column.
233
274
  # This is a low level method that does not respect virtual attributes. It
234
- # should probably be avoided. Look into using set_with_params instead.
275
+ # should probably be avoided. Look into using set instead.
235
276
  def set_values(values)
236
277
  s = str_columns
237
278
  vals = values.inject({}) do |m, kv|
@@ -254,30 +295,35 @@ module Sequel
254
295
  vals
255
296
  end
256
297
 
257
- # Updates the instance with the supplied values with support for virtual
258
- # attributes, ignoring any values for which no setter method is available.
259
- # Does not save the record.
260
- #
261
- # If no columns have been set for this model (very unlikely), assume symbol
262
- # keys are valid column names, and assign the column value based on that.
263
- def set_with_params(hash)
264
- columns_not_set = model.instance_variable_get(:@columns).blank?
265
- meths = setter_methods
266
- hash.each do |k,v|
267
- m = "#{k}="
268
- if meths.include?(m)
269
- send(m, v)
270
- elsif columns_not_set && (Symbol === k)
271
- self[k] = v
272
- end
273
- end
274
- end
275
-
276
298
  # Returns (naked) dataset that should return only this instance.
277
299
  def this
278
300
  @this ||= dataset.filter(pk_hash).limit(1).naked
279
301
  end
280
302
 
303
+ # Runs set with the passed hash and runs save_changes (which runs any callback methods).
304
+ def update(hash)
305
+ update_restricted(hash, nil, nil)
306
+ end
307
+ alias_method :update_with_params, :update
308
+
309
+ # Update all values using the entries in the hash, ignoring any setting of
310
+ # allowed_columns or restricted columns in the model.
311
+ def update_all(hash)
312
+ update_restricted(hash, false, false)
313
+ end
314
+
315
+ # Update all values using the entries in the hash, except for the keys
316
+ # given in except.
317
+ def update_except(hash, *except)
318
+ update_restricted(hash, false, except.flatten)
319
+ end
320
+
321
+ # Update the values using the entries in the hash, only if the key
322
+ # is included in only.
323
+ def update_only(hash, *only)
324
+ update_restricted(hash, only.flatten, false)
325
+ end
326
+
281
327
  # Sets the values attributes with set_values and then updates
282
328
  # the record in the database using those values. This is a
283
329
  # low level method that does not run the usual save callbacks.
@@ -287,18 +333,52 @@ module Sequel
287
333
  this.update(set_values(values))
288
334
  end
289
335
 
290
- # Runs set_with_params and runs save_changes (which runs any callback methods).
291
- def update_with_params(values)
292
- set_with_params(values)
293
- save_changes
294
- end
295
-
296
336
  private
297
337
 
338
+ # Set the columns, filtered by the only and except arrays.
339
+ def set_restricted(hash, only, except)
340
+ columns_not_set = model.instance_variable_get(:@columns).blank?
341
+ meths = setter_methods(only, except)
342
+ strict_param_setting = @strict_param_setting
343
+ hash.each do |k,v|
344
+ m = "#{k}="
345
+ if meths.include?(m)
346
+ send(m, v)
347
+ elsif columns_not_set && (Symbol === k)
348
+ self[k] = v
349
+ elsif strict_param_setting
350
+ raise Error, "method #{m} doesn't exist or access is restricted to it"
351
+ end
352
+ end
353
+ end
354
+
298
355
  # Returns all methods that can be used for attribute
299
- # assignment (those that end with =)
300
- def setter_methods
301
- methods.grep(/=\z/)
356
+ # assignment (those that end with =), modified by the only
357
+ # and except arguments:
358
+ #
359
+ # * only
360
+ # * false - Don't modify the results
361
+ # * nil - if the model has allowed_columns, use only these, otherwise, don't modify
362
+ # * Array - allow only the given methods to be used
363
+ # * except
364
+ # * false - Don't modify the results
365
+ # * nil - if the model has restricted_columns, remove these, otherwise, don't modify
366
+ # * Array - remove the given methods
367
+ #
368
+ # only takes precedence over except, and if only is not used, certain methods are always
369
+ # restricted (RESTRICTED_SETTER_METHODS). The primary key is restricted by default as
370
+ # well, see Model.unrestrict_primary_key to change this.
371
+ def setter_methods(only, except)
372
+ only = only.nil? ? model.allowed_columns : only
373
+ except = except.nil? ? model.restricted_columns : except
374
+ if only
375
+ only.map{|x| "#{x}="}
376
+ else
377
+ meths = methods.grep(/=\z/) - RESTRICTED_SETTER_METHODS
378
+ meths -= Array(primary_key).map{|x| "#{x}="} if primary_key && model.restrict_primary_key?
379
+ meths -= except.map{|x| "#{x}="} if except
380
+ meths
381
+ end
302
382
  end
303
383
 
304
384
  # Typecast the value to the column's type if typecasting. Calls the database's
@@ -309,5 +389,11 @@ module Sequel
309
389
  raise(Error, "nil/NULL is not allowed for the #{column} column") if value.nil? && (col_schema[:allow_null] == false)
310
390
  model.db.typecast_value(col_schema[:type], value)
311
391
  end
392
+
393
+ # Set the columns, filtered by the only and except arrays.
394
+ def update_restricted(hash, only, except)
395
+ set_restricted(hash, only, except)
396
+ save_changes
397
+ end
312
398
  end
313
399
  end