ocean-dynamo 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d067b77a882722c6a949672647c910338812ccf7
4
- data.tar.gz: 48f95744f784e8cef31a65fe496c9d69737521f8
3
+ metadata.gz: a28d5f280ebcb046fad6b812a8a4394ca2b64dfc
4
+ data.tar.gz: fc0f313738efd5370163056bf2915b83645f53b7
5
5
  SHA512:
6
- metadata.gz: 8e46bbfcce5105eef9737075b4042c049472ecb06bb7b938503d920a176083ffbeb0c281a4eba79698d34de7e3bec5c8483a4f8a7ca4f6dd9ee1df880fea24f3
7
- data.tar.gz: c519f71fea7afc08ebfe2496c13cec6f3292ada38e51dc15893396005ae70ec29694d0dcfd9702a92a36b99f6c4e614f2b14a7a90b1d639b60ee874bc0f26a3c
6
+ metadata.gz: 0259caaebfa02274f8103695c741670d6190be669ecd1c0d92d8165ea1a058f628ae96072c946fc77fc6561a0d7bb1ba075c0773201e6d3f641a69bdfec1b70b
7
+ data.tar.gz: 2b0108bbef441a611ebdaed44b76512b7ae62d75491fe0788d30f2d0ea1d5f0e80fa08a3c6a10547197d2f2640db9f7970f15e3e9e126c10ffbf778e4692b2ea
@@ -10,11 +10,11 @@ OceanDynamo requires Ruby 2.0 and Ruby on Rails 4.0.0 or later.
10
10
 
11
11
  === Features
12
12
 
13
- As one important use case for OceanDynamo is to facilitate the conversion of SQL based
14
- ActiveRecord models to DynamoDB based models, it is important that the syntax and semantics
15
- of OceanDynamo's operations are as close as possible to those of ActiveRecord, including
16
- callbacks, exceptions and support methods. Ocean-dynamo follows this pattern closely and
17
- is of course based on ActiveModel.
13
+ As one important use case for OceanDynamo is to facilitate the conversion of SQL
14
+ databases to no-SQL DynamoDB databases, it is important that the syntax and semantics
15
+ of OceanDynamo are as close as possible to those of ActiveRecord. This includes
16
+ callbacks, exceptions and method chaining semantics. OceanDynamo follows this pattern
17
+ closely and is of course based on ActiveModel.
18
18
 
19
19
  The attribute and persistence layer of OceanDynamo is modeled on that of ActiveRecord:
20
20
  there's +save+, +save!+, +create+, +update+, +update!+, +update_attributes+, +find_each+,
@@ -23,9 +23,8 @@ is always to implement as much of the ActiveRecord interface as possible, withou
23
23
  compromising scalability. This makes the task of switching from SQL to no-SQL much easier.
24
24
 
25
25
  Thanks to its structural similarity to ActiveRecord, OceanDynamo works with FactoryGirl.
26
- To facilitate testing, future versions will keep track of and delete instances after tests.
27
26
 
28
- OceanDynamo uses primary indices to retrieve related table items,
27
+ OceanDynamo uses only primary indices to retrieve related table items and collections,
29
28
  which means it will scale without limits.
30
29
 
31
30
  === Example
@@ -38,7 +37,7 @@ The following example shows the basic syntax for declaring a DynamoDB-based sche
38
37
 
39
38
  dynamo_schema(:uuid) do
40
39
  attribute :credentials, :string
41
- attribute :token
40
+ attribute :token, :string, default: Proc { SecureRandom.uuid }
42
41
  attribute :steps, :serialized, default: []
43
42
  attribute :max_seconds_in_queue, :integer, default: 1.day
44
43
  attribute :default_poison_limit, :integer, default: 5
@@ -61,7 +60,7 @@ The following example shows the basic syntax for declaring a DynamoDB-based sche
61
60
  Each attribute has a name, a type (+:string+, +:integer+, +:float+, +:datetime+, +:boolean+,
62
61
  or +:serialized+) where +:string+ is the default. Each attribute also optionally has a default
63
62
  value, which can be a Proc. The hash key attribute is by default +:id+ (overridden as +:uuid+ in
64
- the example above) and is a +:string+. The keys can also be explicitly declared.
63
+ the example above) and is a +:string+.
65
64
 
66
65
  The +:string+, +:integer+, +:float+ and +:datetime+ types can also store sets of their type.
67
66
  Sets are represented as arrays, may not contain duplicates and may not be empty.
