activerecord 1.13.2 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (144) hide show
  1. data/CHANGELOG +452 -10
  2. data/RUNNING_UNIT_TESTS +1 -1
  3. data/lib/active_record.rb +5 -2
  4. data/lib/active_record/acts/list.rb +1 -1
  5. data/lib/active_record/acts/tree.rb +29 -25
  6. data/lib/active_record/aggregations.rb +3 -2
  7. data/lib/active_record/associations.rb +783 -337
  8. data/lib/active_record/associations/association_collection.rb +7 -12
  9. data/lib/active_record/associations/association_proxy.rb +62 -24
  10. data/lib/active_record/associations/belongs_to_association.rb +27 -46
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  12. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +38 -38
  13. data/lib/active_record/associations/has_many_association.rb +61 -56
  14. data/lib/active_record/associations/has_many_through_association.rb +144 -0
  15. data/lib/active_record/associations/has_one_association.rb +22 -16
  16. data/lib/active_record/base.rb +482 -182
  17. data/lib/active_record/calculations.rb +225 -0
  18. data/lib/active_record/callbacks.rb +7 -7
  19. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +162 -47
  20. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -1
  21. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +2 -1
  22. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +21 -1
  23. data/lib/active_record/connection_adapters/abstract_adapter.rb +34 -2
  24. data/lib/active_record/connection_adapters/db2_adapter.rb +107 -61
  25. data/lib/active_record/connection_adapters/mysql_adapter.rb +29 -6
  26. data/lib/active_record/connection_adapters/openbase_adapter.rb +349 -0
  27. data/lib/active_record/connection_adapters/{oci_adapter.rb → oracle_adapter.rb} +125 -59
  28. data/lib/active_record/connection_adapters/postgresql_adapter.rb +24 -21
  29. data/lib/active_record/connection_adapters/sqlite_adapter.rb +47 -8
  30. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +36 -16
  31. data/lib/active_record/connection_adapters/sybase_adapter.rb +684 -0
  32. data/lib/active_record/fixtures.rb +42 -17
  33. data/lib/active_record/locking.rb +36 -15
  34. data/lib/active_record/migration.rb +111 -8
  35. data/lib/active_record/observer.rb +25 -1
  36. data/lib/active_record/reflection.rb +103 -41
  37. data/lib/active_record/schema.rb +2 -2
  38. data/lib/active_record/schema_dumper.rb +55 -18
  39. data/lib/active_record/timestamp.rb +6 -6
  40. data/lib/active_record/validations.rb +65 -40
  41. data/lib/active_record/vendor/db2.rb +10 -5
  42. data/lib/active_record/vendor/simple.rb +693 -702
  43. data/lib/active_record/version.rb +2 -2
  44. data/rakefile +4 -4
  45. data/test/aaa_create_tables_test.rb +25 -6
  46. data/test/abstract_unit.rb +39 -1
  47. data/test/adapter_test.rb +31 -4
  48. data/test/associations_cascaded_eager_loading_test.rb +106 -0
  49. data/test/associations_go_eager_test.rb +85 -16
  50. data/test/associations_join_model_test.rb +338 -0
  51. data/test/associations_test.rb +129 -50
  52. data/test/base_test.rb +204 -49
  53. data/test/binary_test.rb +1 -1
  54. data/test/calculations_test.rb +169 -0
  55. data/test/callbacks_test.rb +5 -23
  56. data/test/class_inheritable_attributes_test.rb +1 -1
  57. data/test/column_alias_test.rb +1 -1
  58. data/test/connections/native_mysql/connection.rb +1 -0
  59. data/test/connections/native_openbase/connection.rb +22 -0
  60. data/test/connections/{native_oci → native_oracle}/connection.rb +7 -9
  61. data/test/connections/native_sqlite/connection.rb +1 -1
  62. data/test/connections/native_sqlite3/connection.rb +1 -0
  63. data/test/connections/native_sqlite3/in_memory_connection.rb +1 -0
  64. data/test/connections/native_sybase/connection.rb +24 -0
  65. data/test/defaults_test.rb +18 -0
  66. data/test/deprecated_associations_test.rb +2 -2
  67. data/test/deprecated_finder_test.rb +0 -6
  68. data/test/finder_test.rb +26 -23
  69. data/test/fixtures/accounts.yml +10 -0
  70. data/test/fixtures/author.rb +31 -6
  71. data/test/fixtures/author_favorites.yml +4 -0
  72. data/test/fixtures/categories/special_categories.yml +9 -0
  73. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  74. data/test/fixtures/categories_posts.yml +4 -0
  75. data/test/fixtures/categorization.rb +5 -0
  76. data/test/fixtures/categorizations.yml +11 -0
  77. data/test/fixtures/category.rb +6 -0
  78. data/test/fixtures/company.rb +17 -5
  79. data/test/fixtures/company_in_module.rb +19 -5
  80. data/test/fixtures/db_definitions/db2.drop.sql +3 -0
  81. data/test/fixtures/db_definitions/db2.sql +121 -100
  82. data/test/fixtures/db_definitions/db22.sql +2 -2
  83. data/test/fixtures/db_definitions/firebird.drop.sql +4 -0
  84. data/test/fixtures/db_definitions/firebird.sql +26 -0
  85. data/test/fixtures/db_definitions/mysql.drop.sql +3 -0
  86. data/test/fixtures/db_definitions/mysql.sql +21 -1
  87. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  88. data/test/fixtures/db_definitions/openbase.sql +282 -0
  89. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  90. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  91. data/test/fixtures/db_definitions/{oci.drop.sql → oracle.drop.sql} +6 -0
  92. data/test/fixtures/db_definitions/{oci.sql → oracle.sql} +25 -4
  93. data/test/fixtures/db_definitions/{oci2.drop.sql → oracle2.drop.sql} +0 -0
  94. data/test/fixtures/db_definitions/{oci2.sql → oracle2.sql} +0 -0
  95. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  96. data/test/fixtures/db_definitions/postgresql.sql +22 -1
  97. data/test/fixtures/db_definitions/schema.rb +32 -0
  98. data/test/fixtures/db_definitions/sqlite.drop.sql +3 -0
  99. data/test/fixtures/db_definitions/sqlite.sql +18 -0
  100. data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
  101. data/test/fixtures/db_definitions/sqlserver.sql +23 -3
  102. data/test/fixtures/db_definitions/sybase.drop.sql +31 -0
  103. data/test/fixtures/db_definitions/sybase.sql +204 -0
  104. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  105. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  106. data/test/fixtures/developers.yml +6 -1
  107. data/test/fixtures/developers_projects.yml +4 -0
  108. data/test/fixtures/funny_jokes.yml +14 -0
  109. data/test/fixtures/joke.rb +6 -0
  110. data/test/fixtures/legacy_thing.rb +3 -0
  111. data/test/fixtures/legacy_things.yml +3 -0
  112. data/test/fixtures/mixin.rb +1 -1
  113. data/test/fixtures/person.rb +4 -1
  114. data/test/fixtures/post.rb +26 -1
  115. data/test/fixtures/project.rb +1 -0
  116. data/test/fixtures/reader.rb +4 -0
  117. data/test/fixtures/readers.yml +4 -0
  118. data/test/fixtures/reply.rb +2 -1
  119. data/test/fixtures/tag.rb +5 -0
  120. data/test/fixtures/tagging.rb +6 -0
  121. data/test/fixtures/taggings.yml +18 -0
  122. data/test/fixtures/tags.yml +7 -0
  123. data/test/fixtures/tasks.yml +2 -2
  124. data/test/fixtures/topic.rb +2 -2
  125. data/test/fixtures/topics.yml +1 -0
  126. data/test/fixtures_test.rb +47 -13
  127. data/test/inheritance_test.rb +2 -2
  128. data/test/locking_test.rb +15 -1
  129. data/test/method_scoping_test.rb +248 -13
  130. data/test/migration_test.rb +68 -11
  131. data/test/mixin_nested_set_test.rb +1 -1
  132. data/test/modules_test.rb +6 -1
  133. data/test/readonly_test.rb +1 -1
  134. data/test/reflection_test.rb +63 -9
  135. data/test/schema_dumper_test.rb +41 -0
  136. data/test/{synonym_test_oci.rb → synonym_test_oracle.rb} +1 -1
  137. data/test/threaded_connections_test.rb +10 -0
  138. data/test/unconnected_test.rb +12 -5
  139. data/test/validations_test.rb +197 -10
  140. metadata +295 -260
  141. data/test/fixtures/db_definitions/create_oracle_db.bat +0 -0
  142. data/test/fixtures/db_definitions/create_oracle_db.sh +0 -0
  143. data/test/fixtures/fixture_database.sqlite +0 -0
  144. data/test/fixtures/fixture_database_2.sqlite +0 -0
@@ -15,7 +15,7 @@ connection.rb otherwise (on Postgres, at least) tests for default values will fa
15
15
  The easiest way to run the unit tests is through Rake. The default task runs
16
16
  the entire test suite for all the adapters. You can also run the suite on just
17
17
  one adapter by using the tasks test_mysql_ruby, test_ruby_mysql, test_sqlite,
18
- or test_postresql. For more information, checkout the full array of rake tasks with "rake -T"
18
+ or test_postgresql. For more information, checkout the full array of rake tasks with "rake -T"
19
19
 
20
20
  Rake can be found at http://rake.rubyforge.org
21
21
 
@@ -38,10 +38,10 @@ require 'active_record/base'
38
38
  require 'active_record/observer'
39
39
  require 'active_record/validations'
40
40
  require 'active_record/callbacks'
41
+ require 'active_record/reflection'
41
42
  require 'active_record/associations'
42
43
  require 'active_record/aggregations'
43
44
  require 'active_record/transactions'
