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,24 +1,79 @@
1
+ # This file holds general class methods for Sequel::Model
2
+
1
3
  module Sequel
2
4
  class Model
3
- # If possible, set the dataset for the model subclass as soon as it
4
- # is created.
5
- def self.inherited(subclass)
6
- begin
7
- if subclass.superclass == Model
8
- unless subclass.name.empty?
9
- subclass.set_dataset(Model.db[subclass.implicit_table_name])
10
- end
11
- elsif ds = subclass.superclass.instance_variable_get(:@dataset)
12
- subclass.set_dataset(ds.clone)
13
- end
14
- rescue StandardError
5
+ # Whether to lazily load the schema for future subclasses. Unless turned
6
+ # off, checks the database for the table schema whenever a subclass is
7
+ # created
8
+ @@lazy_load_schema = false
9
+
10
+ # The default primary key for tables, inherited by future subclasses
11
+ @primary_key = :id
12
+
13
+ # Whether to typecast attribute values on assignment, inherited by
14
+ # future subclasses.
15
+ @typecast_on_assignment = true
16
+
17
+ # The default primary key for classes (default: :id)
18
+ metaattr_accessor :primary_key
19
+
20
+ # Whether to typecast attribute values on assignment (default: true)
21
+ metaattr_accessor :typecast_on_assignment
22
+
23
+ # Dataset methods to proxy via metaprogramming
24
+ DATASET_METHODS = %w'<< all avg count delete distinct eager eager_graph each each_page
25
+ empty? except exclude filter first from_self full_outer_join get graph
26
+ group group_and_count group_by having import inner_join insert
27
+ insert_multiple intersect interval invert_order join join_table last
28
+ left_outer_join limit map multi_insert naked order order_by order_more
29
+ paginate print query range reverse_order right_outer_join select
30
+ select_all select_more set set_graph_aliases single_value size to_csv
31
+ transform union uniq unordered update where'
32
+
33
+ # Returns the first record from the database matching the conditions.
34
+ # If a hash is given, it is used as the conditions. If another
35
+ # object is given, it finds the first record whose primary key(s) match
36
+ # the given argument(s). If caching is used, the cache is checked
37
+ # first before a dataset lookup is attempted unless a hash is supplied.
38
+ def self.[](*args)
39
+ args = args.first if (args.size == 1)
40
+ raise(Error::InvalidFilter, "Did you mean to supply a hash?") if args === true || args === false
41
+
42
+ if Hash === args
43
+ dataset[args]
44
+ else
45
+ @cache_store ? cache_lookup(args) : dataset[primary_key_hash(args)]
15
46
  end
16
47
  end
48
+
49
+ # Returns the columns in the result set in their original order.
50
+ # Generally, this will used the columns determined via the database
51
+ # schema, but in certain cases (e.g. models that are based on a joined
52
+ # dataset) it will use Dataset#columns to find the columns, which
53
+ # may be empty if the Dataset has no records.
54
+ def self.columns
55
+ @columns || set_columns(dataset.naked.columns || raise(Error, "Could not fetch columns for #{self}"))
56
+ end
57
+
58
+ # Creates new instance with values set to passed-in Hash, saves it
59
+ # (running any callbacks), and returns the instance if the object
60
+ # was saved correctly. If there was an error saving the object,
61
+ # returns false.
62
+ def self.create(values = {}, &block)
63
+ obj = new(values, &block)
64
+ return false if obj.save == false
65
+ obj
66
+ end
17
67
 
68
+ # Returns the dataset associated with the Model class.
69
+ def self.dataset
70
+ @dataset || raise(Error, "No dataset associated with #{self}")
71
+ end
72
+
18
73
  # Returns the database associated with the Model class.
19
74
  def self.db
20
75
  return @db if @db
21
- @db = self == Model ? ::Sequel::DATABASES.first : superclass.db
76
+ @db = self == Model ? DATABASES.first : superclass.db
22
77
  raise(Error, "No database associated with #{self}") unless @db
23
78
  @db
24
79
  end
@@ -31,14 +86,10 @@ module Sequel
31
86
  end
32
87
  end
33
88
 
34
- # Returns the implicit table name for the model class.
35
- def self.implicit_table_name
36
- name.demodulize.underscore.pluralize.to_sym
37
- end
38
-
39
- # Returns the dataset associated with the Model class.
40
- def self.dataset
41
- @dataset || raise(Error, "No dataset associated with #{self}")
89
+ # Returns the cached schema information if available or gets it
90
+ # from the database.
91
+ def self.db_schema
92
+ @db_schema ||= get_db_schema
42
93
  end
