sequel 1.5.1 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,48 +1,62 @@
1
1
  module Sequel
2
- module Plugins; end
2
+ # Empty namespace that plugins should use to store themselves,
3
+ # so they can be loaded via Model.is.
4
+ #
5
+ # Plugins should be modules with one of the following conditions:
6
+ # * A singleton method named apply, which takes a model and
7
+ # additional arguments.
8
+ # * A module inside the plugin module named InstanceMethods,
9
+ # which will be included in the model class.
10
+ # * A module inside the plugin module named ClassMethods,
11
+ # which will extend the model class.
12
+ # * A module inside the plugin module named DatasetMethods,
13
+ # which will extend the model's dataset.
14
+ module Plugins
15
+ end
3
16
 
4
17
  class Model
5
- class << self
6
- # Loads a plugin for use with the model class, passing optional arguments
7
- # to the plugin.
8
- def is(plugin, *args)
9
- m = plugin_module(plugin)
10
- if m.respond_to?(:apply)
11
- m.apply(self, *args)
12
- end
13
- if m.const_defined?("InstanceMethods")
14
- class_def(:"#{plugin}_opts") {args.first}
15
- include(m::InstanceMethods)
16
- end
17
- if m.const_defined?("ClassMethods")
18
- meta_def(:"#{plugin}_opts") {args.first}
19
- metaclass.send(:include, m::ClassMethods)
20
- end
21
- if m.const_defined?("DatasetMethods")
22
- unless @dataset
23
- raise Sequel::Error, "Plugin cannot be applied because the model class has no dataset"
24
- end
25
- dataset.meta_def(:"#{plugin}_opts") {args.first}
26
- dataset.metaclass.send(:include, m::DatasetMethods)
27
- end
18
+ # Loads a plugin for use with the model class, passing optional arguments
19
+ # to the plugin. If the plugin has a DatasetMethods module and the model
20
+ # doesn't have a dataset, raise an Error.
21
+ def self.is(plugin, *args)
22
+ m = plugin_module(plugin)
23
+ raise(Error, "Plugin cannot be applied because the model class has no dataset") if m.const_defined?("DatasetMethods") && !@dataset
24
+ if m.respond_to?(:apply)
25
+ m.apply(self, *args)
26
+ end
27
+ if m.const_defined?("InstanceMethods")
28
+ class_def(:"#{plugin}_opts") {args.first}
29
+ include(m::InstanceMethods)
30
+ end
31
+ if m.const_defined?("ClassMethods")
32
+ meta_def(:"#{plugin}_opts") {args.first}
33
+ extend(m::ClassMethods)
34
+ end
35
+ if m.const_defined?("DatasetMethods")
36
+ dataset.meta_def(:"#{plugin}_opts") {args.first}
37
+ dataset.metaclass.send(:include, m::DatasetMethods)
38
+ def_dataset_method(*m::DatasetMethods.instance_methods)
28
39
  end
29
- alias_method :is_a, :is
30
-
31
- private
32
- # Returns the module for the specified plugin. If the module is not
33
- # defined, the corresponding plugin gem is automatically loaded.
34
- def plugin_module(plugin)
35
- module_name = plugin.to_s.gsub(/(^|_)(.)/) {$2.upcase}
36
- if not Sequel::Plugins.const_defined?(module_name)
37
- require plugin_gem(plugin)
38
- end
39
- Sequel::Plugins.const_get(module_name)
40
- end
40
+ end
41
+ metaalias :is_a, :is
41
42
 
42
- # Returns the gem name for the given plugin.
43
- def plugin_gem(plugin)
44
- "sequel_#{plugin}"
45
- end
43
+ ### Private Class Methods ###
44
+
45
+ # Returns the gem name for the given plugin.
46
+ def self.plugin_gem(plugin) # :nodoc:
47
+ "sequel_#{plugin}"
48
+ end
49
+
50
+ # Returns the module for the specified plugin. If the module is not
51
+ # defined, the corresponding plugin gem is automatically loaded.
52
+ def self.plugin_module(plugin) # :nodoc:
53
+ module_name = plugin.to_s.gsub(/(^|_)(.)/){|x| x[-1..-1].upcase}
54
+ if not Sequel::Plugins.const_defined?(module_name)
55
+ require plugin_gem(plugin)
56
+ end
57
+ Sequel::Plugins.const_get(module_name)
46
58
  end
