activerecord 3.2.19 → 5.0.0

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

Potentially problematic release.


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

Files changed (264) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1715 -604
  3. data/MIT-LICENSE +2 -2
  4. data/README.rdoc +40 -45
  5. data/examples/performance.rb +33 -22
  6. data/examples/simple.rb +3 -4
  7. data/lib/active_record/aggregations.rb +76 -51
  8. data/lib/active_record/association_relation.rb +35 -0
  9. data/lib/active_record/associations/alias_tracker.rb +54 -40
  10. data/lib/active_record/associations/association.rb +76 -56
  11. data/lib/active_record/associations/association_scope.rb +125 -93
  12. data/lib/active_record/associations/belongs_to_association.rb +57 -28
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +7 -2
  14. data/lib/active_record/associations/builder/association.rb +120 -32
  15. data/lib/active_record/associations/builder/belongs_to.rb +115 -62
  16. data/lib/active_record/associations/builder/collection_association.rb +61 -53
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +117 -43
  18. data/lib/active_record/associations/builder/has_many.rb +9 -65
  19. data/lib/active_record/associations/builder/has_one.rb +18 -52
  20. data/lib/active_record/associations/builder/singular_association.rb +18 -19
  21. data/lib/active_record/associations/collection_association.rb +268 -186
  22. data/lib/active_record/associations/collection_proxy.rb +1003 -63
  23. data/lib/active_record/associations/foreign_association.rb +11 -0
  24. data/lib/active_record/associations/has_many_association.rb +81 -41
  25. data/lib/active_record/associations/has_many_through_association.rb +76 -55
  26. data/lib/active_record/associations/has_one_association.rb +51 -21
  27. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  28. data/lib/active_record/associations/join_dependency/join_association.rb +83 -108
  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 +239 -155
  32. data/lib/active_record/associations/preloader/association.rb +97 -62
  33. data/lib/active_record/associations/preloader/collection_association.rb +2 -8
  34. data/lib/active_record/associations/preloader/has_many_through.rb +7 -3
  35. data/lib/active_record/associations/preloader/has_one.rb +0 -8
  36. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  37. data/lib/active_record/associations/preloader/through_association.rb +75 -33
  38. data/lib/active_record/associations/preloader.rb +111 -79
  39. data/lib/active_record/associations/singular_association.rb +35 -13
  40. data/lib/active_record/associations/through_association.rb +41 -19
  41. data/lib/active_record/associations.rb +727 -501
  42. data/lib/active_record/attribute/user_provided_default.rb +28 -0
  43. data/lib/active_record/attribute.rb +213 -0
  44. data/lib/active_record/attribute_assignment.rb +32 -162
  45. data/lib/active_record/attribute_decorators.rb +67 -0
  46. data/lib/active_record/attribute_methods/before_type_cast.rb +52 -7
  47. data/lib/active_record/attribute_methods/dirty.rb +101 -61
  48. data/lib/active_record/attribute_methods/primary_key.rb +50 -36
  49. data/lib/active_record/attribute_methods/query.rb +7 -6
  50. data/lib/active_record/attribute_methods/read.rb +56 -117
  51. data/lib/active_record/attribute_methods/serialization.rb +43 -96
  52. data/lib/active_record/attribute_methods/time_zone_conversion.rb +93 -42
  53. data/lib/active_record/attribute_methods/write.rb +34 -45
  54. data/lib/active_record/attribute_methods.rb +333 -144
  55. data/lib/active_record/attribute_mutation_tracker.rb +70 -0
  56. data/lib/active_record/attribute_set/builder.rb +108 -0
  57. data/lib/active_record/attribute_set.rb +108 -0
  58. data/lib/active_record/attributes.rb +265 -0
  59. data/lib/active_record/autosave_association.rb +285 -223
  60. data/lib/active_record/base.rb +95 -490
  61. data/lib/active_record/callbacks.rb +95 -61
  62. data/lib/active_record/coders/json.rb +13 -0
  63. data/lib/active_record/coders/yaml_column.rb +28 -19
  64. data/lib/active_record/collection_cache_key.rb +40 -0
  65. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +724 -277
  66. data/lib/active_record/connection_adapters/abstract/database_limits.rb +9 -0
  67. data/lib/active_record/connection_adapters/abstract/database_statements.rb +199 -192
  68. data/lib/active_record/connection_adapters/abstract/query_cache.rb +31 -26
  69. data/lib/active_record/connection_adapters/abstract/quoting.rb +140 -57
  70. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  71. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +147 -0
  72. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +419 -276
  73. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +105 -0
  74. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +963 -276
  75. data/lib/active_record/connection_adapters/abstract/transaction.rb +232 -0
  76. data/lib/active_record/connection_adapters/abstract_adapter.rb +397 -106
  77. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +643 -342
  78. data/lib/active_record/connection_adapters/column.rb +30 -259
  79. data/lib/active_record/connection_adapters/connection_specification.rb +263 -0
  80. data/lib/active_record/connection_adapters/determine_if_preparable_visitor.rb +22 -0
  81. data/lib/active_record/connection_adapters/mysql/column.rb +50 -0
  82. data/lib/active_record/connection_adapters/mysql/database_statements.rb +125 -0
  83. data/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb +70 -0
  84. data/lib/active_record/connection_adapters/mysql/quoting.rb +51 -0
  85. data/lib/active_record/connection_adapters/mysql/schema_creation.rb +67 -0
  86. data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +93 -0
  87. data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +54 -0
  88. data/lib/active_record/connection_adapters/mysql/type_metadata.rb +32 -0
  89. data/lib/active_record/connection_adapters/mysql2_adapter.rb +47 -196
  90. data/lib/active_record/connection_adapters/postgresql/column.rb +15 -0
  91. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +170 -0
  92. data/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb +42 -0
  93. data/lib/active_record/connection_adapters/postgresql/oid/array.rb +70 -0
  94. data/lib/active_record/connection_adapters/postgresql/oid/bit.rb +52 -0
  95. data/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +13 -0
  96. data/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +15 -0
  97. data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +48 -0
  98. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +21 -0
  99. data/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +13 -0
  100. data/lib/active_record/connection_adapters/postgresql/oid/enum.rb +19 -0
  101. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +59 -0
  102. data/lib/active_record/connection_adapters/postgresql/oid/inet.rb +13 -0
  103. data/lib/active_record/connection_adapters/postgresql/oid/json.rb +10 -0
  104. data/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +23 -0
  105. data/lib/active_record/connection_adapters/postgresql/oid/money.rb +39 -0
  106. data/lib/active_record/connection_adapters/postgresql/oid/point.rb +43 -0
  107. data/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb +50 -0
  108. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +93 -0
  109. data/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +15 -0
  110. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +109 -0
  111. data/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +21 -0
  112. data/lib/active_record/connection_adapters/postgresql/oid/vector.rb +26 -0
  113. data/lib/active_record/connection_adapters/postgresql/oid/xml.rb +28 -0
  114. data/lib/active_record/connection_adapters/postgresql/oid.rb +31 -0
  115. data/lib/active_record/connection_adapters/postgresql/quoting.rb +116 -0
  116. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +49 -0
  117. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +180 -0
  118. data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +47 -0
  119. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +682 -0
  120. data/lib/active_record/connection_adapters/postgresql/type_metadata.rb +35 -0
  121. data/lib/active_record/connection_adapters/postgresql/utils.rb +77 -0
  122. data/lib/active_record/connection_adapters/postgresql_adapter.rb +558 -1039
  123. data/lib/active_record/connection_adapters/schema_cache.rb +74 -36
  124. data/lib/active_record/connection_adapters/sql_type_metadata.rb +32 -0
  125. data/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb +19 -0
  126. data/lib/active_record/connection_adapters/sqlite3/quoting.rb +48 -0
  127. data/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +22 -0
  128. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +538 -24
  129. data/lib/active_record/connection_adapters/statement_pool.rb +31 -12
  130. data/lib/active_record/connection_handling.rb +155 -0
  131. data/lib/active_record/core.rb +561 -0
  132. data/lib/active_record/counter_cache.rb +146 -105
  133. data/lib/active_record/dynamic_matchers.rb +101 -64
  134. data/lib/active_record/enum.rb +234 -0
  135. data/lib/active_record/errors.rb +153 -56
  136. data/lib/active_record/explain.rb +15 -63
  137. data/lib/active_record/explain_registry.rb +30 -0
  138. data/lib/active_record/explain_subscriber.rb +10 -6
  139. data/lib/active_record/fixture_set/file.rb +77 -0
  140. data/lib/active_record/fixtures.rb +355 -232
  141. data/lib/active_record/gem_version.rb +15 -0
  142. data/lib/active_record/inheritance.rb +144 -79
  143. data/lib/active_record/integration.rb +66 -13
  144. data/lib/active_record/internal_metadata.rb +56 -0
  145. data/lib/active_record/legacy_yaml_adapter.rb +46 -0
  146. data/lib/active_record/locale/en.yml +9 -1
  147. data/lib/active_record/locking/optimistic.rb +77 -56
  148. data/lib/active_record/locking/pessimistic.rb +6 -6
  149. data/lib/active_record/log_subscriber.rb +53 -28
  150. data/lib/active_record/migration/command_recorder.rb +166 -33
  151. data/lib/active_record/migration/compatibility.rb +126 -0
  152. data/lib/active_record/migration/join_table.rb +15 -0
  153. data/lib/active_record/migration.rb +792 -264
  154. data/lib/active_record/model_schema.rb +192 -130
  155. data/lib/active_record/nested_attributes.rb +238 -145
  156. data/lib/active_record/no_touching.rb +52 -0
  157. data/lib/active_record/null_relation.rb +89 -0
  158. data/lib/active_record/persistence.rb +357 -157
  159. data/lib/active_record/query_cache.rb +22 -43
  160. data/lib/active_record/querying.rb +34 -23
  161. data/lib/active_record/railtie.rb +88 -48
  162. data/lib/active_record/railties/console_sandbox.rb +3 -4
  163. data/lib/active_record/railties/controller_runtime.rb +5 -4
  164. data/lib/active_record/railties/databases.rake +170 -422
  165. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  166. data/lib/active_record/readonly_attributes.rb +2 -5
  167. data/lib/active_record/reflection.rb +715 -189
  168. data/lib/active_record/relation/batches/batch_enumerator.rb +67 -0
  169. data/lib/active_record/relation/batches.rb +203 -50
  170. data/lib/active_record/relation/calculations.rb +203 -194
  171. data/lib/active_record/relation/delegation.rb +103 -25
  172. data/lib/active_record/relation/finder_methods.rb +457 -261
  173. data/lib/active_record/relation/from_clause.rb +32 -0
  174. data/lib/active_record/relation/merger.rb +167 -0
  175. data/lib/active_record/relation/predicate_builder/array_handler.rb +43 -0
  176. data/lib/active_record/relation/predicate_builder/association_query_handler.rb +88 -0
  177. data/lib/active_record/relation/predicate_builder/base_handler.rb +17 -0
  178. data/lib/active_record/relation/predicate_builder/basic_object_handler.rb +17 -0
  179. data/lib/active_record/relation/predicate_builder/class_handler.rb +27 -0
  180. data/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +57 -0
  181. data/lib/active_record/relation/predicate_builder/range_handler.rb +33 -0
  182. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  183. data/lib/active_record/relation/predicate_builder.rb +153 -48
  184. data/lib/active_record/relation/query_attribute.rb +19 -0
  185. data/lib/active_record/relation/query_methods.rb +1019 -194
  186. data/lib/active_record/relation/record_fetch_warning.rb +49 -0
  187. data/lib/active_record/relation/spawn_methods.rb +46 -150
  188. data/lib/active_record/relation/where_clause.rb +174 -0
  189. data/lib/active_record/relation/where_clause_factory.rb +38 -0
  190. data/lib/active_record/relation.rb +450 -245
  191. data/lib/active_record/result.rb +104 -12
  192. data/lib/active_record/runtime_registry.rb +22 -0
  193. data/lib/active_record/sanitization.rb +120 -94
  194. data/lib/active_record/schema.rb +28 -18
  195. data/lib/active_record/schema_dumper.rb +141 -74
  196. data/lib/active_record/schema_migration.rb +50 -0
  197. data/lib/active_record/scoping/default.rb +64 -57
  198. data/lib/active_record/scoping/named.rb +93 -108
  199. data/lib/active_record/scoping.rb +73 -121
  200. data/lib/active_record/secure_token.rb +38 -0
  201. data/lib/active_record/serialization.rb +7 -5
  202. data/lib/active_record/statement_cache.rb +113 -0
  203. data/lib/active_record/store.rb +173 -15
  204. data/lib/active_record/suppressor.rb +58 -0
  205. data/lib/active_record/table_metadata.rb +68 -0
  206. data/lib/active_record/tasks/database_tasks.rb +313 -0
  207. data/lib/active_record/tasks/mysql_database_tasks.rb +151 -0
  208. data/lib/active_record/tasks/postgresql_database_tasks.rb +110 -0
  209. data/lib/active_record/tasks/sqlite_database_tasks.rb +59 -0
  210. data/lib/active_record/timestamp.rb +42 -24
  211. data/lib/active_record/touch_later.rb +58 -0
  212. data/lib/active_record/transactions.rb +233 -105
  213. data/lib/active_record/type/adapter_specific_registry.rb +130 -0
  214. data/lib/active_record/type/date.rb +7 -0
  215. data/lib/active_record/type/date_time.rb +7 -0
  216. data/lib/active_record/type/hash_lookup_type_map.rb +23 -0
  217. data/lib/active_record/type/internal/abstract_json.rb +29 -0
  218. data/lib/active_record/type/internal/timezone.rb +15 -0
  219. data/lib/active_record/type/serialized.rb +63 -0
  220. data/lib/active_record/type/time.rb +20 -0
  221. data/lib/active_record/type/type_map.rb +64 -0
  222. data/lib/active_record/type.rb +72 -0
  223. data/lib/active_record/type_caster/connection.rb +29 -0
  224. data/lib/active_record/type_caster/map.rb +19 -0
  225. data/lib/active_record/type_caster.rb +7 -0
  226. data/lib/active_record/validations/absence.rb +23 -0
  227. data/lib/active_record/validations/associated.rb +33 -18
  228. data/lib/active_record/validations/length.rb +24 -0
  229. data/lib/active_record/validations/presence.rb +66 -0
  230. data/lib/active_record/validations/uniqueness.rb +128 -68
  231. data/lib/active_record/validations.rb +48 -40
  232. data/lib/active_record/version.rb +5 -7
  233. data/lib/active_record.rb +71 -47
  234. data/lib/rails/generators/active_record/migration/migration_generator.rb +56 -8
  235. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +24 -0
  236. data/lib/rails/generators/active_record/migration/templates/migration.rb +28 -16
  237. data/lib/rails/generators/active_record/migration.rb +18 -8
  238. data/lib/rails/generators/active_record/model/model_generator.rb +38 -16
  239. data/lib/rails/generators/active_record/model/templates/application_record.rb +5 -0
  240. data/lib/rails/generators/active_record/model/templates/model.rb +7 -6
  241. data/lib/rails/generators/active_record/model/templates/module.rb +1 -1
  242. data/lib/rails/generators/active_record.rb +3 -11
  243. metadata +188 -134
  244. data/examples/associations.png +0 -0
  245. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -63
  246. data/lib/active_record/associations/join_helper.rb +0 -55
  247. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  248. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +0 -32
  249. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +0 -191
  250. data/lib/active_record/connection_adapters/mysql_adapter.rb +0 -441
  251. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -583
  252. data/lib/active_record/dynamic_finder_match.rb +0 -68
  253. data/lib/active_record/dynamic_scope_match.rb +0 -23
  254. data/lib/active_record/fixtures/file.rb +0 -65
  255. data/lib/active_record/identity_map.rb +0 -162
  256. data/lib/active_record/observer.rb +0 -121
  257. data/lib/active_record/serializers/xml_serializer.rb +0 -203
  258. data/lib/active_record/session_store.rb +0 -360
  259. data/lib/active_record/test_case.rb +0 -73
  260. data/lib/rails/generators/active_record/model/templates/migration.rb +0 -15
  261. data/lib/rails/generators/active_record/observer/observer_generator.rb +0 -15
  262. data/lib/rails/generators/active_record/observer/templates/observer.rb +0 -4
  263. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +0 -25
  264. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +0 -12
