sequel 1.4.0 → 1.5.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,9 +1,26 @@
1
1
  module Sequel
2
2
  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
15
+ end
16
+ end
17
+
3
18
  # Returns the database associated with the Model class.
4
19
  def self.db
5
- @db ||= (superclass != Object) && superclass.db or
6
- raise Error, "No database associated with #{self}"
20
+ return @db if @db
21
+ @db = self == Model ? ::Sequel::DATABASES.first : superclass.db
22
+ raise(Error, "No database associated with #{self}") unless @db
23
+ @db
7
24
  end
8
25
 
9
26
  # Sets the database associated with the Model class.
@@ -14,12 +31,6 @@ module Sequel
14
31
  end
15
32
  end
16
33
 
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
34
  # Returns the implicit table name for the model class.
24
35
  def self.implicit_table_name
25
36
  name.demodulize.underscore.pluralize.to_sym
@@ -27,34 +38,39 @@ module Sequel
27
38
 
28
39
  # Returns the dataset associated with the Model class.
29
40
  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
41
+ @dataset || raise(Error, "No dataset associated with #{self}")
40
42
  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)
43
+
44
+ # If a block is given, define a method on the dataset with the given argument name using
45
+ # the given block as well as a method on the model that calls the
46
+ # dataset method.
47
+ #
48
+ # If a block is not given, define a method on the model for each argument
49
+ # that calls the dataset method of the same argument name.
50
+ def self.def_dataset_method(*args, &block)
51
+ raise(Error, "No arguments given") if args.empty?
52
+ if block_given?
53
+ raise(Error, "Defining a dataset method using a block requires only one argument") if args.length > 1
54
+ dataset.meta_def(args.first, &block)
55
+ end
56
+ args.each{|arg| instance_eval("def #{arg}(*args, &block); dataset.#{arg}(*args, &block) end", __FILE__, __LINE__)}
50
57
  end
51
58
 
52
59
  # Returns the columns in the result set in their original order.
53
60
  #
54
61
  # See Dataset#columns for more information.
55
62
  def self.columns
56
- @columns ||= dataset.columns or
63
+ return @columns if @columns
64
+ @columns = dataset.naked.columns or
57
65
  raise Error, "Could not fetch columns for #{self}"
66
+ def_column_accessor(*@columns)
67
+ @str_columns = nil
68
+ @columns
69
+ end
70
+
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}
58
74
  end
59
75
 
60
76
  # Sets the dataset associated with the Model class.
@@ -64,6 +80,32 @@ module Sequel
64
80
  @dataset.set_model(self)
65
81
  @dataset.extend(Associations::EagerLoading)
66
82
  @dataset.transform(@transform) if @transform
83
+ begin
84
+ @columns = nil
85
+ columns
86
+ rescue StandardError
87
+ end
88
+ 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
67
109
  end
68
110
 
69
111
  # Returns the database assoiated with the object's Model class.
@@ -83,6 +125,11 @@ module Sequel
83
125
  model.columns
84
126
  end
85
127
 
128
+ # Returns the str_columns associated with the object's Model class.
129
+ def str_columns
130
+ model.str_columns
131
+ end
132
+
86
133
  # Serializes column with YAML or through marshalling.
87
134
  def self.serialize(*columns)
88
135
  format = columns.pop[:format] if Hash === columns.last
@@ -108,13 +155,11 @@ module Sequel
108
155
  # # ...
109
156
  #
110
157
  # end
158
+ @models = {}
111
159
  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
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
118
164
  end
119
-
120
165
  end
@@ -18,8 +18,8 @@ module Sequel
18
18
  obj
19
19
  end
20
20
 
21
- class_def(:set) {|v| store.delete(cache_key); super}
22
- class_def(:save) {store.delete(cache_key); super}
21
+ class_def(:update_values) {|v| store.delete(cache_key); super}
22
+ class_def(:save) {store.delete(cache_key) unless new?; super}
23
23
  class_def(:delete) {store.delete(cache_key); super}
24
24
  end
25
25
 
@@ -39,4 +39,4 @@ module Sequel
39
39
  "#{self}:#{values.join(',')}"
40
40
  end
41
41
  end
