activerecord 4.0.13 → 4.1.0.beta1

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +745 -2700
  3. data/README.rdoc +2 -2
  4. data/examples/performance.rb +30 -18
  5. data/examples/simple.rb +4 -4
  6. data/lib/active_record.rb +2 -6
  7. data/lib/active_record/aggregations.rb +2 -1
  8. data/lib/active_record/association_relation.rb +0 -4
  9. data/lib/active_record/associations.rb +87 -43
  10. data/lib/active_record/associations/alias_tracker.rb +1 -3
  11. data/lib/active_record/associations/association.rb +8 -16
  12. data/lib/active_record/associations/association_scope.rb +5 -16
  13. data/lib/active_record/associations/belongs_to_association.rb +34 -25
  14. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
  15. data/lib/active_record/associations/builder/association.rb +78 -54
  16. data/lib/active_record/associations/builder/belongs_to.rb +91 -58
  17. data/lib/active_record/associations/builder/collection_association.rb +47 -45
  18. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +107 -25
  19. data/lib/active_record/associations/builder/has_many.rb +2 -2
  20. data/lib/active_record/associations/builder/has_one.rb +5 -7
  21. data/lib/active_record/associations/builder/singular_association.rb +6 -7
  22. data/lib/active_record/associations/collection_association.rb +68 -105
  23. data/lib/active_record/associations/collection_proxy.rb +12 -15
  24. data/lib/active_record/associations/has_many_association.rb +11 -9
  25. data/lib/active_record/associations/has_many_through_association.rb +16 -12
  26. data/lib/active_record/associations/has_one_association.rb +1 -1
  27. data/lib/active_record/associations/join_dependency.rb +204 -165
  28. data/lib/active_record/associations/join_dependency/join_association.rb +43 -101
  29. data/lib/active_record/associations/join_dependency/join_base.rb +6 -8
  30. data/lib/active_record/associations/join_dependency/join_part.rb +18 -37
  31. data/lib/active_record/associations/join_helper.rb +2 -11
  32. data/lib/active_record/associations/preloader.rb +89 -34
  33. data/lib/active_record/associations/preloader/association.rb +43 -25
  34. data/lib/active_record/associations/preloader/collection_association.rb +2 -2
  35. data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
  36. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  37. data/lib/active_record/associations/preloader/through_association.rb +58 -26
  38. data/lib/active_record/associations/singular_association.rb +6 -5
  39. data/lib/active_record/associations/through_association.rb +2 -2
  40. data/lib/active_record/attribute_assignment.rb +5 -2
  41. data/lib/active_record/attribute_methods.rb +45 -40
  42. data/lib/active_record/attribute_methods/before_type_cast.rb +2 -1
  43. data/lib/active_record/attribute_methods/dirty.rb +8 -22
  44. data/lib/active_record/attribute_methods/primary_key.rb +1 -7
  45. data/lib/active_record/attribute_methods/read.rb +55 -28
  46. data/lib/active_record/attribute_methods/serialization.rb +12 -33
  47. data/lib/active_record/attribute_methods/time_zone_conversion.rb +1 -13
  48. data/lib/active_record/attribute_methods/write.rb +37 -12
  49. data/lib/active_record/autosave_association.rb +207 -207
  50. data/lib/active_record/base.rb +5 -1
  51. data/lib/active_record/callbacks.rb +2 -2
  52. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +2 -7
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +11 -22
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +12 -14
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -5
  56. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  57. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +84 -0
  58. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +9 -8
  59. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +52 -83
  60. data/lib/active_record/connection_adapters/abstract/transaction.rb +0 -5
  61. data/lib/active_record/connection_adapters/abstract_adapter.rb +14 -97
  62. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +58 -60
  63. data/lib/active_record/connection_adapters/column.rb +1 -35
  64. data/lib/active_record/connection_adapters/connection_specification.rb +2 -2
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +3 -4
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +16 -15
  67. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +24 -18
  68. data/lib/active_record/connection_adapters/postgresql/cast.rb +20 -16
  69. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +23 -43
  70. data/lib/active_record/connection_adapters/postgresql/oid.rb +19 -12
  71. data/lib/active_record/connection_adapters/postgresql/quoting.rb +28 -23
  72. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +8 -30
  73. data/lib/active_record/connection_adapters/postgresql_adapter.rb +92 -75
  74. data/lib/active_record/connection_adapters/schema_cache.rb +8 -29
  75. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +31 -64
  76. data/lib/active_record/connection_handling.rb +2 -2
  77. data/lib/active_record/core.rb +22 -43
  78. data/lib/active_record/counter_cache.rb +7 -7
  79. data/lib/active_record/enum.rb +100 -0
  80. data/lib/active_record/errors.rb +10 -5
  81. data/lib/active_record/fixture_set/file.rb +2 -1
  82. data/lib/active_record/fixtures.rb +171 -74
  83. data/lib/active_record/inheritance.rb +16 -22
  84. data/lib/active_record/integration.rb +52 -1
  85. data/lib/active_record/locking/optimistic.rb +7 -2
  86. data/lib/active_record/locking/pessimistic.rb +1 -1
  87. data/lib/active_record/log_subscriber.rb +5 -12
  88. data/lib/active_record/migration.rb +62 -46
  89. data/lib/active_record/migration/command_recorder.rb +7 -13
  90. data/lib/active_record/model_schema.rb +7 -14
  91. data/lib/active_record/nested_attributes.rb +10 -8
  92. data/lib/active_record/no_touching.rb +52 -0
  93. data/lib/active_record/null_relation.rb +3 -3
  94. data/lib/active_record/persistence.rb +16 -34
  95. data/lib/active_record/querying.rb +14 -12
  96. data/lib/active_record/railtie.rb +0 -50
  97. data/lib/active_record/railties/databases.rake +12 -15
  98. data/lib/active_record/readonly_attributes.rb +0 -6
  99. data/lib/active_record/reflection.rb +189 -75
  100. data/lib/active_record/relation.rb +69 -94
  101. data/lib/active_record/relation/batches.rb +57 -23
  102. data/lib/active_record/relation/calculations.rb +36 -43
  103. data/lib/active_record/relation/delegation.rb +54 -39
  104. data/lib/active_record/relation/finder_methods.rb +107 -62
  105. data/lib/active_record/relation/merger.rb +7 -20
  106. data/lib/active_record/relation/predicate_builder.rb +57 -38
  107. data/lib/active_record/relation/predicate_builder/array_handler.rb +29 -0
  108. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  109. data/lib/active_record/relation/query_methods.rb +110 -98
  110. data/lib/active_record/relation/spawn_methods.rb +1 -2
  111. data/lib/active_record/result.rb +45 -6
  112. data/lib/active_record/runtime_registry.rb +5 -0
  113. data/lib/active_record/sanitization.rb +6 -8
  114. data/lib/active_record/schema_dumper.rb +16 -5
  115. data/lib/active_record/schema_migration.rb +24 -25
  116. data/lib/active_record/scoping/default.rb +5 -18
  117. data/lib/active_record/scoping/named.rb +8 -29
  118. data/lib/active_record/store.rb +56 -28
  119. data/lib/active_record/tasks/database_tasks.rb +8 -4
  120. data/lib/active_record/timestamp.rb +4 -4
  121. data/lib/active_record/transactions.rb +8 -10
  122. data/lib/active_record/validations/presence.rb +1 -1
  123. data/lib/active_record/validations/uniqueness.rb +1 -6
  124. data/lib/active_record/version.rb +1 -1
  125. data/lib/rails/generators/active_record.rb +2 -8
  126. data/lib/rails/generators/active_record/migration.rb +18 -0
  127. data/lib/rails/generators/active_record/migration/migration_generator.rb +4 -0
  128. data/lib/rails/generators/active_record/model/model_generator.rb +4 -0
  129. metadata +32 -45
  130. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -65
  131. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  132. data/lib/active_record/tasks/firebird_database_tasks.rb +0 -56
  133. data/lib/active_record/tasks/oracle_database_tasks.rb +0 -45
  134. data/lib/active_record/tasks/sqlserver_database_tasks.rb +0 -48
  135. data/lib/active_record/test_case.rb +0 -102
