sequel 1.5.1 → 2.0.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.
@@ -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