sskirby-activerecord 3.2.1

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.
Files changed (150) hide show
  1. data/CHANGELOG.md +6749 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +222 -0
  4. data/examples/associations.png +0 -0
  5. data/examples/performance.rb +177 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record.rb +147 -0
  8. data/lib/active_record/aggregations.rb +255 -0
  9. data/lib/active_record/associations.rb +1604 -0
  10. data/lib/active_record/associations/alias_tracker.rb +79 -0
  11. data/lib/active_record/associations/association.rb +239 -0
  12. data/lib/active_record/associations/association_scope.rb +119 -0
  13. data/lib/active_record/associations/belongs_to_association.rb +79 -0
  14. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +34 -0
  15. data/lib/active_record/associations/builder/association.rb +55 -0
  16. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  17. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  18. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
  19. data/lib/active_record/associations/builder/has_many.rb +71 -0
  20. data/lib/active_record/associations/builder/has_one.rb +62 -0
  21. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  22. data/lib/active_record/associations/collection_association.rb +574 -0
  23. data/lib/active_record/associations/collection_proxy.rb +132 -0
  24. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +62 -0
  25. data/lib/active_record/associations/has_many_association.rb +108 -0
  26. data/lib/active_record/associations/has_many_through_association.rb +180 -0
  27. data/lib/active_record/associations/has_one_association.rb +73 -0
  28. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  29. data/lib/active_record/associations/join_dependency.rb +214 -0
  30. data/lib/active_record/associations/join_dependency/join_association.rb +154 -0
  31. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  32. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  33. data/lib/active_record/associations/join_helper.rb +55 -0
  34. data/lib/active_record/associations/preloader.rb +177 -0
  35. data/lib/active_record/associations/preloader/association.rb +127 -0
  36. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  37. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  38. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  39. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  40. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  41. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  42. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  43. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  44. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  45. data/lib/active_record/associations/singular_association.rb +64 -0
  46. data/lib/active_record/associations/through_association.rb +83 -0
  47. data/lib/active_record/attribute_assignment.rb +221 -0
  48. data/lib/active_record/attribute_methods.rb +272 -0
  49. data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
  50. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  51. data/lib/active_record/attribute_methods/dirty.rb +101 -0
  52. data/lib/active_record/attribute_methods/primary_key.rb +114 -0
  53. data/lib/active_record/attribute_methods/query.rb +39 -0
  54. data/lib/active_record/attribute_methods/read.rb +135 -0
  55. data/lib/active_record/attribute_methods/serialization.rb +93 -0
  56. data/lib/active_record/attribute_methods/time_zone_conversion.rb +62 -0
  57. data/lib/active_record/attribute_methods/write.rb +69 -0
  58. data/lib/active_record/autosave_association.rb +422 -0
  59. data/lib/active_record/base.rb +716 -0
  60. data/lib/active_record/callbacks.rb +275 -0
  61. data/lib/active_record/coders/yaml_column.rb +41 -0
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +452 -0
  63. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +188 -0
  64. data/lib/active_record/connection_adapters/abstract/database_limits.rb +58 -0
  65. data/lib/active_record/connection_adapters/abstract/database_statements.rb +388 -0
  66. data/lib/active_record/connection_adapters/abstract/query_cache.rb +82 -0
  67. data/lib/active_record/connection_adapters/abstract/quoting.rb +115 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +492 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +598 -0
  70. data/lib/active_record/connection_adapters/abstract_adapter.rb +296 -0
  71. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +653 -0
  72. data/lib/active_record/connection_adapters/column.rb +270 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +288 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +426 -0
  75. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1261 -0
  76. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  77. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -0
  78. data/lib/active_record/connection_adapters/sqlite_adapter.rb +577 -0
  79. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  80. data/lib/active_record/counter_cache.rb +119 -0
  81. data/lib/active_record/dynamic_finder_match.rb +56 -0
  82. data/lib/active_record/dynamic_matchers.rb +79 -0
  83. data/lib/active_record/dynamic_scope_match.rb +23 -0
  84. data/lib/active_record/errors.rb +195 -0
  85. data/lib/active_record/explain.rb +85 -0
  86. data/lib/active_record/explain_subscriber.rb +21 -0
  87. data/lib/active_record/fixtures.rb +906 -0
  88. data/lib/active_record/fixtures/file.rb +65 -0
  89. data/lib/active_record/identity_map.rb +156 -0
  90. data/lib/active_record/inheritance.rb +167 -0
  91. data/lib/active_record/integration.rb +49 -0
  92. data/lib/active_record/locale/en.yml +40 -0
  93. data/lib/active_record/locking/optimistic.rb +183 -0
  94. data/lib/active_record/locking/pessimistic.rb +77 -0
  95. data/lib/active_record/log_subscriber.rb +68 -0
  96. data/lib/active_record/migration.rb +765 -0
  97. data/lib/active_record/migration/command_recorder.rb +105 -0
  98. data/lib/active_record/model_schema.rb +366 -0
  99. data/lib/active_record/nested_attributes.rb +469 -0
  100. data/lib/active_record/observer.rb +121 -0
  101. data/lib/active_record/persistence.rb +372 -0
  102. data/lib/active_record/query_cache.rb +74 -0
  103. data/lib/active_record/querying.rb +58 -0
  104. data/lib/active_record/railtie.rb +119 -0
  105. data/lib/active_record/railties/console_sandbox.rb +6 -0
  106. data/lib/active_record/railties/controller_runtime.rb +49 -0
  107. data/lib/active_record/railties/databases.rake +620 -0
  108. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  109. data/lib/active_record/readonly_attributes.rb +26 -0
  110. data/lib/active_record/reflection.rb +534 -0
  111. data/lib/active_record/relation.rb +534 -0
  112. data/lib/active_record/relation/batches.rb +90 -0
  113. data/lib/active_record/relation/calculations.rb +354 -0
  114. data/lib/active_record/relation/delegation.rb +49 -0
  115. data/lib/active_record/relation/finder_methods.rb +398 -0
  116. data/lib/active_record/relation/predicate_builder.rb +58 -0
  117. data/lib/active_record/relation/query_methods.rb +417 -0
  118. data/lib/active_record/relation/spawn_methods.rb +148 -0
  119. data/lib/active_record/result.rb +34 -0
  120. data/lib/active_record/sanitization.rb +194 -0
  121. data/lib/active_record/schema.rb +58 -0
  122. data/lib/active_record/schema_dumper.rb +204 -0
  123. data/lib/active_record/scoping.rb +152 -0
  124. data/lib/active_record/scoping/default.rb +142 -0
  125. data/lib/active_record/scoping/named.rb +202 -0
  126. data/lib/active_record/serialization.rb +18 -0
  127. data/lib/active_record/serializers/xml_serializer.rb +202 -0
  128. data/lib/active_record/session_store.rb +358 -0
  129. data/lib/active_record/store.rb +50 -0
  130. data/lib/active_record/test_case.rb +73 -0
  131. data/lib/active_record/timestamp.rb +113 -0
  132. data/lib/active_record/transactions.rb +360 -0
  133. data/lib/active_record/translation.rb +22 -0
  134. data/lib/active_record/validations.rb +83 -0
  135. data/lib/active_record/validations/associated.rb +43 -0
  136. data/lib/active_record/validations/uniqueness.rb +180 -0
  137. data/lib/active_record/version.rb +10 -0
  138. data/lib/rails/generators/active_record.rb +25 -0
  139. data/lib/rails/generators/active_record/migration.rb +15 -0
  140. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  141. data/lib/rails/generators/active_record/migration/templates/migration.rb +31 -0
  142. data/lib/rails/generators/active_record/model/model_generator.rb +43 -0
  143. data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
  144. data/lib/rails/generators/active_record/model/templates/model.rb +7 -0
  145. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  146. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  147. data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
  148. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
  149. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
  150. metadata +242 -0