42
- end
42
+ end
@@ -0,0 +1,81 @@
1
+ module Sequel
2
+ class Model
3
+ include Sequel::Deprecation
4
+ extend Sequel::Deprecation
5
+
6
+ # Check the Model.associate method to remove the :from option
7
+
8
+ def self.is_dataset_magic_method?(m) #:nodoc:
9
+ method_name = m.to_s
10
+ Sequel::Dataset::MAGIC_METHODS.each_key do |r|
11
+ return true if method_name =~ r
12
+ end
13
+ false
14
+ end
15
+
16
+ def self.method_missing(m, *args, &block) #:nodoc:
17
+ Thread.exclusive do
18
+ if dataset.respond_to?(m) || is_dataset_magic_method?(m)
19
+ instance_eval("def #{m}(*args, &block); deprecate('Sequel::Model.method_missing', 'Please define Sequel::Model.#{m} or use def_dataset_method :#{m}'); dataset.#{m}(*args, &block); end")
20
+ end
21
+ end
22
+ respond_to?(m) ? send(m, *args, &block) : super(m, *args)
23
+ end
24
+
25
+ def method_missing(m, *args, &block) #:nodoc:
26
+ if m.to_s =~ /=\z/
27
+ attribute = m.to_s.chop
28
+ values.keys.each do |k|
29
+ next unless k.to_s == attribute
30
+ deprecate("Sequel::Model#method_missing", "Use model[:#{attribute}] = ...")
31
+ return self[attribute.to_sym] = args.first
32
+ end
33
+ super
34
+ else
35
+ attribute = m.to_s
36
+ values.keys.each do |k|
37
+ next unless k.to_s == attribute
38
+ deprecate("Sequel::Model#method_missing", "Use model[:#{attribute}]")
39
+ return self[attribute.to_sym]
40
+ end
41
+ super
42
+ end
43
+ end
44
+
45
+ def self.create_with_params(params) #:nodoc:
46
+ deprecate("Sequel::Model.create_with_params", "Use .create")
47
+ create(params)
48
+ end
49
+
50
+ def self.create_with(params) #:nodoc:
51
+ deprecate("Sequel::Model.create_with", "Use .create")
52
+ create(params)
53
+ end
54
+
55
+ def update_with(params) #:nodoc:
56
+ deprecate("Sequel::Model#update_with", "Use #update_with_params")
57
+ update_with_params(params)
58
+ end
59
+
60
+ def new_record? #:nodoc:
61
+ deprecate("Sequel::Model#new_record?", "Use #new?")
62
+ new?
63
+ end
64
+
65
+ def set(values) #:nodoc:
66
+ deprecate("Sequel::Model#set", "Use #update_values")
67
+ update_values(values)
68
+ end
69
+
70
+ def update(values) #:nodoc:
71
+ deprecate("Sequel::Model#update", "Use #update_values")
72
+ update_values(values)
73
+ end
74
+
75
+ # deprecated, please use many_to_one instead
76
+ def self.one_to_one(*args, &block) #:nodoc:
77
+ deprecate("Sequel::Model.one_to_one", "Use many_to_one")
78
+ many_to_one(*args, &block)
79
+ end
80
+ end
81
+ end
@@ -1,68 +1,321 @@
1
1
  # Eager loading makes it so that you can load all associated records for a
2
2
  # set of objects in a single query, instead of a separate query for each object.
3
3
  #
4
- # The basic idea for how it works is that the dataset is first loaded normally.
5
- # Then it goes through all associations that have been specified via .eager.
6
- # It loads each of those associations separately, then associates them back
7
- # to the original dataset via primary/foreign keys. Due to the necessity of
8
- # all objects being present, you need to use .all to use eager loading, as it
9
- # can't work with .each.
10
- #
11
- # This implementation avoids the complexity of extracting an object graph out
12
- # of a single dataset, by building the object graph out of multiple datasets,
13
- # one for each association. By using a separate dataset for each association,
14
- # it avoids problems such as aliasing conflicts and creating cartesian product
15
- # result sets if multiple *_to_many eager associations are requested.
16
- #
17
- # One limitation of using this method is that you cannot filter the dataset
18
- # based on values of columns in an associated table, since the associations are loaded
19
- # in separate queries. To do that you need to load all associations in the
20
- # same query, and extract an object graph from the results of that query.
4
+ # Two separate implementations are provided. .eager should be used most of the
5
+ # time, as it loads associated records using one query per association. However,
6
+ # it does not allow you the ability to filter based on columns in associated tables. .eager_graph loads
7
+ # all records in one query. Using .eager_graph you can filter based on columns in associated
8
+ # tables. However, .eager_graph can be much slower than .eager, especially if multiple
9
+ # *_to_many associations are joined.
21
10
  #