@@ -1,213 +1,297 @@
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
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
9
21
 
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(base.connection, 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)
22
+ def columns
23
+ @tables.flat_map(&:column_aliases)
25
24
  end
26
- self
27
- end
28
25
 
29
- def join_associations
30
- join_parts.last(join_parts.length - 1)
31
- end
26
+ # An array of [column_name, alias] pairs for the table
27
+ def column_aliases(node)
28
+ @name_and_alias_cache[node]
29
+ end
32
30
 
33
- def join_base
34
- join_parts.first
35
- end
31
+ def column_alias(node, column)
32
+ @alias_cache[node][column]
33
+ end
36
34
 
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)
42
- }
43
- }.flatten
44
- end
35
+ class Table < Struct.new(:node, :columns) # :nodoc:
36
+ def table
37
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
38
+ end
45
39
 
46
- def instantiate(rows)
47
- primary_key = join_base.aliased_primary_key
48
- parents = {}
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
49
47
 
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
48
+ attr_reader :alias_tracker, :base_klass, :join_root
56
49
 
57
- remove_duplicate_results!(active_record, records, @associations)
58
- records
50
+ def self.make_tree(associations)
51
+ hash = {}
52
+ walk_tree associations, hash
53
+ hash
59
54
  end
60
55
 
61
- def remove_duplicate_results!(base, records, associations)
56
+ def self.walk_tree(associations, hash)
62
57
  case associations