@@ -0,0 +1,16 @@
1
+ #FIXME Remove if ArJdbcMysql will give.
2
+ module ArJdbcMySQL #:nodoc:
3
+ class Error < StandardError
4
+ attr_accessor :error_number, :sql_state
5
+
6
+ def initialize msg
7
+ super
8
+ @error_number = nil
9
+ @sql_state = nil
10
+ end
11
+
12
+ # Mysql gem compatibility
13
+ alias_method :errno, :error_number
14
+ alias_method :error, :message
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ require 'active_support/concern'
2
+ require 'active_support/core_ext/class/attribute'
3
+
4
+ module ActiveRecord
5
+ module ReadonlyAttributes
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_attr_readonly, :instance_writer => false
10
+ self._attr_readonly = []
11
+ end
12
+
13
+ module ClassMethods
14
+ # Attributes listed as readonly will be used to create a new record but update operations will
15
+ # ignore these fields.
16
+ def attr_readonly(*attributes)
17
+ self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
18
+ end
19
+
20
+ # Returns an array of all the attributes that have been specified as readonly.
21
+ def readonly_attributes
22
+ self._attr_readonly
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,534 @@
1
+ require 'active_support/core_ext/class/attribute'
2
+ require 'active_support/core_ext/object/inclusion'
3
+
4
+ module ActiveRecord
5
+ # = Active Record Reflection
6
+ module Reflection # :nodoc:
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :reflections
11
+ self.reflections = {}
12
+ end
13
+
14
+ # Reflection enables to interrogate Active Record classes and objects
15
+ # about their associations and aggregations. This information can,
16
+ # for example, be used in a form builder that takes an Active Record object
17
+ # and creates input fields for all of the attributes depending on their type
18
+ # and displays the associations to other objects.
19
+ #
20
+ # MacroReflection class has info for AggregateReflection and AssociationReflection
21
+ # classes.
22
+ module ClassMethods
23
+ def create_reflection(macro, name, options, active_record)
24
+ case macro
25
+ when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
26
+ klass = options[:through] ? ThroughReflection : AssociationReflection
27
+ reflection = klass.new(macro, name, options, active_record)
28
+ when :composed_of
29
+ reflection = AggregateReflection.new(macro, name, options, active_record)
30
+ end
31
+
32
+ self.reflections = self.reflections.merge(name => reflection)
33
+ reflection
34
+ end
35
+
36
+ # Returns an array of AggregateReflection objects for all the aggregations in the class.
37
+ def reflect_on_all_aggregations
38
+ reflections.values.grep(AggregateReflection)
39
+ end
40
+
41
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
42
+ #
43
+ # Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
44
+ #
45
+ def reflect_on_aggregation(aggregation)
46
+ reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
47
+ end
48
+
49
+ # Returns an array of AssociationReflection objects for all the
50
+ # associations in the class. If you only want to reflect on a certain
51
+ # association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>,
52
+ # <tt>:belongs_to</tt>) as the first parameter.
53
+ #
54
+ # Example:
55
+ #
56
+ # Account.reflect_on_all_associations # returns an array of all associations
57
+ # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
58
+ #
59
+ def reflect_on_all_associations(macro = nil)
60
+ association_reflections = reflections.values.grep(AssociationReflection)
61
+ macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
62
+ end
63
+
64
+ # Returns the AssociationReflection object for the +association+ (use the symbol).
65
+ #
66
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
67
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
68
+ #
69
+ def reflect_on_association(association)
70
+ reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
71
+ end
72
+
73
+ # Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
74
+ def reflect_on_all_autosave_associations
75
+ reflections.values.select { |reflection| reflection.options[:autosave] }
76
+ end
77
+ end
78
+
79
+
80
+ # Abstract base class for AggregateReflection and AssociationReflection. Objects of
81
+ # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
82
+ class MacroReflection
83
+ # Returns the name of the macro.
84
+ #
85
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
86
+ # <tt>has_many :clients</tt> returns <tt>:clients</tt>
87
+ attr_reader :name
88
+
89
+ # Returns the macro type.
90
+ #
91
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:composed_of</tt>
92
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
93
+ attr_reader :macro
94
+
95
+ # Returns the hash of options used for the macro.
96
+ #
97
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt>
98
+ # <tt>has_many :clients</tt> returns +{}+
99
+ attr_reader :options
100
+
101
+ attr_reader :active_record
102
+
103
+ attr_reader :plural_name # :nodoc:
104
+
105
+ def initialize(macro, name, options, active_record)
106
+ @macro = macro
107
+ @name = name
108
+ @options = options
109
+ @active_record = active_record
110
+ @plural_name = active_record.pluralize_table_names ?
111
+ name.to_s.pluralize : name.to_s
112
+ end
113
+
114
+ # Returns the class for the macro.
115
+ #
116
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
117
+ # <tt>has_many :clients</tt> returns the Client class
118
+ def klass
119
+ @klass ||= class_name.constantize
120
+ end
121
+
122
+ # Returns the class name for the macro.
123
+ #
124
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
125
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
126
+ def class_name
127
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
128
+ end
129
+
130
+ # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
131
+ # and +other_aggregation+ has an options hash assigned to it.
132
+ def ==(other_aggregation)
133
+ super ||
134
+ other_aggregation.kind_of?(self.class) &&
135
+ name == other_aggregation.name &&
136
+ other_aggregation.options &&
137
+ active_record == other_aggregation.active_record
138
+ end
139
+
140
+ def sanitized_conditions #:nodoc:
141
+ @sanitized_conditions ||= klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
142
+ end
143
+
144
+ private
145
+ def derive_class_name
146
+ name.to_s.camelize
147
+ end
148
+ end
149
+
150
+
151
+ # Holds all the meta-data about an aggregation as it was specified in the
152
+ # Active Record class.
153
+ class AggregateReflection < MacroReflection #:nodoc:
154
+ end
155
+
156
+ # Holds all the meta-data about an association as it was specified in the
157
+ # Active Record class.
158
+ class AssociationReflection < MacroReflection #:nodoc:
159
+ # Returns the target association's class.
160
+ #
161
+ # class Author < ActiveRecord::Base
162
+ # has_many :books
163
+ # end
164
+ #
165
+ # Author.reflect_on_association(:books).klass
166
+ # # => Book
167
+ #
168
+ # <b>Note:</b> Do not call +klass.new+ or +klass.create+ to instantiate
169
+ # a new association object. Use +build_association+ or +create_association+
170
+ # instead. This allows plugins to hook into association object creation.
171
+ def klass
172
+ @klass ||= active_record.send(:compute_type, class_name)
173
+ end
174
+
175
+ def initialize(macro, name, options, active_record)
176
+ super
177
+ @collection = macro.in?([:has_many, :has_and_belongs_to_many])
178
+ end
179
+
180
+ # Returns a new, unsaved instance of the associated class. +options+ will
181
+ # be passed to the class's constructor.
182
+ def build_association(*options, &block)
183
+ klass.new(*options, &block)
184
+ end
185
+
186
+ def table_name
187
+ @table_name ||= klass.table_name
188
+ end
189
+
190
+ def quoted_table_name
191
+ @quoted_table_name ||= klass.quoted_table_name
192
+ end
193
+
194
+ def foreign_key
195
+ @foreign_key ||= options[:foreign_key] || derive_foreign_key
196
+ end
197
+
198
+ def foreign_type
199
+ @foreign_type ||= options[:foreign_type] || "#{name}_type"
200
+ end
201
+
202
+ def type
203
+ @type ||= options[:as] && "#{options[:as]}_type"
204
+ end
205
+
206
+ def primary_key_column
207
+ @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
208
+ end
209
+
210
+ def association_foreign_key
211
+ @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
212
+ end
213
+
214
+ # klass option is necessary to support loading polymorphic associations
215
+ def association_primary_key(klass = nil)
216
+ options[:primary_key] || primary_key(klass || self.klass)
217
+ end
218
+
219
+ def active_record_primary_key
220
+ @active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
221
+ end
222
+
223
+ def counter_cache_column
224
+ if options[:counter_cache] == true
225
+ "#{active_record.name.demodulize.underscore.pluralize}_count"
226
+ elsif options[:counter_cache]
227
+ options[:counter_cache].to_s
228
+ end
229
+ end
230
+
231
+ def columns(tbl_name, log_msg)
232
+ @columns ||= klass.connection.columns(tbl_name, log_msg)
233
+ end
234
+
235
+ def reset_column_information
236
+ @columns = nil
237
+ end
238
+
239
+ def check_validity!
240
+ check_validity_of_inverse!
241
+ end
242
+
243
+ def check_validity_of_inverse!
244
+ unless options[:polymorphic]
245
+ if has_inverse? && inverse_of.nil?
246
+ raise InverseOfAssociationNotFoundError.new(self)
247
+ end
248
+ end
249
+ end
250
+
251
+ def through_reflection
252
+ nil
253
+ end
254
+
255
+ def source_reflection
256
+ nil
257
+ end
258
+
259
+ # A chain of reflections from this one back to the owner. For more see the explanation in
260
+ # ThroughReflection.
261
+ def chain
262
+ [self]
263
+ end
264
+
265
+ def nested?
266
+ false
267
+ end
268
+
269
+ # An array of arrays of conditions. Each item in the outside array corresponds to a reflection
270
+ # in the #chain. The inside arrays are simply conditions (and each condition may itself be
271
+ # a hash, array, arel predicate, etc...)
272
+ def conditions
273
+ [[options[:conditions]].compact]
274
+ end
275
+
276
+ alias :source_macro :macro
277
+
278
+ def has_inverse?
279
+ @options[:inverse_of]
280
+ end
281
+
282
+ def inverse_of
283
+ if has_inverse?
284
+ @inverse_of ||= klass.reflect_on_association(options[:inverse_of])
285
+ end
286
+ end
287
+
288
+ def polymorphic_inverse_of(associated_class)
289
+ if has_inverse?
290
+ if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
291
+ inverse_relationship
292
+ else
293
+ raise InverseOfAssociationNotFoundError.new(self, associated_class)
294
+ end
295
+ end
296
+ end
297
+
298
+ # Returns whether or not this association reflection is for a collection
299
+ # association. Returns +true+ if the +macro+ is either +has_many+ or
300
+ # +has_and_belongs_to_many+, +false+ otherwise.
301
+ def collection?
302
+ @collection
303
+ end
304
+
305
+ # Returns whether or not the association should be validated as part of
306
+ # the parent's validation.
307
+ #
308
+ # Unless you explicitly disable validation with
309
+ # <tt>:validate => false</tt>, validation will take place when:
310
+ #
311
+ # * you explicitly enable validation; <tt>:validate => true</tt>
312
+ # * you use autosave; <tt>:autosave => true</tt>
313
+ # * the association is a +has_many+ association
314
+ def validate?
315
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
316
+ end
317
+
318
+ # Returns +true+ if +self+ is a +belongs_to+ reflection.
319
+ def belongs_to?
320
+ macro == :belongs_to
321
+ end
322
+
323
+ def association_class
324
+ case macro
325
+ when :belongs_to
326
+ if options[:polymorphic]
327
+ Associations::BelongsToPolymorphicAssociation
328
+ else
329
+ Associations::BelongsToAssociation
330
+ end
331
+ when :has_and_belongs_to_many
332
+ Associations::HasAndBelongsToManyAssociation
333
+ when :has_many
334
+ if options[:through]
335
+ Associations::HasManyThroughAssociation
336
+ else
337
+ Associations::HasManyAssociation
338
+ end
339
+ when :has_one
340
+ if options[:through]
341
+ Associations::HasOneThroughAssociation
342
+ else
343
+ Associations::HasOneAssociation
344
+ end
345
+ end
346
+ end
347
+
348
+ private
349
+ def derive_class_name
350
+ class_name = name.to_s.camelize
351
+ class_name = class_name.singularize if collection?
352
+ class_name
353
+ end
354
+
355
+ def derive_foreign_key
356
+ if belongs_to?
357
+ "#{name}_id"
358
+ elsif options[:as]
359
+ "#{options[:as]}_id"
360
+ else
361
+ active_record.name.foreign_key
362
+ end
363
+ end
364
+
365
+ def primary_key(klass)
366
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
367
+ end
368
+ end
369
+
370
+ # Holds all the meta-data about a :through association as it was specified
371
+ # in the Active Record class.
372
+ class ThroughReflection < AssociationReflection #:nodoc:
373
+ delegate :foreign_key, :foreign_type, :association_foreign_key,
374
+ :active_record_primary_key, :type, :to => :source_reflection
375
+
376
+ # Gets the source of the through reflection. It checks both a singularized
377
+ # and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
378
+ #
379
+ # class Post < ActiveRecord::Base
380
+ # has_many :taggings
381
+ # has_many :tags, :through => :taggings
382
+ # end
383
+ #
384
+ def source_reflection
385
+ @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
386
+ end
387
+
388
+ # Returns the AssociationReflection object specified in the <tt>:through</tt> option
389
+ # of a HasManyThrough or HasOneThrough association.
390
+ #
391
+ # class Post < ActiveRecord::Base
392
+ # has_many :taggings
393
+ # has_many :tags, :through => :taggings
394
+ # end
395
+ #
396
+ # tags_reflection = Post.reflect_on_association(:tags)
397
+ # taggings_reflection = tags_reflection.through_reflection
398
+ #
399
+ def through_reflection
400
+ @through_reflection ||= active_record.reflect_on_association(options[:through])
401
+ end
402
+
403
+ # Returns an array of reflections which are involved in this association. Each item in the
404
+ # array corresponds to a table which will be part of the query for this association.
405
+ #
406
+ # The chain is built by recursively calling #chain on the source reflection and the through
407
+ # reflection. The base case for the recursion is a normal association, which just returns
408
+ # [self] as its #chain.
409
+ def chain
410
+ @chain ||= begin
411
+ chain = source_reflection.chain + through_reflection.chain
412
+ chain[0] = self # Use self so we don't lose the information from :source_type
413
+ chain
414
+ end
415
+ end
416
+
417
+ # Consider the following example:
418
+ #
419
+ # class Person
420
+ # has_many :articles
421
+ # has_many :comment_tags, :through => :articles
422
+ # end
423
+ #
424
+ # class Article
425
+ # has_many :comments
426
+ # has_many :comment_tags, :through => :comments, :source => :tags
427
+ # end
428
+ #
429
+ # class Comment
430
+ # has_many :tags
431
+ # end
432
+ #
433
+ # There may be conditions on Person.comment_tags, Article.comment_tags and/or Comment.tags,
434
+ # but only Comment.tags will be represented in the #chain. So this method creates an array
435
+ # of conditions corresponding to the chain. Each item in the #conditions array corresponds
436
+ # to an item in the #chain, and is itself an array of conditions from an arbitrary number
437
+ # of relevant reflections, plus any :source_type or polymorphic :as constraints.
438
+ def conditions
439
+ @conditions ||= begin
440
+ conditions = source_reflection.conditions.map { |c| c.dup }
441
+
442
+ # Add to it the conditions from this reflection if necessary.
443
+ conditions.first << options[:conditions] if options[:conditions]
444
+
445
+ through_conditions = through_reflection.conditions
446
+
447
+ if options[:source_type]
448
+ through_conditions.first << { foreign_type => options[:source_type] }
449
+ end
450
+
451
+ # Recursively fill out the rest of the array from the through reflection
452
+ conditions += through_conditions
453
+
454
+ # And return
455
+ conditions
456
+ end
457
+ end
458
+
459
+ # The macro used by the source association
460
+ def source_macro
461
+ source_reflection.source_macro
462
+ end
463
+
464
+ # A through association is nested if there would be more than one join table
465
+ def nested?
466
+ chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
467
+ end
468
+
469
+ # We want to use the klass from this reflection, rather than just delegate straight to
470
+ # the source_reflection, because the source_reflection may be polymorphic. We still
471
+ # need to respect the source_reflection's :primary_key option, though.
472
+ def association_primary_key(klass = nil)
473
+ # Get the "actual" source reflection if the immediate source reflection has a
474
+ # source reflection itself
475
+ source_reflection = self.source_reflection
476
+ while source_reflection.source_reflection
477
+ source_reflection = source_reflection.source_reflection
478
+ end
479
+
480
+ source_reflection.options[:primary_key] || primary_key(klass || self.klass)
481
+ end
482
+
483
+ # Gets an array of possible <tt>:through</tt> source reflection names:
484
+ #
485
+ # [:singularized, :pluralized]
486
+ #
487
+ def source_reflection_names
488
+ @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
489
+ end
490
+
491
+ def source_options
492
+ source_reflection.options
493
+ end
494
+
495
+ def through_options
496
+ through_reflection.options
497
+ end
498
+
499
+ def check_validity!
500
+ if through_reflection.nil?
501
+ raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
502
+ end
503
+
504
+ if through_reflection.options[:polymorphic]
505
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
506
+ end
507
+
508
+ if source_reflection.nil?
509
+ raise HasManyThroughSourceAssociationNotFoundError.new(self)
510
+ end
511
+
512
+ if options[:source_type] && source_reflection.options[:polymorphic].nil?
513
+ raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
514
+ end
515
+
516
+ if source_reflection.options[:polymorphic] && options[:source_type].nil?
517
+ raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
518
+ end
519
+
520
+ if macro == :has_one && through_reflection.collection?
521
+ raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
522
+ end
523
+
524
+ check_validity_of_inverse!
525
+ end
526
+
527
+ private
528
+ def derive_class_name
529
+ # get the class_name of the belongs_to association of the through reflection
530
+ options[:source_type] || source_reflection.class_name
531
+ end
532
+ end
533
+ end
534
+ end