activerecord 4.0.13 → 4.1.0.beta1

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

Potentially problematic release.


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

Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +745 -2700
  3. data/README.rdoc +2 -2
  4. data/examples/performance.rb +30 -18
  5. data/examples/simple.rb +4 -4
  6. data/lib/active_record.rb +2 -6
  7. data/lib/active_record/aggregations.rb +2 -1
  8. data/lib/active_record/association_relation.rb +0 -4
  9. data/lib/active_record/associations.rb +87 -43
  10. data/lib/active_record/associations/alias_tracker.rb +1 -3
  11. data/lib/active_record/associations/association.rb +8 -16
  12. data/lib/active_record/associations/association_scope.rb +5 -16
  13. data/lib/active_record/associations/belongs_to_association.rb +34 -25
  14. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +1 -1
  15. data/lib/active_record/associations/builder/association.rb +78 -54
  16. data/lib/active_record/associations/builder/belongs_to.rb +91 -58
  17. data/lib/active_record/associations/builder/collection_association.rb +47 -45
  18. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +107 -25
  19. data/lib/active_record/associations/builder/has_many.rb +2 -2
  20. data/lib/active_record/associations/builder/has_one.rb +5 -7
  21. data/lib/active_record/associations/builder/singular_association.rb +6 -7
  22. data/lib/active_record/associations/collection_association.rb +68 -105
  23. data/lib/active_record/associations/collection_proxy.rb +12 -15
  24. data/lib/active_record/associations/has_many_association.rb +11 -9
  25. data/lib/active_record/associations/has_many_through_association.rb +16 -12
  26. data/lib/active_record/associations/has_one_association.rb +1 -1
  27. data/lib/active_record/associations/join_dependency.rb +204 -165
  28. data/lib/active_record/associations/join_dependency/join_association.rb +43 -101
  29. data/lib/active_record/associations/join_dependency/join_base.rb +6 -8
  30. data/lib/active_record/associations/join_dependency/join_part.rb +18 -37
  31. data/lib/active_record/associations/join_helper.rb +2 -11
  32. data/lib/active_record/associations/preloader.rb +89 -34
  33. data/lib/active_record/associations/preloader/association.rb +43 -25
  34. data/lib/active_record/associations/preloader/collection_association.rb +2 -2
  35. data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
  36. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  37. data/lib/active_record/associations/preloader/through_association.rb +58 -26
  38. data/lib/active_record/associations/singular_association.rb +6 -5
  39. data/lib/active_record/associations/through_association.rb +2 -2
  40. data/lib/active_record/attribute_assignment.rb +5 -2
  41. data/lib/active_record/attribute_methods.rb +45 -40
  42. data/lib/active_record/attribute_methods/before_type_cast.rb +2 -1
  43. data/lib/active_record/attribute_methods/dirty.rb +8 -22
  44. data/lib/active_record/attribute_methods/primary_key.rb +1 -7
  45. data/lib/active_record/attribute_methods/read.rb +55 -28
  46. data/lib/active_record/attribute_methods/serialization.rb +12 -33
  47. data/lib/active_record/attribute_methods/time_zone_conversion.rb +1 -13
  48. data/lib/active_record/attribute_methods/write.rb +37 -12
  49. data/lib/active_record/autosave_association.rb +207 -207
  50. data/lib/active_record/base.rb +5 -1
  51. data/lib/active_record/callbacks.rb +2 -2
  52. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +2 -7
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +11 -22
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +12 -14
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +1 -5
  56. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  57. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +84 -0
  58. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +9 -8
  59. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +52 -83
  60. data/lib/active_record/connection_adapters/abstract/transaction.rb +0 -5
  61. data/lib/active_record/connection_adapters/abstract_adapter.rb +14 -97
  62. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +58 -60
  63. data/lib/active_record/connection_adapters/column.rb +1 -35
  64. data/lib/active_record/connection_adapters/connection_specification.rb +2 -2
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +3 -4
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +16 -15
  67. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +24 -18
  68. data/lib/active_record/connection_adapters/postgresql/cast.rb +20 -16
  69. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +23 -43
  70. data/lib/active_record/connection_adapters/postgresql/oid.rb +19 -12
  71. data/lib/active_record/connection_adapters/postgresql/quoting.rb +28 -23
  72. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +8 -30
  73. data/lib/active_record/connection_adapters/postgresql_adapter.rb +92 -75
  74. data/lib/active_record/connection_adapters/schema_cache.rb +8 -29
  75. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +31 -64
  76. data/lib/active_record/connection_handling.rb +2 -2
  77. data/lib/active_record/core.rb +22 -43
  78. data/lib/active_record/counter_cache.rb +7 -7
  79. data/lib/active_record/enum.rb +100 -0
  80. data/lib/active_record/errors.rb +10 -5
  81. data/lib/active_record/fixture_set/file.rb +2 -1
  82. data/lib/active_record/fixtures.rb +171 -74
  83. data/lib/active_record/inheritance.rb +16 -22
  84. data/lib/active_record/integration.rb +52 -1
  85. data/lib/active_record/locking/optimistic.rb +7 -2
  86. data/lib/active_record/locking/pessimistic.rb +1 -1
  87. data/lib/active_record/log_subscriber.rb +5 -12
  88. data/lib/active_record/migration.rb +62 -46
  89. data/lib/active_record/migration/command_recorder.rb +7 -13
  90. data/lib/active_record/model_schema.rb +7 -14
  91. data/lib/active_record/nested_attributes.rb +10 -8
  92. data/lib/active_record/no_touching.rb +52 -0
  93. data/lib/active_record/null_relation.rb +3 -3
  94. data/lib/active_record/persistence.rb +16 -34
  95. data/lib/active_record/querying.rb +14 -12
  96. data/lib/active_record/railtie.rb +0 -50
  97. data/lib/active_record/railties/databases.rake +12 -15
  98. data/lib/active_record/readonly_attributes.rb +0 -6
  99. data/lib/active_record/reflection.rb +189 -75
  100. data/lib/active_record/relation.rb +69 -94
  101. data/lib/active_record/relation/batches.rb +57 -23
  102. data/lib/active_record/relation/calculations.rb +36 -43
  103. data/lib/active_record/relation/delegation.rb +54 -39
  104. data/lib/active_record/relation/finder_methods.rb +107 -62
  105. data/lib/active_record/relation/merger.rb +7 -20
  106. data/lib/active_record/relation/predicate_builder.rb +57 -38
  107. data/lib/active_record/relation/predicate_builder/array_handler.rb +29 -0
  108. data/lib/active_record/relation/predicate_builder/relation_handler.rb +13 -0
  109. data/lib/active_record/relation/query_methods.rb +110 -98
  110. data/lib/active_record/relation/spawn_methods.rb +1 -2
  111. data/lib/active_record/result.rb +45 -6
  112. data/lib/active_record/runtime_registry.rb +5 -0
  113. data/lib/active_record/sanitization.rb +6 -8
  114. data/lib/active_record/schema_dumper.rb +16 -5
  115. data/lib/active_record/schema_migration.rb +24 -25
  116. data/lib/active_record/scoping/default.rb +5 -18
  117. data/lib/active_record/scoping/named.rb +8 -29
  118. data/lib/active_record/store.rb +56 -28
  119. data/lib/active_record/tasks/database_tasks.rb +8 -4
  120. data/lib/active_record/timestamp.rb +4 -4
  121. data/lib/active_record/transactions.rb +8 -10
  122. data/lib/active_record/validations/presence.rb +1 -1
  123. data/lib/active_record/validations/uniqueness.rb +1 -6
  124. data/lib/active_record/version.rb +1 -1
  125. data/lib/rails/generators/active_record.rb +2 -8
  126. data/lib/rails/generators/active_record/migration.rb +18 -0
  127. data/lib/rails/generators/active_record/migration/migration_generator.rb +4 -0
  128. data/lib/rails/generators/active_record/model/model_generator.rb +4 -0
  129. metadata +32 -45
  130. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -65
  131. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  132. data/lib/active_record/tasks/firebird_database_tasks.rb +0 -56
  133. data/lib/active_record/tasks/oracle_database_tasks.rb +0 -45
  134. data/lib/active_record/tasks/sqlserver_database_tasks.rb +0 -48
  135. data/lib/active_record/test_case.rb +0 -102