@@ -1,128 +1,80 @@
1
+ require 'active_record/associations/join_dependency/join_part'
2
+
1
3
  module ActiveRecord
2
4
  module Associations
3
5
  class JoinDependency # :nodoc:
4
6
  class JoinAssociation < JoinPart # :nodoc:
5
- include JoinHelper
6
-
7
7
  # The reflection of the association represented
8
8
  attr_reader :reflection
9
9
 
10
- # The JoinDependency object which this JoinAssociation exists within. This is mainly
11
- # relevant for generating aliases which do not conflict with other joins which are
12
- # part of the query.
13
- attr_reader :join_dependency
14
-
15
- # A JoinBase instance representing the active record we are joining onto.
16
- # (So in Author.has_many :posts, the Author would be that base record.)
17
- attr_reader :parent
18
-
19
- # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
20
- attr_accessor :join_type
21
-
22
- # These implement abstract methods from the superclass
23
- attr_reader :aliased_prefix
24
-
25
- attr_reader :tables
26
-
27
- delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
28
- delegate :table, :table_name, :to => :parent, :prefix => :parent
29
- delegate :alias_tracker, :to => :join_dependency
10
+ attr_accessor :tables
30
11
 
31
- alias :alias_suffix :parent_table_name
32
-
33
- def initialize(reflection, join_dependency, parent = nil)
34
- reflection.check_validity!
35
-
36
- if reflection.options[:polymorphic]
37
- raise EagerLoadPolymorphicError.new(reflection)
38
- end
39
-
40
- super(reflection.klass)
12
+ def initialize(reflection, children)
13
+ super(reflection.klass, children)
41
14
 