63
58
  when Symbol, String
64
- reflection = base.reflections[associations]
65
- remove_uniq_by_reflection(reflection, records)
59
+ hash[associations.to_sym] ||= {}
66
60
  when Array
67
- associations.each do |association|
68
- remove_duplicate_results!(base, records, association)
61
+ associations.each do |assoc|
62
+ walk_tree assoc, hash
69
63
  end
70
64
  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
-
86
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
65
+ associations.each do |k,v|
66
+ cache = hash[k] ||= {}
67
+ walk_tree v, cache
87
68
  end
69
+ else
70
+ raise ConfigurationError, associations.inspect
88
71
  end
89
72
  end
90
73
 
91
- protected
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_with_joins(base.connection, base.table_name, joins, base.type_caster)
97
+ tree = self.class.make_tree associations
98
+ @join_root = JoinBase.new base, build(tree, base)
99
+ @join_root.children.each { |child| construct_tables! @join_root, child }
100
+ end
92
101
 
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] ||= {}
102
+ def reflections
103
+ join_root.drop(1).map!(&:reflection)
105
104
  end
106
105
 
107
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
108
- parent ||= join_parts.last
109
- case associations
110
- when Symbol, String
111
- reflection = parent.reflections[associations.to_s.intern] or
112
- raise ConfigurationError, "Association named '#{ associations }' was not found on #{parent.active_record.name}; 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
121
- when Array
122
- associations.each do |association|
123
- build(association, parent, join_type)
106
+ def join_constraints(outer_joins, join_type)
107
+ joins = join_root.children.flat_map { |child|
108
+
109
+ if join_type == Arel::Nodes::OuterJoin
110
+ make_left_outer_joins join_root, child
111
+ else
112
+ make_inner_joins join_root, child
124
113
  end
125
- 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)
114
+ }
115
+
116
+ joins.concat outer_joins.flat_map { |oj|
117
+ if join_root.match? oj.join_root
118
+ walk join_root, oj.join_root
119
+ else
120
+ oj.join_root.children.flat_map { |child|
121
+ make_outer_joins oj.join_root, child
122
+ }
129
123
  end
130
- else
131
- raise ConfigurationError, associations.inspect
132
- end
124
+ }
125
+ end
126
+
127
+ def aliases
128
+ Aliases.new join_root.each_with_index.map { |join_part,i|
129
+ columns = join_part.column_names.each_with_index.map { |column_name,j|
130
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
131
+ }
132
+ Aliases::Table.new(join_part, columns)
133
+ }
133
134
  end