59
+
60
+ metaprivate :plugin_gem, :plugin_module
47
61
  end
48
62
  end
@@ -1,13 +1,56 @@
1
+ # This file holds general instance methods for Sequel::Model
2
+
1
3
  module Sequel
2
4
  class Model
3
- attr_reader :values
5
+ # The columns that have been updated. This isn't completely accurate,
6
+ # see Model#[]=.
4
7
  attr_reader :changed_columns
5
8
 
6
- # Returns value of attribute.
9
+ # The hash of attribute values. Keys are symbols with the names of the
10
+ # underlying database columns.
11
+ attr_reader :values
12
+
13
+ # Whether this model instance should typecast on attribute assignment
14
+ attr_writer :typecast_on_assignment
15
+
16
+ class_attr_reader :columns, :dataset, :db, :primary_key, :str_columns
17
+
18
+ # Creates new instance with values set to passed-in Hash.
19
+ # If a block is given, yield the instance to the block.
20
+ # This method runs the after_initialize hook after
21
+ # it has optionally yielded itself to the block.
22
+ #
23
+ # Arguments:
24
+ # * values - should be a hash with symbol keys, though
25
+ # string keys will work if from_db is false.
26
+ # * from_db - should only be set by Model.load, forget it
27
+ # exists.
28
+ def initialize(values = nil, from_db = false, &block)
29
+ values ||= {}
30
+ @changed_columns = []
31
+ @typecast_on_assignment = model.typecast_on_assignment
32
+ @db_schema = model.db_schema
33
+ if from_db
34
+ @new = false
35
+ @values = values
36
+ else
37
+ @values = {}
38
+ @new = true
39
+ set_with_params(values)
40
+ end
41
+ @changed_columns.clear
42
+
43
+ yield self if block
44
+ after_initialize
45
+ end
46
+
47
+ # Returns value of the column's attribute.
7
48
  def [](column)
8
49
  @values[column]
9
50
  end
10
- # Sets value of attribute and marks the column as changed.
51
+
52
+ # Sets value of the column's attribute and marks the column as changed.
53
+ # If the column already has the same value, this is a no-op.
11
54
  def []=(column, value)
12
55
  # If it is new, it doesn't have a value yet, so we should
13
56
  # definitely set the new value.
@@ -15,27 +58,10 @@ module Sequel
15
58
  # NULL in the database, so assume it has changed.
16
59
  if new? || !@values.include?(column) || value != @values[column]
17
60
  @changed_columns << column unless @changed_columns.include?(column)
18
- @values[column] = value
61
+ @values[column] = typecast_value(column, value)
19
62
  end
20
63
  end
21
64
 
22
- # Enumerates through all attributes.
23
- #
24
- # === Example:
25
- # Ticket.find(7).each { |k, v| puts "#{k} => #{v}" }
26
- def each(&block)
27
- @values.each(&block)
28
- end
29
- # Returns attribute names.
30
- def keys
31
- @values.keys
32
- end
33
-
34
- # Returns value for <tt>:id</tt> attribute.
35
- def id
36
- @values[:id]
37
- end
38
-
39
65
  # Compares model instances by values.
40
66
  def ==(obj)
41
67
  (obj.class == model) && (obj.values == @values)
@@ -48,201 +74,126 @@ module Sequel
48
74
  pk.nil? ? false : (obj.class == model) && (obj.pk == pk)
49
75
  end
50
76
 
51
- # Unique for objects with the same class and pk (if pk is not nil), or
52
- # the same class and values (if pk is nil).
53
- def hash
54
- [model, pk.nil? ? @values.sort_by{|k,v| k.to_s} : pk].hash
55
- end
77
+ # class is defined in Object, but it is also a keyword,
78
+ # and since a lot of instance methods call class methods,
79
+ # the model makes it so you can use model instead of
80
+ # self.class.
81
+ alias_method :model, :class
56
82
 
57
- # Returns key for primary key.
58
- def self.primary_key
59
- :id
60
- end
61
-
62
- # Returns primary key attribute hash.
63
- def self.primary_key_hash(value)
64
- {:id => value}
83
+ # Deletes and returns self. Does not run destroy hooks.
84
+ # Look into using destroy instead.
85
+ def delete
86
+ before_delete
87
+ this.delete
88
+ self
65
89
  end