22
11
  # You can cascade the eager loading (loading associations' associations)
23
- # with no limit to the depth of the cascades. You do this by passing a hash to .eager
12
+ # with no limit to the depth of the cascades. You do this by passing a hash to .eager or .eager_graph
24
13
  # with the keys being associations of the current model and values being
25
14
  # associations of the model associated with the current model via the key.
15
+ #
16
+ # You cannot eagerly load an association with a block argument, as the block argument is
17
+ # evaluated in terms of a specific instance of the model, and no specific instance exists.
26
18
  #
27
- # The associations' order, if defined, is respected. You cannot eagerly load
28
- # an association with a block argument, as the block argument is evaluated in
29
- # terms of a specific instance of the model, and no specific instance exists
30
- # when eagerly loading.
19
+ # The arguments can be symbols or hashes with symbol keys (for cascaded
20
+ # eager loading). Examples:
21
+ #
22
+ # Album.eager(:artist).all
23
+ # Album.eager_graph(:artist).all
24
+ # Album.eager(:artist, :genre).all
25
+ # Album.eager_graph(:artist, :genre).all
26
+ # Album.eager(:artist).eager(:genre).all
27
+ # Album.eager_graph(:artist).eager(:genre).all
28
+ # Artist.eager(:albums=>:tracks).all
29
+ # Artist.eager_graph(:albums=>:tracks).all
30
+ # Artist.eager(:albums=>{:tracks=>:genre}).all
31
+ # Artist.eager_graph(:albums=>{:tracks=>:genre}).all
31
32
  module Sequel::Model::Associations::EagerLoading
32
- # Add associations to the list of associations to eagerly load.
33
- # Associations can be a symbol or a hash with symbol keys (for cascaded
34
- # eager loading). Examples:
33
+ # Add the .eager! and .eager_graph! mutation methods to the dataset.
34
+ def self.extended(obj)
35
+ obj.def_mutation_method(:eager, :eager_graph)
36
+ end
37
+
38
+ # The preferred eager loading method. Loads all associated records using one
39
+ # query for each association.
40
+ #
41
+ # The basic idea for how it works is that the dataset is first loaded normally.
42
+ # Then it goes through all associations that have been specified via .eager.
43
+ # It loads each of those associations separately, then associates them back
44
+ # to the original dataset via primary/foreign keys. Due to the necessity of
45
+ # all objects being present, you need to use .all to use eager loading, as it
46
+ # can't work with .each.
47
+ #
48
+ # This implementation avoids the complexity of extracting an object graph out
49
+ # of a single dataset, by building the object graph out of multiple datasets,
50
+ # one for each association. By using a separate dataset for each association,
51
+ # it avoids problems such as aliasing conflicts and creating cartesian product
52
+ # result sets if multiple *_to_many eager associations are requested.
53
+ #
54
+ # One limitation of using this method is that you cannot filter the dataset
55
+ # based on values of columns in an associated table, since the associations are loaded
56
+ # in separate queries. To do that you need to load all associations in the
57
+ # same query, and extract an object graph from the results of that query. If you
58
+ # need to filter based on columns in associated tables, look at .eager_graph
59
+ # or join the tables you need to filter on manually.
35
60
  #
36
- # Album.eager(:artist).all
37
- # Album.eager(:artist, :genre).all
38
- # Album.eager(:artist).eager(:genre).all
39
- # Artist.eager(:albums=>:tracks).all
40
- # Artist.eager(:albums=>{:tracks=>:genre}).all
61
+ # Each association's order, if definied, is respected. Eager also works
62
+ # on a limited dataset.
41
63
  def eager(*associations)
42
- raise(ArgumentError, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
64
+ model = check_model
43
65
  opt = @opts[:eager]
44
66
  opt = opt ? opt.dup : {}
45
- check = Proc.new do |a|
46
- raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(a)
47
- raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block]
48
- end
49
67
  associations.flatten.each do |association|
50
68
  case association
51
69
  when Symbol
52
- check.call(association)
70
+ check_association(model, association)
53
71
  opt[association] = nil
54
72
  when Hash
55
- association.keys.each{|assoc| check.call(assoc)}
73
+ association.keys.each{|assoc| check_association(model, assoc)}
56
74
  opt.merge!(association)
57
75
  else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
58
76
  end
59
77
  end