@@ -33,7 +33,6 @@ module ActiveRecord
33
33
  def initialize(klass, association) #:nodoc:
34
34
  @association = association
35
35
  super klass, klass.arel_table
36
- self.default_scoped = true
37
36
  merge! association.scope(nullify: false)
38
37
  end
39
38
 
@@ -418,13 +417,13 @@ module ActiveRecord
418
417
  #
419
418
  # Pet.find(1, 2, 3)
420
419
  # # => ActiveRecord::RecordNotFound
421
- def delete_all
422
- @association.delete_all
420
+ def delete_all(dependent = nil)
421
+ @association.delete_all(dependent)
423
422
  end
424
423
 
425
- # Deletes the records of the collection directly from the database.
426
- # This will _always_ remove the records ignoring the +:dependent+
427
- # option.
424
+ # Deletes the records of the collection directly from the database
425
+ # ignoring the +:dependent+ option. It invokes +before_remove+,
426
+ # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
428
427
  #
429
428
  # class Person < ActiveRecord::Base
430
429
  # has_many :pets
@@ -671,6 +670,8 @@ module ActiveRecord
671
670
  # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
672
671
  # # ]
673
672
  def count(column_name = nil, options = {})
673
+ # TODO: Remove options argument as soon we remove support to
674
+ # activerecord-deprecated_finders.
674
675
  @association.count(column_name, options)
