activerecord 4.0.4 → 4.1.16

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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1632 -1797
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/examples/performance.rb +30 -18
  6. data/examples/simple.rb +4 -4
  7. data/lib/active_record/aggregations.rb +2 -1
  8. data/lib/active_record/association_relation.rb +4 -0
  9. data/lib/active_record/associations/alias_tracker.rb +49 -29
  10. data/lib/active_record/associations/association.rb +9 -17
  11. data/lib/active_record/associations/association_scope.rb +59 -49
  12. data/lib/active_record/associations/belongs_to_association.rb +34 -25
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +6 -1
  14. data/lib/active_record/associations/builder/association.rb +84 -54
  15. data/lib/active_record/associations/builder/belongs_to.rb +90 -58
  16. data/lib/active_record/associations/builder/collection_association.rb +47 -45
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +119 -25
  18. data/lib/active_record/associations/builder/has_many.rb +3 -3
  19. data/lib/active_record/associations/builder/has_one.rb +5 -7
  20. data/lib/active_record/associations/builder/singular_association.rb +6 -7
  21. data/lib/active_record/associations/collection_association.rb +121 -111
  22. data/lib/active_record/associations/collection_proxy.rb +73 -18
  23. data/lib/active_record/associations/has_many_association.rb +14 -11
  24. data/lib/active_record/associations/has_many_through_association.rb +33 -6
  25. data/lib/active_record/associations/has_one_association.rb +1 -1
  26. data/lib/active_record/associations/join_dependency/join_association.rb +46 -104
  27. data/lib/active_record/associations/join_dependency/join_base.rb +6 -8
  28. data/lib/active_record/associations/join_dependency/join_part.rb +18 -37
  29. data/lib/active_record/associations/join_dependency.rb +208 -168
  30. data/lib/active_record/associations/preloader/association.rb +69 -27
  31. data/lib/active_record/associations/preloader/collection_association.rb +2 -2
  32. data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
  33. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  34. data/lib/active_record/associations/preloader/through_association.rb +58 -26
  35. data/lib/active_record/associations/preloader.rb +63 -49
  36. data/lib/active_record/associations/singular_association.rb +6 -5
  37. data/lib/active_record/associations/through_association.rb +30 -9
  38. data/lib/active_record/associations.rb +116 -42
  39. data/lib/active_record/attribute_assignment.rb +6 -3
  40. data/lib/active_record/attribute_methods/before_type_cast.rb +2 -1
  41. data/lib/active_record/attribute_methods/dirty.rb +35 -26
  42. data/lib/active_record/attribute_methods/primary_key.rb +8 -1
  43. data/lib/active_record/attribute_methods/read.rb +56 -29
  44. data/lib/active_record/attribute_methods/serialization.rb +44 -12
  45. data/lib/active_record/attribute_methods/time_zone_conversion.rb +13 -1
  46. data/lib/active_record/attribute_methods/write.rb +59 -26
  47. data/lib/active_record/attribute_methods.rb +82 -43
  48. data/lib/active_record/autosave_association.rb +209 -194
  49. data/lib/active_record/base.rb +6 -2
  50. data/lib/active_record/callbacks.rb +2 -2
  51. data/lib/active_record/coders/json.rb +13 -0
  52. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +5 -10
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +14 -24
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +13 -13
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +6 -3
  56. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  57. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +90 -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 +45 -70
  60. data/lib/active_record/connection_adapters/abstract/transaction.rb +1 -0
  61. data/lib/active_record/connection_adapters/abstract_adapter.rb +28 -96
  62. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +74 -66
  63. data/lib/active_record/connection_adapters/column.rb +1 -35
  64. data/lib/active_record/connection_adapters/connection_specification.rb +231 -43
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +10 -5
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +24 -17
  67. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +22 -15
  68. data/lib/active_record/connection_adapters/postgresql/cast.rb +12 -4
  69. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -44
  70. data/lib/active_record/connection_adapters/postgresql/oid.rb +38 -14
  71. data/lib/active_record/connection_adapters/postgresql/quoting.rb +37 -12
  72. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +20 -11
  73. data/lib/active_record/connection_adapters/postgresql_adapter.rb +98 -52
  74. data/lib/active_record/connection_adapters/schema_cache.rb +8 -29
  75. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -60
  76. data/lib/active_record/connection_handling.rb +39 -5
  77. data/lib/active_record/core.rb +38 -54
  78. data/lib/active_record/counter_cache.rb +9 -10
  79. data/lib/active_record/dynamic_matchers.rb +6 -2
  80. data/lib/active_record/enum.rb +199 -0
  81. data/lib/active_record/errors.rb +22 -5
  82. data/lib/active_record/fixture_set/file.rb +2 -1
  83. data/lib/active_record/fixtures.rb +173 -76
  84. data/lib/active_record/gem_version.rb +15 -0
  85. data/lib/active_record/inheritance.rb +23 -9
  86. data/lib/active_record/integration.rb +54 -1
  87. data/lib/active_record/locking/optimistic.rb +7 -2
  88. data/lib/active_record/locking/pessimistic.rb +1 -1
  89. data/lib/active_record/log_subscriber.rb +6 -13
  90. data/lib/active_record/migration/command_recorder.rb +8 -2
  91. data/lib/active_record/migration.rb +91 -56
  92. data/lib/active_record/model_schema.rb +7 -14
  93. data/lib/active_record/nested_attributes.rb +25 -13
  94. data/lib/active_record/no_touching.rb +52 -0
  95. data/lib/active_record/null_relation.rb +26 -6
  96. data/lib/active_record/persistence.rb +23 -29
  97. data/lib/active_record/querying.rb +15 -12
  98. data/lib/active_record/railtie.rb +12 -61
  99. data/lib/active_record/railties/databases.rake +37 -56
  100. data/lib/active_record/readonly_attributes.rb +0 -6
  101. data/lib/active_record/reflection.rb +230 -79
  102. data/lib/active_record/relation/batches.rb +74 -24
  103. data/lib/active_record/relation/calculations.rb +52 -48
  104. data/lib/active_record/relation/delegation.rb +54 -39
  105. data/lib/active_record/relation/finder_methods.rb +210 -67
  106. data/lib/active_record/relation/merger.rb +15 -12
  107. data/lib/active_record/relation/predicate_builder/array_handler.rb +29 -0
  108. data/lib/active_record/relation/predicate_builder/relation_handler.rb +17 -0
  109. data/lib/active_record/relation/predicate_builder.rb +81 -40
  110. data/lib/active_record/relation/query_methods.rb +185 -108
  111. data/lib/active_record/relation/spawn_methods.rb +8 -5
  112. data/lib/active_record/relation.rb +79 -84
  113. data/lib/active_record/result.rb +45 -6
  114. data/lib/active_record/runtime_registry.rb +5 -0
  115. data/lib/active_record/sanitization.rb +4 -4
  116. data/lib/active_record/schema_dumper.rb +18 -6
  117. data/lib/active_record/schema_migration.rb +31 -18
  118. data/lib/active_record/scoping/default.rb +5 -18
  119. data/lib/active_record/scoping/named.rb +14 -29
  120. data/lib/active_record/scoping.rb +5 -0
  121. data/lib/active_record/store.rb +67 -18
  122. data/lib/active_record/tasks/database_tasks.rb +66 -26
  123. data/lib/active_record/tasks/mysql_database_tasks.rb +16 -10
  124. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  125. data/lib/active_record/tasks/sqlite_database_tasks.rb +5 -1
  126. data/lib/active_record/timestamp.rb +6 -6
  127. data/lib/active_record/transactions.rb +10 -12
  128. data/lib/active_record/validations/presence.rb +1 -1
  129. data/lib/active_record/validations/uniqueness.rb +19 -9
  130. data/lib/active_record/version.rb +4 -7
  131. data/lib/active_record.rb +5 -7
  132. data/lib/rails/generators/active_record/migration/migration_generator.rb +4 -0
  133. data/lib/rails/generators/active_record/migration.rb +18 -0
  134. data/lib/rails/generators/active_record/model/model_generator.rb +4 -0
  135. data/lib/rails/generators/active_record.rb +2 -8
  136. metadata +18 -30
  137. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -65
  138. data/lib/active_record/associations/join_helper.rb +0 -45
  139. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  140. data/lib/active_record/tasks/firebird_database_tasks.rb +0 -56
  141. data/lib/active_record/tasks/oracle_database_tasks.rb +0 -45
  142. data/lib/active_record/tasks/sqlserver_database_tasks.rb +0 -48
  143. data/lib/active_record/test_case.rb +0 -96
