activerecord 3.1.10 → 4.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (237) hide show
  1. checksums.yaml +6 -6
  2. data/CHANGELOG.md +1837 -338
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +39 -43
  5. data/examples/performance.rb +51 -20
  6. data/examples/simple.rb +4 -4
  7. data/lib/active_record/aggregations.rb +57 -43
  8. data/lib/active_record/association_relation.rb +35 -0
  9. data/lib/active_record/associations/alias_tracker.rb +47 -39
  10. data/lib/active_record/associations/association.rb +71 -85
  11. data/lib/active_record/associations/association_scope.rb +138 -89
  12. data/lib/active_record/associations/belongs_to_association.rb +65 -25
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +9 -3
  14. data/lib/active_record/associations/builder/association.rb +125 -29
  15. data/lib/active_record/associations/builder/belongs_to.rb +91 -60
  16. data/lib/active_record/associations/builder/collection_association.rb +69 -49
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +113 -42
  18. data/lib/active_record/associations/builder/has_many.rb +8 -64
  19. data/lib/active_record/associations/builder/has_one.rb +12 -52
  20. data/lib/active_record/associations/builder/singular_association.rb +22 -29
  21. data/lib/active_record/associations/collection_association.rb +294 -187
  22. data/lib/active_record/associations/collection_proxy.rb +961 -94
  23. data/lib/active_record/associations/foreign_association.rb +11 -0
  24. data/lib/active_record/associations/has_many_association.rb +118 -23
  25. data/lib/active_record/associations/has_many_through_association.rb +115 -45
  26. data/lib/active_record/associations/has_one_association.rb +57 -24
  27. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  28. data/lib/active_record/associations/join_dependency/join_association.rb +76 -102
  29. data/lib/active_record/associations/join_dependency/join_base.rb +7 -9
  30. data/lib/active_record/associations/join_dependency/join_part.rb +30 -37
  31. data/lib/active_record/associations/join_dependency.rb +230 -156
  32. data/lib/active_record/associations/preloader/association.rb +96 -55
  33. data/lib/active_record/associations/preloader/collection_association.rb +3 -3
  34. data/lib/active_record/associations/preloader/has_many_through.rb +7 -3
  35. data/lib/active_record/associations/preloader/has_one.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 +61 -32
  38. data/lib/active_record/associations/preloader.rb +113 -87
  39. data/lib/active_record/associations/singular_association.rb +29 -13
  40. data/lib/active_record/associations/through_association.rb +37 -19
  41. data/lib/active_record/associations.rb +505 -371
  42. data/lib/active_record/attribute.rb +163 -0
  43. data/lib/active_record/attribute_assignment.rb +212 -0
  44. data/lib/active_record/attribute_decorators.rb +66 -0
  45. data/lib/active_record/attribute_methods/before_type_cast.rb +52 -7
  46. data/lib/active_record/attribute_methods/dirty.rb +141 -51
  47. data/lib/active_record/attribute_methods/primary_key.rb +87 -36
  48. data/lib/active_record/attribute_methods/query.rb +5 -4
  49. data/lib/active_record/attribute_methods/read.rb +74 -117
  50. data/lib/active_record/attribute_methods/serialization.rb +70 -0
  51. data/lib/active_record/attribute_methods/time_zone_conversion.rb +49 -47
  52. data/lib/active_record/attribute_methods/write.rb +60 -21
  53. data/lib/active_record/attribute_methods.rb +409 -48
  54. data/lib/active_record/attribute_set/builder.rb +106 -0
  55. data/lib/active_record/attribute_set.rb +81 -0
  56. data/lib/active_record/attributes.rb +147 -0
  57. data/lib/active_record/autosave_association.rb +279 -232
  58. data/lib/active_record/base.rb +84 -1969
  59. data/lib/active_record/callbacks.rb +66 -28
  60. data/lib/active_record/coders/json.rb +13 -0
  61. data/lib/active_record/coders/yaml_column.rb +18 -21
  62. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +422 -243
  63. data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
  64. data/lib/active_record/connection_adapters/abstract/database_statements.rb +170 -194
  65. data/lib/active_record/connection_adapters/abstract/query_cache.rb +32 -19
  66. data/lib/active_record/connection_adapters/abstract/quoting.rb +79 -57
  67. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  68. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +125 -0
  69. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +273 -170
  70. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +50 -0
  71. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +731 -254
  72. data/lib/active_record/connection_adapters/abstract/transaction.rb +215 -0
  73. data/lib/active_record/connection_adapters/abstract_adapter.rb +339 -95
  74. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +946 -0
  75. data/lib/active_record/connection_adapters/column.rb +33 -221
  76. data/lib/active_record/connection_adapters/connection_specification.rb +275 -0
  77. data/lib/active_record/connection_adapters/mysql2_adapter.rb +140 -602
  78. data/lib/active_record/connection_adapters/mysql_adapter.rb +254 -756
  79. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +93 -0
  80. data/lib/active_record/connection_adapters/postgresql/column.rb +20 -0
  81. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +232 -0
  82. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +100 -0
  83. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +52 -0
  84. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +13 -0
  85. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +15 -0
  86. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +46 -0
  87. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +11 -0
  88. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +36 -0
  89. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +13 -0
  90. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +19 -0
  91. data/lib/active_record/connection_adapters/postgresql/oid/float.rb +21 -0
  92. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +59 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +13 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +13 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/integer.rb +11 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/json.rb +35 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +23 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +43 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +43 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +79 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +19 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/time.rb +11 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +109 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +21 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +26 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +28 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid.rb +36 -0
  108. data/lib/active_record/connection_adapters/postgresql/quoting.rb +108 -0
  109. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  110. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +152 -0
  111. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +596 -0
  112. data/lib/active_record/connection_adapters/postgresql/utils.rb +77 -0
  113. data/lib/active_record/connection_adapters/postgresql_adapter.rb +445 -902
  114. data/lib/active_record/connection_adapters/schema_cache.rb +94 -0
  115. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +578 -25
  116. data/lib/active_record/connection_handling.rb +132 -0
  117. data/lib/active_record/core.rb +579 -0
  118. data/lib/active_record/counter_cache.rb +159 -102
  119. data/lib/active_record/dynamic_matchers.rb +140 -0
  120. data/lib/active_record/enum.rb +197 -0
  121. data/lib/active_record/errors.rb +102 -34
  122. data/lib/active_record/explain.rb +38 -0
  123. data/lib/active_record/explain_registry.rb +30 -0
  124. data/lib/active_record/explain_subscriber.rb +29 -0
  125. data/lib/active_record/fixture_set/file.rb +56 -0
  126. data/lib/active_record/fixtures.rb +318 -260
  127. data/lib/active_record/gem_version.rb +15 -0
  128. data/lib/active_record/inheritance.rb +247 -0
  129. data/lib/active_record/integration.rb +113 -0
  130. data/lib/active_record/legacy_yaml_adapter.rb +30 -0
  131. data/lib/active_record/locale/en.yml +8 -1
  132. data/lib/active_record/locking/optimistic.rb +80 -52
  133. data/lib/active_record/locking/pessimistic.rb +27 -5
  134. data/lib/active_record/log_subscriber.rb +25 -18
  135. data/lib/active_record/migration/command_recorder.rb +130 -38
  136. data/lib/active_record/migration/join_table.rb +15 -0
  137. data/lib/active_record/migration.rb +532 -201
  138. data/lib/active_record/model_schema.rb +342 -0
  139. data/lib/active_record/nested_attributes.rb +229 -139
  140. data/lib/active_record/no_touching.rb +52 -0
  141. data/lib/active_record/null_relation.rb +81 -0
  142. data/lib/active_record/persistence.rb +304 -99
  143. data/lib/active_record/query_cache.rb +25 -43
  144. data/lib/active_record/querying.rb +68 -0
  145. data/lib/active_record/railtie.rb +86 -45
  146. data/lib/active_record/railties/console_sandbox.rb +3 -4
  147. data/lib/active_record/railties/controller_runtime.rb +7 -4
  148. data/lib/active_record/railties/databases.rake +198 -377
  149. data/lib/active_record/railties/jdbcmysql_error.rb +2 -2
  150. data/lib/active_record/readonly_attributes.rb +23 -0
  151. data/lib/active_record/reflection.rb +516 -165
  152. data/lib/active_record/relation/batches.rb +96 -45
  153. data/lib/active_record/relation/calculations.rb +221 -144
  154. data/lib/active_record/relation/delegation.rb +140 -0
  155. data/lib/active_record/relation/finder_methods.rb +362 -243
  156. data/lib/active_record/relation/merger.rb +193 -0
  157. data/lib/active_record/relation/predicate_builder/array_handler.rb +48 -0
  158. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  159. data/lib/active_record/relation/predicate_builder.rb +135 -41
  160. data/lib/active_record/relation/query_methods.rb +982 -155
  161. data/lib/active_record/relation/spawn_methods.rb +50 -110
  162. data/lib/active_record/relation.rb +371 -180
  163. data/lib/active_record/result.rb +109 -12
  164. data/lib/active_record/runtime_registry.rb +22 -0
  165. data/lib/active_record/sanitization.rb +191 -0
  166. data/lib/active_record/schema.rb +19 -13
  167. data/lib/active_record/schema_dumper.rb +111 -61
  168. data/lib/active_record/schema_migration.rb +53 -0
  169. data/lib/active_record/scoping/default.rb +135 -0
  170. data/lib/active_record/scoping/named.rb +164 -0
  171. data/lib/active_record/scoping.rb +87 -0
  172. data/lib/active_record/serialization.rb +7 -45
  173. data/lib/active_record/serializers/xml_serializer.rb +14 -65
  174. data/lib/active_record/statement_cache.rb +111 -0
  175. data/lib/active_record/store.rb +205 -0
  176. data/lib/active_record/tasks/database_tasks.rb +299 -0
  177. data/lib/active_record/tasks/mysql_database_tasks.rb +159 -0
  178. data/lib/active_record/tasks/postgresql_database_tasks.rb +101 -0
  179. data/lib/active_record/tasks/sqlite_database_tasks.rb +55 -0
  180. data/lib/active_record/timestamp.rb +35 -14
  181. data/lib/active_record/transactions.rb +141 -74
  182. data/lib/active_record/translation.rb +22 -0
  183. data/lib/active_record/type/big_integer.rb +13 -0
  184. data/lib/active_record/type/binary.rb +50 -0
  185. data/lib/active_record/type/boolean.rb +31 -0
  186. data/lib/active_record/type/date.rb +50 -0
  187. data/lib/active_record/type/date_time.rb +54 -0
  188. data/lib/active_record/type/decimal.rb +64 -0
  189. data/lib/active_record/type/decimal_without_scale.rb +11 -0
  190. data/lib/active_record/type/decorator.rb +14 -0
  191. data/lib/active_record/type/float.rb +19 -0
  192. data/lib/active_record/type/hash_lookup_type_map.rb +23 -0
  193. data/lib/active_record/type/integer.rb +59 -0
  194. data/lib/active_record/type/mutable.rb +16 -0
  195. data/lib/active_record/type/numeric.rb +36 -0
  196. data/lib/active_record/type/serialized.rb +62 -0
  197. data/lib/active_record/type/string.rb +40 -0
  198. data/lib/active_record/type/text.rb +11 -0
  199. data/lib/active_record/type/time.rb +26 -0
  200. data/lib/active_record/type/time_value.rb +38 -0
  201. data/lib/active_record/type/type_map.rb +64 -0
  202. data/lib/active_record/type/unsigned_integer.rb +15 -0
  203. data/lib/active_record/type/value.rb +110 -0
  204. data/lib/active_record/type.rb +23 -0
  205. data/lib/active_record/validations/associated.rb +27 -18
  206. data/lib/active_record/validations/presence.rb +67 -0
  207. data/lib/active_record/validations/uniqueness.rb +125 -66
  208. data/lib/active_record/validations.rb +37 -30
  209. data/lib/active_record/version.rb +5 -7
  210. data/lib/active_record.rb +80 -25
  211. data/lib/rails/generators/active_record/migration/migration_generator.rb +54 -9
  212. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  213. data/lib/rails/generators/active_record/migration/templates/migration.rb +25 -11
  214. data/lib/rails/generators/active_record/migration.rb +11 -8
  215. data/lib/rails/generators/active_record/model/model_generator.rb +17 -4
  216. data/lib/rails/generators/active_record/model/templates/model.rb +5 -2
  217. data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
  218. data/lib/rails/generators/active_record.rb +3 -11
  219. metadata +132 -53
  220. data/examples/associations.png +0 -0
  221. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -62
  222. data/lib/active_record/associations/join_helper.rb +0 -55
  223. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  224. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -135
  225. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -556
  226. data/lib/active_record/dynamic_finder_match.rb +0 -56
  227. data/lib/active_record/dynamic_scope_match.rb +0 -23
  228. data/lib/active_record/identity_map.rb +0 -163
  229. data/lib/active_record/named_scope.rb +0 -200
  230. data/lib/active_record/observer.rb +0 -121
  231. data/lib/active_record/session_store.rb +0 -358
  232. data/lib/active_record/test_case.rb +0 -69
  233. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -17
  234. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  235. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  236. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  237. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -16