43
94
 
44
95
  # If a block is given, define a method on the dataset with the given argument name using
@@ -56,110 +107,265 @@ module Sequel
56
107
  args.each{|arg| instance_eval("def #{arg}(*args, &block); dataset.#{arg}(*args, &block) end", __FILE__, __LINE__)}
57
108
  end
58
109
 
59
- # Returns the columns in the result set in their original order.
110
+ # Deletes all records in the model's table.
111
+ def self.delete_all
112
+ dataset.delete
113
+ end
114
+
115
+ # Like delete_all, but invokes before_destroy and after_destroy hooks if used.
116
+ def self.destroy_all
117
+ dataset.destroy
118
+ end
119
+
120
+ # Returns a dataset with custom SQL that yields model objects.
121
+ def self.fetch(*args)
122
+ db.fetch(*args).set_model(self)
123
+ end
124
+
125
+ # Finds a single record according to the supplied filter, e.g.:
60
126
  #
61
- # See Dataset#columns for more information.
62
- def self.columns
63
- return @columns if @columns
64
- @columns = dataset.naked.columns or
65
- raise Error, "Could not fetch columns for #{self}"
66
- def_column_accessor(*@columns)
67
- @str_columns = nil
68
- @columns
127
+ # Ticket.find :author => 'Sharon' # => record
128
+ def self.find(*args, &block)
129
+ dataset.filter(*args, &block).first
130
+ end
131
+
132
+ # Like find but invokes create with given conditions when record does not
133
+ # exists.
134
+ def self.find_or_create(cond)
135
+ find(cond) || create(cond)
136
+ end
137
+
138
+ # 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.
141
+ def self.inherited(subclass)
142
+ sup_class = subclass.superclass
143
+ 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")
146
+ unless ivs.include?("@dataset")
147
+ begin
148
+ if sup_class == Model
149
+ subclass.set_dataset(Model.db[subclass.implicit_table_name]) unless subclass.name.empty?
150
+ elsif ds = sup_class.instance_variable_get(:@dataset)
151
+ subclass.set_dataset(ds.clone)
152
+ end
153
+ rescue
154
+ end
155
+ end
156
+ end
157
+
158
+ # Returns the implicit table name for the model class.
159
+ def self.implicit_table_name
160
+ name.demodulize.underscore.pluralize.to_sym
69
161
  end
70
162
 
71
- # Returns the columns as a list of frozen strings.
72
- def self.str_columns
73
- @str_columns ||= columns.map{|c| c.to_s.freeze}
163
+ # Set whether to lazily load the schema for future model classes.
164
+ # When the schema is lazy loaded, the schema information is grabbed
165
+ # during the first instantiation of the class instead of
166
+ # when the class is created.
167
+ def self.lazy_load_schema=(value)
168
+ @@lazy_load_schema = value
169
+ end
170
+
171
+ # Initializes a model instance as an existing record. This constructor is
172
+ # used by Sequel to initialize model instances when fetching records.
173
+ # #load requires that values be a hash where all keys are symbols. It
174
+ # probably should not be used by external code.
175
+ def self.load(values)
176
+ new(values, true)
177
+ end
178
+
179
+ # Mark the model as not having a primary key. Not having a primary key
180
+ # can cause issues, among which is that you won't be able to update records.
181
+ def self.no_primary_key
182
+ @primary_key = nil
183
+ end
184
+
185
+ # Returns primary key attribute hash. If using a composite primary key
186
+ # value such be an array with values for each primary key in the correct
187
+ # order. For a standard primary key, value should be an object with a
188
+ # compatible type for the key. If the model does not have a primary key,
189
+ # raises an Error.
190
+ def self.primary_key_hash(value)
191
+ raise(Error, "#{self} does not have a primary key") unless key = @primary_key
192
+ case key
193
+ when Array
194
+ hash = {}
195
+ key.each_with_index{|k,i| hash[k] = value[i]}
196
+ hash
197
+ else
198
+ {key => value}
199
+ end
74
200
  end
75
201
 