66
90
 
67
- # Sets primary key, regular and composite are possible.
68
- #
69
- # == Example:
70
- # class Tagging < Sequel::Model
71
- # # composite key
72
- # set_primary_key :taggable_id, :tag_id
73
- # end
74
- #
75
- # class Person < Sequel::Model
76
- # # regular key
77
- # set_primary_key :person_id
78
- # end
79
- #
80
- # <i>You can even set it to nil!</i>
81
- def self.set_primary_key(*key)
82
- # if k is nil, we go to no_primary_key
83
- if key.empty? || (key.size == 1 && key.first == nil)
84
- return no_primary_key
85
- end
86
-
87
- # backwards compat
88
- key = (key.length == 1) ? key[0] : key.flatten
89
-
90
- # redefine primary_key
91
- meta_def(:primary_key) {key}
92
-
93
- unless key.is_a? Array # regular primary key
94
- class_def(:this) do
95
- @this ||= dataset.filter(key => @values[key]).limit(1).naked
96
- end
97
- class_def(:pk) do
98
- @pk ||= @values[key]
99
- end
100
- class_def(:pk_hash) do
101
- @pk ||= {key => @values[key]}
102
- end
103
- class_def(:cache_key) do
104
- pk = @values[key] || (raise Error, 'no primary key for this record')
105
- @cache_key ||= "#{self.class}:#{pk}"
106
- end
107
- meta_def(:primary_key_hash) do |v|
108
- {key => v}
109
- end
110
- else # composite key
111
- exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
112
- block = eval("proc {@this ||= self.class.dataset.filter(#{exp_list.join(',')}).limit(1).naked}")
113
- class_def(:this, &block)
114
-
115
- exp_list = key.map {|k| "@values[#{k.inspect}]"}
116
- block = eval("proc {@pk ||= [#{exp_list.join(',')}]}")
117
- class_def(:pk, &block)
118
-
119
- exp_list = key.map {|k| "#{k.inspect} => @values[#{k.inspect}]"}
120
- block = eval("proc {@this ||= {#{exp_list.join(',')}}}")
121
- class_def(:pk_hash, &block)
122
-
123
- exp_list = key.map {|k| '#{@values[%s]}' % k.inspect}.join(',')
124
- block = eval('proc {@cache_key ||= "#{self.class}:%s"}' % exp_list)
125
- class_def(:cache_key, &block)
126
-
127
- meta_def(:primary_key_hash) do |v|
128
- key.inject({}) {|m, i| m[i] = v.shift; m}
129
- end
91
+ # Like delete but runs hooks before and after delete.
92
+ # If before_destroy returns false, returns false without
93
+ # deleting the object the the database. Otherwise, deletes
94
+ # the item from the database and returns self.
95
+ def destroy
96
+ db.transaction do
97
+ return false if before_destroy == false
98
+ delete
99
+ after_destroy
130
100
  end
101
+ self
131
102
  end
132
103
 
133
- def self.no_primary_key #:nodoc:
134
- meta_def(:primary_key) {nil}
135
- meta_def(:primary_key_hash) {|v| raise Error, "#{self} does not have a primary key"}
136
- class_def(:this) {raise Error, "No primary key is associated with this model"}
137
- class_def(:pk) {raise Error, "No primary key is associated with this model"}
138
- class_def(:pk_hash) {raise Error, "No primary key is associated with this model"}
139
- class_def(:cache_key) {raise Error, "No primary key is associated with this model"}
104
+ # Enumerates through all attributes.
105
+ #
106
+ # Example:
107
+ # Ticket.find(7).each { |k, v| puts "#{k} => #{v}" }
108
+ def each(&block)
109
+ @values.each(&block)
140
110
  end
141
-
142
- # Creates new instance with values set to passed-in Hash, saves it
143
- # (running any callbacks), and returns the instance.
144
- def self.create(values = {}, &block)
145
- obj = new(values, &block)
146
- obj.save
147
- obj
111
+
112
+ # Returns true when current instance exists, false otherwise.
113
+ def exists?
114
+ this.count > 0
148
115
  end
149
116
 