@@ -1,213 +1,287 @@
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, :active_record
9
-
10
- def initialize(base, associations, joins)
11
- @active_record = base
12
- @table_joins = joins
13
- @join_parts = [JoinBase.new(base)]
14
- @associations = {}
15
- @reflections = []
16
- @alias_tracker = AliasTracker.new(joins)
17
- @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
18
- build(associations)
19
- end
20
-
21
- def graft(*associations)
22
- associations.each do |association|
23
- join_associations.detect {|a| association == a} ||
24
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
25
- end
26
- self
27
- end
28
-
29
- def join_associations
30
- join_parts.last(join_parts.length - 1)
31
- end
32
-
33
- def join_base
34
- join_parts.first
35
- end
36
-
37
- def columns
38
- join_parts.collect { |join_part|
39
- table = join_part.aliased_table
40
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
41
- table[column_name].as Arel.sql(aliased_name)
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
+ }
42
14
  }
43
- }.flatten
44
- end
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
45
21
 
46
- def instantiate(rows)
47
- primary_key = join_base.aliased_primary_key
48
- parents = {}
22
+ def columns
23
+ @tables.flat_map { |t| t.column_aliases }
24
+ end
49
25
 
50
- records = rows.map { |model|
51
- primary_id = model[primary_key]
52
- parent = parents[primary_id] ||= join_base.instantiate(model)
53
- construct(parent, @associations, join_associations, model)
54
- parent
55
- }.uniq
26
+ # An array of [column_name, alias] pairs for the table
27
+ def column_aliases(node)
28
+ @name_and_alias_cache[node]
29
+ end
56
30
 