76
- # Sets the dataset associated with the Model class.
202
+ # Serializes column with YAML or through marshalling. Arguments should be
203
+ # column symbols, with an optional trailing hash with a :format key
204
+ # set to :yaml or :marshal (:yaml is the default). Setting this adds
205
+ # a transform to the model and dataset so that columns values will be serialized
206
+ # when saved and deserialized when returned from the database.
207
+ def self.serialize(*columns)
208
+ format = columns.extract_options![:format] || :yaml
209
+ @transform = columns.inject({}) do |m, c|
210
+ m[c] = format
211
+ m
212
+ end
213
+ @dataset.transform(@transform) if @dataset
214
+ end
215
+
216
+ # Sets the dataset associated with the Model class. ds can be a Symbol
217
+ # (specifying a table name in the current database), or a Dataset.
218
+ # If a dataset is used, the model's database is changed to the given
219
+ # dataset. If a symbol is used, a dataset is created from the current
220
+ # database with the table name given. Other arguments raise an Error.
221
+ #
222
+ # This sets the model of the the given/created dataset to the current model
223
+ # and adds a destroy method to it. It also extends the dataset with
224
+ # the Associations::EagerLoading methods, and assigns a transform to it
225
+ # if there is one associated with the model. Finally, it attempts to
226
+ # determine the database schema based on the given/created dataset unless
227
+ # lazy_load_schema is set.
77
228
  def self.set_dataset(ds)
78
- @db = ds.db
79
- @dataset = ds
229
+ @dataset = case ds
230
+ when Symbol
231
+ db[ds]
232
+ when Dataset
233
+ @db = ds.db
234
+ ds
235
+ else
236
+ raise(Error, "Model.set_dataset takes a Symbol or a Sequel::Dataset")
237
+ end
80
238
  @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
81
245
  @dataset.extend(Associations::EagerLoading)
82
246
  @dataset.transform(@transform) if @transform
83
247
  begin
84
- @columns = nil
85
- columns
86
- rescue StandardError
248
+ (@db_schema = get_db_schema) unless @@lazy_load_schema
249
+ rescue
87
250
  end
251
+ self
88
252
  end
89
- class << self; alias :dataset= :set_dataset; end
90
-
91
- class << self
92
- private
93
- def def_column_accessor(*columns)
94
- Thread.exclusive do
95
- columns.each do |column|
96
- im = instance_methods
97
- meth = "#{column}="
98
- define_method(column){self[column]} unless im.include?(column.to_s)
99
- unless im.include?(meth)
100
- define_method(meth) do |*v|
101
- len = v.length
102
- raise(ArgumentError, "wrong number of arguments (#{len} for 1)") unless len == 1
103
- self[column] = v.first
104
- end
105
- end
106
- end
107
- end
108
- end
109
- end
110
-
111
- # Returns the database assoiated with the object's Model class.
112
- def db
113
- @db ||= model.db
253
+ metaalias :dataset=, :set_dataset
254
+
255
+ # Sets primary key, regular and composite are possible.
256
+ #
257
+ # Example:
258
+ # class Tagging < Sequel::Model
259
+ # # composite key
260
+ # set_primary_key :taggable_id, :tag_id
261
+ # end
262
+ #
263
+ # class Person < Sequel::Model
264
+ # # regular key
265
+ # set_primary_key :person_id
266
+ # end
267
+ #
268
+ # You can set it to nil to not have a primary key, but that
269
+ # cause certain things not to work, see #no_primary_key.
270
+ def self.set_primary_key(*key)
271
+ @primary_key = (key.length == 1) ? key[0] : key.flatten
114
272
  end
115
273
 
116
- # Returns the dataset assoiated with the object's Model class.
274
+ # Returns the columns as a list of frozen strings instead
275
+ # of a list of symbols. This makes it possible to check
276
+ # whether a column exists without creating a symbol, which
277
+ # would be a memory leak if called with user input.
278
+ def self.str_columns
279
+ @str_columns ||= columns.map{|c| c.to_s.freeze}
280
+ end
281
+
282
+ # Defines a method that returns a filtered dataset. Subsets
283
+ # create dataset methods, so they can be chained for scoping.
284
+ # For example:
285
+ #
286
+ # Topic.subset(:popular, :num_posts > 100)
287
+ # Topic.subset(:recent, :created_on > Date.today - 7)
288
+ #
289
+ # Allows you to do:
117
290
  #