150
- # Updates the instance with the supplied values with support for virtual
151
- # attributes, ignoring any values for which no setter method is available.
152
- # Does not save the record.
153
- #
154
- # If no columns have been set for this model (very unlikely), assume symbol
155
- # keys are valid column names, and assign the column value based on that.
156
- def set_with_params(hash)
157
- columns_not_set = !model.instance_variable_get(:@columns)
158
- meths = setter_methods
159
- hash.each do |k,v|
160
- m = "#{k}="
161
- if meths.include?(m)
162
- send(m, v)
163
- elsif columns_not_set && (Symbol === k)
164
- self[k] = v
165
- end
166
- end
117
+ # Unique for objects with the same class and pk (if pk is not nil), or
118
+ # the same class and values (if pk is nil).
119
+ def hash
120
+ [model, pk.nil? ? @values.sort_by{|k,v| k.to_s} : pk].hash
167
121
  end
168
122
 
169
- # Runs set_with_params and saves the changes (which runs any callback methods).
170
- def update_with_params(values)
171
- set_with_params(values)
172
- save_changes
123
+ # Returns value for the :id attribute, even if the primary key is
124
+ # not id. To get the primary key value, use #pk.
125
+ def id
126
+ @values[:id]
173
127
  end
174
128
 
175
- # Returns (naked) dataset bound to current instance.
176
- def this
177
- @this ||= self.class.dataset.filter(:id => @values[:id]).limit(1).naked
129
+ # Returns a string representation of the model instance including
130
+ # the class name and values.
131
+ def inspect
132
+ "#<#{model.name} @values=#{@values.inspect}>"
178
133
  end
179
-
180
- # Returns a key unique to the underlying record for caching
181
- def cache_key
182
- pk = @values[:id] || (raise Error, 'no primary key for this record')
183
- @cache_key ||= "#{self.class}:#{pk}"
134
+
135
+ # Returns attribute names as an array of symbols.
136
+ def keys
137
+ @values.keys
184
138
  end
185
139
 
186
- # Returns primary key column(s) for object's Model class.
187
- def primary_key
188
- @primary_key ||= self.class.primary_key
140
+ # Returns true if the current instance represents a new record.
141
+ def new?
142
+ @new
189
143
  end
190
144
 
191
- # Returns the primary key value identifying the model instance. If the
192
- # model's primary key is changed (using #set_primary_key or #no_primary_key)
193
- # this method is redefined accordingly.
145
+ # Returns the primary key value identifying the model instance.
146
+ # Raises an error if this model does not have a primary key.
147
+ # If the model has a composite primary key, returns an array of values.
194
148
  def pk
195
- @pk ||= @values[:id]
196
- end
197
-
198
- # Returns a hash identifying the model instance. Stock implementation.
199
- def pk_hash
200
- @pk_hash ||= {:id => @values[:id]}
201
- end
202
-
203
- # Creates new instance with values set to passed-in Hash.
204
- #
205
- # This method guesses whether the record exists when
206
- # <tt>new_record</tt> is set to false.
207
- def initialize(values = nil, from_db = false, &block)
208
- values ||= {}
209
- @changed_columns = []
210
- if from_db
211
- @new = false
212
- @values = values
149
+ raise(Error, "No primary key is associated with this model") unless key = primary_key
150
+ case key
151
+ when Array
152
+ key.collect{|k| @values[k]}
213
153
  else
214
- @values = {}
215
- @new = true
216
- set_with_params(values)
154
+ @values[key]
217
155
  end
218
- @changed_columns.clear
219
-
220
- yield self if block
221
- after_initialize
222
156
  end
223
157
 
224
- # Initializes a model instance as an existing record. This constructor is
225
- # used by Sequel to initialize model instances when fetching records.
226
- def self.load(values)
227
- new(values, true)
228
- end
229
-
230
- # Returns true if the current instance represents a new record.
231
- def new?
232
- @new
158
+ # Returns a hash identifying the model instance. It should be true that:
159
+ #
160
+ # Model[model_instance.pk_hash] === model_instance
161
+ def pk_hash
162
+ model.primary_key_hash(pk)
233
163
  end
234
164
 
235
- # Returns true when current instance exists, false otherwise.
236
- def exists?
237
- this.count > 0
165
+ # Reloads attributes from database and returns self. Also clears all
166
+ # cached association information. Raises an Error if the record no longer
167
+ # exists in the database.
168
+ def refresh
169
+ @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
173
+ self
238
174
  end