60
- ds = clone(:eager=>opt)
61
- ds.add_callback(:post_load, :eager_load) unless @opts[:eager]
62
- ds
78
+ clone(:eager=>opt)
79
+ end
80
+
81
+ # The secondary eager loading method. Loads all associations in a single query. This
82
+ # method should only be used if you need to filter based on columns in associated tables.
83
+ #
84
+ # This method builds an object graph using the .graph method. Then it uses the graph
85
+ # to build the associations, and finally replaces the graph with a simple array
86
+ # of model objects.
87
+ #
88
+ # Be very careful when using this with multiple *_to_many associations, as you can
89
+ # create large cartesian products. If you must graph multiple *_to_many associations,
90
+ # make sure your filters are specific if you have a large database.
91
+ #
92
+ # This does not respect each association's order, as all associations are loaded in
93
+ # a single query. If you want to order the results, you must manually call .order.
94
+ #
95
+ # eager_graph probably won't work the way you suspect with limit, unless you are
96
+ # only graphing many_to_one associations.
97
+ def eager_graph(*associations)
98
+ model = check_model
99
+ table_name = model.table_name
100
+ ds = if @opts[:eager_graph]
101
+ self
102
+ else
103
+ # Each of the following have a symbol key for the table alias, with the following values:
104
+ # :requirements - array of requirements for this association
105
+ # :alias_association_type_map - the type of association for this association
106
+ # :alias_association_name_map - the name of the association for this association
107
+ clone(:eager_graph=>{:requirements=>{}, :master=>model.table_name, :alias_association_type_map=>{}, :alias_association_name_map=>{}, :reciprocals=>{}})
108
+ end
109
+ ds.eager_graph_associations(ds, model, table_name, [], *associations)
63
110
  end
64
111
 