44
- require 'active_record/reflection'
45
45
  require 'active_record/timestamp'
46
46
  require 'active_record/acts/list'
47
47
  require 'active_record/acts/tree'
@@ -49,6 +49,7 @@ require 'active_record/acts/nested_set'
49
49
  require 'active_record/locking'
50
50
  require 'active_record/migration'
51
51
  require 'active_record/schema'
52
+ require 'active_record/calculations'
52
53
 
53
54
  ActiveRecord::Base.class_eval do
54
55
  include ActiveRecord::Validations
@@ -63,10 +64,11 @@ ActiveRecord::Base.class_eval do
63
64
  include ActiveRecord::Acts::Tree
64
65
  include ActiveRecord::Acts::List
65
66
  include ActiveRecord::Acts::NestedSet
67
+ include ActiveRecord::Calculations
66
68
  end
67
69
 
68
70
  unless defined?(RAILS_CONNECTION_ADAPTERS)
69
- RAILS_CONNECTION_ADAPTERS = %w(mysql postgresql sqlite firebird sqlserver db2 oci)
71
+ RAILS_CONNECTION_ADAPTERS = %w( mysql postgresql sqlite firebird sqlserver db2 oracle sybase openbase )
70
72
  end
71
73
 
72
74
  RAILS_CONNECTION_ADAPTERS.each do |adapter|
@@ -74,3 +76,4 @@ RAILS_CONNECTION_ADAPTERS.each do |adapter|
74
76
  end
75
77
 
76
78
  require 'active_record/query_cache'
79
+ require 'active_record/schema_dumper'
@@ -174,7 +174,7 @@ module ActiveRecord
174
174
 
175
175
  def bottom_item(except = nil)
176
176
  conditions = scope_condition
177
- conditions = "#{conditions} AND id != #{except.id}" if except
177
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
178
178
  acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
179
179
  end
180
180
 
@@ -45,40 +45,44 @@ module ActiveRecord
45
45
  configuration.update(options) if options.is_a?(Hash)
46
46
 
47
47
  belongs_to :parent, :class_name => name, :foreign_key => configuration[:foreign_key], :counter_cache => configuration[:counter_cache]
48
- has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => true
48
+ has_many :children, :class_name => name, :foreign_key => configuration[:foreign_key], :order => configuration[:order], :dependent => :destroy
49
49
 
50
- module_eval <<-END
50
+ class_eval <<-EOV
51
+ include ActiveRecord::Acts::Tree::InstanceMethods
52
+
51
53
  def self.roots