@@ -1,15 +1,79 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
3
  class JoinDependency # :nodoc:
4
- autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
5
4
  autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
6
5
  autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
7
6
 
8
- attr_reader :join_parts, :reflections, :alias_tracker, :base_klass
7
+ class Aliases # :nodoc:
8
+ def initialize(tables)
9
+ @tables = tables
10
+ @alias_cache = tables.each_with_object({}) { |table,h|
11
+ h[table.node] = table.columns.each_with_object({}) { |column,i|
12
+ i[column.name] = column.alias
13
+ }
14
+ }
15
+ @name_and_alias_cache = tables.each_with_object({}) { |table,h|
16
+ h[table.node] = table.columns.map { |column|
17
+ [column.name, column.alias]
18
+ }
19
+ }
20
+ end
21
+
22
+ def columns
23
+ @tables.flat_map { |t| t.column_aliases }
24
+ end
25
+
26
+ # An array of [column_name, alias] pairs for the table
27
+ def column_aliases(node)
28
+ @name_and_alias_cache[node]
29
+ end
30
+
31
+ def column_alias(node, column)
32
+ @alias_cache[node][column]
33
+ end
34
+
35
+ class Table < Struct.new(:node, :columns)
36
+ def table
37
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
38
+ end
39
+
40
+ def column_aliases
41
+ t = table
42
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
43
+ end
44
+ end
45
+ Column = Struct.new(:name, :alias)
46
+ end
47
+
48
+ attr_reader :alias_tracker, :base_klass, :join_root
49
+
50
+ def self.make_tree(associations)
51
+ hash = {}
52
+ walk_tree associations, hash
53
+ hash
54
+ end
55
+
56
+ def self.walk_tree(associations, hash)
57
+ case associations
58
+ when Symbol, String
59
+ hash[associations.to_sym] ||= {}
60
+ when Array
61
+ associations.each do |assoc|
62
+ walk_tree assoc, hash
63
+ end
64
+ when Hash
65
+ associations.each do |k,v|
66
+ cache = hash[k] ||= {}
67
+ walk_tree v, cache
68
+ end
69
+ else
70
+ raise ConfigurationError, associations.inspect
71
+ end
72
+ end
9
73
 