57
- remove_duplicate_results!(active_record, records, @associations)
58
- records
59
- end
31
+ def column_alias(node, column)
32
+ @alias_cache[node][column]
33
+ end
60
34
 
61
- def remove_duplicate_results!(base, records, associations)
62
- case associations
63
- when Symbol, String
64
- reflection = base.reflections[associations]
65
- remove_uniq_by_reflection(reflection, records)
66
- when Array
67
- associations.each do |association|
68
- remove_duplicate_results!(base, records, association)
35
+ class Table < Struct.new(:node, :columns)
36
+ def table
37
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
69
38
  end
70
- when Hash
71
- associations.keys.each do |name|
72
- reflection = base.reflections[name]
73
- remove_uniq_by_reflection(reflection, records)
74
-
75
- parent_records = []
76
- records.each do |record|
77
- if descendant = record.send(reflection.name)
78
- if reflection.collection?
79
- parent_records.concat descendant.target.uniq
80
- else
81
- parent_records << descendant
82
- end
83
- end
84
- end
85
39
 
86
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
40
+ def column_aliases
41
+ t = table
42
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
87
43
  end
88
44
  end
45
+ Column = Struct.new(:name, :alias)
89
46
  end
90
47
 
91
- protected
48
+ attr_reader :alias_tracker, :base_klass, :join_root
92
49
 