675
676
  end
676
677
 
@@ -727,7 +728,7 @@ module ActiveRecord
727
728
  end
728
729
 
729
730
  # Returns +true+ if the collection is empty. If the collection has been
730
- # loaded or the <tt>:counter_sql</tt> option is provided, it is equivalent
731
+ # loaded it is equivalent
731
732
  # to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
732
733
  # it is equivalent to <tt>collection.exists?</tt>. If the collection has
733
734
  # not already been loaded and you are going to fetch the records anyway it
@@ -788,12 +789,12 @@ module ActiveRecord
788
789
  # has_many :pets
789
790
  # end
790
791
  #
791
- # person.pets.count #=> 1
792
- # person.pets.many? #=> false
792
+ # person.pets.count # => 1
793
+ # person.pets.many? # => false
793
794
  #
794
795
  # person.pets << Pet.new(name: 'Snoopy')
795
- # person.pets.count #=> 2
796
- # person.pets.many? #=> true
796
+ # person.pets.count # => 2
797
+ # person.pets.many? # => true
797
798
  #
798
799
  # You can also pass a block to define criteria. The
799
800
  # behavior is the same, it returns true if the collection
@@ -833,10 +834,6 @@ module ActiveRecord
833
834
  !!@association.include?(record)
834
835
  end
835
836
 
836
- def arel
837
- scope.arel
838
- end
839
-
840
837
  def proxy_association
841
838
  @association
842
839
  end
@@ -9,7 +9,7 @@ module ActiveRecord
9
9
 
10
10
  def handle_dependency
11
11
  case options[:dependent]
12
- when :restrict, :restrict_with_exception
12
+ when :restrict_with_exception
13
13
  raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
14
14
 
15
15
  when :restrict_with_error
@@ -59,8 +59,6 @@ module ActiveRecord
59
59
  def count_records
60
60
  count = if has_cached_counter?
61
61
  owner.send(:read_attribute, cached_counter_attribute_name)
62
- elsif options[:counter_sql] || options[:finder_sql]
63
- reflection.klass.count_by_sql(custom_counter_sql)
64
62
  else
65
63
  scope.count
66
64
  end
@@ -73,15 +71,15 @@ module ActiveRecord
73
71
  [association_scope.limit_value, count].compact.min
74
72
  end
75
73
 
76
- def has_cached_counter?(reflection = reflection())
74
+ def has_cached_counter?(reflection = reflection)
77
75
  owner.attribute_present?(cached_counter_attribute_name(reflection))
78
76
  end
79
77
 
80
- def cached_counter_attribute_name(reflection = reflection())
78
+ def cached_counter_attribute_name(reflection = reflection)
81
79
  options[:counter_cache] || "#{reflection.name}_count"
82
80
  end
83
81
 
84
- def update_counter(difference, reflection = reflection())
82
+ def update_counter(difference, reflection = reflection)
85
83
  if has_cached_counter?(reflection)
86
84
  counter = cached_counter_attribute_name(reflection)
87
85
  owner.class.update_counters(owner.id, counter => difference)