52
- self.find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
54
+ find(:all, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
53
55
  end
56
+
54
57
  def self.root
55
- self.find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
58
+ find(:first, :conditions => "#{configuration[:foreign_key]} IS NULL", :order => #{configuration[:order].nil? ? "nil" : %Q{"#{configuration[:order]}"}})
56
59
  end
57
- END
58
-
59
- # Returns list of ancestors, starting from parent until root.
60
- #
61
- # subchild1.ancestors # => [child1, root]
62
- define_method(:ancestors) do
63
- node, nodes = self, []
64
- nodes << node = node.parent until not node.has_parent?
65
- nodes
66
- end
60
+ EOV
61
+ end
62
+ end
67
63
 
68
- define_method(:root) do
69
- node = self
70
- node = node.parent until not node.has_parent?
71
- node
72
- end
64
+ module InstanceMethods
65
+ # Returns list of ancestors, starting from parent until root.
66
+ #
67
+ # subchild1.ancestors # => [child1, root]
68
+ def ancestors
69
+ node, nodes = self, []
70
+ nodes << node = node.parent until not node.has_parent?
71
+ nodes
72
+ end
73
73
 
74
- define_method(:siblings) do
75
- self_and_siblings - [self]
76
- end
74
+ def root
75
+ node = self
76
+ node = node.parent until not node.has_parent?
77
+ node
78
+ end
77
79
 
78
- define_method(:self_and_siblings) do
79
- has_parent? ? parent.children : self.class.roots
80
- end
80
+ def siblings
81
+ self_and_siblings - [self]
82
+ end
81
83
 
84
+ def self_and_siblings
85
+ has_parent? ? parent.children : self.class.roots
82
86
  end
83
87
  end
84
88
  end
@@ -1,7 +1,6 @@
1
1
  module ActiveRecord
2
2
  module Aggregations # :nodoc:
3
- def self.append_features(base)
4
- super
3
+ def self.included(base)
5
4
  base.extend(ClassMethods)
6
5
  end
7
6
 
@@ -134,6 +133,8 @@ module ActiveRecord
134
133
 
135
134
  reader_method(name, class_name, mapping)
136
135
  writer_method(name, class_name, mapping)
136
+
137
+ create_reflection(:composed_of, part_id, options, self)
137
138
  end
138
139
 
139
140
  private
@@ -1,12 +1,59 @@
1
1
  require 'active_record/associations/association_proxy'
2
2
  require 'active_record/associations/association_collection'
3
3
  require 'active_record/associations/belongs_to_association'
4
+ require 'active_record/associations/belongs_to_polymorphic_association'
4
5
  require 'active_record/associations/has_one_association'
5
6
  require 'active_record/associations/has_many_association'
7
+ require 'active_record/associations/has_many_through_association'
6
8
  require 'active_record/associations/has_and_belongs_to_many_association'
7
9
  require 'active_record/deprecated_associations'
8
10
 
9
11
  module ActiveRecord
12
+ class HasManyThroughAssociationNotFoundError < ActiveRecordError #:nodoc:
13
+ def initialize(reflection)
14
+ @reflection = reflection
15
+ end
16
+
17
+ def message
18
+ "Could not find the association #{@reflection.options[:through].inspect} in model #{@reflection.klass}"
19
+ end
20
+ end
21
+
22
+ class HasManyThroughAssociationPolymorphicError < ActiveRecordError #:nodoc:
23
+ def initialize(owner_class_name, reflection, source_reflection)
24
+ @owner_class_name = owner_class_name
25
+ @reflection = reflection
26
+ @source_reflection = source_reflection
27
+ end
28
+
29
+ def message
30
+ "Cannot have a has_many :through association '#{@owner_class_name}##{@reflection.name}' on the polymorphic object '#{@source_reflection.class_name}##{@source_reflection.name}'."
31
+ end
32
+ end
33
+
34
+ class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
35
+ def initialize(reflection)
36
+ @reflection = reflection
37
+ @through_reflection = reflection.through_reflection
38
+ @source_reflection_names = reflection.source_reflection_names
39
+ @source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
40
+ end
41
+
42
+ def message
43
+ "Could not find the source association(s) #{@source_reflection_names.collect(&:inspect).to_sentence :connector => 'or'} in model #{@through_reflection.klass}. Try 'has_many #{@reflection.name.inspect}, :through => #{@through_reflection.name.inspect}, :source => <name>'. Is it one of #{@source_associations.to_sentence :connector => 'or'}?"
44
+ end
45
+ end
46
+
47
+ class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
48
+ def initialize(reflection)
49
+ @reflection = reflection
50
+ end
51
+
52
+ def message
53
+ "Can not eagerly load the polymorphic association #{@reflection.name.inspect}"
54
+ end
55
+ end
56
+
10
57
  module Associations # :nodoc:
11
58
  def self.append_features(base)
12
59
  super
@@ -123,15 +170,13 @@ module ActiveRecord
123
170
  # === Association extensions
124
171
  #
125
172
  # The proxy objects that controls the access to associations can be extended through anonymous modules. This is especially
126
- # beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this associatio.
173
+ # beneficial for adding new finders, creators, and other factory-type methods that are only used as part of this association.
127
174
  # Example:
128
175
  #
129
176
  # class Account < ActiveRecord::Base
130
177
  # has_many :people do
131
178
  # def find_or_create_by_name(name)
132
- # first_name, *last_name = name.split
133
- # last_name = last_name.join " "
134
- #
179
+ # first_name, last_name = name.split(" ", 2)
135
180
  # find_or_create_by_first_name_and_last_name(first_name, last_name)
136
181
  # end
137
182
  # end
@@ -145,9 +190,7 @@ module ActiveRecord
145
190
  #
146
191
  # module FindOrCreateByNameExtension
147
192
  # def find_or_create_by_name(name)
148
- # first_name, *last_name = name.split
149
- # last_name = last_name.join " "
150
- #
193
+ # first_name, last_name = name.split(" ", 2)
151
194
  # find_or_create_by_first_name_and_last_name(first_name, last_name)
152
195
  # end
153
196
  # end
@@ -160,6 +203,64 @@ module ActiveRecord
160
203
  # has_many :people, :extend => FindOrCreateByNameExtension
161
204
  # end
162
205
  #
206
+ # === Association Join Models
207
+ #
208
+ # Has Many associations can be configured with the :through option to use an explicit join model to retrieve the data. This
209
+ # operates similarly to a <tt>has_and_belongs_to_many</tt> association. The advantage is that you're able to add validations,
210
+ # callbacks, and extra attributes on the join model. Consider the following schema:
211
+ #
212
+ # class Author < ActiveRecord::Base
213
+ # has_many :authorships
214
+ # has_many :books, :through => :authorships
215
+ # end
216
+ #
217
+ # class Authorship < ActiveRecord::Base
218
+ # belongs_to :author
219
+ # belongs_to :book
220
+ # end
221
+ #
222
+ # @author = Author.find :first
223
+ # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to.
224
+ # @author.books # selects all books by using the Authorship join model
225
+ #
226
+ # You can also go through a has_many association on the join model:
227
+ #
228
+ # class Firm < ActiveRecord::Base
229
+ # has_many :clients
230
+ # has_many :invoices, :through => :clients
231
+ # end
232
+ #
233
+ # class Client < ActiveRecord::Base
234
+ # belongs_to :firm
235
+ # has_many :invoices
236
+ # end
237
+ #
238
+ # class Invoice < ActiveRecord::Base
239
+ # belongs_to :client
240
+ # end
241
+ #
242
+ # @firm = Firm.find :first
243
+ # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
244
+ # @firm.invoices # selects all invoices by going through the Client join model.
245
+ #
246
+ # === Polymorphic Associations
247
+ #
248
+ # Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they
249
+ # specify an interface that a has_many association must adhere to.
250
+ #
251
+ # class Asset < ActiveRecord::Base
252
+ # belongs_to :attachable, :polymorphic => true
253
+ # end
254
+ #
255
+ # class Post < ActiveRecord::Base
256
+ # has_many :assets, :as => :attachable # The <tt>:as</tt> option specifies the polymorphic interface to use.
257
+ # end
258
+ #
259
+ # @asset.attachable = @post
260
+ #
261
+ # This works by using a type column in addition to a foreign key to specify the associated record. In the Asset example, you'd need
262
+ # an attachable_id integer column and an attachable_type string column.
263
+ #
163
264
  # == Caching
164
265
  #
165
266
  # All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically
@@ -220,9 +321,57 @@ module ActiveRecord
220
321
  # in both conditions and orders. So :order => "posts.id DESC" will work while :order => "id DESC" will not. This may require that
221
322
  # you alter the :order and :conditions on the association definitions themselves.
222
323
  #
223
- # It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will also not pull
324
+ # It's currently not possible to use eager loading on multiple associations from the same table. Eager loading will not pull
224
325
  # additional attributes on join tables, so "rich associations" with has_and_belongs_to_many is not a good fit for eager loading.
326
+ #
327
+ # == Table Aliasing
225
328
  #
329
+ # ActiveRecord uses table aliasing in the case that a table is referenced multiple times in a join. If a table is referenced only once,
330
+ # the standard table name is used. The second time, the table is aliased as #{reflection_name}_#{parent_table_name}. Indexes are appended
331
+ # for any more successive uses of the table name.
332
+ #
333
+ # Post.find :all, :include => :comments
334
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ...
335
+ # Post.find :all, :include => :special_comments # STI
336
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... AND comments.type = 'SpecialComment'
337
+ # Post.find :all, :include => [:comments, :special_comments] # special_comments is the reflection name, posts is the parent table name
338
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments ON ... LEFT OUTER JOIN comments special_comments_posts
339
+ #
340
+ # Acts as tree example:
341
+ #
342
+ # TreeMixin.find :all, :include => :children
343
+ # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
344
+ # TreeMixin.find :all, :include => {:children => :parent} # using cascading eager includes
345
+ # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
346
+ # LEFT OUTER JOIN parents_mixins ...
347
+ # TreeMixin.find :all, :include => {:children => {:parent => :children}}
348
+ # # => SELECT ... FROM mixins LEFT OUTER JOIN mixins childrens_mixins ...
349
+ # LEFT OUTER JOIN parents_mixins ...
350
+ # LEFT OUTER JOIN mixins childrens_mixins_2
351
+ #
352
+ # Has and Belongs to Many join tables use the same idea, but add a _join suffix:
353
+ #
354
+ # Post.find :all, :include => :categories
355
+ # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
356
+ # Post.find :all, :include => {:categories => :posts}
357
+ # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
358
+ # LEFT OUTER JOIN categories_posts posts_categories_join LEFT OUTER JOIN posts posts_categories
359
+ # Post.find :all, :include => {:categories => {:posts => :categories}}
360
+ # # => SELECT ... FROM posts LEFT OUTER JOIN categories_posts ... LEFT OUTER JOIN categories ...
361
+ # LEFT OUTER JOIN categories_posts posts_categories_join LEFT OUTER JOIN posts posts_categories
362
+ # LEFT OUTER JOIN categories_posts categories_posts_join LEFT OUTER JOIN categories categories_posts
363
+ #
364
+ # If you wish to specify your own custom joins using a :joins option, those table names will take precedence over the eager associations..
365
+ #
366
+ # Post.find :all, :include => :comments, :joins => "inner join comments ..."
367
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments_posts ON ... INNER JOIN comments ...
368
+ # Post.find :all, :include => [:comments, :special_comments], :joins => "inner join comments ..."
369
+ # # => SELECT ... FROM posts LEFT OUTER JOIN comments comments_posts ON ...
370
+ # LEFT OUTER JOIN comments special_comments_posts ...
371
+ # INNER JOIN comments ...
372
+ #
373
+ # Table aliases are automatically truncated according to the maximum length of table identifiers according to the specific database.
374
+ #
226
375
  # == Modules
227
376
  #
228
377
  # By default, associations will look for objects within the current module scope. Consider:
@@ -273,7 +422,7 @@ module ActiveRecord
273
422
  # * <tt>collection=objects</tt> - replaces the collections content by deleting and adding objects as appropriate.
274
423
  # * <tt>collection_singular_ids=ids</tt> - replace the collection by the objects identified by the primary keys in +ids+
275
424
  # * <tt>collection.clear</tt> - removes every object from the collection. This destroys the associated objects if they
276
- # are <tt>:dependent</tt>, deletes them directly from the database if they are <tt>:exclusively_dependent</tt>,
425
+ # are <tt>:dependent</tt>, deletes them directly from the database if they are <tt>:dependent => :delete_all</tt>,
277
426
  # and sets their foreign keys to NULL otherwise.
278
427
  # * <tt>collection.empty?</tt> - returns true if there are no associated objects.
279
428
  # * <tt>collection.size</tt> - returns the number of associated objects.
@@ -312,10 +461,11 @@ module ActiveRecord
312
461
  # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
313
462
  # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_many association will use "person_id"
314
463
  # as the default foreign_key.
315
- # * <tt>:dependent</tt> - if set to :destroy (or true) all the associated objects are destroyed
464
+ # * <tt>:dependent</tt> - if set to :destroy all the associated objects are destroyed
316
465
  # alongside this object by calling their destroy method. If set to :delete_all all associated
317
466
  # objects are deleted *without* calling their destroy method. If set to :nullify all associated
318
467
  # objects' foreign keys are set to NULL *without* calling their save callbacks.
468
+ # NOTE: :dependent => true is deprecated and has been replaced with :dependent => :destroy.
319
469
  # May not be set if :exclusively_dependent is also set.
320
470
  # * <tt>:exclusively_dependent</tt> - Deprecated; equivalent to :dependent => :delete_all. If set to true all
321
471
  # the associated object are deleted in one SQL statement without having their
@@ -328,69 +478,46 @@ module ActiveRecord
328
478
  # specified but +:counter_sql+, +:counter_sql+ will be generated by replacing SELECT ... FROM with SELECT COUNT(*) FROM.
329
479
  # * <tt>:extend</tt> - specify a named module for extending the proxy, see "Association extensions".
330
480
  # * <tt>:include</tt> - specify second-order associations that should be eager loaded when the collection is loaded.
481
+ # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
482
+ # * <tt>:limit</tt>: An integer determining the limit on the number of rows that should be returned.
483
+ # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
484
+ # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
485
+ # include the joined columns.
486
+ # * <tt>:as</tt>: Specifies a polymorphic interface (See #belongs_to).
487
+ # * <tt>:through</tt>: Specifies a Join Model to perform the query through. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
488
+ # are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
489
+ # or <tt>has_many</tt> association.
490
+ # * <tt>:source</tt>: Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
491
+ # inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either +:subscribers+ or
492
+ # +:subscriber+ on +Subscription+, unless a +:source+ is given.
331
493
  #
332
494
  # Option examples:
333
495
  # has_many :comments, :order => "posted_on"
334
496
  # has_many :comments, :include => :author
335
497
  # has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
336
- # has_many :tracks, :order => "position", :dependent => true
498
+ # has_many :tracks, :order => "position", :dependent => :destroy
499
+ # has_many :comments, :dependent => :nullify
500
+ # has_many :tags, :as => :taggable
501
+ # has_many :subscribers, :through => :subscriptions, :source => :user
337
502
  # has_many :subscribers, :class_name => "Person", :finder_sql =>
338
503
  # 'SELECT DISTINCT people.* ' +
339
504
  # 'FROM people p, post_subscriptions ps ' +
340
505
  # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
341
506
  # 'ORDER BY p.first_name'
342
507
  def has_many(association_id, options = {}, &extension)
343
- options.assert_valid_keys(
344
- :foreign_key, :class_name, :exclusively_dependent, :dependent,
345
- :conditions, :order, :include, :finder_sql, :counter_sql,
346
- :before_add, :after_add, :before_remove, :after_remove, :extend,
347
- :group
348
- )
349
-
350
- options[:extend] = create_extension_module(association_id, extension) if block_given?
351
-
352
- association_name, association_class_name, association_class_primary_key_name =
353
- associate_identification(association_id, options[:class_name], options[:foreign_key])
354
-
355
- require_association_class(association_class_name)
508
+ reflection = create_has_many_reflection(association_id, options, &extension)
356
509
 
357
- raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.' if options[:dependent] and options[:exclusively_dependent]
510
+ configure_dependency_for_has_many(reflection)
358
511
 
359
- if options[:exclusively_dependent]
360
- options[:dependent] = :delete_all
361
- #warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.")
512
+ if options[:through]
513
+ collection_reader_method(reflection, HasManyThroughAssociation)
514
+ else
515
+ add_multiple_associated_save_callbacks(reflection.name)
516
+ add_association_callbacks(reflection.name, reflection.options)
517
+ collection_accessor_methods(reflection, HasManyAssociation)
362
518
  end
363
519
 
364
- # See HasManyAssociation#delete_records. Dependent associations
365
- # delete children, otherwise foreign key is set to NULL.
366
- case options[:dependent]
367
- when :destroy, true
368
- module_eval "before_destroy '#{association_name}.each { |o| o.destroy }'"
369
- when :delete_all
370
- module_eval "before_destroy { |record| #{association_class_name}.delete_all(%(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
371
- when :nullify
372
- module_eval "before_destroy { |record| #{association_class_name}.update_all(%(#{association_class_primary_key_name} = NULL), %(#{association_class_primary_key_name} = \#{record.quoted_id})) }"
373
- when nil, false
374
- # pass
375
- else
376
- raise ArgumentError, 'The :dependent option expects either true, :destroy, :delete_all, or :nullify'
377
- end
378
-
379
-
380
- add_multiple_associated_save_callbacks(association_name)
381
- add_association_callbacks(association_name, options)
382
-
383
- collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasManyAssociation)
384
-
385
- # deprecated api
386
- deprecated_collection_count_method(association_name)
387
- deprecated_add_association_relation(association_name)
388
- deprecated_remove_association_relation(association_name)
389
- deprecated_has_collection_method(association_name)
390
- deprecated_find_in_collection_method(association_name)
391
- deprecated_find_all_in_collection_method(association_name)
392
- deprecated_collection_create_method(association_name)
393
- deprecated_collection_build_method(association_name)
520
+ add_deprecated_api_for_has_many(reflection.name)
394
521
  end
395
522
 
396
523
  # Adds the following methods for retrieval and query of a single associated object.
@@ -431,46 +558,32 @@ module ActiveRecord
431
558
  # * <tt>:include</tt> - specify second-order associations that should be eager loaded when this object is loaded.
432
559
  #
433
560
  # Option examples:
434
- # has_one :credit_card, :dependent => true
561
+ # has_one :credit_card, :dependent => :destroy # destroys the associated credit card
562
+ # has_one :credit_card, :dependent => :nullify # updates the associated records foriegn key value to null rather than destroying it
435
563
  # has_one :last_comment, :class_name => "Comment", :order => "posted_on"
436
564
  # has_one :project_manager, :class_name => "Person", :conditions => "role = 'project_manager'"
437
565
  def has_one(association_id, options = {})
438
- options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend)
439
-
440
- association_name, association_class_name, association_class_primary_key_name =
441
- associate_identification(association_id, options[:class_name], options[:foreign_key], false)
442
-
443
- require_association_class(association_class_name)
566
+ reflection = create_has_one_reflection(association_id, options)
444
567
 