42
15
  @reflection = reflection
43
- @join_dependency = join_dependency
44
- @parent = parent
45
- @join_type = Arel::InnerJoin
46
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
47
- @tables = construct_tables.reverse
16
+ @tables = nil
48
17
  end
49
18
 
50
- def ==(other)
51
- other.class == self.class &&
52
- other.reflection == reflection &&
53
- other.parent == parent
19
+ def match?(other)
20
+ return true if self == other
21
+ super && reflection == other.reflection
54
22
  end
55
23
 
56
- def find_parent_in(other_join_dependency)
57
- other_join_dependency.join_parts.detect do |join_part|
58
- case parent
59
- when JoinBase
60
- parent.base_klass == join_part.base_klass
61
- else
62
- parent == join_part
63
- end
64
- end
65
- end
24
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
25
+ joins = []
26
+ tables = tables.reverse
66
27
 
67
- def join_to(manager)
68
- tables = @tables.dup
69
- foreign_table = parent_table
70
- foreign_klass = parent.base_klass
28
+ scope_chain_iter = scope_chain.reverse_each
71
29
 
72
30
  # The chain starts with the target table, but we want to end with it here (makes
73
31
  # more sense in this context), so we reverse
74
- chain.reverse.each_with_index do |reflection, i|
32
+ chain.reverse_each do |reflection|
75
33
  table = tables.shift
34
+ klass = reflection.klass
76
35
 
77
36
  case reflection.source_macro
78
37
  when :belongs_to
79
38
  key = reflection.association_primary_key
80
39
  foreign_key = reflection.foreign_key
81
- when :has_and_belongs_to_many
82
- # Join the join table first...
83
- manager.from(join(
84
- table,
85
- table[reflection.foreign_key].
86
- eq(foreign_table[reflection.active_record_primary_key])
87
- ))
88
-
89
- foreign_table, table = table, tables.shift
90
-
91
- key = reflection.association_primary_key
92
- foreign_key = reflection.association_foreign_key
93
40
  else
94
41
  key = reflection.foreign_key
95
42
  foreign_key = reflection.active_record_primary_key
96
43
  end
97
44
 
98
- constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
45
+ constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
99
46
 
100
- scope_chain_items = scope_chain[i]
47
+ scope_chain_items = scope_chain_iter.next.map do |item|
48
+ if item.is_a?(Relation)
49
+ item
50
+ else
51
+ ActiveRecord::Relation.create(klass, table).instance_exec(node, &item)
52
+ end
53
+ end
101
54
 
102
55
  if reflection.type
103
- scope_chain_items += [
104
- ActiveRecord::Relation.new(reflection.klass, table)
56
+ scope_chain_items <<
57
+ ActiveRecord::Relation.create(klass, table)
105
58
  .where(reflection.type => foreign_klass.base_class.name)
106
- ]
107
59
  end
108
60
 
109
- scope_chain_items += [reflection.klass.send(:build_default_scope, ActiveRecord::Relation.new(reflection.klass, table))].compact
61
+ scope_chain_items.concat [klass.send(:build_default_scope)].compact
110
62
 
111
- scope_chain_items.each do |item|
112
- unless item.is_a?(Relation)
113
- item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
114
- end
63
+ rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
64
+ left.merge right
65
+ end
115
66
 