134
135
 
135
- def find_join_association(name_or_reflection, parent)
136
- if String === name_or_reflection
137
- name_or_reflection = name_or_reflection.to_sym
136
+ def instantiate(result_set, aliases)
137
+ primary_key = aliases.column_alias(join_root, join_root.primary_key)
138
+
139
+ seen = Hash.new { |i, object_id|
140
+ i[object_id] = Hash.new { |j, child_class|
141
+ j[child_class] = {}
142
+ }
143
+ }
144
+
145
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
146
+ parents = model_cache[join_root]
147
+ column_aliases = aliases.column_aliases join_root
148
+
149
+ message_bus = ActiveSupport::Notifications.instrumenter
150
+
151
+ payload = {
152
+ record_count: result_set.length,
153
+ class_name: join_root.base_klass.name
154
+ }
155
+
156
+ message_bus.instrument('instantiation.active_record', payload) do
157
+ result_set.each { |row_hash|
158
+ parent_key = primary_key ? row_hash[primary_key] : row_hash
159
+ parent = parents[parent_key] ||= join_root.instantiate(row_hash, column_aliases)
160
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
161
+ }
138
162
  end
139
163
 
140
- join_associations.detect { |j|
141
- j.reflection == name_or_reflection && j.parent == parent
164
+ parents.values
165
+ end
166
+
167
+ private
168
+
169
+ def make_constraints(parent, child, tables, join_type)
170
+ chain = child.reflection.chain
171
+ foreign_table = parent.table
172
+ foreign_klass = parent.base_klass
173
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
174
+ end
175
+
176
+ def make_outer_joins(parent, child)
177
+ tables = table_aliases_for(parent, child)
178
+ join_type = Arel::Nodes::OuterJoin
179
+ info = make_constraints parent, child, tables, join_type
180
+
181
+ [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
182
+ end
183
+
184
+ def make_left_outer_joins(parent, child)
185
+ tables = child.tables
186
+ join_type = Arel::Nodes::OuterJoin
187
+ info = make_constraints parent, child, tables, join_type
188
+
189
+ [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) }
190
+ end
191
+
192
+ def make_inner_joins(parent, child)
193
+ tables = child.tables
194
+ join_type = Arel::Nodes::InnerJoin
195
+ info = make_constraints parent, child, tables, join_type
196
+
197
+ [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
198
+ end
199
+
200
+ def table_aliases_for(parent, node)
201
+ node.reflection.chain.map { |reflection|
202
+ alias_tracker.aliased_table_for(
203
+ reflection.table_name,
204
+ table_alias_for(reflection, parent, reflection != node.reflection)
205
+ )
142
206
  }
143
207
  end
144
208
 
145
- def remove_uniq_by_reflection(reflection, records)
146
- if reflection && reflection.collection?
147
- records.each { |record| record.send(reflection.name).target.uniq! }
148
- end
209
+ def construct_tables!(parent, node)
210
+ node.tables = table_aliases_for(parent, node)
211
+ node.children.each { |child| construct_tables! node, child }
149
212
  end
150
213
 
151
- def build_join_association(reflection, parent)
152
- JoinAssociation.new(reflection, self, parent)
214
+ def table_alias_for(reflection, parent, join)
215
+ name = "#{reflection.plural_name}_#{parent.table_name}"
216
+ name << "_join" if join
217
+ name
153
218
  end
154
219
 
155
- def construct(parent, associations, join_parts, row)
156
- case associations
157
- when Symbol, String
158
- name = associations.to_s
220
+ def walk(left, right)
221
+ intersection, missing = right.children.map { |node1|
222
+ [left.children.find { |node2| node1.match? node2 }, node1]
223
+ }.partition(&:first)
159
224
 
160
- join_part = join_parts.detect { |j|
161
- j.reflection.name.to_s == name &&
162
- j.parent_table_name == parent.class.table_name }
225
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
226
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
227
+ end
163
228
 
164
- raise(ConfigurationError, "No such association") unless join_part
229
+ def find_reflection(klass, name)
230
+ klass._reflect_on_association(name) or
231
+ raise ConfigurationError, "Can't join '#{ klass.name }' to association named '#{ name }'; perhaps you misspelled it?"
232
+ end
165
233
 
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
234
+ def build(associations, base_klass)
235
+ associations.map do |name, right|
236
+ reflection = find_reflection base_klass, name
237
+ reflection.check_validity!
238
+ reflection.check_eager_loadable!
239
+
240
+ if reflection.polymorphic?
241
+ raise EagerLoadPolymorphicError.new(reflection)
176
242
  end
177
- else
178
- raise ConfigurationError, associations.inspect
243
+
244
+ JoinAssociation.new reflection, build(right, reflection.klass)
179
245
  end
180
246
  end
181
247
 
182
- def construct_association(record, join_part, row)
183
- return if record.id.to_s != join_part.parent.record_id(row).to_s
248
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
249
+ return if ar_parent.nil?
184
250
 
185
- macro = join_part.reflection.macro
186
- if macro == :has_one
187
- return record.association(join_part.reflection.name).target 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)
251
+ parent.children.each do |node|
252
+ if node.reflection.collection?
253
+ other = ar_parent.association(node.reflection.name)
195
254
  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)
