sequel 1.3 → 1.4.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.
- data/CHANGELOG +127 -0
- data/COPYING +1 -0
- data/README +5 -4
- data/Rakefile +78 -25
- data/lib/sequel.rb +1 -2
- data/lib/sequel_model.rb +324 -0
- data/lib/sequel_model/associations.rb +351 -0
- data/lib/sequel_model/base.rb +120 -0
- data/lib/sequel_model/caching.rb +42 -0
- data/lib/sequel_model/eager_loading.rb +169 -0
- data/lib/sequel_model/hooks.rb +55 -0
- data/lib/sequel_model/plugins.rb +47 -0
- data/lib/sequel_model/pretty_table.rb +73 -0
- data/lib/sequel_model/record.rb +336 -0
- data/lib/sequel_model/schema.rb +48 -0
- data/lib/sequel_model/validations.rb +15 -0
- data/spec/associations_spec.rb +712 -0
- data/spec/base_spec.rb +239 -0
- data/spec/caching_spec.rb +150 -0
- data/spec/deprecated_relations_spec.rb +153 -0
- data/spec/eager_loading_spec.rb +260 -0
- data/spec/hooks_spec.rb +269 -0
- data/spec/model_spec.rb +543 -0
- data/spec/plugins_spec.rb +74 -0
- data/spec/rcov.opts +4 -0
- data/spec/record_spec.rb +593 -0
- data/spec/schema_spec.rb +69 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/validations_spec.rb +246 -0
- metadata +90 -56
@@ -0,0 +1,351 @@
|
|
1
|
+
# Associations are used in order to specify relationships between model classes
|
2
|
+
# that reflect relations between tables in the database using foreign keys.
|
3
|
+
#
|
4
|
+
# Each kind of association adds a number of methods to the model class which
|
5
|
+
# are specialized according to the association type and optional parameters
|
6
|
+
# given in the definition. Example:
|
7
|
+
#
|
8
|
+
# class Project < Sequel::Model
|
9
|
+
# many_to_one :portfolio
|
10
|
+
# one_to_many :milestones
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# The project class now has the following methods:
|
14
|
+
# * Project#portfolio, Project#portfolio=
|
15
|
+
# * Project#milestones, Project#add_milestone, Project#remove_milestone,
|
16
|
+
# Project#milestones_dataset
|
17
|
+
#
|
18
|
+
# By default the classes for the associations are inferred from the association
|
19
|
+
# name, so for example the Project#portfolio will return an instance of
|
20
|
+
# Portfolio, and Project#milestones will return an array of Milestone
|
21
|
+
# instances, in similar fashion to how ActiveRecord infers class names.
|
22
|
+
#
|
23
|
+
# Association definitions are also reflected by the class, e.g.:
|
24
|
+
#
|
25
|
+
# >> Project.associations
|
26
|
+
# => [:portfolio, :milestones]
|
27
|
+
# >> Project.association_reflection(:portfolio)
|
28
|
+
# => {:type => :many_to_one, :name => :portfolio, :class_name => "Portfolio"}
|
29
|
+
#
|
30
|
+
# Associations can be defined by either using the associate method, or by
|
31
|
+
# calling one of the three methods: many_to_one, one_to_many, many_to_many.
|
32
|
+
# Sequel::Model also provides aliases for these methods that conform to
|
33
|
+
# ActiveRecord conventions: belongs_to, has_many, has_and_belongs_to_many.
|
34
|
+
# For example, the following three statements are equivalent:
|
35
|
+
#
|
36
|
+
# associate :one_to_many, :attributes
|
37
|
+
# one_to_many :attributes
|
38
|
+
# has_many :attributes
|
39
|
+
module Sequel::Model::Associations
|
40
|
+
# Array of all association reflections
|
41
|
+
def all_association_reflections
|
42
|
+
association_reflections.values
|
43
|
+
end
|
44
|
+
|
45
|
+
# Associates a related model with the current model. The following types are
|
46
|
+
# supported:
|
47
|
+
#
|
48
|
+
# * :many_to_one - Foreign key in current model's table points to
|
49
|
+
# associated model's primary key. Each associated model object can
|
50
|
+
# be associated with more than one current model objects. Each current
|
51
|
+
# model object can be associated with only one associated model object.
|
52
|
+
# Similar to ActiveRecord/DataMapper's belongs_to.
|
53
|
+
# * :one_to_many - Foreign key in associated model's table points to this
|
54
|
+
# model's primary key. Each current model object can be associated with
|
55
|
+
# more than one associated model objects. Each associated model object
|
56
|
+
# can be associated with only one current model object.
|
57
|
+
# Similar to ActiveRecord/DataMapper's has_many.
|
58
|
+
# * :many_to_many - A join table is used that has a foreign key that points
|
59
|
+
# to this model's primary key and a foreign key that points to the
|
60
|
+
# associated model's primary key. Each current model object can be
|
61
|
+
# associated with many associated model objects, and each associated
|
62
|
+
# model object can be associated with many current model objects.
|
63
|
+
# Similar to ActiveRecord/DataMapper's has_and_belongs_to_many.
|
64
|
+
#
|
65
|
+
# The following options can be supplied:
|
66
|
+
# * *ALL types*:
|
67
|
+
# - :class - The associated class or its name. If not
|
68
|
+
# given, uses the association's name, which is camelized (and
|
69
|
+
# singularized if type is :{one,many}_to_many)
|
70
|
+
# - :eager - The associations to eagerly load when loading the associated object.
|
71
|
+
# For many_to_one associations, this is ignored unless this association is
|
72
|
+
# being eagerly loaded, as it doesn't save queries unless multiple objects
|
73
|
+
# can be loaded at once.
|
74
|
+
# * :many_to_one:
|
75
|
+
# - :key - foreign_key in current model's table that references
|
76
|
+
# associated model's primary key, as a symbol. Defaults to :"#{name}_id".
|
77
|
+
# * :one_to_many:
|
78
|
+
# - :key - foreign key in associated model's table that references
|
79
|
+
# current model's primary key, as a symbol. Defaults to
|
80
|
+
# :"#{self.name.underscore}_id".
|
81
|
+
# - :reciprocal - the string name of the instance variable of the reciprocal many_to_one association,
|
82
|
+
# if it exists. By default, sequel will try to determine it by looking at the
|
83
|
+
# associated model's assocations for a many_to_one association that matches
|
84
|
+
# the current association's key. Set to nil to not use a reciprocal.
|
85
|
+
# - :order - the column(s) by which to order the association dataset. Can be a
|
86
|
+
# singular column or an array.
|
87
|
+
# * :many_to_many:
|
88
|
+
# - :join_table - name of table that includes the foreign keys to both
|
89
|
+
# the current model and the associated model, as a symbol. Defaults to the name
|
90
|
+
# of current model and name of associated model, pluralized,
|
91
|
+
# underscored, sorted, and joined with '_'.
|
92
|
+
# - :left_key - foreign key in join table that points to current model's
|
93
|
+
# primary key, as a symbol.
|
94
|
+
# - :right_key - foreign key in join table that points to associated
|
95
|
+
# model's primary key, as a symbol.
|
96
|
+
# - :select - the attributes to select. Defaults to the associated class's
|
97
|
+
# table_name.*, which means it doesn't include the attributes from the join
|
98
|
+
# join table. If you want to include the join table attributes, you can
|
99
|
+
# use this option, but beware that the join table attributes can clash with
|
100
|
+
# attributes from the model table, so you should alias any attributes that have
|
101
|
+
# the same name in both the join table and the associated table.
|
102
|
+
# - :order - the column(s) by which to order the association dataset. Can be a
|
103
|
+
# singular column or an array.
|
104
|
+
def associate(type, name, opts = {}, &block)
|
105
|
+
# check arguments
|
106
|
+
raise ArgumentError unless [:many_to_one, :one_to_many, :many_to_many].include?(type) && Symbol === name
|
107
|
+
|
108
|
+
# merge early so we don't modify opts
|
109
|
+
opts = opts.merge(:type => type, :name => name, :block => block, :cache => true)
|
110
|
+
|
111
|
+
# deprecation
|
112
|
+
if opts[:from]
|
113
|
+
STDERR << "The :from option is deprecated, please use the :class option instead.\r\n"
|
114
|
+
opts[:class] = opts[:from]
|
115
|
+
end
|
116
|
+
|
117
|
+
# find class
|
118
|
+
case opts[:class]
|
119
|
+
when String, Symbol
|
120
|
+
# Delete :class to allow late binding
|
121
|
+
opts[:class_name] ||= opts.delete(:class).to_s
|
122
|
+
when Class
|
123
|
+
opts[:class_name] ||= opts[:class].name
|
124
|
+
end
|
125
|
+
|
126
|
+
send(:"def_#{type}", name, opts)
|
127
|
+
|
128
|
+
# don't add to association_reflections until we are sure there are no errors
|
129
|
+
association_reflections[name] = opts
|
130
|
+
end
|
131
|
+
|
132
|
+
# The association reflection hash for the association of the given name.
|
133
|
+
def association_reflection(name)
|
134
|
+
association_reflections[name]
|
135
|
+
end
|
136
|
+
|
137
|
+
# Array of association name symbols
|
138
|
+
def associations
|
139
|
+
association_reflections.keys
|
140
|
+
end
|
141
|
+
|
142
|
+
# deprecated, please use many_to_one instead
|
143
|
+
def one_to_one(*args, &block)
|
144
|
+
STDERR << "one_to_one relation definitions are deprecated, please use many_to_one instead.\r\n"
|
145
|
+
many_to_one(*args, &block)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Shortcut for adding a one_to_many association, see associate
|
149
|
+
def one_to_many(*args, &block)
|
150
|
+
associate(:one_to_many, *args, &block)
|
151
|
+
end
|
152
|
+
alias_method :has_many, :one_to_many
|
153
|
+
|
154
|
+
# Shortcut for adding a many_to_one association, see associate
|
155
|
+
def many_to_one(*args, &block)
|
156
|
+
associate(:many_to_one, *args, &block)
|
157
|
+
end
|
158
|
+
alias_method :belongs_to, :many_to_one
|
159
|
+
|
160
|
+
# Shortcut for adding a many_to_many association, see associate
|
161
|
+
def many_to_many(*args, &block)
|
162
|
+
associate(:many_to_many, *args, &block)
|
163
|
+
end
|
164
|
+
alias_method :has_and_belongs_to_many, :many_to_many
|
165
|
+
|
166
|
+
private
|
167
|
+
# The class related to the given association reflection
|
168
|
+
def associated_class(opts)
|
169
|
+
opts[:class] ||= opts[:class_name].constantize
|
170
|
+
end
|
171
|
+
|
172
|
+
# Name symbol for add association method
|
173
|
+
def association_add_method_name(name)
|
174
|
+
:"add_#{name.to_s.singularize}"
|
175
|
+
end
|
176
|
+
|
177
|
+
# Name symbol of association instance variable
|
178
|
+
def association_ivar(name)
|
179
|
+
:"@#{name}"
|
180
|
+
end
|
181
|
+
|
182
|
+
# Name symbol for remove_method_name
|
183
|
+
def association_remove_method_name(name)
|
184
|
+
:"remove_#{name.to_s.singularize}"
|
185
|
+
end
|
186
|
+
|
187
|
+
# Hash storing the association reflections. Keys are association name
|
188
|
+
# symbols, values are association reflection hashes.
|
189
|
+
def association_reflections
|
190
|
+
@association_reflections ||= {}
|
191
|
+
end
|
192
|
+
|
193
|
+
# Defines an association
|
194
|
+
def def_association_dataset_methods(name, opts, &block)
|
195
|
+
dataset_method = :"#{name}_dataset"
|
196
|
+
helper_method = :"#{name}_helper"
|
197
|
+
dataset_block = opts[:block]
|
198
|
+
ivar = association_ivar(name)
|
199
|
+
|
200
|
+
# define a method returning the association dataset (with optional order)
|
201
|
+
if order = opts[:order]
|
202
|
+
class_def(dataset_method) {instance_eval(&block).order(order)}
|
203
|
+
else
|
204
|
+
class_def(dataset_method, &block)
|
205
|
+
end
|
206
|
+
|
207
|
+
# If a block is given, define a helper method for it, because it takes
|
208
|
+
# an argument. This is unnecessary in Ruby 1.9, as that has instance_exec.
|
209
|
+
if dataset_block
|
210
|
+
class_def(helper_method, &dataset_block)
|
211
|
+
end
|
212
|
+
|
213
|
+
class_def(name) do |*reload|
|
214
|
+
if !reload[0] && obj = instance_variable_get(ivar)
|
215
|
+
obj
|
216
|
+
else
|
217
|
+
ds = send(dataset_method)
|
218
|
+
# if the a dataset block was specified, we need to call it and use
|
219
|
+
# the result as the dataset to fetch records from.
|
220
|
+
if dataset_block
|
221
|
+
ds = send(helper_method, ds)
|
222
|
+
end
|
223
|
+
if eager = opts[:eager]
|
224
|
+
ds = ds.eager(eager)
|
225
|
+
end
|
226
|
+
objs = ds.all
|
227
|
+
if reciprocal = self.class.send(:reciprocal_association, opts)
|
228
|
+
objs.each{|o| o.instance_variable_set(reciprocal, self)}
|
229
|
+
end
|
230
|
+
instance_variable_set(ivar, objs)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Defines an association getter method, caching the block result in an
|
236
|
+
# instance variable. The defined method takes an optional reload parameter
|
237
|
+
# that can be set to true in order to bypass the cache.
|
238
|
+
def def_association_getter(name, &block)
|
239
|
+
ivar = association_ivar(name)
|
240
|
+
class_def(name) do |*reload|
|
241
|
+
if !reload[0] && obj = instance_variable_get(ivar)
|
242
|
+
obj
|
243
|
+
else
|
244
|
+
instance_variable_set(ivar, instance_eval(&block))
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Adds many_to_many association instance methods
|
250
|
+
def def_many_to_many(name, opts)
|
251
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
252
|
+
ivar = association_ivar(name)
|
253
|
+
left = (opts[:left_key] ||= default_remote_key)
|
254
|
+
right = (opts[:right_key] ||= :"#{name.to_s.singularize}_id")
|
255
|
+
opts[:class_name] ||= name.to_s.singularize.camelize
|
256
|
+
join_table = (opts[:join_table] ||= default_join_table_name(opts))
|
257
|
+
database = db
|
258
|
+
|
259
|
+
def_association_dataset_methods(name, opts) do
|
260
|
+
klass = assoc_class[opts]
|
261
|
+
key = (opts[:right_primary_key] ||= :"#{klass.table_name}__#{klass.primary_key}")
|
262
|
+
selection = (opts[:select] ||= klass.table_name.all)
|
263
|
+
klass.select(selection).inner_join(join_table, right => key, left => pk)
|
264
|
+
end
|
265
|
+
|
266
|
+
class_def(association_add_method_name(name)) do |o|
|
267
|
+
database[join_table].insert(left => pk, right => o.pk)
|
268
|
+
if arr = instance_variable_get(ivar)
|
269
|
+
arr.push(o)
|
270
|
+
end
|
271
|
+
o
|
272
|
+
end
|
273
|
+
class_def(association_remove_method_name(name)) do |o|
|
274
|
+
database[join_table].filter(left => pk, right => o.pk).delete
|
275
|
+
if arr = instance_variable_get(ivar)
|
276
|
+
arr.delete(o)
|
277
|
+
end
|
278
|
+
o
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# Adds many_to_one association instance methods
|
283
|
+
def def_many_to_one(name, opts)
|
284
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
285
|
+
ivar = association_ivar(name)
|
286
|
+
|
287
|
+
key = (opts[:key] ||= :"#{name}_id")
|
288
|
+
opts[:class_name] ||= name.to_s.camelize
|
289
|
+
|
290
|
+
def_association_getter(name) {(fk = send(key)) ? assoc_class[opts][fk] : nil}
|
291
|
+
class_def(:"#{name}=") do |o|
|
292
|
+
instance_variable_set(ivar, o)
|
293
|
+
send(:"#{key}=", (o.pk if o))
|
294
|
+
end
|
295
|
+
end
|
296
|
+
|
297
|
+
# Adds one_to_many association instance methods
|
298
|
+
def def_one_to_many(name, opts)
|
299
|
+
assoc_class = method(:associated_class) # late binding of association dataset
|
300
|
+
ivar = association_ivar(name)
|
301
|
+
key = (opts[:key] ||= default_remote_key)
|
302
|
+
opts[:class_name] ||= name.to_s.singularize.camelize
|
303
|
+
|
304
|
+
def_association_dataset_methods(name, opts) {assoc_class[opts].filter(key => pk)}
|
305
|
+
|
306
|
+
class_def(association_add_method_name(name)) do |o|
|
307
|
+
o.send(:"#{key}=", pk)
|
308
|
+
o.save!
|
309
|
+
if arr = instance_variable_get(ivar)
|
310
|
+
arr.push(o)
|
311
|
+
end
|
312
|
+
o
|
313
|
+
end
|
314
|
+
class_def(association_remove_method_name(name)) do |o|
|
315
|
+
o.send(:"#{key}=", nil)
|
316
|
+
o.save!
|
317
|
+
if arr = instance_variable_get(ivar)
|
318
|
+
arr.delete(o)
|
319
|
+
end
|
320
|
+
o
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# Name symbol for default join table
|
325
|
+
def default_join_table_name(opts)
|
326
|
+
([opts[:class_name], self.name.demodulize]. \
|
327
|
+
map{|i| i.pluralize.underscore}.sort.join('_')).to_sym
|
328
|
+
end
|
329
|
+
|
330
|
+
# Name symbol for default foreign key
|
331
|
+
def default_remote_key
|
332
|
+
:"#{name.demodulize.underscore}_id"
|
333
|
+
end
|
334
|
+
|
335
|
+
# Sets the reciprocal association variable in the reflection, if one exists
|
336
|
+
def reciprocal_association(reflection)
|
337
|
+
if reflection[:type] != :one_to_many
|
338
|
+
nil
|
339
|
+
elsif reflection.include?(:reciprocal)
|
340
|
+
reflection[:reciprocal]
|
341
|
+
else
|
342
|
+
key = reflection[:key]
|
343
|
+
associated_class(reflection).all_association_reflections.each do |assoc_reflect|
|
344
|
+
if assoc_reflect[:type] == :many_to_one && assoc_reflect[:key] == key
|
345
|
+
return reflection[:reciprocal] = "@#{assoc_reflect[:name]}".freeze
|
346
|
+
end
|
347
|
+
end
|
348
|
+
reflection[:reciprocal] = nil
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Sequel
|
2
|
+
class Model
|
3
|
+
# Returns the database associated with the Model class.
|
4
|
+
def self.db
|
5
|
+
@db ||= (superclass != Object) && superclass.db or
|
6
|
+
raise Error, "No database associated with #{self}"
|
7
|
+
end
|
8
|
+
|
9
|
+
# Sets the database associated with the Model class.
|
10
|
+
def self.db=(db)
|
11
|
+
@db = db
|
12
|
+
if @dataset
|
13
|
+
set_dataset(db[table_name])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Called when a database is opened in order to automatically associate the
|
18
|
+
# first opened database with model classes.
|
19
|
+
def self.database_opened(db)
|
20
|
+
@db = db if (self == Model) && !@db
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns the implicit table name for the model class.
|
24
|
+
def self.implicit_table_name
|
25
|
+
name.demodulize.underscore.pluralize.to_sym
|
26
|
+
end
|
27
|
+
|
28
|
+
# Returns the dataset associated with the Model class.
|
29
|
+
def self.dataset
|
30
|
+
unless @dataset
|
31
|
+
if ds = super_dataset
|
32
|
+
set_dataset(ds.clone)
|
33
|
+
elsif !name.empty?
|
34
|
+
set_dataset(db[implicit_table_name])
|
35
|
+
else
|
36
|
+
raise Error, "No dataset associated with #{self}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@dataset
|
40
|
+
end
|
41
|
+
|
42
|
+
# def self.dataset
|
43
|
+
# @dataset ||= super_dataset ||
|
44
|
+
# (!(n = name).empty? && db[n.underscore.pluralize.to_sym]) ||
|
45
|
+
# (raise Error, "No dataset associated with #{self}")
|
46
|
+
# end
|
47
|
+
|
48
|
+
def self.super_dataset # :nodoc:
|
49
|
+
superclass.dataset if (superclass != Sequel::Model) && superclass.respond_to?(:dataset)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns the columns in the result set in their original order.
|
53
|
+
#
|
54
|
+
# See Dataset#columns for more information.
|
55
|
+
def self.columns
|
56
|
+
@columns ||= dataset.columns or
|
57
|
+
raise Error, "Could not fetch columns for #{self}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Sets the dataset associated with the Model class.
|
61
|
+
def self.set_dataset(ds)
|
62
|
+
@db = ds.db
|
63
|
+
@dataset = ds
|
64
|
+
@dataset.set_model(self)
|
65
|
+
@dataset.extend(Associations::EagerLoading)
|
66
|
+
@dataset.transform(@transform) if @transform
|
67
|
+
end
|
68
|
+
|
69
|
+
# Returns the database assoiated with the object's Model class.
|
70
|
+
def db
|
71
|
+
@db ||= model.db
|
72
|
+
end
|
73
|
+
|
74
|
+
# Returns the dataset assoiated with the object's Model class.
|
75
|
+
#
|
76
|
+
# See Dataset for more information.
|
77
|
+
def dataset
|
78
|
+
model.dataset
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the columns associated with the object's Model class.
|
82
|
+
def columns
|
83
|
+
model.columns
|
84
|
+
end
|
85
|
+
|
86
|
+
# Serializes column with YAML or through marshalling.
|
87
|
+
def self.serialize(*columns)
|
88
|
+
format = columns.pop[:format] if Hash === columns.last
|
89
|
+
format ||= :yaml
|
90
|
+
|
91
|
+
@transform = columns.inject({}) do |m, c|
|
92
|
+
m[c] = format
|
93
|
+
m
|
94
|
+
end
|
95
|
+
@dataset.transform(@transform) if @dataset
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Lets you create a Model class with its table name already set or reopen
|
100
|
+
# an existing Model.
|
101
|
+
#
|
102
|
+
# Makes given dataset inherited.
|
103
|
+
#
|
104
|
+
# === Example:
|
105
|
+
# class Comment < Sequel::Model(:something)
|
106
|
+
# table_name # => :something
|
107
|
+
#
|
108
|
+
# # ...
|
109
|
+
#
|
110
|
+
# end
|
111
|
+
def self.Model(source)
|
112
|
+
@models ||= {}
|
113
|
+
@models[source] ||= Class.new(Sequel::Model) do
|
114
|
+
meta_def(:inherited) do |c|
|
115
|
+
c.set_dataset(source.is_a?(Dataset) ? source : c.db[source])
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|