@@ -100,7 +98,7 @@ module ActiveRecord
100
98
  # it will be decremented twice.
101
99
  #
102
100
  # Hence this method.
103
- def inverse_updates_counter_cache?(reflection = reflection())
101
+ def inverse_updates_counter_cache?(reflection = reflection)
104
102
  counter_name = cached_counter_attribute_name(reflection)
105
103
  reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
106
104
  inverse_reflection.counter_cache_column == counter_name
@@ -110,7 +108,7 @@ module ActiveRecord
110
108
  # Deletes the records according to the <tt>:dependent</tt> option.
111
109
  def delete_records(records, method)
112
110
  if method == :destroy
113
- records.each { |r| r.destroy }
111
+ records.each(&:destroy!)
114
112
  update_counter(-records.length) unless inverse_updates_counter_cache?
115
113
  else
116
114
  if records == :all
@@ -128,7 +126,11 @@ module ActiveRecord
128
126
  end
129
127
 
130
128
  def foreign_key_present?
131
- owner.attribute_present?(reflection.association_primary_key)
129
+ if reflection.klass.primary_key
130
+ owner.attribute_present?(reflection.association_primary_key)
131
+ else
132
+ false
133
+ end
132
134
  end
133
135
  end
134
136
  end
@@ -84,22 +84,12 @@ module ActiveRecord
84
84
  @through_records[record.object_id] ||= begin
85
85
  ensure_mutable
86
86
 
87
- through_record = through_association.build(*options_for_through_record)
87
+ through_record = through_association.build
88
88
  through_record.send("#{source_reflection.name}=", record)
89
89
  through_record
90
90
  end
91
91
  end
92
92
 
93
- def options_for_through_record
94
- [through_scope_attributes]
95
- end
96
-
97
- def through_scope_attributes
98
- scope.where_values_hash(through_association.reflection.name.to_s).
99
- except!(through_association.reflection.foreign_key,
100
- through_association.reflection.klass.inheritance_column.to_sym)
101
- end
102
-
103
93
  def save_through_record(record)
104
94
  build_through_record(record).save!
105
95
  ensure
@@ -150,7 +140,21 @@ module ActiveRecord
150
140
 
151
141
  case method
152
142
  when :destroy
153
- count = scope.destroy_all.length
143
+ if scope.klass.primary_key
144
+ count = scope.destroy_all.length
145
+ else
146
+ scope.to_a.each do |record|
147
+ record.run_callbacks :destroy
148
+ end
149
+
150
+ arel = scope.arel
151
+
152
+ stmt = Arel::DeleteManager.new arel.engine
153
+ stmt.from scope.klass.arel_table
154
+ stmt.wheres = arel.constraints
155
+
156
+ count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
157
+ end
154
158
  when :nullify
155
159
  count = scope.update_all(source_reflection.foreign_key => nil)
156
160
  else
@@ -6,7 +6,7 @@ module ActiveRecord
6
6
 
7
7
  def handle_dependency
8
8
  case options[:dependent]
9
- when :restrict, :restrict_with_exception
9
+ when :restrict_with_exception
10
10
  raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target
11
11
 
12
12
  when :restrict_with_error
@@ -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,191 @@ 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
96
  @alias_tracker = AliasTracker.new(base.connection, joins)
38
97
  @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
39
- build(associations)
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 }
40
101
  end
41
102
 
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
103
+ def reflections
104
+ join_root.drop(1).map!(&:reflection)
48
105
  end
49
106
 
50
- def join_associations
51
- join_parts.last(join_parts.length - 1)
52
- end
107
+ def join_constraints(outer_joins)
108
+ joins = join_root.children.flat_map { |child|
109
+ make_inner_joins join_root, child
110
+ }
53
111
 
54
- def join_base
55
- join_parts.first
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 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
+ primary_id = type_caster.type_cast row_hash[primary_key]
148
+ parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
149
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
150
+ }
108
151
 
109
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
110
- end
111
- end
152
+ parents.values
112
153
  end
113
154
 
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] ||= {}
155
+ private
156
+
157
+ def make_constraints(parent, child, tables, join_type)
158
+ chain = child.reflection.chain
159
+ foreign_table = parent.table
160
+ foreign_klass = parent.base_klass
161
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
126
162
  end