116
- constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
67
+ if rel && !rel.arel.constraints.empty?
68
+ constraint = constraint.and rel.arel.constraints
117
69
  end
118
70
 
119
- manager.from(join(table, constraint))
71
+ joins << table.create_join(table, table.create_on(constraint), join_type)
120
72
 
121
73
  # The current table in this iteration becomes the foreign table in the next
122
- foreign_table, foreign_klass = table, reflection.klass
74
+ foreign_table, foreign_klass = table, klass
123
75
  end
124
76
 
125
- manager
77
+ joins
126
78
  end
127
79
 
128
80
  # Builds equality condition.
@@ -134,42 +86,32 @@ module ActiveRecord
134
86
  # end
135
87
  #
136
88
  # If I execute `Physician.joins(:appointments).to_a` then
137
- # reflection #=> #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
138
- # table #=> #<Arel::Table @name="appointments" ...>
139
- # key #=> physician_id
140
- # foreign_table #=> #<Arel::Table @name="physicians" ...>
141
- # foreign_key #=> id
89
+ # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
90
+ # table # => #<Arel::Table @name="appointments" ...>
91
+ # key # => physician_id
92
+ # foreign_table # => #<Arel::Table @name="physicians" ...>
93
+ # foreign_key # => id
142
94
  #
143
- def build_constraint(reflection, table, key, foreign_table, foreign_key)
95
+ def build_constraint(klass, table, key, foreign_table, foreign_key)
144
96
  constraint = table[key].eq(foreign_table[foreign_key])
145
97
 
146
- if reflection.klass.finder_needs_type_condition?
98
+ if klass.finder_needs_type_condition?
147
99
  constraint = table.create_and([
148
100
  constraint,
149
- reflection.klass.send(:type_condition, table)
101
+ klass.send(:type_condition, table)
150
102
  ])
151
103
  end
152
104
 
153
105
  constraint
154
106
  end
155
107
 
156
- def join_relation(joining_relation)
157
- self.join_type = Arel::OuterJoin
158
- joining_relation.joins(self)
159
- end
160
-
161
108
  def table
162
- tables.last
109
+ tables.first
163
110
  end
164
111
 
165
112
  def aliased_table_name
166
113
  table.table_alias || table.name
167
114
  end
168
-
169
- def scope_chain
170
- @scope_chain ||= reflection.scope_chain.reverse
171
- end
172
-
173
115
  end
174
116
  end
175
117
  end
@@ -1,18 +1,16 @@
1
+ require 'active_record/associations/join_dependency/join_part'
2
+
1
3
  module ActiveRecord
2
4
  module Associations
3
5
  class JoinDependency # :nodoc:
4
6
  class JoinBase < JoinPart # :nodoc:
5
- def ==(other)
6
- other.class == self.class &&
7
- other.base_klass == base_klass
8
- end
9
-
10
- def aliased_prefix
11
- "t0"
7
+ def match?(other)
8
+ return true if self == other
9
+ super && base_klass == other.base_klass
12
10
  end
13
11
 
14
12
  def table
15
- Arel::Table.new(table_name, arel_engine)
13
+ base_klass.arel_table
16
14
  end
17
15
 
18
16
  def aliased_table_name
@@ -8,34 +8,36 @@ module ActiveRecord
8
8
  # operations (for example a has_and_belongs_to_many JoinAssociation would result in
9
9
  # two; one for the join table and one for the target table).
10
10
  class JoinPart # :nodoc:
11
+ include Enumerable
12
+
11
13
  # The Active Record class which this join part is associated 'about'; for a JoinBase
12
14
  # this is the actual base model, for a JoinAssociation this is the target model of the
13
15
  # association.
14
- attr_reader :base_klass
16
+ attr_reader :base_klass, :children
15
17
 
16
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :base_klass
18
+ delegate :table_name, :column_names, :primary_key, :to => :base_klass
17
19
 
18
- def initialize(base_klass)
20
+ def initialize(base_klass, children)
19
21
  @base_klass = base_klass
20
- @cached_record = {}
21
22
  @column_names_with_alias = nil
23
+ @children = children
22
24
  end
23
25
 
24
- def aliased_table
25
- Arel::Nodes::TableAlias.new table, aliased_table_name
26
+ def name
27
+ reflection.name
26
28
  end
27
29
 
28
- def ==(other)
29
- raise NotImplementedError
30
+ def match?(other)
31
+ self.class == other.class
30
32
  end
31
33
 