93
- def cache_joined_association(association)
94
- associations = []
95
- parent = association.parent
96
- while parent != join_base
97
- associations.unshift(parent.reflection.name)
98
- parent = parent.parent
99
- end
100
- ref = @associations
101
- associations.each do |key|
102
- ref = ref[key]
103
- end
104
- ref[association.reflection.name] ||= {}
50
+ def self.make_tree(associations)
51
+ hash = {}
52
+ walk_tree associations, hash
53
+ hash
105
54
  end
106
55
 
107
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
108
- parent ||= join_parts.last
56
+ def self.walk_tree(associations, hash)
109
57
  case associations
110
58
  when Symbol, String
111
- reflection = parent.reflections[associations.to_s.intern] or
112
- raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
113
- unless join_association = find_join_association(reflection, parent)
114
- @reflections << reflection
115
- join_association = build_join_association(reflection, parent)
116
- join_association.join_type = join_type
117
- @join_parts << join_association
118
- cache_joined_association(join_association)
119
- end
120
- join_association
59
+ hash[associations.to_sym] ||= {}
121
60
  when Array
122
- associations.each do |association|
123
- build(association, parent, join_type)
61
+ associations.each do |assoc|
62
+ walk_tree assoc, hash
124
63
  end
125
64
  when Hash