445
568
  module_eval do
446
569
  after_save <<-EOF
447
- association = instance_variable_get("@#{association_name}")
570
+ association = instance_variable_get("@#{reflection.name}")
448
571
  unless association.nil?
449
- association["#{association_class_primary_key_name}"] = id
572
+ association["#{reflection.primary_key_name}"] = id
450
573
  association.save(true)
451
- association.send(:construct_sql)
452
574
  end
453
575
  EOF
454
576
  end
455
577
 
456
- association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
457
- association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
458
- association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, HasOneAssociation)
578
+ association_accessor_methods(reflection, HasOneAssociation)
579
+ association_constructor_method(:build, reflection, HasOneAssociation)
580
+ association_constructor_method(:create, reflection, HasOneAssociation)
459
581
 
460
- case options[:dependent]
461
- when :destroy, true
462
- module_eval "before_destroy '#{association_name}.destroy unless #{association_name}.nil?'"
463
- when :nullify
464
- module_eval "before_destroy '#{association_name}.update_attribute(\"#{association_class_primary_key_name}\", nil)'"
465
- when nil, false
466
- # pass
467
- else
468
- raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
469
- end
582
+ configure_dependency_for_has_one(reflection)
470
583
 
471
584
  # deprecated api
472
- deprecated_has_association_method(association_name)
473
- deprecated_association_comparison_method(association_name, association_class_name)
585
+ deprecated_has_association_method(reflection.name)
586
+ deprecated_association_comparison_method(reflection.name, reflection.class_name)
474
587
  end
475
588
 
476
589
  # Adds the following methods for retrieval and query for a single associated object that this object holds an id to.
@@ -507,56 +620,78 @@ module ActiveRecord
507
620
  # * <tt>:counter_cache</tt> - caches the number of belonging objects on the associate class through use of increment_counter
508
621
  # and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it's
509
622
  # destroyed. This requires that a column named "#{table_name}_count" (such as comments_count for a belonging Comment class)
510
- # is used on the associate class (such as a Post class).
623
+ # is used on the associate class (such as a Post class). You can also specify a custom counter cache column by given that
624
+ # name instead of a true/false value to this option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
511
625
  # * <tt>:include</tt> - specify second-order associations that should be eager loaded when this object is loaded.
626
+ # # <tt>:polymorphic</tt> - specify this association is a polymorphic association by passing true.
512
627
  #
513
628
  # Option examples:
514
629
  # belongs_to :firm, :foreign_key => "client_of"
515
630
  # belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
516
631
  # belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
517
632
  # :conditions => 'discounts > #{payments_count}'
633
+ # belongs_to :attachable, :polymorphic => true
518
634
  def belongs_to(association_id, options = {})
519
- options.assert_valid_keys(:class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend)
520
-
521
- association_name, association_class_name, class_primary_key_name =
522
- associate_identification(association_id, options[:class_name], options[:foreign_key], false)
523
-
524
- require_association_class(association_class_name)
525
-
526
- association_class_primary_key_name = options[:foreign_key] || association_class_name.foreign_key
527
-
528
- association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
529
- association_constructor_method(:build, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
530
- association_constructor_method(:create, association_name, association_class_name, association_class_primary_key_name, options, BelongsToAssociation)
531
-
532
- module_eval do
533
- before_save <<-EOF
534
- association = instance_variable_get("@#{association_name}")
535
- if not association.nil?
536
- if association.new_record?
537
- association.save(true)
538
- association.send(:construct_sql)
635
+ reflection = create_belongs_to_reflection(association_id, options)
636
+
637
+ if reflection.options[:polymorphic]
638
+ association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
639
+
640
+ module_eval do
641
+ before_save <<-EOF
642
+ association = instance_variable_get("@#{reflection.name}")
643
+ if !association.nil?
644
+ if association.new_record?
645
+ association.save(true)
646
+ end
647
+
648
+ if association.updated?
649
+ self["#{reflection.primary_key_name}"] = association.id
650
+ self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s
651
+ end
539
652
  end
540
- self["#{association_class_primary_key_name}"] = association.id if association.updated?
541
- end
542
- EOF
543
- end
653
+ EOF
654
+ end
655
+ else
656
+ association_accessor_methods(reflection, BelongsToAssociation)
657
+ association_constructor_method(:build, reflection, BelongsToAssociation)
658
+ association_constructor_method(:create, reflection, BelongsToAssociation)
659
+
660
+ module_eval do
661
+ before_save <<-EOF
662
+ association = instance_variable_get("@#{reflection.name}")
663
+ if !association.nil?
664
+ if association.new_record?
665
+ association.save(true)
666
+ end
667
+
668
+ if association.updated?
669
+ self["#{reflection.primary_key_name}"] = association.id
670
+ end
671
+ end
672
+ EOF
673
+ end
544
674
 
675
+ # deprecated api
676
+ deprecated_has_association_method(reflection.name)
677
+ deprecated_association_comparison_method(reflection.name, reflection.class_name)
678
+ end
679
+
545
680
  if options[:counter_cache]
681
+ cache_column = options[:counter_cache] == true ?
682
+ "#{self.to_s.underscore.pluralize}_count" :
683
+ options[:counter_cache]
684
+
546
685
  module_eval(
547
- "after_create '#{association_class_name}.increment_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" +
548
- " unless #{association_name}.nil?'"
686
+ "after_create '#{reflection.name}.class.increment_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +
687
+ " unless #{reflection.name}.nil?'"
549
688
  )
550
689
 
551
690
  module_eval(
552
- "before_destroy '#{association_class_name}.decrement_counter(\"#{self.to_s.underscore.pluralize + "_count"}\", #{association_class_primary_key_name})" +
553
- " unless #{association_name}.nil?'"
691
+ "before_destroy '#{reflection.name}.class.decrement_counter(\"#{cache_column}\", #{reflection.primary_key_name})" +
692
+ " unless #{reflection.name}.nil?'"
554
693
  )
555
694
  end
556
-
557
- # deprecated api
558
- deprecated_has_association_method(association_name)
559
- deprecated_association_comparison_method(association_name, association_class_name)
560
695
  end
561
696
 
562
697
  # Associates two classes via an intermediate join table. Unless the join table is explicitly specified as
@@ -627,6 +762,11 @@ module ActiveRecord
627
762
  # with a manual one
628
763
  # * <tt>:extend</tt> - anonymous module for extending the proxy, see "Association extensions".
629
764
  # * <tt>:include</tt> - specify second-order associations that should be eager loaded when the collection is loaded.
765
+ # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
766
+ # * <tt>:limit</tt>: An integer determining the limit on the number of rows that should be returned.
767
+ # * <tt>:offset</tt>: An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
768
+ # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
769
+ # include the joined columns.
630
770
  #
631
771
  # Option examples:
632
772
  # has_and_belongs_to_many :projects
@@ -636,43 +776,29 @@ module ActiveRecord
636
776
  # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql =>
637
777
  # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}'
638
778
  def has_and_belongs_to_many(association_id, options = {}, &extension)
639
- options.assert_valid_keys(
640
- :class_name, :table_name, :foreign_key, :association_foreign_key, :conditions, :include,
641
- :join_table, :finder_sql, :delete_sql, :insert_sql, :order, :uniq, :before_add, :after_add,
642
- :before_remove, :after_remove, :extend
643
- )
644
-
645
- options[:extend] = create_extension_module(association_id, extension) if block_given?
646
-
647
- association_name, association_class_name, association_class_primary_key_name =
648
- associate_identification(association_id, options[:class_name], options[:foreign_key])
649
-
650
- require_association_class(association_class_name)
651
-
652
- options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(association_class_name))
653
-
654
- add_multiple_associated_save_callbacks(association_name)
655
-
656
- collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, HasAndBelongsToManyAssociation)
779
+ reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
780
+
781
+ add_multiple_associated_save_callbacks(reflection.name)
782
+ collection_accessor_methods(reflection, HasAndBelongsToManyAssociation)
657
783
 