239
-
240
- # Creates or updates the associated record. This method can also
241
- # accept a list of specific columns to update.
175
+ alias_method :reload, :refresh
176
+
177
+ # Creates or updates the record, after making sure the record
178
+ # is valid. If the record is not valid, returns false.
179
+ # If before_save, before_create (if new?), or before_update
180
+ # (if !new?) return false, returns false. Otherwise,
181
+ # returns self.
242
182
  def save(*columns)
243
- before_save
183
+ return false unless valid?
184
+ save!(*columns)
185
+ end
186
+
187
+ # Creates or updates the record, without attempting to validate
188
+ # it first. You can provide an optional list of columns to update,
189
+ # in which case it only updates those columns.
190
+ # If before_save, before_create (if new?), or before_update
191
+ # (if !new?) return false, returns false. Otherwise,
192
+ # returns self.
193
+ def save!(*columns)
194
+ return false if before_save == false
244
195
  if @new
245
- before_create
196
+ return false if before_create == false
246
197
  iid = model.dataset.insert(@values)
247
198
  # if we have a regular primary key and it's not set in @values,
248
199
  # we assume it's the last inserted id
@@ -256,7 +207,7 @@ module Sequel
256
207
  @new = false
257
208
  after_create
258
209
  else
259
- before_update
210
+ return false if before_update == false
260
211
  if columns.empty?
261
212
  this.update(@values)
262
213
  @changed_columns = []
@@ -291,10 +242,10 @@ module Sequel
291
242
  when String
292
243
  # Prevent denial of service via memory exhaustion by only
293
244
  # calling to_sym if the symbol already exists.
294
- raise(::Sequel::Error, "all string keys must be a valid columns") unless s.include?(k)
245
+ raise(Error, "all string keys must be a valid columns") unless s.include?(k)
295
246
  k.to_sym
296
247
  else
297
- raise(::Sequel::Error, "Only symbols and strings allows as keys")
248
+ raise(Error, "Only symbols and strings allows as keys")
298
249
  end
299
250
  m[k] = v
300
251
  m
@@ -303,46 +254,60 @@ module Sequel
303
254
  vals
304
255
  end
305
256
 
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
+ # Returns (naked) dataset that should return only this instance.
277
+ def this
278
+ @this ||= dataset.filter(pk_hash).limit(1).naked
279
+ end
280
+
306
281
  # Sets the values attributes with set_values and then updates
307
282
  # the record in the database using those values. This is a
308
283
  # low level method that does not run the usual save callbacks.
309
284
  # It should probably be avoided. Look into using update_with_params instead.
310
285
  def update_values(values)
286
+ before_update_values
311
287
  this.update(set_values(values))
312
288
  end
313
289
 
314
- # Reloads values from database and returns self.
315
- def refresh
316
- @values = this.first || raise(Error, "Record not found")
317
- model.all_association_reflections.each do |r|
318
- instance_variable_set("@#{r[:name]}", nil)
319
- end
320
- self
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
321
294
  end
322
- alias_method :reload, :refresh
323
295
 
324
- # Like delete but runs hooks before and after delete.
325
- def destroy
326
- db.transaction do
327
- before_destroy
328
- delete
329
- after_destroy
330
- end
331
- self
296
+ private
297
+
298
+ # Returns all methods that can be used for attribute
299
+ # assignment (those that end with =)
300
+ def setter_methods
301
+ methods.grep(/=\z/)
332
302
  end
333
-
334
- # Deletes and returns self. Does not run callbacks.
335
- # Look into using destroy instead.
336
- def delete
337
- this.delete
338
- self
303
+
304
+ # Typecast the value to the column's type if typecasting. Calls the database's
305
+ # typecast_value method, so database adapters can override/augment the handling
306
+ # for database specific column types.
307
+ def typecast_value(column, value)
308
+ return value unless @typecast_on_assignment && @db_schema && (col_schema = @db_schema[column])
309
+ raise(Error, "nil/NULL is not allowed for the #{column} column") if value.nil? && (col_schema[:allow_null] == false)
310
+ model.db.typecast_value(col_schema[:type], value)
339
311
  end
340
-
341
- private
342
- # Returns all methods that can be used for attribute
343
- # assignment (those that end with =)
344
- def setter_methods
345
- methods.grep(/=\z/)
346
- end
347
312
  end
348
313
  end