10
74
  # base is the base class on which operation is taking place.
11
75
  # associations is the list of associations which are joined using hash, symbol or array.
12
- # joins is the list of all string join commnads and arel nodes.
76
+ # joins is the list of all string join commands and arel nodes.
13
77
  #
14
78
  # Example :
15
79
  #
@@ -19,216 +83,192 @@ module ActiveRecord
19
83
  # end
20
84
  #
21
85
  # If I execute `@physician.patients.to_a` then
22
- # base #=> Physician
23
- # associations #=> []
24
- # joins #=> [#<Arel::Nodes::InnerJoin: ...]
86
+ # base # => Physician
87
+ # associations # => []
88
+ # joins # => [#<Arel::Nodes::InnerJoin: ...]
25
89
  #
26
90
  # However if I execute `Physician.joins(:appointments).to_a` then
27
- # base #=> Physician
28
- # associations #=> [:appointments]
29
- # joins #=> []
91
+ # base # => Physician
92
+ # associations # => [:appointments]
93
+ # joins # => []
30
94
  #
31
95
  def initialize(base, associations, joins)
32
- @base_klass = base
33
- @table_joins = joins
34
- @join_parts = [JoinBase.new(base)]
35
- @associations = {}
36
- @reflections = []
37
- @alias_tracker = AliasTracker.new(base.connection, joins)
38
- @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
39
- build(associations)
40
- end
41
-
42
- def graft(*associations)
43
- associations.each do |association|
44
- join_associations.detect {|a| association == a} ||
45
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
46
- end
47
- self
96
+ @alias_tracker = AliasTracker.create(base.connection, joins)
97
+ @alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1
98
+ tree = self.class.make_tree associations
99
+ @join_root = JoinBase.new base, build(tree, base)
100
+ @join_root.children.each { |child| construct_tables! @join_root, child }
48
101
  end
49
102
 