@@ -84,7 +83,7 @@ value will return the empty string, <tt>""</tt>.
84
83
  connect: :late, # true, :late, nil/false
85
84
  create: false, # If true, create the table if nonexistent
86
85
  locking: :lock_version, # The name of the lock attribute or nil/false
87
- timestamps: [:created_at, :updated_at] # An array of timestamp columns or nil/false
86
+ timestamps: [:created_at, :updated_at] # A two-element array of timestamp columns, or nil/false
88
87
  ) do
89
88
  # Attribute definitions
90
89
  ...
@@ -97,6 +96,8 @@ value will return the empty string, <tt>""</tt>.
97
96
 
98
97
  The following example shows how to set up a +has_many+ / +belongs_to+ relation:
99
98
 
99
+ class Child < OceanDynamo::Table; end
100
+
100
101
  class Parent < OceanDynamo::Table
101
102
  dynamo_schema do
102
103
  attribute :this
@@ -105,8 +106,8 @@ The following example shows how to set up a +has_many+ / +belongs_to+ relation:
105
106
  end
106
107
  has_many :children
107
108
  end
108
-
109
-
109
+
110
+
110
111
  class Child < OceanDynamo::Table
111
112
  dynamo_schema(:uuid) do
112
113
  attribute :foo
@@ -126,6 +127,7 @@ Restrictions for +belongs_to+ tables:
126
127
 
127
128
  Restrictions for +has_many+ tables:
128
129
  * The +has_many+ table may not have a range key.
130
+ * +has_many+ must be placed after the +dynamo_schema+ attribute block.
129
131
 
130
132
  These restrictions allow OceanDynamo to implement the +has_many+ / +belongs_to+
131
133
  relation in a very efficient and massively scalable way.
@@ -147,9 +149,9 @@ and matching, the fact that the range key is a string can be used to implement
147
149
  wildcard matching of additional attributes. This gives, amongst other things, the
148
150
  equivalent of an SQL GROUP BY request, again without requiring any secondary indices.
149
151
 
150
- It's our goal to use a similar technique to implement has_and_belongs_to_many relations,
152
+ It's our goal to use a similar technique to implement +has_and_belongs_to_many+ relations,
151
153
  which means that secondary indices won't be necessary for the vast majority of
152
- OceanDynamo use cases. This ultimately means reduced operational costs, as well as
154
+ DynamoDB tables. This ultimately means reduced operational costs, as well as
153
155
  reduced complexity.
154
156
 
155
157
 
@@ -158,22 +160,18 @@ reduced complexity.
158
160
  OceanDynamo is fully usable as an ActiveModel and can be used by Rails
159
161
  controllers. OceanDynamo implements much of the infrastructure of ActiveRecord;
160
162
  for instance, +read_attribute+, +write_attribute+, and much of the control logic and
161
- parameters.
162
-
163
- * has_many now works, in a minimalistic fashion. To implement the rest of the interface,
164
- we need association proxies.
163
+ internal organisation.
165
164
 
166
- * Instances of classes with one or more has_many relations now save all relations after
167
- the instance has been saved. To save only dirty association collections, we need
168
- association proxies. Collections which are nil are not saved. Collections which are []
169
- are saved, which means they clear all children. This difference is crucial, until we
170
- get association proxies.
165
+ * +belongs_to+ now handles setting and retrieving the parent association correctly in
166
+ all cases, including direct and mass assignment (+:master+, +:master_id+, etc).
167
+ * +has_many=+ now destroys rather than deletes.
168
+ * Got rid of the +fake_dynamo+ purge message.
169
+ * Relations now take an optional reload arg, as in <tt>parent.children(true)</tt>.
171
170
 
172
- * #reload clears all relations.
173
171
 
174
172
  === Future milestones
175
173
 
176
- * Association proxies, to implement the same kind of interace as ActiveRecord, e.g.:
174
+ * Association proxies, to implement ActiveRecord-style method chaining, e.g.:
177
175
  <code>blog_entry.comments.build(body: "Cool!").save!</code>
178
176
 
179
177
  * The +has_and_belongs_to_many+ assocation.
@@ -184,7 +182,7 @@ parameters.
184
182
  === Current use
185
183
 