118
- # See Dataset for more information.
119
- def dataset
120
- model.dataset
291
+ # Topic.filter(:username.like('%joe%')).popular.recent
292
+ #
293
+ # to get topics with a username that includes joe that
294
+ # have more than 100 posts and were created less than
295
+ # 7 days ago.
296
+ def self.subset(name, *args, &block)
297
+ def_dataset_method(name){filter(*args, &block)}
121
298
  end
122
299
 
123
- # Returns the columns associated with the object's Model class.
124
- def columns
125
- model.columns
300
+ # Returns name of primary table for the dataset.
301
+ def self.table_name
302
+ dataset.opts[:from].first
126
303
  end
127
304
 
128
- # Returns the str_columns associated with the object's Model class.
129
- def str_columns
130
- model.str_columns
305
+ # Add model methods that call dataset methods
306
+ def_dataset_method(*DATASET_METHODS)
307
+
308
+ ### Private Class Methods ###
309
+
310
+ # Create the column accessors
311
+ def self.def_column_accessor(*columns) # :nodoc:
312
+ columns.each do |column|
313
+ im = instance_methods
314
+ meth = "#{column}="
315
+ define_method(column){self[column]} unless im.include?(column.to_s)
316
+ unless im.include?(meth)
317
+ define_method(meth) do |*v|
318
+ len = v.length
319
+ raise(ArgumentError, "wrong number of arguments (#{len} for 1)") unless len == 1
320
+ self[column] = v.first
321
+ end
322
+ end
323
+ end
131
324
  end
132
325
 
133
- # Serializes column with YAML or through marshalling.
134
- def self.serialize(*columns)
135
- format = columns.pop[:format] if Hash === columns.last
136
- format ||= :yaml
137
-
138
- @transform = columns.inject({}) do |m, c|
139
- m[c] = format
140
- m
326
+ # Get the schema from the database, fall back on checking the columns
327
+ # via the database if that will return inaccurate results or if
328
+ # it raises an error.
329
+ def self.get_db_schema # :nodoc:
330
+ set_columns(nil)
331
+ return nil unless @dataset
332
+ schema_hash = {}
333
+ ds_opts = dataset.opts
334
+ single_table = ds_opts[:from] && (ds_opts[:from].length == 1) \
335
+ && !ds_opts.include?(:join) && !ds_opts.include?(:sql)
336
+ get_columns = proc{columns rescue []}
337
+ if single_table && (schema_array = (db.schema(table_name) rescue nil))
338
+ schema_array.each{|k,v| schema_hash[k] = v}
339
+ if ds_opts.include?(:select)
340
+ # Dataset only selects certain columns, delete the other
341
+ # columns from the schema
342
+ cols = get_columns.call
343
+ schema_hash.delete_if{|k,v| !cols.include?(k)}
344
+ cols.each{|c| schema_hash[c] ||= {}}
345
+ else
346
+ # Dataset is for a single table with all columns,
347
+ # so set the columns based on the order they were
348
+ # returned by the schema.
349
+ set_columns(schema_array.collect{|k,v| k})
350
+ end
351
+ else
352
+ # If the dataset uses multiple tables or custom sql or getting
353
+ # the schema raised an error, just get the columns and
354
+ # create an empty schema hash for it.
355
+ get_columns.call.each{|c| schema_hash[c] = {}}
141
356
  end
142
- @dataset.transform(@transform) if @dataset
357
+ schema_hash
358
+ end
359
+
360
+ # Set the columns for this model, reset the str_columns,
361
+ # and create accessor methods for each column.
362
+ def self.set_columns(new_columns) # :nodoc:
363
+ @columns = new_columns
364
+ def_column_accessor(*new_columns) if new_columns
365
+ @str_columns = nil
366
+ @columns
143
367
  end
144
- end
145
368
 
146
- # Lets you create a Model class with its table name already set or reopen
147
- # an existing Model.
148
- #
149
- # Makes given dataset inherited.
150
- #
151
- # === Example:
152
- # class Comment < Sequel::Model(:something)
153
- # table_name # => :something
154
- #
155
- # # ...
156
- #
157
- # end
158
- @models = {}
159
- def self.Model(source)
160
- return @models[source] if @models[source]
161
- klass = Class.new(Sequel::Model)
162
- klass.set_dataset(source.is_a?(Dataset) ? source : Model.db[source])
163
- @models[source] = klass
369
+ metaprivate :def_column_accessor, :get_db_schema, :set_columns
164
370
  end
165
371
  end