126
- associations.keys.sort_by { |a| a.to_s }.each do |name|
127
- join_association = build(name, parent, join_type)
128
- build(associations[name], join_association, join_type)
65
+ associations.each do |k,v|
66
+ cache = hash[k] ||= {}
67
+ walk_tree v, cache
129
68
  end
130
69
  else
131
70
  raise ConfigurationError, associations.inspect
132
71
  end
133
72
  end
134
73
 
135
- def find_join_association(name_or_reflection, parent)
136
- if String === name_or_reflection
137
- name_or_reflection = name_or_reflection.to_sym
138
- end
74
+ # base is the base class on which operation is taking place.
75
+ # associations is the list of associations which are joined using hash, symbol or array.
76
+ # joins is the list of all string join commands and arel nodes.
77
+ #
78
+ # Example :
79
+ #
80
+ # class Physician < ActiveRecord::Base
81
+ # has_many :appointments
82
+ # has_many :patients, through: :appointments
83
+ # end
84
+ #
85
+ # If I execute `@physician.patients.to_a` then
86
+ # base # => Physician
87
+ # associations # => []
88
+ # joins # => [#<Arel::Nodes::InnerJoin: ...]
89
+ #
90
+ # However if I execute `Physician.joins(:appointments).to_a` then
91
+ # base # => Physician
92
+ # associations # => [:appointments]
93
+ # joins # => []
94
+ #
95
+ def initialize(base, associations, joins)
96
+ @alias_tracker = AliasTracker.create(base.connection, joins)
97
+ @alias_tracker.aliased_table_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 }
101
+ end
139
102
 