255
+ elsif ar_parent.association_cached?(node.reflection.name)
256
+ model = ar_parent.association(node.reflection.name).target
257
+ construct(model, node, row, rs, seen, model_cache, aliases)
258
+ next
259
+ end
260
+
261
+ key = aliases.column_alias(node, node.primary_key)
262
+ id = row[key]
263
+ if id.nil?
264
+ nil_association = ar_parent.association(node.reflection.name)
265
+ nil_association.loaded!
266
+ next
267
+ end
268
+
269
+ model = seen[ar_parent.object_id][node.base_klass][id]
270
+
271
+ if model
272
+ construct(model, node, row, rs, seen, model_cache, aliases)
200
273
  else
201
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
274
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
275
+ model.readonly!
276
+ seen[ar_parent.object_id][node.base_klass][id] = model
277
+ construct(model, node, row, rs, seen, model_cache, aliases)
202
278
  end
203
279
  end
204
- association
205
280
  end
206
281
 
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)
282
+ def construct_model(record, node, row, model_cache, id, aliases)
283
+ model = model_cache[node][id] ||= node.instantiate(row,
284
+ aliases.column_aliases(node))
285
+ other = record.association(node.reflection.name)
286
+
287
+ if node.reflection.collection?
288
+ other.target.push(model)
289
+ else
290
+ other.target = model
291
+ end
292
+
293
+ other.set_inverse_instance(model)
294
+ model
211
295
  end