112
+ protected
113
+ # Call graph on the association with the correct arguments,
114
+ # update the eager_graph data structure, and recurse into
115
+ # eager_graph_associations if there are any passed in associations
116
+ # (which would be dependencies of the current association)
117
+ #
118
+ # Arguments:
119
+ # * ds - Current dataset
120
+ # * model - Current Model
121
+ # * ta - table_alias used for the parent association
122
+ # * requirements - an array, used as a stack for requirements
123
+ # * r - association reflection for the current association
124
+ # * *associations - any associations dependent on this one
125
+ def eager_graph_association(ds, model, ta, requirements, r, *associations)
126
+ klass = model.send(:associated_class, r)
127
+ assoc_name = r[:name]
128
+ assoc_table_alias = ds.eager_unique_table_alias(ds, assoc_name)
129
+ ds = case assoc_type = r[:type]
130
+ when :many_to_one
131
+ ds.graph(klass, {klass.primary_key=>:"#{ta}__#{r[:key]}"}, :table_alias=>assoc_table_alias)
132
+ when :one_to_many
133
+ ds = ds.graph(klass, {r[:key]=>:"#{ta}__#{model.primary_key}"}, :table_alias=>assoc_table_alias)
134
+ # We only load reciprocals for one_to_many associations, as other reciprocals don't make sense
135
+ ds.opts[:eager_graph][:reciprocals][assoc_table_alias] = model.send(:reciprocal_association, r)
136
+ ds
137
+ when :many_to_many
138
+ ds = ds.graph(r[:join_table], {r[:left_key]=>:"#{ta}__#{model.primary_key}"}, :select=>false, :table_alias=>ds.eager_unique_table_alias(ds, r[:join_table]))
139
+ ds.graph(klass, {klass.primary_key=>r[:right_key]}, :table_alias=>assoc_table_alias)
140
+ end
141
+ eager_graph = ds.opts[:eager_graph]
142
+ eager_graph[:requirements][assoc_table_alias] = requirements.dup
143
+ eager_graph[:alias_association_name_map][assoc_table_alias] = assoc_name
144
+ eager_graph[:alias_association_type_map][assoc_table_alias] = assoc_type
145
+ ds = ds.eager_graph_associations(ds, klass, assoc_table_alias, requirements + [assoc_table_alias], *associations) unless associations.empty?
146
+ ds
147
+ end
148
+
149
+ # Check the associations are valid for the given model.
150
+ # Call eager_graph_association on each association.
151
+ #
152
+ # Arguments:
153
+ # * ds - Current dataset
154
+ # * model - Current Model
155
+ # * ta - table_alias used for the parent association
156
+ # * requirements - an array, used as a stack for requirements
157
+ # * *associations - the associations to add to the graph
158
+ def eager_graph_associations(ds, model, ta, requirements, *associations)
159
+ return ds if associations.empty?
160
+ associations.flatten.each do |association|
161
+ ds = case association
162
+ when Symbol
163
+ ds.eager_graph_association(ds, model, ta, requirements, check_association(model, association))
164
+ when Hash
165
+ association.each do |assoc, assoc_assocs|
166
+ ds = ds.eager_graph_association(ds, model, ta, requirements, check_association(model, assoc), assoc_assocs)
167
+ end
168
+ ds
169
+ else raise(ArgumentError, 'Associations must be in the form of a symbol or hash')
170
+ end
171
+ end
172
+ ds
173
+ end
174
+
175
+ # Build associations out of the array of returned object graphs.
176
+ def eager_graph_build_associations(record_graphs)
177
+ # Dup the tables that will be used, so that self is not modified.
178
+ eager_graph = @opts[:eager_graph]
179
+ master = eager_graph[:master]
180
+ requirements = eager_graph[:requirements]
181
+ alias_map = eager_graph[:alias_association_name_map]
182
+ type_map = eager_graph[:alias_association_type_map]
183
+ reciprocal_map = eager_graph[:reciprocals]
184
+
185
+ # Make dependency map hash out of requirements array for each association.
186
+ # This builds a tree of dependencies that will be used for recursion
187
+ # to ensure that all parts of the object graph are loaded into the
188
+ # appropriate subordinate association.
189
+ dependency_map = {}
190
+ # Sort the associations be requirements length, so that
191
+ # requirements are added to the dependency hash before their
192
+ # dependencies.
193
+ requirements.sort_by{|a| a[1].length}.each do |ta, deps|
194
+ if deps.empty?
195
+ dependency_map[ta] = {}
196
+ else
197
+ deps = deps.dup
198
+ hash = dependency_map[deps.shift]
199
+ deps.each do |dep|
200
+ hash = hash[dep]
201
+ end
202
+ hash[ta] = {}
203
+ end
204
+ end
205
+
206
+ # This mapping is used to make sure that duplicate entries in the
207
+ # result set are mapped to a single record. For example, using a
208
+ # single one_to_many association with 10 associated records,
209
+ # the main object will appear in the object graph 10 times.
210
+ # We map by primary key, if available, or by the object's entire values,
211
+ # if not. The mapping must be per table, so create sub maps for each table
212
+ # alias.
213
+ records_map = {master=>{}}
214
+ alias_map.keys.each{|ta| records_map[ta] = {}}
215
+
216
+ # This will hold the final record set that we will be replacing the object graph with.
217
+ records = []
218
+ record_graphs.each do |record_graph|
219
+ primary_record = record_graph[master]
220
+ key = primary_record.pk || primary_record.values.sort_by{|x| x[0].to_s}
221
+ if cached_pr = records_map[master][key]
222
+ primary_record = cached_pr
223
+ else
224
+ records_map[master][key] = primary_record
225
+ # Only add it to the list of records to return if it is a new record
226
+ records.push(primary_record)
227
+ end
228
+ # Build all associations for the current object and it's dependencies
229
+ eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, primary_record, record_graph)
230
+ end
231
+
232
+ # Remove duplicate records from all associations if this graph could possibly be a cartesian product
233
+ eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map) if type_map.reject{|k,v| v == :many_to_one}.length > 1
234
+
235
+ # Replace the array of object graphs with an array of model objects
236
+ record_graphs.replace(records)
237
+ end
238
+
239
+ # Creates a unique table alias that hasn't already been used in the query.
240
+ # Will either be the table_alias itself or table_alias_N for some integer
241
+ # N (starting at 0 and increasing until an unused one is found).
242
+ def eager_unique_table_alias(ds, table_alias)
243
+ if (graph = ds.opts[:graph]) && (table_aliases = graph[:table_aliases]) && (table_aliases.include?(table_alias))
244
+ i = 0
245
+ loop do
246
+ ta = :"#{table_alias}_#{i}"
247
+ return ta unless table_aliases[ta]
248
+ i += 1
249
+ end
250
+ else
251
+ table_alias
252
+ end
253
+ end
254
+
65
255
  private