127
163
 
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
164
+ def make_outer_joins(parent, child)
165
+ tables = table_aliases_for(parent, child)
166
+ join_type = Arel::OuterJoin
167
+ joins = make_constraints parent, child, tables, join_type
168
+
169
+ joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
154
170
  end
155
171
 
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
172
+ def make_inner_joins(parent, child)
173
+ tables = child.tables
174
+ join_type = Arel::InnerJoin
175
+ joins = make_constraints parent, child, tables, join_type
160
176
 
161
- join_associations.detect { |j|
162
- j.reflection == name_or_reflection && j.parent == parent
177
+ joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
178
+ end
179
+
180
+ def table_aliases_for(parent, node)
181
+ node.reflection.chain.map { |reflection|
182
+ alias_tracker.aliased_table_for(
183
+ reflection.table_name,
184
+ table_alias_for(reflection, parent, reflection != node.reflection)
185
+ )
163
186
  }
164
187
  end
165
188
 
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
189
+ def construct_tables!(parent, node)
190
+ node.tables = table_aliases_for(parent, node)
191
+ node.children.each { |child| construct_tables! node, child }
170
192
  end
171
193
 
172
- def build_join_association(reflection, parent)
173
- JoinAssociation.new(reflection, self, parent)
194
+ def table_alias_for(reflection, parent, join)
195
+ name = "#{reflection.plural_name}_#{parent.table_name}"
196
+ name << "_join" if join
197
+ name
174
198
  end
175
199
 
176
- def construct(parent, associations, join_parts, row)
177
- case associations
178
- when Symbol, String
179
- name = associations.to_s
200
+ def walk(left, right)
201
+ intersection, missing = right.children.map { |node1|
202
+ [left.children.find { |node2| node1.match? node2 }, node1]
203
+ }.partition(&:first)
180
204
 
181
- join_part = join_parts.detect { |j|
182
- j.reflection.name.to_s == name &&
183
- j.parent_table_name == parent.class.table_name }
205
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
206
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
207
+ end
184
208
 
185
- raise(ConfigurationError, "No such association") unless join_part
209
+ def find_reflection(klass, name)
210
+ klass.reflect_on_association(name) or
211
+ raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
212
+ end
186
213
 
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
214
+ def build(associations, base_klass)
215
+ associations.map do |name, right|
216
+ reflection = find_reflection base_klass, name
217
+ reflection.check_validity!
218
+
219
+ if reflection.options[:polymorphic]
220
+ raise EagerLoadPolymorphicError.new(reflection)
197
221
  end
198
- else
199
- raise ConfigurationError, associations.inspect
222
+
223
+ JoinAssociation.new reflection, build(right, reflection.klass)
200
224
  end
201
225
  end
202
226
 
203
- def construct_association(record, join_part, row)
204
- return if record.id.to_s != join_part.parent.record_id(row).to_s
227
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
228
+ primary_id = ar_parent.id
205
229
 
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)
230
+ parent.children.each do |node|
231
+ if node.reflection.collection?
232
+ other = ar_parent.association(node.reflection.name)
216
233
  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
234
  else
222
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
235
+ if ar_parent.association_cache.key?(node.reflection.name)
236
+ model = ar_parent.association(node.reflection.name).target
237
+ construct(model, node, row, rs, seen, model_cache, aliases)
238
+ next
239
+ end
240
+ end
241
+
242
+ key = aliases.column_alias(node, node.primary_key)
243
+ id = row[key]
244
+ next if id.nil?
245
+
246
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
247
+
248
+ if model
249
+ construct(model, node, row, rs, seen, model_cache, aliases)
250
+ else
251
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
252
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
253
+ construct(model, node, row, rs, seen, model_cache, aliases)
223
254
  end
224
255
  end
225
- association
226
256
  end
227
257
 
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)
258
+ def construct_model(record, node, row, model_cache, id, aliases)
259
+ model = model_cache[node][id] ||= node.instantiate(row,
260
+ aliases.column_aliases(node))
261
+ other = record.association(node.reflection.name)
262
+
263
+ if node.reflection.collection?
264
+ other.target.push(model)
265
+ else
266
+ other.target = model
267
+ end
268
+
269
+ other.set_inverse_instance(model)
270
+ model
232
271
  end
233
272
  end
234
273
  end