50
- def join_associations
51
- join_parts.last(join_parts.length - 1)
103
+ def reflections
104
+ join_root.drop(1).map!(&:reflection)
52
105
  end
53
106
 
54
- def join_base
55
- join_parts.first
107
+ def join_constraints(outer_joins)
108
+ joins = join_root.children.flat_map { |child|
109
+ make_inner_joins join_root, child
110
+ }
111
+
112
+ joins.concat outer_joins.flat_map { |oj|
113
+ if join_root.match? oj.join_root
114
+ walk join_root, oj.join_root
115
+ else
116
+ oj.join_root.children.flat_map { |child|
117
+ make_outer_joins oj.join_root, child
118
+ }
119
+ end
120
+ }
56
121
  end
57
122
 
58
- def columns
59
- join_parts.collect { |join_part|
60
- table = join_part.aliased_table
61
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
62
- table[column_name].as Arel.sql(aliased_name)
123
+ def aliases
124
+ Aliases.new join_root.each_with_index.map { |join_part,i|
125
+ columns = join_part.column_names.each_with_index.map { |column_name,j|
126
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
63
127
  }
64
- }.flatten
128
+ Aliases::Table.new(join_part, columns)
129
+ }
65
130
  end
66
131
 
67
- def instantiate(rows)
68
- primary_key = join_base.aliased_primary_key
69
- parents = {}
70
-
71
- records = rows.map { |model|
72
- primary_id = model[primary_key]
73
- parent = parents[primary_id] ||= join_base.instantiate(model)
74
- construct(parent, @associations, join_associations, model)
75
- parent
76
- }.uniq
132
+ def instantiate(result_set, aliases)
133
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
134
+ type_caster = result_set.column_type primary_key
77
135
 
78
- remove_duplicate_results!(base_klass, records, @associations)
79
- records
80
- end
136
+ seen = Hash.new { |h,parent_klass|
137
+ h[parent_klass] = Hash.new { |i,parent_id|
138
+ i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
139
+ }
140
+ }
81
141
 
82
- protected
142
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
143
+ parents = model_cache[join_root]
144
+ column_aliases = aliases.column_aliases join_root
83
145
 
84
- def remove_duplicate_results!(base, records, associations)
85
- case associations
86
- when Symbol, String
87
- reflection = base.reflections[associations]
88
- remove_uniq_by_reflection(reflection, records)
89
- when Array
90
- associations.each do |association|
91
- remove_duplicate_results!(base, records, association)
92
- end
93
- when Hash
94
- associations.each_key do |name|
95
- reflection = base.reflections[name]
96
- remove_uniq_by_reflection(reflection, records)
97
-
98
- parent_records = []
99
- records.each do |record|
100
- if descendant = record.send(reflection.name)
101
- if reflection.collection?
102
- parent_records.concat descendant.target.uniq
103
- else
104
- parent_records << descendant
105
- end
106
- end
107
- end
146
+ result_set.each { |row_hash|
147
+ parent_key = primary_key ? row_hash[primary_key] : row_hash
148
+ primary_id = type_caster.type_cast parent_key
149
+ parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
150
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
151
+ }
108
152
 
109
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
110
- end
111
- end
153
+ parents.values
112
154
  end
113
155
 
114
- def cache_joined_association(association)
115
- associations = []
116
- parent = association.parent
117
- while parent != join_base
118
- associations.unshift(parent.reflection.name)
119
- parent = parent.parent
120
- end
121
- ref = @associations
122
- associations.each do |key|
123
- ref = ref[key]
124
- end
125
- ref[association.reflection.name] ||= {}
156
+ private
157
+
158
+ def make_constraints(parent, child, tables, join_type)
159
+ chain = child.reflection.chain
160
+ foreign_table = parent.table
161
+ foreign_klass = parent.base_klass
162
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
126
163
  end
127
164
 