256
+ # Make sure a standard (non-polymorphic model) is used for this dataset, and return the model
257
+ def check_model
258
+ raise(ArgumentError, 'No model for this dataset') unless @opts[:models] && model = @opts[:models][nil]
259
+ model
260
+ end
261
+
262
+ # Make sure the association is valid for this model, and return the association's reflection
263
+ def check_association(model, association)
264
+ raise(ArgumentError, 'Invalid association') unless reflection = model.association_reflection(association)
265
+ raise(ArgumentError, 'Cannot eagerly load associations with block arguments') if reflection[:block]
266
+ reflection
267
+ end
268
+
269
+ # Build associations for the current object. This is called recursively
270
+ # to build object's dependencies.
271
+ def eager_graph_build_associations_graph(dependency_map, alias_map, type_map, reciprocal_map, records_map, current, record_graph)
272
+ return if dependency_map.empty?
273
+ # Don't clobber the instance variable array for *_to_many associations if it has already been setup
274
+ dependency_map.keys.each do |ta|
275
+ current.instance_variable_set("@#{alias_map[ta]}", []) unless type_map[ta] == :many_to_one || current.instance_variable_get("@#{alias_map[ta]}")
276
+ end
277
+ dependency_map.each do |ta, deps|
278
+ rec = record_graph[ta]
279
+ key = rec.pk || rec.values.sort_by{|x| x[0].to_s}
280
+ if cached_rec = records_map[ta][key]
281
+ rec = cached_rec
282
+ else
283
+ records_map[ta][rec.pk] = rec
284
+ end
285
+ ivar = "@#{alias_map[ta]}"
286
+ case assoc_type = type_map[ta]
287
+ when :many_to_one
288
+ current.instance_variable_set(ivar, rec)
289
+ else
290
+ list = current.instance_variable_get(ivar)
291
+ list.push(rec)
292
+ if (assoc_type == :one_to_many) && (reciprocal = reciprocal_map[ta])
293
+ rec.instance_variable_set(reciprocal, current)
294
+ end
295
+ end
296
+ # Recurse into dependencies of the current object
297
+ eager_graph_build_associations_graph(deps, alias_map, type_map, reciprocal_map, records_map, rec, record_graph)
298
+ end
299
+ end
300
+
301
+ # If the result set is the result of a cartesian product, then it is possible that
302
+ # there a multiple records for each association when there should only be one.
303
+ def eager_graph_make_associations_unique(records, dependency_map, alias_map, type_map)
304
+ records.each do |record|
305
+ dependency_map.each do |ta, deps|
306
+ list = if type_map[ta] == :many_to_one
307
+ item = record.send(alias_map[ta])
308
+ [item] if item
309
+ else
310
+ list = record.send(alias_map[ta])
311
+ list.uniq!
312
+ # Recurse into dependencies
313
+ list.each{|rec| eager_graph_make_associations_unique(rec, deps, alias_map, type_map)}
314
+ end
315
+ end
316
+ end
317
+ end
318
+
66
319
  # Eagerly load all specified associations
67
320
  def eager_load(a)
68
321
  return if a.empty?
@@ -136,9 +389,9 @@ module Sequel::Model::Associations::EagerLoading
136
389
  right = reflection[:right_key]
137
390
  right_pk = (reflection[:right_primary_key] || :"#{assoc_table}__#{assoc_class.primary_key}")
138
391
  join_table = reflection[:join_table]
139
- fkey = (opts[:left_key_alias] ||= :"x_foreign_key_x")
140
- table_selection = (opts[:select] ||= assoc_table.all)
141
- key_selection = (opts[:left_key_select] ||= :"#{join_table}__#{left}___#{fkey}")
392
+ fkey = (reflection[:left_key_alias] ||= :"x_foreign_key_x")
393
+ table_selection = (reflection[:select] ||= assoc_table.*)
394
+ key_selection = (reflection[:left_key_select] ||= :"#{join_table}__#{left}___#{fkey}")
142
395
  h = key_hash[model.primary_key]
143
396
  ds = assoc_class.select(table_selection, key_selection).inner_join(join_table, right=>right_pk, left=>h.keys)
144
397
  end
@@ -166,4 +419,11 @@ module Sequel::Model::Associations::EagerLoading
166
419
  end
167
420
  end
168
421
  end
422
+
423
+ # Build associations from the graph if .eager_graph was used,
424
+ # and/or load other associations if .eager was used.
425
+ def post_load(all_records)
426
+ eager_graph_build_associations(all_records) if @opts[:eager_graph]
427
+ eager_load(all_records) if @opts[:eager]
428
+ end
169
429
  end