140
- join_associations.detect { |j|
141
- j.reflection == name_or_reflection && j.parent == parent
103
+ def reflections
104
+ join_root.drop(1).map!(&:reflection)
105
+ end
106
+
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
+ }
121
+ end
122
+
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}"
127
+ }
128
+ Aliases::Table.new(join_part, columns)
142
129
  }
143
130
  end
144
131
 
145
- def remove_uniq_by_reflection(reflection, records)
146
- if reflection && reflection.collection?
147
- records.each { |record| record.send(reflection.name).target.uniq! }
132
+ def instantiate(result_set, aliases)
133
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
134
+
135
+ seen = Hash.new { |h,parent_klass|
136
+ h[parent_klass] = Hash.new { |i,parent_id|
137
+ i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
138
+ }
139
+ }
140
+
141
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
142
+ parents = model_cache[join_root]
143
+ column_aliases = aliases.column_aliases join_root
144
+
145
+ message_bus = ActiveSupport::Notifications.instrumenter
146
+
147
+ payload = {
148
+ record_count: result_set.length,
149
+ class_name: join_root.base_klass.name
150
+ }
151
+
152
+ message_bus.instrument('instantiation.active_record', payload) do
153
+ result_set.each { |row_hash|
154
+ parent_key = primary_key ? row_hash[primary_key] : row_hash
155
+ parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases)
156
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
157
+ }
148
158
  end
159
+
160
+ parents.values
149
161
  end
150
162
 
151
- def build_join_association(reflection, parent)
152
- JoinAssociation.new(reflection, self, parent)
163
+ private
164
+
165
+ def make_constraints(parent, child, tables, join_type)
166
+ chain = child.reflection.chain
167
+ foreign_table = parent.table
168
+ foreign_klass = parent.base_klass
169
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
153
170
  end
154
171
 
155
- def construct(parent, associations, join_parts, row)
156
- case associations
157
- when Symbol, String
158
- name = associations.to_s
172
+ def make_outer_joins(parent, child)
173
+ tables = table_aliases_for(parent, child)
174
+ join_type = Arel::Nodes::OuterJoin
175
+ info = make_constraints parent, child, tables, join_type
159
176
 