128
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
129
- parent ||= join_parts.last
130
- case associations
131
- when Symbol, String
132
- reflection = parent.reflections[associations.intern] or
133
- raise ConfigurationError, "Association named '#{ associations }' was not found on #{ parent.base_klass.name }; perhaps you misspelled it?"
134
- unless join_association = find_join_association(reflection, parent)
135
- @reflections << reflection
136
- join_association = build_join_association(reflection, parent)
137
- join_association.join_type = join_type
138
- @join_parts << join_association
139
- cache_joined_association(join_association)
140
- end
141
- join_association
142
- when Array
143
- associations.each do |association|
144
- build(association, parent, join_type)
145
- end
146
- when Hash
147
- associations.keys.sort_by { |a| a.to_s }.each do |name|
148
- join_association = build(name, parent, join_type)
149
- build(associations[name], join_association, join_type)
150
- end
151
- else
152
- raise ConfigurationError, associations.inspect
153
- end
165
+ def make_outer_joins(parent, child)
166
+ tables = table_aliases_for(parent, child)
167
+ join_type = Arel::Nodes::OuterJoin
168
+ joins = make_constraints parent, child, tables, join_type
169
+
170
+ joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
154
171
  end
155
172
 
156
- def find_join_association(name_or_reflection, parent)
157
- if String === name_or_reflection
158
- name_or_reflection = name_or_reflection.to_sym
159
- end
173
+ def make_inner_joins(parent, child)
174
+ tables = child.tables
175
+ join_type = Arel::Nodes::InnerJoin
176
+ joins = make_constraints parent, child, tables, join_type
160
177
 
