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,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