32
- # An Arel::Table for the active_record
33
- def table
34
- raise NotImplementedError
34
+ def each(&block)
35
+ yield self
36
+ children.each { |child| child.each(&block) }
35
37
  end
36
38
 
37
- # The prefix to be used when aliasing columns in the active_record's table
38
- def aliased_prefix
39
+ # An Arel::Table for the active_record
40
+ def table
39
41
  raise NotImplementedError
40
42
  end
41
43
 
@@ -44,24 +46,7 @@ module ActiveRecord
44
46
  raise NotImplementedError
45
47
  end
46
48
 
47
- # The alias for the primary key of the active_record's table
48
- def aliased_primary_key
49
- "#{aliased_prefix}_r0"
50
- end
51
-
52
- # An array of [column_name, alias] pairs for the table
53
- def column_names_with_alias
54
- unless @column_names_with_alias
55
- @column_names_with_alias = []
56
-
57
- ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i|
58
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
59
- end
60
- end
61
- @column_names_with_alias
62
- end
63
-
64
- def extract_record(row)
49
+ def extract_record(row, column_names_with_alias)
65
50
  # This code is performance critical as it is called per row.
66
51
  # see: https://github.com/rails/rails/pull/12185
67
52
  hash = {}
@@ -78,12 +63,8 @@ module ActiveRecord
78
63
  hash
79
64
  end
80
65
 
81
- def record_id(row)
82
- row[aliased_primary_key]
83
- end
84
-
85
- def instantiate(row)
86
- @cached_record[record_id(row)] ||= base_klass.instantiate(extract_record(row))
66
+ def instantiate(row, aliases)
67
+ base_klass.instantiate(extract_record(row, aliases))
87
68
  end
88
69
  end
89
70
  end
@@ -10,21 +10,12 @@ module ActiveRecord
10
10
  private
11
11
 
12
12
  def construct_tables