161
- join_associations.detect { |j|
162
- j.reflection == name_or_reflection && j.parent == parent
178
+ joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
179
+ end
180
+
181
+ def table_aliases_for(parent, node)
182
+ node.reflection.chain.map { |reflection|
183
+ alias_tracker.aliased_table_for(
184
+ reflection.table_name,
185
+ table_alias_for(reflection, parent, reflection != node.reflection)
186
+ )
163
187
  }
164
188
  end
165
189
 
166
- def remove_uniq_by_reflection(reflection, records)
167
- if reflection && reflection.collection?
168
- records.each { |record| record.send(reflection.name).target.uniq! }
169
- end
190
+ def construct_tables!(parent, node)
191
+ node.tables = table_aliases_for(parent, node)
192
+ node.children.each { |child| construct_tables! node, child }
170
193
  end
171
194
 
172
- def build_join_association(reflection, parent)
173
- JoinAssociation.new(reflection, self, parent)
195
+ def table_alias_for(reflection, parent, join)
196
+ name = "#{reflection.plural_name}_#{parent.table_name}"
197
+ name << "_join" if join
198
+ name
174
199
  end
175
200
 
176
- def construct(parent, associations, join_parts, row)
177
- case associations
178
- when Symbol, String
179
- name = associations.to_s
201
+ def walk(left, right)
202
+ intersection, missing = right.children.map { |node1|
203
+ [left.children.find { |node2| node1.match? node2 }, node1]
204
+ }.partition(&:first)
180
205
 
181
- join_part = join_parts.detect { |j|
182
- j.reflection.name.to_s == name &&
183
- j.parent_table_name == parent.class.table_name }
206
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
207
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
208
+ end
184
209
 
185
- raise(ConfigurationError, "No such association") unless join_part
210
+ def find_reflection(klass, name)
211
+ klass._reflect_on_association(name) or
212
+ raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
213
+ end
186
214
 
187
- join_parts.delete(join_part)
188
- construct_association(parent, join_part, row)
189
- when Array
190
- associations.each do |association|
191
- construct(parent, association, join_parts, row)
192
- end
193
- when Hash
194
- associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
195
- association = construct(parent, association_name, join_parts, row)
196
- construct(association, assoc, join_parts, row) if association
215
+ def build(associations, base_klass)
216
+ associations.map do |name, right|
217
+ reflection = find_reflection base_klass, name
218
+ reflection.check_validity!
219
+
220
+ if reflection.options[:polymorphic]
221
+ raise EagerLoadPolymorphicError.new(reflection)
197
222
  end
198
- else
199
- raise ConfigurationError, associations.inspect
223
+
224
+ JoinAssociation.new reflection, build(right, reflection.klass)
200
225
  end
201
226
  end
202
227
 
203
- def construct_association(record, join_part, row)
204
- return if record.id.to_s != join_part.parent.record_id(row).to_s
228
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
229
+ primary_id = ar_parent.id
205
230
 
206
- macro = join_part.reflection.macro
207
- if macro == :has_one
208
- return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
209
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
210
- set_target_and_inverse(join_part, association, record)
211
- else
212
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
213
- case macro
214
- when :has_many, :has_and_belongs_to_many
215
- other = record.association(join_part.reflection.name)
231
+ parent.children.each do |node|
232
+ if node.reflection.collection?
233
+ other = ar_parent.association(node.reflection.name)
216
234
  other.loaded!
217
- other.target.push(association) if association
218
- other.set_inverse_instance(association)
219
- when :belongs_to
220
- set_target_and_inverse(join_part, association, record)
221
235
  else
222
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
236
+ if ar_parent.association_cache.key?(node.reflection.name)
237
+ model = ar_parent.association(node.reflection.name).target
238
+ construct(model, node, row, rs, seen, model_cache, aliases)
239
+ next
240
+ end
241
+ end
242
+
243
+ key = aliases.column_alias(node, node.primary_key)
244
+ id = row[key]
245
+ next if id.nil?
246
+
247
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
248
+
249
+ if model
250
+ construct(model, node, row, rs, seen, model_cache, aliases)
251
+ else
252
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
253
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
254
+ construct(model, node, row, rs, seen, model_cache, aliases)
223
255
  end
224
256
  end
225
- association
226
257
  end
227
258
 
228
- def set_target_and_inverse(join_part, association, record)
229
- other = record.association(join_part.reflection.name)
230
- other.target = association
231
- other.set_inverse_instance(association)
259
+ def construct_model(record, node, row, model_cache, id, aliases)
260
+ model = model_cache[node][id] ||= node.instantiate(row,
261
+ aliases.column_aliases(node))
262
+ other = record.association(node.reflection.name)
263
+
264
+ if node.reflection.collection?
265
+ other.target.push(model)
266
+ else
267
+ other.target = model
268
+ end
269
+
270
+ other.set_inverse_instance(model)
271
+ model
232
272
  end
233
273
  end
234
274
  end
@@ -3,6 +3,7 @@ module ActiveRecord
3
3
  class Preloader
4
4
  class Association #:nodoc:
5
5
  attr_reader :owners, :reflection, :preload_scope, :model, :klass
6
+ attr_reader :preloaded_records
6
7
 
7
8
  def initialize(klass, owners, reflection, preload_scope)
8
9
  @klass = klass
@@ -12,15 +13,14 @@ module ActiveRecord
12
13
  @model = owners.first && owners.first.class
13
14
  @scope = nil
14
15
  @owners_by_key = nil
16
+ @preloaded_records = []
15
17
  end
16
18
 
17
- def run
18
- unless owners.first.association(reflection.name).loaded?
19
- preload
20
- end
19
+ def run(preloader)
20
+ preload(preloader)
21
21
  end
22
22
 
23
- def preload
23
+ def preload(preloader)
24
24
  raise NotImplementedError
25
25
  end
26
26
 
@@ -29,6 +29,10 @@ module ActiveRecord
29
29
  end
30
30
 
31
31
  def records_for(ids)
32
+ query_scope(ids)
33
+ end
34
+
35
+ def query_scope(ids)
32
36
  scope.where(association_key.in(ids))
33
37
  end
34
38
 
@@ -52,13 +56,16 @@ module ActiveRecord
52
56
  raise NotImplementedError
53
57
  end
54
58
 
55
- # We're converting to a string here because postgres will return the aliased association
56
- # key in a habtm as a string (for whatever reason)
57
59
  def owners_by_key
58
- @owners_by_key ||= owners.group_by do |owner|
59
- key = owner[owner_key_name]
60
- key && key.to_s
61
- end
60
+ @owners_by_key ||= if key_conversion_required?
61
+ owners.group_by do |owner|
62
+ owner[owner_key_name].to_s
63
+ end
64
+ else
65
+ owners.group_by do |owner|
66
+ owner[owner_key_name]
67
+ end
68
+ end
62
69
  end
63
70
 
64
71
  def options
@@ -67,38 +74,64 @@ module ActiveRecord
67
74
 
68
75
  private
69
76
 
70
- def associated_records_by_owner
77
+ def associated_records_by_owner(preloader)
71
78
  owners_map = owners_by_key
72
79
  owner_keys = owners_map.keys.compact
73
80
 
74
- if klass.nil? || owner_keys.empty?
75
- records = []
76
- else
81
+ # Each record may have multiple owners, and vice-versa
82
+ records_by_owner = owners.each_with_object({}) do |owner,h|
83
+ h[owner] = []
84
+ end
85
+
86
+ if owner_keys.any?
77
87
  # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
78
88
  # Make several smaller queries if necessary or make one query if the adapter supports it
79
89
  sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
80
- records = sliced.map { |slice| records_for(slice).to_a }.flatten
81
- end
82
-
83
- # Each record may have multiple owners, and vice-versa
84
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
85
- records.each do |record|
86
- owner_key = record[association_key_name].to_s
87
90
 
88
- owners_map[owner_key].each do |owner|
89
- records_by_owner[owner] << record
91
+ records = load_slices sliced
92
+ records.each do |record, owner_key|
93
+ owners_map[owner_key].each do |owner|
94
+ records_by_owner[owner] << record
95
+ end
90
96
  end
91
97
  end
98
+
92
99
  records_by_owner
93
100
  end
94
101
 
102
+ def key_conversion_required?
103
+ association_key_type != owner_key_type
104
+ end
105
+
106
+ def association_key_type
107
+ column = @klass.column_types[association_key_name.to_s]
108
+ column && column.type
109
+ end
110
+
111
+ def owner_key_type
112
+ column = @model.column_types[owner_key_name.to_s]
113
+ column && column.type
114
+ end
115
+
116
+ def load_slices(slices)
117
+ @preloaded_records = slices.flat_map { |slice|
118
+ records_for(slice)
119
+ }
120
+
121
+ @preloaded_records.map { |record|
122
+ key = record[association_key_name]
123
+ key = key.to_s if key_conversion_required?
124
+
125
+ [record, key]
126
+ }
127
+ end
128
+
95
129
  def reflection_scope
96
130
  @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
97
131
  end
98
132
 
99
133
  def build_scope
100
134
  scope = klass.unscoped
101
- scope.default_scoped = true
102
135
 
103
136
  values = reflection_scope.values
104
137
  preload_values = preload_scope.values
@@ -106,14 +139,23 @@ module ActiveRecord
106
139
  scope.where_values = Array(values[:where]) + Array(preload_values[:where])
107
140
  scope.references_values = Array(values[:references]) + Array(preload_values[:references])
108
141
 
109
- scope.select! preload_values[:select] || values[:select] || table[Arel.star]
142
+ scope._select! preload_values[:select] || values[:select] || table[Arel.star]
110
143
  scope.includes! preload_values[:includes] || values[:includes]
111
144
 
145
+ if preload_values.key? :order
146
+ scope.order! preload_values[:order]
147
+ else
148
+ if values.key? :order
149
+ scope.order! values[:order]
150
+ end
151
+ end
152
+
112
153
  if options[:as]
113
154
  scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
114
155
  end
115
156
 
116
- scope
157
+ scope.unscope_values = Array(values[:unscope])
158
+ klass.default_scoped.merge(scope)
117
159
  end
118
160
  end
119
161
  end
@@ -9,8 +9,8 @@ module ActiveRecord
9
9
  super.order(preload_scope.values[:order] || reflection_scope.values[:order])
10
10
  end
11
11
 
12
- def preload
13
- associated_records_by_owner.each do |owner, records|
12
+ def preload(preloader)
13
+ associated_records_by_owner(preloader).each do |owner, records|
14
14
  association = owner.association(reflection.name)
15
15
  association.loaded!
16
16
  association.target.concat(records)
@@ -4,7 +4,7 @@ module ActiveRecord
4
4
  class HasManyThrough < CollectionAssociation #:nodoc:
5
5
  include ThroughAssociation
6
6
 
7
- def associated_records_by_owner
7
+ def associated_records_by_owner(preloader)
8
8
  records_by_owner = super
9
9
 
10
10
  if reflection_scope.distinct_value
@@ -5,13 +5,13 @@ module ActiveRecord
5
5
 
6
6
  private
7
7
 
8
- def preload
9
- associated_records_by_owner.each do |owner, associated_records|
8
+ def preload(preloader)
9
+ associated_records_by_owner(preloader).each do |owner, associated_records|
10
10
  record = associated_records.first
11
11
 
12
12
  association = owner.association(reflection.name)
13
13
  association.target = record
14
- association.set_inverse_instance(record)
14
+ association.set_inverse_instance(record) if record
15
15
  end
16
16
  end
17
17