658
784
  # Don't use a before_destroy callback since users' before_destroy
659
785
  # callbacks will be executed after the association is wiped out.
660
- old_method = "destroy_without_habtm_shim_for_#{association_name}"
786
+ old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
661
787
  class_eval <<-end_eval
662
788
  alias_method :#{old_method}, :destroy_without_callbacks
663
789
  def destroy_without_callbacks
664
- #{association_name}.clear
790
+ #{reflection.name}.clear
665
791
  #{old_method}
666
792
  end
667
793
  end_eval
668
794
 
669
- add_association_callbacks(association_name, options)
795
+ add_association_callbacks(reflection.name, options)
670
796
 
671
797
  # deprecated api
672
- deprecated_collection_count_method(association_name)
673
- deprecated_add_association_relation(association_name)
674
- deprecated_remove_association_relation(association_name)
675
- deprecated_has_collection_method(association_name)
798
+ deprecated_collection_count_method(reflection.name)
799
+ deprecated_add_association_relation(reflection.name)
800
+ deprecated_remove_association_relation(reflection.name)
801
+ deprecated_has_collection_method(reflection.name)
676
802
  end
677
803
 
678
804
  private
@@ -686,93 +812,81 @@ module ActiveRecord
686
812
  table_name_prefix + join_table + table_name_suffix
687
813
  end
688
814
 
689
- def associate_identification(association_id, association_class_name, foreign_key, plural = true)
690
- if association_class_name !~ /::/
691
- association_class_name = type_name_with_module(
692
- association_class_name ||
693
- Inflector.camelize(plural ? Inflector.singularize(association_id.id2name) : association_id.id2name)
694
- )
695
- end
696
-
697
- primary_key_name = foreign_key || name.foreign_key
698
-
699
- return association_id.id2name, association_class_name, primary_key_name
700
- end
701
-
702
- def association_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
703
- define_method(association_name) do |*params|
815
+ def association_accessor_methods(reflection, association_proxy_class)
816
+ define_method(reflection.name) do |*params|
704
817
  force_reload = params.first unless params.empty?
705
- association = instance_variable_get("@#{association_name}")
706
- if association.nil? or force_reload
707
- association = association_proxy_class.new(self,
708
- association_name, association_class_name,
709
- association_class_primary_key_name, options)
818
+ association = instance_variable_get("@#{reflection.name}")
819
+
820
+ if association.nil? || force_reload
821
+ association = association_proxy_class.new(self, reflection)
710
822
  retval = association.reload
711
823
  unless retval.nil?
712
- instance_variable_set("@#{association_name}", association)
824
+ instance_variable_set("@#{reflection.name}", association)
713
825
  else
714
- instance_variable_set("@#{association_name}", nil)
826
+ instance_variable_set("@#{reflection.name}", nil)
715
827
  return nil
716
828
  end
717
829
  end
718
830
  association
719
831
  end
720
832
 
721
- define_method("#{association_name}=") do |new_value|
722
- association = instance_variable_get("@#{association_name}")
833
+ define_method("#{reflection.name}=") do |new_value|
834
+ association = instance_variable_get("@#{reflection.name}")
723
835
  if association.nil?
724
- association = association_proxy_class.new(self,
725
- association_name, association_class_name,
726
- association_class_primary_key_name, options)
836
+ association = association_proxy_class.new(self, reflection)
727
837
  end
838
+
728
839
  association.replace(new_value)
840
+
729
841
  unless new_value.nil?
730
- instance_variable_set("@#{association_name}", association)
842
+ instance_variable_set("@#{reflection.name}", association)
731
843
  else
732
- instance_variable_set("@#{association_name}", nil)
844
+ instance_variable_set("@#{reflection.name}", nil)
733
845
  return nil
734
846
  end
847
+
735
848
  association
736
849
  end
737
850
 
738
- define_method("set_#{association_name}_target") do |target|
851
+ define_method("set_#{reflection.name}_target") do |target|
739
852
  return if target.nil?
740
- association = association_proxy_class.new(self,
741
- association_name, association_class_name,
742
- association_class_primary_key_name, options)
853
+ association = association_proxy_class.new(self, reflection)
743
854
  association.target = target
744
- instance_variable_set("@#{association_name}", association)
855
+ instance_variable_set("@#{reflection.name}", association)
745
856
  end
746
857
  end
747
858
 