212
296
  end
213
297
  end
@@ -2,34 +2,37 @@ 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
+ @preloaded_records = []
15
16
  end
16
17
 
17
- def run
18
- unless owners.first.association(reflection.name).loaded?
19
- preload
20
- end
18
+ def run(preloader)
19
+ preload(preloader)
21
20
  end
22
21
 
23
- def preload
22
+ def preload(preloader)
24
23
  raise NotImplementedError
25
24
  end
26
25
 
27
- def scoped
28
- @scoped ||= build_scope
26
+ def scope
27
+ @scope ||= build_scope
29
28
  end
30
29
 
31
30
  def records_for(ids)
32
- scoped.where(association_key.in(ids))
31
+ query_scope(ids)
32
+ end
33
+
34
+ def query_scope(ids)
35
+ scope.where(association_key_name => ids)
33
36
  end
34
37
 
35
38
  def table
@@ -44,7 +47,7 @@ module ActiveRecord
44
47
  # This is overridden by HABTM as the condition should be on the foreign_key column in
45
48
  # the join table
46
49
  def association_key
47
- table[association_key_name]
50
+ klass.arel_attribute(association_key_name, table)
48
51
  end
49
52
 
50
53
  # The name of the key on the model which declares the association
@@ -52,72 +55,104 @@ module ActiveRecord
52
55
  raise NotImplementedError