13
- tables = []
14
- chain.each do |reflection|
15
- tables << alias_tracker.aliased_table_for(
13
+ chain.map do |reflection|
14
+ alias_tracker.aliased_table_for(
16
15
  table_name_for(reflection),
17
16
  table_alias_for(reflection, reflection != self.reflection)
18
17
  )
19
-
20
- if reflection.source_macro == :has_and_belongs_to_many
21
- tables << alias_tracker.aliased_table_for(
22
- (reflection.source_reflection || reflection).join_table,
23
- table_alias_for(reflection, true)
24
- )
25
- end
26
18
  end
27
- tables
28
19
  end
29
20
 
30
21
  def table_name_for(reflection)
@@ -42,12 +42,9 @@ module ActiveRecord
42
42
  autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
43
43
  autoload :HasOne, 'active_record/associations/preloader/has_one'
44
44
  autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
45
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
46
45
  autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
47
46
  end
48
47
 
49
- attr_reader :records, :associations, :preload_scope, :model
50
-
51
48
  # Eager loads the named associations for the given Active Record record(s).
52
49
  #
53
50
  # In this description, 'association name' shall refer to the name passed
@@ -82,38 +79,47 @@ module ActiveRecord
82
79
  # [ :books, :author ]
83
80
  # { author: :avatar }
84
81
  # [ :books, { author: :avatar } ]
85
- def initialize(records, associations, preload_scope = nil)
86
- @records = Array.wrap(records).compact.uniq
87
- @associations = Array.wrap(associations)
88
- @preload_scope = preload_scope || Relation.new(nil, nil)
89
- end
90
82
 
91
- def run
92
- unless records.empty?
93
- associations.each { |association| preload(association) }
83
+ NULL_RELATION = Struct.new(:values).new({})
84
+
85
+ def preload(records, associations, preload_scope = nil)
86
+ records = Array.wrap(records).compact.uniq
87
+ associations = Array.wrap(associations)
88
+ preload_scope = preload_scope || NULL_RELATION
89
+
90
+ if records.empty?
91
+ []
92
+ else
93
+ associations.flat_map { |association|
94
+ preloaders_on association, records, preload_scope
95
+ }
94
96
  end
95
97
  end
96
98
 
97
99
  private
98
100
 
99
- def preload(association)
101
+ def preloaders_on(association, records, scope)
100
102
  case association
101
103
  when Hash
102
- preload_hash(association)
104
+ preloaders_for_hash(association, records, scope)
103
105
  when Symbol
104
- preload_one(association)
106
+ preloaders_for_one(association, records, scope)
105
107
  when String
106
- preload_one(association.to_sym)
108
+ preloaders_for_one(association.to_sym, records, scope)
107
109
  else
108
110
  raise ArgumentError, "#{association.inspect} was not recognised for preload"
109
111
  end
110
112
  end
111
113
 
112
- def preload_hash(association)
113
- association.each do |parent, child|
114
- Preloader.new(records, parent, preload_scope).run
115
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
116
- end
114
+ def preloaders_for_hash(association, records, scope)
115
+ parent, child = association.to_a.first # hash should only be of length 1
116
+
117
+ loaders = preloaders_for_one parent, records, scope
118
+
119
+ recs = loaders.flat_map(&:preloaded_records).uniq
120
+ loaders.concat Array.wrap(child).flat_map { |assoc|
121
+ preloaders_on assoc, recs, scope
122
+ }
117
123
  end
118
124
 
119
125
  # Not all records have the same class, so group then preload group on the reflection
@@ -123,32 +129,81 @@ module ActiveRecord
123
129
  # Additionally, polymorphic belongs_to associations can have multiple associated
124
130
  # classes, depending on the polymorphic_type field. So we group by the classes as
125
131
  # well.
126
- def preload_one(association)
127
- grouped_records(association).each do |reflection, klasses|
128
- klasses.each do |klass, records|
129
- preloader_for(reflection).new(klass, records, reflection, preload_scope).run
132
+ def preloaders_for_one(association, records, scope)
133
+ grouped_records(association, records).flat_map do |reflection, klasses|
134
+ klasses.map do |rhs_klass, rs|
135
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
136
+ loader.run self
137
+ loader
130
138
  end
131
139
  end
132
140
  end
133
141
 
134
- def grouped_records(association)
135
- h = {}
136
- records.each do |record|
137
- assoc = record.association(association)
138
- klasses = h[assoc.reflection] ||= {}
139
- (klasses[assoc.klass] ||= []) << record
142
+ def grouped_records(association, records)
143
+ reflection_records = records_by_reflection(association, records)
144
+
145
+ reflection_records.each_with_object({}) do |(reflection, r_records),h|
146
+ h[reflection] = r_records.group_by { |record|
147
+ association_klass(reflection, record)
148
+ }
149
+ end
150
+ end
151
+
152
+ def records_by_reflection(association, records)
153
+ records.group_by do |record|
154
+ reflection = record.class.reflect_on_association(association)
155
+
156
+ reflection || raise_config_error(record, association)
140
157
  end
141
- h
142
158
  end
143
159
 
144
- def preloader_for(reflection)
160
+ def raise_config_error(record, association)
161
+ raise ActiveRecord::ConfigurationError,
162
+ "Association named '#{association}' was not found on #{record.class.name}; " \
163
+ "perhaps you misspelled it?"
164
+ end
165
+
166
+ def association_klass(reflection, record)
167
+ if reflection.macro == :belongs_to && reflection.options[:polymorphic]
168
+ klass = record.read_attribute(reflection.foreign_type.to_s)
169
+ klass && klass.constantize
170
+ else
171
+ reflection.klass
172
+ end
173
+ end
174
+
175
+ class AlreadyLoaded
176
+ attr_reader :owners, :reflection
177
+
178
+ def initialize(klass, owners, reflection, preload_scope)
179
+ @owners = owners
180
+ @reflection = reflection
181
+ end
182
+
183
+ def run(preloader); end
184
+
185
+ def preloaded_records
186
+ owners.flat_map { |owner| owner.read_attribute reflection.name }
187
+ end
188
+ end
189
+
190
+ class NullPreloader
191
+ def self.new(klass, owners, reflection, preload_scope); self; end
192
+ def self.run(preloader); end
193
+ end
194
+
195
+ def preloader_for(reflection, owners, rhs_klass)
196
+ return NullPreloader unless rhs_klass
197
+
198
+ if owners.first.association(reflection.name).loaded?
199
+ return AlreadyLoaded
200
+ end
201
+
145
202
  case reflection.macro
146
203
  when :has_many
147
204
  reflection.options[:through] ? HasManyThrough : HasMany
148
205
  when :has_one
149
206
  reflection.options[:through] ? HasOneThrough : HasOne
150
- when :has_and_belongs_to_many
151
- HasAndBelongsToMany
152
207
  when :belongs_to
153
208
  BelongsTo
154
209
  end