186
184
  OceanDynamo is currently used in the Ocean framework (http://wiki.oceanframework.net)
187
- e.g. to implement critical job queues. It will be used increasingly as features are
185
+ e.g. to implement highly scalable job queues. It will be used increasingly as features are
188
186
  added to OceanDynamo and will eventually replace all ActiveRecord tables in Ocean.
189
187
 
190
188
 
@@ -0,0 +1,583 @@
1
+ module ActiveRecord
2
+ # = Active Record Reflection
3
+ module Reflection # :nodoc:
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ class_attribute :reflections
8
+ self.reflections = {}
9
+ end
10
+
11
+ # Reflection enables to interrogate Active Record classes and objects
12
+ # about their associations and aggregations. This information can,
13
+ # for example, be used in a form builder that takes an Active Record object
14
+ # and creates input fields for all of the attributes depending on their type
15
+ # and displays the associations to other objects.
16
+ #
17
+ # MacroReflection class has info for AggregateReflection and AssociationReflection
18
+ # classes.
19
+ module ClassMethods
20
+ def create_reflection(macro, name, scope, options, active_record)
21
+ case macro
22
+ when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
23
+ klass = options[:through] ? ThroughReflection : AssociationReflection
24
+ reflection = klass.new(macro, name, scope, options, active_record)
25
+ when :composed_of
26
+ reflection = AggregateReflection.new(macro, name, scope, options, active_record)
27
+ end
28
+
29
+ self.reflections = self.reflections.merge(name => reflection)
30
+ reflection
31
+ end
32
+
33
+ # Returns an array of AggregateReflection objects for all the aggregations in the class.
34
+ def reflect_on_all_aggregations
35
+ reflections.values.grep(AggregateReflection)
36
+ end
37
+
38
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
39
+ #
40
+ # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
41
+ #
42
+ def reflect_on_aggregation(aggregation)
43
+ reflection = reflections[aggregation]
44
+ reflection if reflection.is_a?(AggregateReflection)
45
+ end
46
+
47
+ # Returns an array of AssociationReflection objects for all the
48
+ # associations in the class. If you only want to reflect on a certain
49
+ # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
50
+ # <tt>:belongs_to</tt>) as the first parameter.
51
+ #
52
+ # Example:
53
+ #
54
+ # Account.reflect_on_all_associations # returns an array of all associations
55
+ # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
56
+ #
57
+ def reflect_on_all_associations(macro = nil)
58
+ association_reflections = reflections.values.grep(AssociationReflection)
59
+ macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
60
+ end
61
+
62
+ # Returns the AssociationReflection object for the +association+ (use the symbol).
63
+ #
64
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
65
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
66
+ #
67
+ def reflect_on_association(association)
68
+ reflection = reflections[association]
69
+ reflection if reflection.is_a?(AssociationReflection)
70
+ end
71
+
72
+ # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
73
+ def reflect_on_all_autosave_associations
74
+ reflections.values.select { |reflection| reflection.options[:autosave] }
75
+ end
76
+ end
77
+
78
+ # Base class for AggregateReflection and AssociationReflection. Objects of
79
+ # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
80
+ #
81
+ # MacroReflection
82
+ # AggregateReflection
83
+ # AssociationReflection
84
+ # ThroughReflection
85
+ class MacroReflection
86
+ # Returns the name of the macro.
87
+ #
88
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
89
+ # <tt>has_many :clients</tt> returns <tt>:clients</tt>
90
+ attr_reader :name
91
+
92
+ # Returns the macro type.
93
+ #
94
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt>
95
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
96
+ attr_reader :macro
97
+
98
+ attr_reader :scope
99
+
100
+ # Returns the hash of options used for the macro.
101
+ #
102
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
103
+ # <tt>has_many :clients</tt> returns +{}+
104
+ attr_reader :options
105
+
106
+ attr_reader :active_record
107
+
108
+ attr_reader :plural_name # :nodoc:
109
+
110
+ def initialize(macro, name, scope, options, active_record)
111
+ @macro = macro
112
+ @name = name
113
+ @scope = scope
114
+ @options = options
115
+ @active_record = active_record
116
+ @plural_name = active_record.pluralize_table_names ?
117
+ name.to_s.pluralize : name.to_s
118
+ end
119
+
120
+ # Returns the class for the macro.
121
+ #
122
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
123
+ # <tt>has_many :clients</tt> returns the Client class
124
+ def klass
125
+ @klass ||= class_name.constantize
126
+ end
127
+
128
+ # Returns the class name for the macro.
129
+ #
130
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
131
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
132
+ def class_name
133
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
134
+ end
135
+
136
+ # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
137
+ # and +other_aggregation+ has an options hash assigned to it.
138
+ def ==(other_aggregation)
139
+ super ||
140
+ other_aggregation.kind_of?(self.class) &&
141
+ name == other_aggregation.name &&
142
+ other_aggregation.options &&
143
+ active_record == other_aggregation.active_record
144
+ end
145
+
146
+ private
147
+ def derive_class_name
148
+ name.to_s.camelize
149
+ end
150
+ end
151
+
152
+
153
+ # Holds all the meta-data about an aggregation as it was specified in the
154
+ # Active Record class.
155
+ class AggregateReflection < MacroReflection #:nodoc:
156
+ def mapping
157
+ mapping = options[:mapping] || [name, name]
158
+ mapping.first.is_a?(Array) ? mapping : [mapping]
159
+ end
160
+ end
161
+
162
+ # Holds all the meta-data about an association as it was specified in the
163
+ # Active Record class.
164
+ class AssociationReflection < MacroReflection #:nodoc:
165
+ # Returns the target association's class.
166
+ #
167
+ # class Author < ActiveRecord::Base
168
+ # has_many :books
169
+ # end
170
+ #
171
+ # Author.reflect_on_association(:books).klass
172
+ # # => Book
173
+ #
174
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
175
+ # a new association object. Use +build_association+ or +create_association+
176
+ # instead. This allows plugins to hook into association object creation.
177
+ def klass
178
+ @klass ||= active_record.send(:compute_type, class_name)
179
+ end
180
+
181
+ def initialize(*args)
182
+ super
183
+ @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
184
+ end
185
+
186
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
187
+ # be passed to the class's constructor.
188
+ def build_association(attributes, &block)
189
+ klass.new(attributes, &block)
190
+ end
191
+
192
+ def table_name
193
+ @table_name ||= klass.table_name
194
+ end
195
+
196
+ def quoted_table_name
197
+ @quoted_table_name ||= klass.quoted_table_name
198
+ end
199
+
200
+ def join_table
201
+ @join_table ||= options[:join_table] || derive_join_table
202
+ end
203
+
204
+ def foreign_key
205
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key
206
+ end
207
+
208
+ def foreign_type
209
+ @foreign_type ||= options[:foreign_type] || "#{name}_type"
210
+ end
211
+
212
+ def type
213
+ @type ||= options[:as] && "#{options[:as]}_type"
214
+ end
215
+
216
+ def primary_key_column
217
+ @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
218
+ end
219
+
220
+ def association_foreign_key
221
+ @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
222
+ end
223
+
224
+ # klass option is necessary to support loading polymorphic associations
225
+ def association_primary_key(klass = nil)
226
+ options[:primary_key] || primary_key(klass || self.klass)
227
+ end
228
+
229
+ def active_record_primary_key
230
+ @active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
231
+ end
232
+
233
+ def counter_cache_column
234
+ if options[:counter_cache] == true
235
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
236
+ elsif options[:counter_cache]
237
+ options[:counter_cache].to_s
238
+ end
239
+ end
240
+
241
+ def columns(tbl_name)
242
+ @columns ||= klass.connection.columns(tbl_name)
243
+ end
244
+
245
+ def reset_column_information
246
+ @columns = nil
247
+ end
248
+
249
+ def check_validity!
250
+ check_validity_of_inverse!
251
+
252
+ if has_and_belongs_to_many? && association_foreign_key == foreign_key
253
+ raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self)
254
+ end
255
+ end
256
+
257
+ def check_validity_of_inverse!
258
+ unless options[:polymorphic]
259
+ if has_inverse? && inverse_of.nil?
260
+ raise InverseOfAssociationNotFoundError.new(self)
261
+ end
262
+ end
263
+ end
264
+
265
+ def through_reflection
266
+ nil
267
+ end
268
+
269
+ def source_reflection
270
+ nil
271
+ end
272
+
273
+ # A chain of reflections from this one back to the owner. For more see the explanation in
274
+ # ThroughReflection.
275
+ def chain
276
+ [self]
277
+ end
278
+
279
+ def nested?
280
+ false
281
+ end
282
+
283
+ # An array of arrays of scopes. Each item in the outside array corresponds to a reflection
284
+ # in the #chain.
285
+ def scope_chain
286
+ scope ? [[scope]] : [[]]
287
+ end
288
+
289
+ alias :source_macro :macro
290
+
291
+ def has_inverse?
292
+ @options[:inverse_of]
293
+ end
294
+
295
+ def inverse_of
296
+ if has_inverse?
297
+ @inverse_of ||= klass.reflect_on_association(options[:inverse_of])
298
+ end
299
+ end
300
+
301
+ def polymorphic_inverse_of(associated_class)
302
+ if has_inverse?
303
+ if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
304
+ inverse_relationship
305
+ else
306
+ raise InverseOfAssociationNotFoundError.new(self, associated_class)
307
+ end
308
+ end
309
+ end
310
+
311
+ # Returns whether or not this association reflection is for a collection
312
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
313
+ # +has_and_belongs_to_many+, +false+ otherwise.
314
+ def collection?
315
+ @collection
316
+ end
317
+
318
+ # Returns whether or not the association should be validated as part of
319
+ # the parent's validation.
320
+ #
321
+ # Unless you explicitly disable validation with
322
+ # <tt>validate: false</tt>, validation will take place when:
323
+ #
324
+ # * you explicitly enable validation; <tt>validate: true</tt>
325
+ # * you use autosave; <tt>autosave: true</tt>
326
+ # * the association is a +has_many+ association
327
+ def validate?
328
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
329
+ end
330
+
331
+ # Returns +true+ if +self+ is a +belongs_to+ reflection.
332
+ def belongs_to?
333
+ macro == :belongs_to
334
+ end
335
+
336
+ def has_and_belongs_to_many?
337
+ macro == :has_and_belongs_to_many
338
+ end
339
+
340
+ def association_class
341
+ case macro
342
+ when :belongs_to
343
+ if options[:polymorphic]
344
+ Associations::BelongsToPolymorphicAssociation
345
+ else
346
+ Associations::BelongsToAssociation
347
+ end
348
+ when :has_and_belongs_to_many
349
+ Associations::HasAndBelongsToManyAssociation
350
+ when :has_many
351
+ if options[:through]
352
+ Associations::HasManyThroughAssociation
353
+ else
354
+ Associations::HasManyAssociation
355
+ end
356
+ when :has_one
357
+ if options[:through]
358
+ Associations::HasOneThroughAssociation
359
+ else
360
+ Associations::HasOneAssociation
361
+ end
362
+ end
363
+ end
364
+
365
+ def polymorphic?
366
+ options.key? :polymorphic
367
+ end
368
+
369
+ private
370
+ def derive_class_name
371
+ class_name = name.to_s.camelize
372
+ class_name = class_name.singularize if collection?
373
+ class_name
374
+ end
375
+
376
+ def derive_foreign_key
377
+ if belongs_to?
378
+ "#{name}_id"
379
+ elsif options[:as]
380
+ "#{options[:as]}_id"
381
+ else
382
+ active_record.name.foreign_key
383
+ end
384
+ end
385
+
386
+ def derive_join_table
387
+ [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
388
+ end
389
+
390
+ def primary_key(klass)
391
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
392
+ end
393
+ end
394
+
395
+ # Holds all the meta-data about a :through association as it was specified
396
+ # in the Active Record class.
397
+ class ThroughReflection < AssociationReflection #:nodoc:
398
+ delegate :foreign_key, :foreign_type, :association_foreign_key,
399
+ :active_record_primary_key, :type, :to => :source_reflection
400
+
401
+ # Gets the source of the through reflection. It checks both a singularized
402
+ # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
403
+ #
404
+ # class Post < ActiveRecord::Base
405
+ # has_many :taggings
406
+ # has_many :tags, through: :taggings
407
+ # end
408
+ #
409
+ # class Tagging < ActiveRecord::Base
410
+ # belongs_to :post
411
+ # belongs_to :tag
412
+ # end
413
+ #
414
+ # tags_reflection = Post.reflect_on_association(:tags)
415
+ #
416
+ # taggings_reflection = tags_reflection.source_reflection
417
+ # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags">
418
+ #
419
+ def source_reflection
420
+ @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
421
+ end
422
+
423
+ # Returns the AssociationReflection object specified in the <tt>:through</tt> option
424
+ # of a HasManyThrough or HasOneThrough association.
425
+ #
426
+ # class Post < ActiveRecord::Base
427
+ # has_many :taggings
428
+ # has_many :tags, through: :taggings
429
+ # end
430
+ #
431
+ # tags_reflection = Post.reflect_on_association(:tags)
432
+ # taggings_reflection = tags_reflection.through_reflection
433
+ #
434
+ def through_reflection
435
+ @through_reflection ||= active_record.reflect_on_association(options[:through])
436
+ end
437
+
438
+ # Returns an array of reflections which are involved in this association. Each item in the
439
+ # array corresponds to a table which will be part of the query for this association.
440
+ #
441
+ # The chain is built by recursively calling #chain on the source reflection and the through
442
+ # reflection. The base case for the recursion is a normal association, which just returns
443
+ # [self] as its #chain.
444
+ #
445
+ # class Post < ActiveRecord::Base
446
+ # has_many :taggings
447
+ # has_many :tags, through: :taggings
448
+ # end
449
+ #
450
+ # tags_reflection = Post.reflect_on_association(:tags)
451
+ # tags_reflection.chain
452
+ # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>,
453
+ # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>]
454
+ #
455
+ def chain
456
+ @chain ||= begin
457
+ chain = source_reflection.chain + through_reflection.chain
458
+ chain[0] = self # Use self so we don't lose the information from :source_type
459
+ chain
460
+ end
461
+ end
462
+
463
+ # Consider the following example:
464
+ #
465
+ # class Person
466
+ # has_many :articles
467
+ # has_many :comment_tags, through: :articles
468
+ # end
469
+ #
470
+ # class Article
471
+ # has_many :comments
472
+ # has_many :comment_tags, through: :comments, source: :tags
473
+ # end
474
+ #
475
+ # class Comment
476
+ # has_many :tags
477
+ # end
478
+ #
479
+ # There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags,
480
+ # but only Comment.tags will be represented in the #chain. So this method creates an array
481
+ # of scopes corresponding to the chain.
482
+ def scope_chain
483
+ @scope_chain ||= begin
484
+ scope_chain = source_reflection.scope_chain.map(&:dup)
485
+
486
+ # Add to it the scope from this reflection (if any)
487
+ scope_chain.first << scope if scope
488
+
489
+ through_scope_chain = through_reflection.scope_chain
490
+
491
+ if options[:source_type]
492
+ through_scope_chain.first <<
493
+ through_reflection.klass.where(foreign_type => options[:source_type])
494
+ end
495
+
496
+ # Recursively fill out the rest of the array from the through reflection
497
+ scope_chain + through_scope_chain
498
+ end
499
+ end
500
+
501
+ # The macro used by the source association
502
+ def source_macro
503
+ source_reflection.source_macro
504
+ end
505
+
506
+ # A through association is nested if there would be more than one join table
507
+ def nested?
508
+ chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
509
+ end
510
+
511
+ # We want to use the klass from this reflection, rather than just delegate straight to
512
+ # the source_reflection, because the source_reflection may be polymorphic. We still
513
+ # need to respect the source_reflection's :primary_key option, though.
514
+ def association_primary_key(klass = nil)
515
+ # Get the "actual" source reflection if the immediate source reflection has a
516
+ # source reflection itself
517
+ source_reflection = self.source_reflection
518
+ while source_reflection.source_reflection
519
+ source_reflection = source_reflection.source_reflection
520
+ end
521
+
522
+ source_reflection.options[:primary_key] || primary_key(klass || self.klass)
523
+ end
524
+
525
+ # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
526
+ #
527
+ # class Post < ActiveRecord::Base
528
+ # has_many :taggings
529
+ # has_many :tags, through: :taggings
530
+ # end
531
+ #
532
+ # tags_reflection = Post.reflect_on_association(:tags)
533
+ # tags_reflection.source_reflection_names
534
+ # # => [:tag, :tags]
535
+ #
536
+ def source_reflection_names
537
+ @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
538
+ end
539
+
540
+ def source_options
541
+ source_reflection.options
542
+ end
543
+
544
+ def through_options
545
+ through_reflection.options
546
+ end
547
+
548
+ def check_validity!
549
+ if through_reflection.nil?
550
+ raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
551
+ end
552
+
553
+ if through_reflection.options[:polymorphic]
554
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
555
+ end
556
+
557
+ if source_reflection.nil?
558
+ raise HasManyThroughSourceAssociationNotFoundError.new(self)
559
+ end
560
+
561
+ if options[:source_type] && source_reflection.options[:polymorphic].nil?
562
+ raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
563
+ end
564
+
565
+ if source_reflection.options[:polymorphic] && options[:source_type].nil?
566
+ raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
567
+ end
568
+
569
+ if macro == :has_one && through_reflection.collection?
570
+ raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
571
+ end
572
+
573
+ check_validity_of_inverse!
574
+ end
575
+
576
+ private
577
+ def derive_class_name
578
+ # get the class_name of the belongs_to association of the through reflection
579
+ options[:source_type] || source_reflection.class_name
580
+ end
581
+ end
582
+ end
583
+ end