53
56
  end
54
57
 
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
- 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
62
- end
63
-
64
58
  def options
65
59
  reflection.options
66
60
  end
67
61
 
68
62
  private
69
63
 
70
- def associated_records_by_owner
71
- owners_map = owners_by_key
72
- owner_keys = owners_map.keys.compact
64
+ def associated_records_by_owner(preloader)
65
+ records = load_records
66
+ owners.each_with_object({}) do |owner, result|
67
+ result[owner] = records[convert_key(owner[owner_key_name])] || []
68
+ end
69
+ end
73
70
 
74
- if klass.nil? || owner_keys.empty?
75
- records = []
71
+ def owner_keys
72
+ unless defined?(@owner_keys)
73
+ @owner_keys = owners.map do |owner|
74
+ owner[owner_key_name]
75
+ end
76
+ @owner_keys.uniq!
77
+ @owner_keys.compact!
78
+ end
79
+ @owner_keys
80
+ end
81
+
82
+ def key_conversion_required?
83
+ @key_conversion_required ||= association_key_type != owner_key_type
84
+ end
85
+
86
+ def convert_key(key)
87
+ if key_conversion_required?
88
+ key.to_s
76
89
  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
90
+ key
81
91
  end
92
+ end
82
93
 
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
94
+ def association_key_type
95
+ @klass.type_for_attribute(association_key_name.to_s).type
96
+ end
87
97
 