160
- join_part = join_parts.detect { |j|
161
- j.reflection.name.to_s == name &&
162
- j.parent_table_name == parent.class.table_name }
177
+ [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
178
+ end
163
179
 
164
- raise(ConfigurationError, "No such association") unless join_part
180
+ def make_inner_joins(parent, child)
181
+ tables = child.tables
182
+ join_type = Arel::Nodes::InnerJoin
183
+ info = make_constraints parent, child, tables, join_type
165
184
 
166
- join_parts.delete(join_part)
167
- construct_association(parent, join_part, row)
168
- when Array
169
- associations.each do |association|
170
- construct(parent, association, join_parts, row)
171
- end
172
- when Hash
173
- associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
174
- association = construct(parent, association_name, join_parts, row)
175
- construct(association, assoc, join_parts, row) if association
185
+ [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
186
+ end
187
+
188
+ def table_aliases_for(parent, node)
189
+ node.reflection.chain.map { |reflection|
190
+ alias_tracker.aliased_table_for(
191
+ reflection.table_name,
192
+ table_alias_for(reflection, parent, reflection != node.reflection)
193
+ )
194
+ }
195
+ end
196
+
197
+ def construct_tables!(parent, node)
198
+ node.tables = table_aliases_for(parent, node)
199
+ node.children.each { |child| construct_tables! node, child }
200
+ end
201
+
202
+ def table_alias_for(reflection, parent, join)
203
+ name = "#{reflection.plural_name}_#{parent.table_name}"
204
+ name << "_join" if join
205
+ name
206
+ end
207
+
208
+ def walk(left, right)
209
+ intersection, missing = right.children.map { |node1|
210
+ [left.children.find { |node2| node1.match? node2 }, node1]
211
+ }.partition(&:first)
212
+
213
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
214
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
215
+ end
216
+
217
+ def find_reflection(klass, name)
218
+ klass._reflect_on_association(name) or
219
+ raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
220
+ end
221
+
222
+ def build(associations, base_klass)
223
+ associations.map do |name, right|
224
+ reflection = find_reflection base_klass, name
225
+ reflection.check_validity!
226
+ reflection.check_eager_loadable!
227
+
228
+ if reflection.polymorphic?
229
+ raise EagerLoadPolymorphicError.new(reflection)
176
230
  end
177
- else
178
- raise ConfigurationError, associations.inspect
231
+
232
+ JoinAssociation.new reflection, build(right, reflection.klass)
179
233
  end
180
234
  end
181
235
 
182
- def construct_association(record, join_part, row)
183
- return if record.id.to_s != join_part.parent.record_id(row).to_s
236
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
237
+ return if ar_parent.nil?
238
+ primary_id = ar_parent.id
184
239
 
185
- macro = join_part.reflection.macro
186
- if macro == :has_one
187
- return if record.association_cache.key?(join_part.reflection.name)
188
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
189
- set_target_and_inverse(join_part, association, record)
190
- else
191
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
192
- case macro
193
- when :has_many, :has_and_belongs_to_many
194
- other = record.association(join_part.reflection.name)
240
+ parent.children.each do |node|
241
+ if node.reflection.collection?
242
+ other = ar_parent.association(node.reflection.name)
195
243
  other.loaded!
196
- other.target.push(association) if association
197
- other.set_inverse_instance(association)
198
- when :belongs_to
199
- set_target_and_inverse(join_part, association, record)
200
244
  else
201
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
245
+ if ar_parent.association_cache.key?(node.reflection.name)
246
+ model = ar_parent.association(node.reflection.name).target
247
+ construct(model, node, row, rs, seen, model_cache, aliases)
248
+ next
249
+ end
250
+ end
251
+
252
+ key = aliases.column_alias(node, node.primary_key)
253
+ id = row[key]
254
+ if id.nil?
255
+ nil_association = ar_parent.association(node.reflection.name)
256
+ nil_association.loaded!
257
+ next
258
+ end
259
+
260
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
261
+
262
+ if model
263
+ construct(model, node, row, rs, seen, model_cache, aliases)
264
+ else
265
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
266
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
267
+ construct(model, node, row, rs, seen, model_cache, aliases)
202
268
  end
203
269
  end
204
- association
205
270
  end
206
271
 
207
- def set_target_and_inverse(join_part, association, record)
208
- other = record.association(join_part.reflection.name)
209
- other.target = association
210
- other.set_inverse_instance(association)
272
+ def construct_model(record, node, row, model_cache, id, aliases)
273
+ model = model_cache[node][id] ||= node.instantiate(row,
274
+ aliases.column_aliases(node))
275
+ other = record.association(node.reflection.name)
276
+
277
+ if node.reflection.collection?
278
+ other.target.push(model)
279
+ else
280
+ other.target = model
281
+ end
282
+
283
+ other.set_inverse_instance(model)
284
+ model
211
285
  end
212
286
  end
213
287
  end
@@ -2,34 +2,38 @@ module ActiveRecord
2
2
  module Associations
3
3
  class Preloader
4
4
  class Association #:nodoc:
5
- attr_reader :owners, :reflection, :preload_options, :model, :klass
6
-
7
- def initialize(klass, owners, reflection, preload_options)
8
- @klass = klass
9
- @owners = owners
10
- @reflection = reflection
11
- @preload_options = preload_options || {}
12
- @model = owners.first && owners.first.class
13
- @scoped = nil
14
- @owners_by_key = nil
5
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
6
+ attr_reader :preloaded_records
7
+
8
+ def initialize(klass, owners, reflection, preload_scope)
9
+ @klass = klass
10
+ @owners = owners
11
+ @reflection = reflection
12
+ @preload_scope = preload_scope
13
+ @model = owners.first && owners.first.class
14
+ @scope = nil
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
 
27
- def scoped
28
- @scoped ||= build_scope
27
+ def scope
28
+ @scope ||= build_scope
29
29
  end
30
30
 
31
31
  def records_for(ids)
32
- scoped.where(association_key.in(ids))
32
+ query_scope(ids)
33
+ end
34
+
35
+ def query_scope(ids)
36
+ scope.where(association_key.in(ids))
33
37
  end
34
38
 
35
39
  def table
@@ -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,57 +74,91 @@ 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
77
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
78
- # Make several smaller queries if necessary or make one query if the adapter supports it
79
- sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
80
- records = sliced.map { |slice| records_for(slice) }.flatten
81
+ # Each record may have multiple owners, and vice-versa
82
+ records_by_owner = owners.each_with_object({}) do |owner,h|
83
+ h[owner] = []
81
84
  end
82
85
 
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
86
+ if owner_keys.any?
87
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
88
+ # Make several smaller queries if necessary or make one query if the adapter supports it
89
+ sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
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
+ @klass.type_for_attribute(association_key_name.to_s).type
108
+ end
109
+
110
+ def owner_key_type
111
+ @model.type_for_attribute(owner_key_name.to_s).type
112
+ end
113
+
114
+ def load_slices(slices)
115
+ @preloaded_records = slices.flat_map { |slice|
116
+ records_for(slice)
117
+ }
118
+
119
+ @preloaded_records.map { |record|
120
+ key = record[association_key_name]
121
+ key = key.to_s if key_conversion_required?
122
+
123
+ [record, key]
124
+ }
125
+ end
126
+
127
+ def reflection_scope
128
+ @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
129
+ end
130
+
95
131
  def build_scope
96
- scope = klass.scoped
132
+ scope = klass.unscoped
97
133
 
98
- scope = scope.where(process_conditions(options[:conditions]))
99
- scope = scope.where(process_conditions(preload_options[:conditions]))
134
+ values = reflection_scope.values
135
+ reflection_binds = reflection_scope.bind_values
136
+ preload_values = preload_scope.values
137
+ preload_binds = preload_scope.bind_values
100
138
 
101
- scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
102
- scope = scope.includes(preload_options[:include] || options[:include])
139
+ scope.where_values = Array(values[:where]) + Array(preload_values[:where])
140
+ scope.references_values = Array(values[:references]) + Array(preload_values[:references])
141
+ scope.bind_values = (reflection_binds + preload_binds)
103
142
 
104
- if options[:as]
105
- scope = scope.where(
106
- klass.table_name => {
107
- reflection.type => model.base_class.sti_name
108
- }
109
- )
143
+ scope._select! preload_values[:select] || values[:select] || table[Arel.star]
144
+ scope.includes! preload_values[:includes] || values[:includes]
145
+ scope.joins! preload_values[:joins] || values[:joins]
146
+ scope.order! preload_values[:order] || values[:order]
147
+
148
+ if preload_values[:reordering] || values[:reordering]
149
+ scope.reordering_value = true
110
150
  end
111
151
 
112
- scope
113
- end
152
+ if preload_values[:readonly] || values[:readonly]
153
+ scope.readonly!
154
+ end
114
155
 
115
- def process_conditions(conditions)
116
- if conditions.respond_to?(:to_proc)
117
- conditions = klass.send(:instance_eval, &conditions)
156
+ if options[:as]
157
+ scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
118
158
  end
119
159
 
120
- conditions
160
+ scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope])
161
+ klass.default_scoped.merge(scope)
121
162
  end
122
163
  end
123
164
  end
@@ -6,11 +6,11 @@ module ActiveRecord
6
6
  private
7
7
 
8
8
  def build_scope
9
- super.order(preload_options[:order] || options[:order])
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)