748
- def collection_accessor_methods(association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
749
- define_method(association_name) do |*params|
859
+ def collection_reader_method(reflection, association_proxy_class)
860
+ define_method(reflection.name) do |*params|
750
861
  force_reload = params.first unless params.empty?
751
- association = instance_variable_get("@#{association_name}")
862
+ association = instance_variable_get("@#{reflection.name}")
863
+
752
864
  unless association.respond_to?(:loaded?)
753
- association = association_proxy_class.new(self,
754
- association_name, association_class_name,
755
- association_class_primary_key_name, options)
756
- instance_variable_set("@#{association_name}", association)
865
+ association = association_proxy_class.new(self, reflection)
866
+ instance_variable_set("@#{reflection.name}", association)
757
867
  end
868
+
758
869
  association.reload if force_reload
870
+
759
871
  association
760
872
  end
873
+ end
761
874
 
762
- define_method("#{association_name}=") do |new_value|
763
- association = instance_variable_get("@#{association_name}")
875
+ def collection_accessor_methods(reflection, association_proxy_class)
876
+ collection_reader_method(reflection, association_proxy_class)
877
+
878
+ define_method("#{reflection.name}=") do |new_value|
879
+ association = instance_variable_get("@#{reflection.name}")
764
880
  unless association.respond_to?(:loaded?)
765
- association = association_proxy_class.new(self,
766
- association_name, association_class_name,
767
- association_class_primary_key_name, options)
768
- instance_variable_set("@#{association_name}", association)
881
+ association = association_proxy_class.new(self, reflection)
882
+ instance_variable_set("@#{reflection.name}", association)
769
883
  end
770
884
  association.replace(new_value)
771
885
  association
772
886
  end
773
887
 
774
- define_method("#{Inflector.singularize(association_name)}_ids=") do |new_value|
775
- send("#{association_name}=", association_class_name.constantize.find(new_value))
888
+ define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
889
+ send("#{reflection.name}=", reflection.class_name.constantize.find(new_value))
776
890
  end
777
891
  end
778
892
 
@@ -783,7 +897,6 @@ module ActiveRecord
783
897
  def add_multiple_associated_save_callbacks(association_name)
784
898
  method_name = "validate_associated_records_for_#{association_name}".to_sym
785
899
  define_method(method_name) do
786
- @new_record_before_save = new_record?
787
900
  association = instance_variable_get("@#{association_name}")
788
901
  if association.respond_to?(:loaded?)
789
902
  if new_record?
@@ -797,9 +910,11 @@ module ActiveRecord
797
910
  end
798
911
 
799
912
  validate method_name
913
+ before_save("@new_record_before_save = new_record?; true")
800
914
 
801
915
  after_callback = <<-end_eval
802
916
  association = instance_variable_get("@#{association_name}")
917
+
803
918
  if association.respond_to?(:loaded?)
804
919
  if @new_record_before_save
805
920
  records_to_save = association
@@ -809,27 +924,22 @@ module ActiveRecord
809
924
  records_to_save.each { |record| association.send(:insert_record, record) }
810
925
  association.send(:construct_sql) # reconstruct the SQL queries now that we know the owner's id
811
926
  end
812
-
813
- @new_record_before_save = false
814
- true
815
927
  end_eval
816
-
928
+
817
929
  # Doesn't use after_save as that would save associations added in after_create/after_update twice
818
930
  after_create(after_callback)
819
931
  after_update(after_callback)
820
932
  end
821
933
 
822
- def association_constructor_method(constructor, association_name, association_class_name, association_class_primary_key_name, options, association_proxy_class)
823
- define_method("#{constructor}_#{association_name}") do |*params|
934
+ def association_constructor_method(constructor, reflection, association_proxy_class)
935
+ define_method("#{constructor}_#{reflection.name}") do |*params|
824
936
  attributees = params.first unless params.empty?
825
937
  replace_existing = params[1].nil? ? true : params[1]
826
- association = instance_variable_get("@#{association_name}")
938
+ association = instance_variable_get("@#{reflection.name}")
827
939
 
828
940
  if association.nil?
829
- association = association_proxy_class.new(self,
830
- association_name, association_class_name,
831
- association_class_primary_key_name, options)
832
- instance_variable_set("@#{association_name}", association)
941
+ association = association_proxy_class.new(self, reflection)
942
+ instance_variable_set("@#{reflection.name}", association)
833
943
  end
834
944
 
835
945
  if association_proxy_class == HasOneAssociation
@@ -839,63 +949,137 @@ module ActiveRecord
839
949
  end
840
950
  end
841
951
  end
952
+
953
+ def count_with_associations(options = {})
954
+ join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
955
+ return count_by_sql(construct_counter_sql_with_included_associations(options, join_dependency))
956
+ end
842
957
 
843
958
  def find_with_associations(options = {})
844
- reflections = reflect_on_included_associations(options[:include])
845
-
846
- guard_against_missing_reflections(reflections, options)
847
-
848
- schema_abbreviations = generate_schema_abbreviations(reflections)
849
- primary_key_table = generate_primary_key_table(reflections, schema_abbreviations)
959
+ join_dependency = JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
960
+ rows = select_all_rows(options, join_dependency)
961
+ return join_dependency.instantiate(rows)
962
+ end
850
963
 
851
- rows = select_all_rows(options, schema_abbreviations, reflections)
852
- records, records_in_order = { }, []
853
- primary_key = primary_key_table[table_name]
854
-
855
- for row in rows
856
- id = row[primary_key]
857
- records_in_order << (records[id] = instantiate(extract_record(schema_abbreviations, table_name, row))) unless records[id]
858
- record = records[id]
964
+ def configure_dependency_for_has_many(reflection)
965
+ if reflection.options[:dependent] && reflection.options[:exclusively_dependent]
966
+ raise ArgumentError, ':dependent and :exclusively_dependent are mutually exclusive options. You may specify one or the other.'
967
+ end
859
968
 
860
- reflections.each do |reflection|
861
- case reflection.macro
862
- when :has_many, :has_and_belongs_to_many
863
- collection = record.send(reflection.name)
864
- collection.loaded
969
+ if reflection.options[:exclusively_dependent]
970
+ reflection.options[:dependent] = :delete_all
971
+ #warn "The :exclusively_dependent option is deprecated. Please use :dependent => :delete_all instead.")
972
+ end
865
973
 
866
- next unless row[primary_key_table[reflection.table_name]]
974
+ # See HasManyAssociation#delete_records. Dependent associations
975
+ # delete children, otherwise foreign key is set to NULL.
867
976
 
868
- association = reflection.klass.send(:instantiate, extract_record(schema_abbreviations, reflection.table_name, row))
869
- collection.target.push(association) unless collection.target.include?(association)
870
- when :has_one, :belongs_to
871
- next unless row[primary_key_table[reflection.table_name]]
977
+ # Add polymorphic type if the :as option is present
978
+ dependent_conditions = %(#{reflection.primary_key_name} = \#{record.quoted_id})
979
+ if reflection.options[:as]
980
+ dependent_conditions += " AND #{reflection.options[:as]}_type = '#{base_class.name}'"
981
+ end
872
982
 
873
- record.send(
874
- "set_#{reflection.name}_target",
875
- reflection.klass.send(:instantiate, extract_record(schema_abbreviations, reflection.table_name, row))
876
- )
877
- end
878
- end
983
+ case reflection.options[:dependent]
984
+ when :destroy, true
985
+ module_eval "before_destroy '#{reflection.name}.each { |o| o.destroy }'"
986
+ when :delete_all
987
+ module_eval "before_destroy { |record| #{reflection.class_name}.delete_all(%(#{dependent_conditions})) }"
988
+ when :nullify
989
+ module_eval "before_destroy { |record| #{reflection.class_name}.update_all(%(#{reflection.primary_key_name} = NULL), %(#{dependent_conditions})) }"
990
+ when nil, false
991
+ # pass
992
+ else
993
+ raise ArgumentError, 'The :dependent option expects either :destroy, :delete_all, or :nullify'
994
+ end
995
+ end
996
+
997
+ def configure_dependency_for_has_one(reflection)
998
+ case reflection.options[:dependent]
999
+ when :destroy, true
1000
+ module_eval "before_destroy '#{reflection.name}.destroy unless #{reflection.name}.nil?'"
1001
+ when :nullify
1002
+ module_eval "before_destroy '#{reflection.name}.update_attribute(\"#{reflection.primary_key_name}\", nil)'"
1003
+ when nil, false
1004
+ # pass
1005
+ else
1006
+ raise ArgumentError, "The :dependent option expects either :destroy or :nullify."
879
1007
  end
880
-
881
- return records_in_order
1008
+ end
1009
+
1010
+
1011
+ def add_deprecated_api_for_has_many(association_name)
1012
+ deprecated_collection_count_method(association_name)
1013
+ deprecated_add_association_relation(association_name)
1014
+ deprecated_remove_association_relation(association_name)
1015
+ deprecated_has_collection_method(association_name)
1016
+ deprecated_find_in_collection_method(association_name)
1017
+ deprecated_find_all_in_collection_method(association_name)
1018
+ deprecated_collection_create_method(association_name)
1019
+ deprecated_collection_build_method(association_name)
882
1020
  end
883
1021
 
1022
+ def create_has_many_reflection(association_id, options, &extension)
1023
+ options.assert_valid_keys(
1024
+ :class_name, :table_name, :foreign_key,
1025
+ :exclusively_dependent, :dependent,
1026
+ :select, :conditions, :include, :order, :group, :limit, :offset,
1027
+ :as, :through, :source,
1028
+ :finder_sql, :counter_sql,
1029
+ :before_add, :after_add, :before_remove, :after_remove,
1030
+ :extend
1031
+ )
884
1032
 
885
- def reflect_on_included_associations(associations)
886
- [ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
1033
+ options[:extend] = create_extension_module(association_id, extension) if block_given?
1034
+
1035
+ create_reflection(:has_many, association_id, options, self)
887
1036
  end
888
1037
 
889
- def guard_against_missing_reflections(reflections, options)
890
- reflections.each do |r|
891
- raise(
892
- ConfigurationError,
893
- "Association was not found; perhaps you misspelled it? " +
894
- "You specified :include => :#{[options[:include]].flatten.join(', :')}"
895
- ) if r.nil?
1038
+ def create_has_one_reflection(association_id, options)
1039
+ options.assert_valid_keys(
1040
+ :class_name, :foreign_key, :remote, :conditions, :order, :include, :dependent, :counter_cache, :extend, :as
1041
+ )
1042
+
1043
+ create_reflection(:has_one, association_id, options, self)
1044
+ end
1045
+
1046
+ def create_belongs_to_reflection(association_id, options)
1047
+ options.assert_valid_keys(
1048
+ :class_name, :foreign_key, :foreign_type, :remote, :conditions, :order, :include, :dependent,
1049
+ :counter_cache, :extend, :polymorphic
1050
+ )
1051
+
1052
+ reflection = create_reflection(:belongs_to, association_id, options, self)
1053
+
1054
+ if options[:polymorphic]
1055
+ reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
896
1056
  end
1057
+
1058
+ reflection
897
1059
  end
898
1060
 
1061
+ def create_has_and_belongs_to_many_reflection(association_id, options, &extension)
1062
+ options.assert_valid_keys(
1063
+ :class_name, :table_name, :join_table, :foreign_key, :association_foreign_key,
1064
+ :select, :conditions, :include, :order, :group, :limit, :offset,
1065
+ :finder_sql, :delete_sql, :insert_sql, :uniq,
1066
+ :before_add, :after_add, :before_remove, :after_remove,
1067
+ :extend
1068
+ )
1069
+
1070
+ options[:extend] = create_extension_module(association_id, extension) if block_given?
1071
+
1072
+ reflection = create_reflection(:has_and_belongs_to_many, association_id, options, self)
1073
+
1074
+ reflection.options[:join_table] ||= join_table_name(undecorated_table_name(self.to_s), undecorated_table_name(reflection.class_name))
1075
+
1076
+ reflection
1077
+ end
1078
+
1079
+ def reflect_on_included_associations(associations)
1080
+ [ associations ].flatten.collect { |association| reflect_on_association(association.to_s.intern) }
1081
+ end
1082
+
899
1083
  def guard_against_unlimitable_reflections(reflections, options)
900
1084
  if (options[:offset] || options[:limit]) && !using_limitable_reflections?(reflections)
901
1085
  raise(
@@ -905,127 +1089,109 @@ module ActiveRecord
905
1089
  end
906
1090
  end
907
1091
 
908
- def generate_schema_abbreviations(reflections)
909
- schema = [ [ table_name, column_names ] ]
910
- schema += reflections.collect { |r| [ r.table_name, r.klass.column_names ] }
911
-
912
- schema_abbreviations = {}
913
- schema.each_with_index do |table_and_columns, i|
914
- table, columns = table_and_columns
915
- columns.each_with_index { |column, j| schema_abbreviations["t#{i}_r#{j}"] = [ table, column ] }
1092
+ def select_all_rows(options, join_dependency)
1093
+ connection.select_all(
1094
+ construct_finder_sql_with_included_associations(options, join_dependency),
1095
+ "#{name} Load Including Associations"
1096
+ )
1097
+ end
1098
+
1099
+ def construct_counter_sql_with_included_associations(options, join_dependency)
1100
+ scope = scope(:find)
1101
+ sql = "SELECT COUNT(DISTINCT #{table_name}.#{primary_key})"
1102
+
1103
+ # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
1104
+ if !Base.connection.supports_count_distinct?
1105
+ sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{table_name}.#{primary_key}"
916
1106
  end
917
1107
 
918
- return schema_abbreviations
919
- end
1108
+ sql << " FROM #{table_name} "
1109
+ sql << join_dependency.join_associations.collect{|join| join.association_join }.join
1110
+
1111
+ add_joins!(sql, options, scope)
1112
+ add_conditions!(sql, options[:conditions], scope)
1113
+ add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
920
1114
 
921
- def generate_primary_key_table(reflections, schema_abbreviations)
922
- primary_key_lookup_table = {}
923
- primary_key_lookup_table[table_name] =
924
- schema_abbreviations.find { |cn, tc| tc == [ table_name, primary_key ] }.first
1115
+ add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
925
1116
 
926
- reflections.collect do |reflection|
927
- primary_key_lookup_table[reflection.klass.table_name] = schema_abbreviations.find { |cn, tc|
928
- tc == [ reflection.klass.table_name, reflection.klass.primary_key ]
929
- }.first
1117
+ if !Base.connection.supports_count_distinct?
1118
+ sql << ")"
930
1119
  end
931
-
932
- return primary_key_lookup_table
933
- end
934
-
935
1120
 
936
- def select_all_rows(options, schema_abbreviations, reflections)
937
- connection.select_all(
938
- construct_finder_sql_with_included_associations(options, schema_abbreviations, reflections),
939
- "#{name} Load Including Associations"
940
- )
1121
+ return sanitize_sql(sql)
941
1122
  end
942
1123
 
943
- def construct_finder_sql_with_included_associations(options, schema_abbreviations, reflections)
944
- sql = "SELECT #{column_aliases(schema_abbreviations)} FROM #{table_name} "
945
- sql << reflections.collect { |reflection| association_join(reflection) }.to_s
946
- sql << "#{options[:joins]} " if options[:joins]
947
-
948
- add_conditions!(sql, options[:conditions])
949
- add_sti_conditions!(sql, reflections)
950
- add_limited_ids_condition!(sql, options) if !using_limitable_reflections?(reflections) && options[:limit]
1124
+ def construct_finder_sql_with_included_associations(options, join_dependency)
1125
+ scope = scope(:find)
1126
+ sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || table_name} "
1127
+ sql << join_dependency.join_associations.collect{|join| join.association_join }.join
1128
+
1129
+ add_joins!(sql, options, scope)
1130
+ add_conditions!(sql, options[:conditions], scope)
1131
+ add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
951
1132
 
952
1133
  sql << "ORDER BY #{options[:order]} " if options[:order]
953
-
954
- add_limit!(sql, options) if using_limitable_reflections?(reflections)
955
-
1134
+
1135
+ add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
1136
+
956
1137
  return sanitize_sql(sql)
957
1138
  end
958
-
959
- def add_limited_ids_condition!(sql, options)
960
- unless (id_list = select_limited_ids_list(options)).empty?
1139
+
1140
+ def add_limited_ids_condition!(sql, options, join_dependency)
1141
+ unless (id_list = select_limited_ids_list(options, join_dependency)).empty?
961
1142
  sql << "#{condition_word(sql)} #{table_name}.#{primary_key} IN (#{id_list}) "
962
1143
  end
963
1144
  end
964
-
965
- def select_limited_ids_list(options)
1145
+
1146
+ def select_limited_ids_list(options, join_dependency)
966
1147
  connection.select_values(
967
- construct_finder_sql_for_association_limiting(options),
1148
+ construct_finder_sql_for_association_limiting(options, join_dependency),
968
1149
  "#{name} Load IDs For Limited Eager Loading"
969
1150
  ).collect { |id| connection.quote(id) }.join(", ")
970
1151
  end
971
-
972
- def construct_finder_sql_for_association_limiting(options)
973
- raise(ArgumentError, "Limited eager loads and conditions on the eager tables is incompatible") if include_eager_conditions?(options)
1152
+
1153
+ def construct_finder_sql_for_association_limiting(options, join_dependency)
1154
+ scope = scope(:find)
1155
+ #sql = "SELECT DISTINCT #{table_name}.#{primary_key} FROM #{table_name} "
1156
+ sql = "SELECT "
1157
+ sql << "DISTINCT #{table_name}." if include_eager_conditions?(options) || include_eager_order?(options)
1158
+ sql << "#{primary_key} FROM #{table_name} "
1159
+
1160
+ if include_eager_conditions?(options) || include_eager_order?(options)
1161
+ sql << join_dependency.join_associations.collect{|join| join.association_join }.join
1162
+ add_joins!(sql, options, scope)
1163
+ end
974
1164
 
975
- sql = "SELECT #{primary_key} FROM #{table_name} "
976
- add_conditions!(sql, options[:conditions])
1165
+ add_conditions!(sql, options[:conditions], scope)
977
1166
  sql << "ORDER BY #{options[:order]} " if options[:order]
978
- add_limit!(sql, options)
1167
+ add_limit!(sql, options, scope)
979
1168
  return sanitize_sql(sql)
980
1169
  end
981
1170
 
982
1171
  def include_eager_conditions?(options)
983
- conditions = options[:conditions]
1172
+ conditions = scope(:find, :conditions) || options[:conditions]
984
1173
  return false unless conditions
985
1174
  conditions = conditions.first if conditions.is_a?(Array)
986
1175
  conditions.scan(/(\w+)\.\w+/).flatten.any? do |condition_table_name|
987
1176
  condition_table_name != table_name
988
1177
  end
989
1178
  end
990
-
991
- def using_limitable_reflections?(reflections)
992
- reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
993
- end
994
-
995
- def add_sti_conditions!(sql, reflections)
996
- sti_conditions = reflections.collect do |reflection|
997
- reflection.klass.send(:type_condition) unless reflection.klass.descends_from_active_record?
998
- end.compact
999
-
1000
- unless sti_conditions.empty?
1001
- sql << condition_word(sql) + sti_conditions.join(" AND ")
1179
+
1180
+ def include_eager_order?(options)
1181
+ order = options[:order]
1182
+ return false unless order
1183
+ order.scan(/(\w+)\.\w+/).flatten.any? do |order_table_name|
1184
+ order_table_name != table_name
1002
1185
  end
1003
1186
  end
1004
1187
 
1005
- def column_aliases(schema_abbreviations)
1006
- schema_abbreviations.collect { |cn, tc| "#{tc[0]}.#{connection.quote_column_name tc[1]} AS #{cn}" }.join(", ")
1188
+ def using_limitable_reflections?(reflections)
1189
+ reflections.reject { |r| [ :belongs_to, :has_one ].include?(r.macro) }.length.zero?
1007
1190
  end
1008
1191
 
1009
- def association_join(reflection)
1010
- case reflection.macro
1011
- when :has_and_belongs_to_many
1012
- " LEFT OUTER JOIN #{reflection.options[:join_table]} ON " +
1013
- "#{reflection.options[:join_table]}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " +
1014
- "#{table_name}.#{primary_key} " +
1015
- " LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
1016
- "#{reflection.options[:join_table]}.#{reflection.options[:association_foreign_key] || reflection.klass.table_name.classify.foreign_key} = " +
1017
- "#{reflection.klass.table_name}.#{reflection.klass.primary_key} "
1018
- when :has_many, :has_one
1019
- " LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
1020
- "#{reflection.klass.table_name}.#{reflection.options[:foreign_key] || table_name.classify.foreign_key} = " +
1021
- "#{table_name}.#{primary_key} "
1022
- when :belongs_to
1023
- " LEFT OUTER JOIN #{reflection.klass.table_name} ON " +
1024
- "#{reflection.klass.table_name}.#{reflection.klass.primary_key} = " +
1025
- "#{table_name}.#{reflection.options[:foreign_key] || reflection.klass.table_name.classify.foreign_key} "
1026
- else
1027
- ""
1028
- end
1192
+ def column_aliases(join_dependency)
1193
+ join_dependency.joins.collect{|join| join.column_names_with_alias.collect{|column_name, aliased_name|
1194
+ "#{join.aliased_table_name}.#{connection.quote_column_name column_name} AS #{aliased_name}"}}.flatten.join(", ")
1029
1195
  end
1030
1196
 
1031
1197
  def add_association_callbacks(association_name, options)
@@ -1040,15 +1206,6 @@ module ActiveRecord
1040
1206
  end
1041
1207
  end
1042
1208
 
1043
- def extract_record(schema_abbreviations, table_name, row)
1044
- record = {}
1045
- row.each do |column, value|
1046
- prefix, column_name = schema_abbreviations[column]
1047
- record[column_name] = value if prefix == table_name
1048
- end
1049
- return record
1050
- end
1051
-
1052
1209
  def condition_word(sql)
1053
1210
  sql =~ /where/i ? " AND " : "WHERE "
1054
1211
  end
@@ -1062,6 +1219,295 @@ module ActiveRecord
1062
1219
 
1063
1220
  extension_module_name.constantize
1064
1221
  end
1222
+
1223
+ class JoinDependency
1224
+ attr_reader :joins, :reflections, :table_aliases
1225
+
1226
+ def initialize(base, associations, joins)
1227
+ @joins = [JoinBase.new(base, joins)]
1228
+ @associations = associations
1229
+ @reflections = []
1230
+ @base_records_hash = {}
1231
+ @base_records_in_order = []
1232
+ @table_aliases = Hash.new { |aliases, table| aliases[table] = 0 }
1233
+ @table_aliases[base.table_name] = 1
1234
+ build(associations)
1235
+ end
1236
+
1237
+ def join_associations
1238
+ @joins[1..-1].to_a
1239
+ end
1240
+
1241
+ def join_base
1242
+ @joins[0]
1243
+ end
1244
+
1245
+ def instantiate(rows)
1246
+ rows.each_with_index do |row, i|
1247
+ primary_id = join_base.record_id(row)
1248
+ unless @base_records_hash[primary_id]
1249
+ @base_records_in_order << (@base_records_hash[primary_id] = join_base.instantiate(row))
1250
+ end
1251
+ construct(@base_records_hash[primary_id], @associations, join_associations.dup, row)
1252
+ end
1253
+ return @base_records_in_order
1254
+ end
1255
+
1256
+ def aliased_table_names_for(table_name)
1257
+ joins.select{|join| join.table_name == table_name }.collect{|join| join.aliased_table_name}
1258
+ end
1259
+
1260
+ protected
1261
+ def build(associations, parent = nil)
1262
+ parent ||= @joins.last
1263
+ case associations
1264
+ when Symbol, String
1265
+ reflection = parent.reflections[associations.to_s.intern] or
1266
+ raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
1267
+ @reflections << reflection
1268
+ @joins << JoinAssociation.new(reflection, self, parent)
1269
+ when Array
1270
+ associations.each do |association|
1271
+ build(association, parent)
1272
+ end
1273
+ when Hash
1274
+ associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
1275
+ build(name, parent)
1276
+ build(associations[name])
1277
+ end
1278
+ else
1279
+ raise ConfigurationError, associations.inspect
1280
+ end
1281
+ end
1282
+
1283
+ def construct(parent, associations, joins, row)
1284
+ case associations
1285
+ when Symbol, String
1286
+ while (join = joins.shift).reflection.name.to_s != associations.to_s
1287
+ raise ConfigurationError, "Not Enough Associations" if joins.empty?
1288
+ end
1289
+ construct_association(parent, join, row)
1290
+ when Array
1291
+ associations.each do |association|
1292
+ construct(parent, association, joins, row)
1293
+ end
1294
+ when Hash
1295
+ associations.keys.sort{|a,b|a.to_s<=>b.to_s}.each do |name|
1296
+ association = construct_association(parent, joins.shift, row)
1297
+ construct(association, associations[name], joins, row) if association
1298
+ end
1299
+ else
1300
+ raise ConfigurationError, associations.inspect
1301
+ end
1302
+ end
1303
+
1304
+ def construct_association(record, join, row)
1305
+ case join.reflection.macro
1306
+ when :has_many, :has_and_belongs_to_many
1307
+ collection = record.send(join.reflection.name)
1308
+ collection.loaded
1309
+
1310
+ return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
1311
+ association = join.instantiate(row)
1312
+ collection.target.push(association) unless collection.target.include?(association)
1313
+ when :has_one, :belongs_to
1314
+ return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
1315
+ association = join.instantiate(row)
1316
+ record.send("set_#{join.reflection.name}_target", association)
1317
+ else
1318
+ raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
1319
+ end
1320
+ return association
1321
+ end
1322
+
1323
+ class JoinBase
1324
+ attr_reader :active_record, :table_joins
1325
+ delegate :table_name, :column_names, :primary_key, :reflections, :sanitize_sql, :to => :active_record
1326
+
1327
+ def initialize(active_record, joins = nil)
1328
+ @active_record = active_record
1329
+ @cached_record = {}
1330
+ @table_joins = joins
1331
+ end
1332
+
1333
+ def aliased_prefix
1334
+ "t0"
1335
+ end
1336
+
1337
+ def aliased_primary_key
1338
+ "#{ aliased_prefix }_r0"
1339
+ end
1340
+
1341
+ def aliased_table_name
1342
+ active_record.table_name
1343
+ end
1344
+
1345
+ def column_names_with_alias
1346
+ unless @column_names_with_alias
1347
+ @column_names_with_alias = []
1348
+ ([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
1349
+ @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
1350
+ end
1351
+ end
1352
+ return @column_names_with_alias
1353
+ end
1354
+
1355
+ def extract_record(row)
1356
+ column_names_with_alias.inject({}){|record, (cn, an)| record[cn] = row[an]; record}
1357
+ end
1358
+
1359
+ def record_id(row)
1360
+ row[aliased_primary_key]
1361
+ end
1362
+
1363
+ def instantiate(row)
1364
+ @cached_record[record_id(row)] ||= active_record.instantiate(extract_record(row))
1365
+ end
1366
+ end
1367
+
1368
+ class JoinAssociation < JoinBase
1369
+ attr_reader :reflection, :parent, :aliased_table_name, :aliased_prefix, :aliased_join_table_name, :parent_table_name
1370
+ delegate :options, :klass, :through_reflection, :source_reflection, :to => :reflection
1371
+
1372
+ def initialize(reflection, join_dependency, parent = nil)
1373
+ reflection.check_validity!
1374
+ if reflection.options[:polymorphic]
1375
+ raise EagerLoadPolymorphicError.new(reflection)
1376
+ end
1377
+
1378
+ super(reflection.klass)
1379
+ @parent = parent
1380
+ @reflection = reflection
1381
+ @aliased_prefix = "t#{ join_dependency.joins.size }"
1382
+ @aliased_table_name = table_name # start with the table name
1383
+ @parent_table_name = parent.active_record.table_name
1384
+
1385
+ if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{aliased_table_name.downcase}\son}
1386
+ join_dependency.table_aliases[aliased_table_name] += 1
1387
+ end
1388
+
1389
+ unless join_dependency.table_aliases[aliased_table_name].zero?
1390
+ # if the table name has been used, then use an alias
1391
+ @aliased_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}"
1392
+ table_index = join_dependency.table_aliases[aliased_table_name]
1393
+ @aliased_table_name = @aliased_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
1394
+ end
1395
+ join_dependency.table_aliases[aliased_table_name] += 1
1396
+
1397
+ if reflection.macro == :has_and_belongs_to_many || (reflection.macro == :has_many && reflection.options[:through])
1398
+ @aliased_join_table_name = reflection.macro == :has_and_belongs_to_many ? reflection.options[:join_table] : reflection.through_reflection.klass.table_name
1399
+ unless join_dependency.table_aliases[aliased_join_table_name].zero?
1400
+ @aliased_join_table_name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}_join"
1401
+ table_index = join_dependency.table_aliases[aliased_join_table_name]
1402
+ @aliased_join_table_name = @aliased_join_table_name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
1403
+ end
1404
+ join_dependency.table_aliases[aliased_join_table_name] += 1
1405
+ end
1406
+ end
1407
+
1408
+ def association_join
1409
+ join = case reflection.macro
1410
+ when :has_and_belongs_to_many
1411
+ " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
1412
+ table_alias_for(options[:join_table], aliased_join_table_name),
1413
+ aliased_join_table_name,
1414
+ options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key,
1415
+ reflection.active_record.table_name, reflection.active_record.primary_key] +
1416
+ " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
1417
+ table_name_and_alias, aliased_table_name, klass.primary_key,
1418
+ aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key
1419
+ ]
1420
+ when :has_many, :has_one
1421
+ case
1422
+ when reflection.macro == :has_many && reflection.options[:through]
1423
+ through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
1424
+ if through_reflection.options[:as] # has_many :through against a polymorphic join
1425
+ polymorphic_foreign_key = through_reflection.options[:as].to_s + '_id'
1426
+ polymorphic_foreign_type = through_reflection.options[:as].to_s + '_type'
1427
+
1428
+ " LEFT OUTER JOIN %s ON (%s.%s = %s.%s AND %s.%s = %s) " % [
1429
+ table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
1430
+ aliased_join_table_name, polymorphic_foreign_key,
1431
+ parent.aliased_table_name, parent.primary_key,
1432
+ aliased_join_table_name, polymorphic_foreign_type, klass.quote(parent.active_record.base_class.name)] +
1433
+ " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [table_name_and_alias,
1434
+ aliased_table_name, primary_key, aliased_join_table_name, options[:foreign_key] || reflection.klass.to_s.classify.foreign_key
1435
+ ]
1436
+ else
1437
+ case source_reflection.macro
1438
+ when :belongs_to
1439
+ first_key = primary_key
1440
+ second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
1441
+ when :has_many
1442
+ first_key = through_reflection.klass.to_s.classify.foreign_key
1443
+ second_key = options[:foreign_key] || primary_key
1444
+ end
1445
+
1446
+ " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
1447
+ table_alias_for(through_reflection.klass.table_name, aliased_join_table_name), aliased_join_table_name,
1448
+ through_reflection.primary_key_name,
1449
+ parent.aliased_table_name, parent.primary_key] +
1450
+ " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
1451
+ table_name_and_alias,
1452
+ aliased_table_name, first_key,
1453
+ aliased_join_table_name, second_key
1454
+ ]
1455
+ end
1456
+
1457
+ when reflection.macro == :has_many && reflection.options[:as]
1458
+ " LEFT OUTER JOIN %s ON %s.%s = %s.%s AND %s.%s = %s" % [
1459
+ table_name_and_alias,
1460
+ aliased_table_name, "#{reflection.options[:as]}_id",
1461
+ parent.aliased_table_name, parent.primary_key,
1462
+ aliased_table_name, "#{reflection.options[:as]}_type",
1463
+ klass.quote(parent.active_record.base_class.name)
1464
+ ]
1465
+
1466
+ else
1467
+ foreign_key = options[:foreign_key] || case reflection.macro
1468
+ when :has_many then reflection.active_record.to_s.classify
1469
+ when :has_one then reflection.active_record.to_s
1470
+ end.foreign_key
1471
+ " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
1472
+ table_name_and_alias,
1473
+ aliased_table_name, foreign_key,
1474
+ parent.aliased_table_name, parent.primary_key
1475
+ ]
1476
+ end
1477
+ when :belongs_to
1478
+ " LEFT OUTER JOIN %s ON %s.%s = %s.%s " % [
1479
+ table_name_and_alias, aliased_table_name, reflection.klass.primary_key,
1480
+ parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key
1481
+ ]
1482
+ else
1483
+ ""
1484
+ end || ''
1485
+ join << %(AND %s.%s = %s ) % [
1486
+ aliased_table_name,
1487
+ reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
1488
+ klass.quote(klass.name)] unless klass.descends_from_active_record?
1489
+ join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
1490
+ join
1491
+ end
1492
+
1493
+ protected
1494
+ def pluralize(table_name)
1495
+ ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
1496
+ end
1497
+
1498
+ def table_alias_for(table_name, table_alias)
1499
+ "#{table_name} #{table_alias if table_name != table_alias}".strip
1500
+ end
1501
+
1502
+ def table_name_and_alias
1503
+ table_alias_for table_name, @aliased_table_name
1504
+ end
1505
+
1506
+ def interpolate_sql(sql)
1507
+ instance_eval("%@#{sql.gsub('@', '\@')}@")
1508
+ end
1509
+ end
1510
+ end
1065
1511
  end
1066
1512
  end
1067
1513
  end