88
- owners_map[owner_key].each do |owner|
89
- records_by_owner[owner] << record
90
- end
98
+ def owner_key_type
99
+ @model.type_for_attribute(owner_key_name.to_s).type
100
+ end
101
+
102
+ def load_records
103
+ return {} if owner_keys.empty?
104
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
105
+ # Make several smaller queries if necessary or make one query if the adapter supports it
106
+ slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
107
+ @preloaded_records = slices.flat_map do |slice|
108
+ records_for(slice)
109
+ end
110
+ @preloaded_records.group_by do |record|
111
+ convert_key(record[association_key_name])
91
112
  end
92
- records_by_owner
113
+ end
114
+
115
+ def reflection_scope
116
+ @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
93
117
  end
94
118
 
95
119
  def build_scope
96
- scope = klass.scoped
120
+ scope = klass.unscoped
97
121
 
98
- scope = scope.where(process_conditions(options[:conditions]))
99
- scope = scope.where(process_conditions(preload_options[:conditions]))
122
+ values = reflection_scope.values
123
+ preload_values = preload_scope.values
100
124
 
101
- scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
102
- scope = scope.includes(preload_options[:include] || options[:include])
125
+ scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause
126
+ scope.references_values = Array(values[:references]) + Array(preload_values[:references])
103
127
 
104
- if options[:as]
105
- scope = scope.where(
106
- klass.table_name => {
107
- reflection.type => model.base_class.sti_name
108
- }
109
- )
128
+ if preload_values[:select] || values[:select]
129
+ scope._select!(preload_values[:select] || values[:select])
130
+ end
131
+ scope.includes! preload_values[:includes] || values[:includes]
132
+ if preload_scope.joins_values.any?
133
+ scope.joins!(preload_scope.joins_values)
134
+ else
135
+ scope.joins!(reflection_scope.joins_values)
110
136
  end
111
137
 
112
- scope
113
- end
138
+ if order_values = preload_values[:order] || values[:order]
139
+ scope.order!(order_values)
140
+ end
141
+
142
+ if preload_values[:reordering] || values[:reordering]
143
+ scope.reordering_value = true
144
+ end
145
+
146
+ if preload_values[:readonly] || values[:readonly]
147
+ scope.readonly!
148
+ end
114
149
 
115
- def process_conditions(conditions)
116
- if conditions.respond_to?(:to_proc)
117
- conditions = klass.send(:instance_eval, &conditions)
150
+ if options[:as]
151
+ scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
118
152
  end
119
153
 
120
- conditions
154
+ scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope])
155
+ klass.default_scoped.merge(scope)
121
156
  end
122
157
  end
123
158
  end