activerecord 4.0.4 → 4.1.16

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

Potentially problematic release.


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

Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1632 -1797
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/examples/performance.rb +30 -18
  6. data/examples/simple.rb +4 -4
  7. data/lib/active_record/aggregations.rb +2 -1
  8. data/lib/active_record/association_relation.rb +4 -0
  9. data/lib/active_record/associations/alias_tracker.rb +49 -29
  10. data/lib/active_record/associations/association.rb +9 -17
  11. data/lib/active_record/associations/association_scope.rb +59 -49
  12. data/lib/active_record/associations/belongs_to_association.rb +34 -25
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +6 -1
  14. data/lib/active_record/associations/builder/association.rb +84 -54
  15. data/lib/active_record/associations/builder/belongs_to.rb +90 -58
  16. data/lib/active_record/associations/builder/collection_association.rb +47 -45
  17. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +119 -25
  18. data/lib/active_record/associations/builder/has_many.rb +3 -3
  19. data/lib/active_record/associations/builder/has_one.rb +5 -7
  20. data/lib/active_record/associations/builder/singular_association.rb +6 -7
  21. data/lib/active_record/associations/collection_association.rb +121 -111
  22. data/lib/active_record/associations/collection_proxy.rb +73 -18
  23. data/lib/active_record/associations/has_many_association.rb +14 -11
  24. data/lib/active_record/associations/has_many_through_association.rb +33 -6
  25. data/lib/active_record/associations/has_one_association.rb +1 -1
  26. data/lib/active_record/associations/join_dependency/join_association.rb +46 -104
  27. data/lib/active_record/associations/join_dependency/join_base.rb +6 -8
  28. data/lib/active_record/associations/join_dependency/join_part.rb +18 -37
  29. data/lib/active_record/associations/join_dependency.rb +208 -168
  30. data/lib/active_record/associations/preloader/association.rb +69 -27
  31. data/lib/active_record/associations/preloader/collection_association.rb +2 -2
  32. data/lib/active_record/associations/preloader/has_many_through.rb +1 -1
  33. data/lib/active_record/associations/preloader/singular_association.rb +3 -3
  34. data/lib/active_record/associations/preloader/through_association.rb +58 -26
  35. data/lib/active_record/associations/preloader.rb +63 -49
  36. data/lib/active_record/associations/singular_association.rb +6 -5
  37. data/lib/active_record/associations/through_association.rb +30 -9
  38. data/lib/active_record/associations.rb +116 -42
  39. data/lib/active_record/attribute_assignment.rb +6 -3
  40. data/lib/active_record/attribute_methods/before_type_cast.rb +2 -1
  41. data/lib/active_record/attribute_methods/dirty.rb +35 -26
  42. data/lib/active_record/attribute_methods/primary_key.rb +8 -1
  43. data/lib/active_record/attribute_methods/read.rb +56 -29
  44. data/lib/active_record/attribute_methods/serialization.rb +44 -12
  45. data/lib/active_record/attribute_methods/time_zone_conversion.rb +13 -1
  46. data/lib/active_record/attribute_methods/write.rb +59 -26
  47. data/lib/active_record/attribute_methods.rb +82 -43
  48. data/lib/active_record/autosave_association.rb +209 -194
  49. data/lib/active_record/base.rb +6 -2
  50. data/lib/active_record/callbacks.rb +2 -2
  51. data/lib/active_record/coders/json.rb +13 -0
  52. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +5 -10
  53. data/lib/active_record/connection_adapters/abstract/database_statements.rb +14 -24
  54. data/lib/active_record/connection_adapters/abstract/query_cache.rb +13 -13
  55. data/lib/active_record/connection_adapters/abstract/quoting.rb +6 -3
  56. data/lib/active_record/connection_adapters/abstract/savepoints.rb +21 -0
  57. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +90 -0
  58. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +9 -8
  59. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +45 -70
  60. data/lib/active_record/connection_adapters/abstract/transaction.rb +1 -0
  61. data/lib/active_record/connection_adapters/abstract_adapter.rb +28 -96
  62. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +74 -66
  63. data/lib/active_record/connection_adapters/column.rb +1 -35
  64. data/lib/active_record/connection_adapters/connection_specification.rb +231 -43
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +10 -5
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +24 -17
  67. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +22 -15
  68. data/lib/active_record/connection_adapters/postgresql/cast.rb +12 -4
  69. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +18 -44
  70. data/lib/active_record/connection_adapters/postgresql/oid.rb +38 -14
  71. data/lib/active_record/connection_adapters/postgresql/quoting.rb +37 -12
  72. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +20 -11
  73. data/lib/active_record/connection_adapters/postgresql_adapter.rb +98 -52
  74. data/lib/active_record/connection_adapters/schema_cache.rb +8 -29
  75. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +55 -60
  76. data/lib/active_record/connection_handling.rb +39 -5
  77. data/lib/active_record/core.rb +38 -54
  78. data/lib/active_record/counter_cache.rb +9 -10
  79. data/lib/active_record/dynamic_matchers.rb +6 -2
  80. data/lib/active_record/enum.rb +199 -0
  81. data/lib/active_record/errors.rb +22 -5
  82. data/lib/active_record/fixture_set/file.rb +2 -1
  83. data/lib/active_record/fixtures.rb +173 -76
  84. data/lib/active_record/gem_version.rb +15 -0
  85. data/lib/active_record/inheritance.rb +23 -9
  86. data/lib/active_record/integration.rb +54 -1
  87. data/lib/active_record/locking/optimistic.rb +7 -2
  88. data/lib/active_record/locking/pessimistic.rb +1 -1
  89. data/lib/active_record/log_subscriber.rb +6 -13
  90. data/lib/active_record/migration/command_recorder.rb +8 -2
  91. data/lib/active_record/migration.rb +91 -56
  92. data/lib/active_record/model_schema.rb +7 -14
  93. data/lib/active_record/nested_attributes.rb +25 -13
  94. data/lib/active_record/no_touching.rb +52 -0
  95. data/lib/active_record/null_relation.rb +26 -6
  96. data/lib/active_record/persistence.rb +23 -29
  97. data/lib/active_record/querying.rb +15 -12
  98. data/lib/active_record/railtie.rb +12 -61
  99. data/lib/active_record/railties/databases.rake +37 -56
  100. data/lib/active_record/readonly_attributes.rb +0 -6
  101. data/lib/active_record/reflection.rb +230 -79
  102. data/lib/active_record/relation/batches.rb +74 -24
  103. data/lib/active_record/relation/calculations.rb +52 -48
  104. data/lib/active_record/relation/delegation.rb +54 -39
  105. data/lib/active_record/relation/finder_methods.rb +210 -67
  106. data/lib/active_record/relation/merger.rb +15 -12
  107. data/lib/active_record/relation/predicate_builder/array_handler.rb +29 -0
  108. data/lib/active_record/relation/predicate_builder/relation_handler.rb +17 -0
  109. data/lib/active_record/relation/predicate_builder.rb +81 -40
  110. data/lib/active_record/relation/query_methods.rb +185 -108
  111. data/lib/active_record/relation/spawn_methods.rb +8 -5
  112. data/lib/active_record/relation.rb +79 -84
  113. data/lib/active_record/result.rb +45 -6
  114. data/lib/active_record/runtime_registry.rb +5 -0
  115. data/lib/active_record/sanitization.rb +4 -4
  116. data/lib/active_record/schema_dumper.rb +18 -6
  117. data/lib/active_record/schema_migration.rb +31 -18
  118. data/lib/active_record/scoping/default.rb +5 -18
  119. data/lib/active_record/scoping/named.rb +14 -29
  120. data/lib/active_record/scoping.rb +5 -0
  121. data/lib/active_record/store.rb +67 -18
  122. data/lib/active_record/tasks/database_tasks.rb +66 -26
  123. data/lib/active_record/tasks/mysql_database_tasks.rb +16 -10
  124. data/lib/active_record/tasks/postgresql_database_tasks.rb +1 -1
  125. data/lib/active_record/tasks/sqlite_database_tasks.rb +5 -1
  126. data/lib/active_record/timestamp.rb +6 -6
  127. data/lib/active_record/transactions.rb +10 -12
  128. data/lib/active_record/validations/presence.rb +1 -1
  129. data/lib/active_record/validations/uniqueness.rb +19 -9
  130. data/lib/active_record/version.rb +4 -7
  131. data/lib/active_record.rb +5 -7
  132. data/lib/rails/generators/active_record/migration/migration_generator.rb +4 -0
  133. data/lib/rails/generators/active_record/migration.rb +18 -0
  134. data/lib/rails/generators/active_record/model/model_generator.rb +4 -0
  135. data/lib/rails/generators/active_record.rb +2 -8
  136. metadata +18 -30
  137. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +0 -65
  138. data/lib/active_record/associations/join_helper.rb +0 -45
  139. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +0 -60
  140. data/lib/active_record/tasks/firebird_database_tasks.rb +0 -56
  141. data/lib/active_record/tasks/oracle_database_tasks.rb +0 -45
  142. data/lib/active_record/tasks/sqlserver_database_tasks.rb +0 -48
  143. data/lib/active_record/test_case.rb +0 -96
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2004-2013 David Heinemeier Hansson
1
+ Copyright (c) 2004-2014 David Heinemeier Hansson
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -175,7 +175,7 @@ by relying on a number of conventions that make it easy for Active Record to inf
175
175
  complex relations and structures from a minimal amount of explicit direction.
176
176
 
177
177
  Convention over Configuration:
178
- * No XML-files!
178
+ * No XML files!
179
179
  * Lots of reflection and run-time extension
180
180
  * Magic is not inherently a bad word
181
181
 
@@ -192,7 +192,7 @@ The latest version of Active Record can be installed with RubyGems:
192
192
 
193
193
  Source code can be downloaded as part of the Rails project on GitHub:
194
194
 
195
- * https://github.com/rails/rails/tree/4-0-stable/activerecord
195
+ * https://github.com/rails/rails/tree/4-1-stable/activerecord
196
196
 
197
197
 
198
198
  == License
@@ -5,12 +5,12 @@ require 'benchmark/ips'
5
5
  TIME = (ENV['BENCHMARK_TIME'] || 20).to_i
6
6
  RECORDS = (ENV['BENCHMARK_RECORDS'] || TIME*1000).to_i
7
7
 
8
- conn = { :adapter => 'sqlite3', :database => ':memory:' }
8
+ conn = { adapter: 'sqlite3', database: ':memory:' }
9
9
 
10
10
  ActiveRecord::Base.establish_connection(conn)
11
11
 
12
12
  class User < ActiveRecord::Base
13
- connection.create_table :users, :force => true do |t|
13
+ connection.create_table :users, force: true do |t|
14
14
  t.string :name, :email
15
15
  t.timestamps
16
16
  end
@@ -19,7 +19,7 @@ class User < ActiveRecord::Base
19
19
  end
20
20
 
21
21
  class Exhibit < ActiveRecord::Base
22
- connection.create_table :exhibits, :force => true do |t|
22
+ connection.create_table :exhibits, force: true do |t|
23
23
  t.belongs_to :user
24
24
  t.string :name
25
25
  t.text :notes
@@ -43,6 +43,8 @@ class Exhibit < ActiveRecord::Base
43
43
  def self.feel(exhibits) exhibits.each { |e| e.feel } end
44
44
  end
45
45
 
46
+ def progress_bar(int); print "." if (int%100).zero? ; end
47
+
46
48
  puts 'Generating data...'
47
49
 
48
50
  module ActiveRecord
@@ -75,30 +77,32 @@ notes = ActiveRecord::Faker::LOREM.join ' '
75
77
  today = Date.today
76
78
 
77
79
  puts "Inserting #{RECORDS} users and exhibits..."
78
- RECORDS.times do
80
+ RECORDS.times do |record|
79
81
  user = User.create(
80
- :created_at => today,
81
- :name => ActiveRecord::Faker.name,
82
- :email => ActiveRecord::Faker.email
82
+ created_at: today,
83
+ name: ActiveRecord::Faker.name,
84
+ email: ActiveRecord::Faker.email
83
85
  )
84
86
 
85
87
  Exhibit.create(
86
- :created_at => today,
87
- :name => ActiveRecord::Faker.name,
88
- :user => user,
89
- :notes => notes
88
+ created_at: today,
89
+ name: ActiveRecord::Faker.name,
90
+ user: user,
91
+ notes: notes
90
92
  )
93
+ progress_bar(record)
91
94
  end
95
+ puts "Done!\n"
92
96
 
93
97
  Benchmark.ips(TIME) do |x|
94
98
  ar_obj = Exhibit.find(1)
95
- attrs = { :name => 'sam' }
96
- attrs_first = { :name => 'sam' }
97
- attrs_second = { :name => 'tom' }
99
+ attrs = { name: 'sam' }
100
+ attrs_first = { name: 'sam' }
101
+ attrs_second = { name: 'tom' }
98
102
  exhibit = {
99
- :name => ActiveRecord::Faker.name,
100
- :notes => notes,
101
- :created_at => Date.today
103
+ name: ActiveRecord::Faker.name,
104
+ notes: notes,
105
+ created_at: Date.today
102
106
  }
103
107
 
104
108
  x.report("Model#id") do
@@ -117,10 +121,18 @@ Benchmark.ips(TIME) do |x|
117
121
  Exhibit.first.look
118
122
  end
119
123
 
124
+ x.report 'Model.take' do
125
+ Exhibit.take
126
+ end
127
+
120
128
  x.report("Model.all limit(100)") do
121
129
  Exhibit.look Exhibit.limit(100)
122
130
  end
123
131
 
132
+ x.report("Model.all take(100)") do
133
+ Exhibit.look Exhibit.take(100)
134
+ end
135
+
124
136
  x.report "Model.all limit(100) with relationship" do
125
137
  Exhibit.feel Exhibit.limit(100).includes(:user)
126
138
  end
@@ -167,6 +179,6 @@ Benchmark.ips(TIME) do |x|
167
179
  end
168
180
 
169
181
  x.report "AR.execute(query)" do
170
- ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}")
182
+ ActiveRecord::Base.connection.execute("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}")
171
183
  end
172
184
  end
data/examples/simple.rb CHANGED
@@ -1,14 +1,14 @@
1
- $LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
1
+ require File.expand_path('../../../load_paths', __FILE__)
2
2
  require 'active_record'
3
3
 
4
4
  class Person < ActiveRecord::Base
5
- establish_connection :adapter => 'sqlite3', :database => 'foobar.db'
6
- connection.create_table table_name, :force => true do |t|
5
+ establish_connection adapter: 'sqlite3', database: 'foobar.db'
6
+ connection.create_table table_name, force: true do |t|
7
7
  t.string :name
8
8
  end
9
9
  end
10
10
 
11
- bob = Person.create!(:name => 'bob')
11
+ bob = Person.create!(name: 'bob')
12
12
  puts Person.all.inspect
13
13
  bob.destroy
14
14
  puts Person.all.inspect
@@ -223,7 +223,8 @@ module ActiveRecord
223
223
  reader_method(name, class_name, mapping, allow_nil, constructor)
224
224
  writer_method(name, class_name, mapping, allow_nil, converter)
225
225
 
226
- create_reflection(:composed_of, part_id, nil, options, self)
226
+ reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
227
+ Reflection.add_aggregate_reflection self, part_id, reflection
227
228
  end
228
229
 
229
230
  private
@@ -9,6 +9,10 @@ module ActiveRecord
9
9
  @association
10
10
  end
11
11
 
12
+ def ==(other)
13
+ other == to_a
14
+ end
15
+
12
16
  private
13
17
 
14
18
  def exec_queries
@@ -5,16 +5,58 @@ module ActiveRecord
5
5
  # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
6
6
  # ActiveRecord::Associations::ThroughAssociationScope
7
7
  class AliasTracker # :nodoc:
8
- attr_reader :aliases, :table_joins, :connection
8
+ attr_reader :aliases, :connection
9
+
10
+ def self.empty(connection)
11
+ new connection, Hash.new(0)
12
+ end
13
+
14
+ def self.create(connection, table_joins)
15
+ if table_joins.empty?
16
+ empty connection
17
+ else
18
+ aliases = Hash.new { |h,k|
19
+ h[k] = initial_count_for(connection, k, table_joins)
20
+ }
21
+ new connection, aliases
22
+ end
23
+ end
24
+
25
+ def self.initial_count_for(connection, name, table_joins)
26
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
27
+ quoted_name = connection.quote_table_name(name).downcase
28
+
29
+ counts = table_joins.map do |join|
30
+ if join.is_a?(Arel::Nodes::StringJoin)
31
+ # Table names + table aliases
32
+ join.left.downcase.scan(
33
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
34
+ ).size
35
+ elsif join.respond_to? :left
36
+ join.left.table_name == name ? 1 : 0
37
+ else
38
+ # this branch is reached by two tests:
39
+ #
40
+ # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
41
+ # with :posts
42
+ #
43
+ # activerecord/test/cases/associations/eager_test.rb:1133
44
+ # with :comments
45
+ #
46
+ 0
47
+ end
48
+ end
49
+
50
+ counts.sum
51
+ end
9
52
 
10
53
  # table_joins is an array of arel joins which might conflict with the aliases we assign here
11
- def initialize(connection = Base.connection, table_joins = [])
12
- @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) }
13
- @table_joins = table_joins
14
- @connection = connection
54
+ def initialize(connection, aliases)
55
+ @aliases = aliases
56
+ @connection = connection
15
57
  end
16
58
 
17
- def aliased_table_for(table_name, aliased_name = nil)
59
+ def aliased_table_for(table_name, aliased_name)
18
60
  table_alias = aliased_name_for(table_name, aliased_name)
19
61
 
20
62
  if table_alias == table_name
@@ -24,9 +66,7 @@ module ActiveRecord
24
66
  end
25
67
  end
26
68
 
27
- def aliased_name_for(table_name, aliased_name = nil)
28
- aliased_name ||= table_name
29
-
69
+ def aliased_name_for(table_name, aliased_name)
30
70
  if aliases[table_name].zero?
31
71
  # If it's zero, we can have our table_name
32
72
  aliases[table_name] = 1
@@ -48,26 +88,6 @@ module ActiveRecord
48
88
 
49
89
  private
50
90
 
51
- def initial_count_for(name)
52
- return 0 if Arel::Table === table_joins
53
-
54
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
55
- quoted_name = connection.quote_table_name(name).downcase
56
-
57
- counts = table_joins.map do |join|
58
- if join.is_a?(Arel::Nodes::StringJoin)
59
- # Table names + table aliases
60
- join.left.downcase.scan(
61
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
62
- ).size
63
- else
64
- join.left.table_name == name ? 1 : 0
65
- end
66
- end
67
-
68
- counts.sum
69
- end
70
-
71
91
  def truncate(name)
72
92
  name.slice(0, connection.table_alias_length - 2)
73
93
  end
@@ -13,7 +13,6 @@ module ActiveRecord
13
13
  # BelongsToAssociation
14
14
  # BelongsToPolymorphicAssociation
15
15
  # CollectionAssociation
16
- # HasAndBelongsToManyAssociation
17
16
  # HasManyAssociation
18
17
  # HasManyThroughAssociation + ThroughAssociation
19
18
  class Association #:nodoc:
@@ -87,11 +86,6 @@ module ActiveRecord
87
86
  target_scope.merge(association_scope)
88
87
  end
89
88
 
90
- def scoped
91
- ActiveSupport::Deprecation.warn "#scoped is deprecated. use #scope instead."
92
- scope
93
- end
94
-
95
89
  # The scope for this association.
96
90
  #
97
91
  # Note that the association_scope is merged into the target_scope only when the
@@ -100,7 +94,7 @@ module ActiveRecord
100
94
  # actually gets built.
101
95
  def association_scope
102
96
  if klass
103
- @association_scope ||= AssociationScope.new(self).scope
97
+ @association_scope ||= AssociationScope.scope(self, klass.connection)
104
98
  end
105
99
  end
106
100
 
@@ -110,11 +104,12 @@ module ActiveRecord
110
104
 
111
105
  # Set the inverse association, if possible
112
106
  def set_inverse_instance(record)
113
- if record && invertible_for?(record)
107
+ if invertible_for?(record)
114
108
  inverse = record.association(inverse_reflection_for(record).name)
115
109
  inverse.target = owner
116
110
  inverse.inversed = true
117
111
  end
112
+ record
118
113
  end
119
114
 
120
115
  # Returns the class of the target. belongs_to polymorphic overrides this to look at the
@@ -126,11 +121,7 @@ module ActiveRecord
126
121
  # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
127
122
  # through association's scope)
128
123
  def target_scope
129
- all = klass.all
130
- scope = AssociationRelation.new(klass, klass.arel_table, self)
131
- scope.merge! all
132
- scope.default_scoped = all.default_scoped?
133
- scope
124
+ AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all)
134
125
  end
135
126
 
136
127
  # Loads the \target if needed and returns it.
@@ -169,7 +160,7 @@ module ActiveRecord
169
160
  def marshal_load(data)
170
161
  reflection_name, ivars = data
171
162
  ivars.each { |name, val| instance_variable_set(name, val) }
172
- @reflection = @owner.class.reflect_on_association(reflection_name)
163
+ @reflection = @owner.class._reflect_on_association(reflection_name)
173
164
  end
174
165
 
175
166
  def initialize_attributes(record) #:nodoc:
@@ -204,13 +195,14 @@ module ActiveRecord
204
195
  creation_attributes.each { |key, value| record[key] = value }
205
196
  end
206
197
 
207
- # Should be true if there is a foreign key present on the owner which
198
+ # Returns true if there is a foreign key present on the owner which
208
199
  # references the target. This is used to determine whether we can load
209
200
  # the target if the owner is currently a new record (and therefore
210
- # without a key).
201
+ # without a key). If the owner is a new record then foreign_key must
202
+ # be present in order to load target.
211
203
  #
212
204
  # Currently implemented by belongs_to (vanilla and polymorphic) and
213
- # has_one/has_many :through associations which go through a belongs_to
205
+ # has_one/has_many :through associations which go through a belongs_to.
214
206
  def foreign_key_present?
215
207
  false
216
208
  end
@@ -1,64 +1,77 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
3
  class AssociationScope #:nodoc:
4
- include JoinHelper
4
+ INSTANCE = new
5
5
 
6
- attr_reader :association, :alias_tracker
6
+ def self.scope(association, connection)
7
+ INSTANCE.scope association, connection
8
+ end
7
9
 
8
- delegate :klass, :owner, :reflection, :interpolate, :to => :association
9
- delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection
10
+ def scope(association, connection)
11
+ klass = association.klass
12
+ reflection = association.reflection
13
+ scope = klass.unscoped
14
+ owner = association.owner
15
+ alias_tracker = AliasTracker.empty connection
10
16
 
11
- def initialize(association)
12
- @association = association
13
- @alias_tracker = AliasTracker.new klass.connection
17
+ scope.extending! Array(reflection.options[:extend])
18
+ add_constraints(scope, owner, klass, reflection, alias_tracker)
14
19
  end
15
20
 
16
- def scope
17
- scope = klass.unscoped
18
- scope.extending! Array(options[:extend])
19
- add_constraints(scope)
21
+ def join_type
22
+ Arel::Nodes::InnerJoin
20
23
  end
21
24
 
22
25
  private
23
26
 
24
- def column_for(table_name, column_name)
27
+ def construct_tables(chain, klass, refl, alias_tracker)
28
+ chain.map do |reflection|
29
+ alias_tracker.aliased_table_for(
30
+ table_name_for(reflection, klass, refl),
31
+ table_alias_for(reflection, refl, reflection != refl)
32
+ )
33
+ end
34
+ end
35
+
36
+ def table_alias_for(reflection, refl, join = false)
37
+ name = "#{reflection.plural_name}_#{alias_suffix(refl)}"
38
+ name << "_join" if join
39
+ name
40
+ end
41
+
42
+ def join(table, constraint)
43
+ table.create_join(table, table.create_on(constraint), join_type)
44
+ end
45
+
46
+ def column_for(table_name, column_name, alias_tracker)
25
47
  columns = alias_tracker.connection.schema_cache.columns_hash(table_name)
26
48
  columns[column_name]
27
49
  end
28
50
 
29
- def bind_value(scope, column, value)
51
+ def bind_value(scope, column, value, alias_tracker)
30
52
  substitute = alias_tracker.connection.substitute_at(
31
53
  column, scope.bind_values.length)
32
54
  scope.bind_values += [[column, value]]
33
55
  substitute
34
56
  end
35
57
 
36
- def bind(scope, table_name, column_name, value)
37
- column = column_for table_name, column_name
38
- bind_value scope, column, value
58
+ def bind(scope, table_name, column_name, value, tracker)
59
+ column = column_for table_name, column_name, tracker
60
+ bind_value scope, column, value, tracker
39
61
  end
40
62
 
41
- def add_constraints(scope)
42
- tables = construct_tables
63
+ def add_constraints(scope, owner, assoc_klass, refl, tracker)
64
+ chain = refl.chain
65
+ scope_chain = refl.scope_chain
66
+
67
+ tables = construct_tables(chain, assoc_klass, refl, tracker)
43
68
 
44
69
  chain.each_with_index do |reflection, i|
45
70
  table, foreign_table = tables.shift, tables.first
46
71
 
47
- if reflection.source_macro == :has_and_belongs_to_many
48
- join_table = tables.shift
49
-
50
- scope = scope.joins(join(
51
- join_table,
52
- table[reflection.association_primary_key].
53
- eq(join_table[reflection.association_foreign_key])
54
- ))
55
-
56
- table, foreign_table = join_table, tables.first
57
- end
58
-
59
72
  if reflection.source_macro == :belongs_to
60
73
  if reflection.options[:polymorphic]
61
- key = reflection.association_primary_key(self.klass)
74
+ key = reflection.association_primary_key(assoc_klass)
62
75
  else
63
76
  key = reflection.association_primary_key
64
77
  end
@@ -70,35 +83,36 @@ module ActiveRecord
70
83
  end
71
84
 
72
85
  if reflection == chain.last
73
- bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key]
86
+ bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker
74
87
  scope = scope.where(table[key].eq(bind_val))
75
88
 
76
89
  if reflection.type
77
90
  value = owner.class.base_class.name
78
- bind_val = bind scope, table.table_name, reflection.type.to_s, value
91
+ bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker
79
92
  scope = scope.where(table[reflection.type].eq(bind_val))
80
93
  end
81
94
  else
82
95
  constraint = table[key].eq(foreign_table[foreign_key])
83
96
 
84
97
  if reflection.type
85
- type = chain[i + 1].klass.base_class.name
86
- constraint = constraint.and(table[reflection.type].eq(type))
98
+ value = chain[i + 1].klass.base_class.name
99
+ bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker
100
+ scope = scope.where(table[reflection.type].eq(bind_val))
87
101
  end
88
102
 
89
103
  scope = scope.joins(join(foreign_table, constraint))
90
104
  end
91
105
 
92
106
  is_first_chain = i == 0
93
- klass = is_first_chain ? self.klass : reflection.klass
107
+ klass = is_first_chain ? assoc_klass : reflection.klass
94
108
 
95
109
  # Exclude the scope of the association itself, because that
96
110
  # was already merged in the #scope method.
97
111
  scope_chain[i].each do |scope_chain_item|
98
- item = eval_scope(klass, scope_chain_item)
112
+ item = eval_scope(klass, scope_chain_item, owner)
99
113
 
100
- if scope_chain_item == self.reflection.scope
101
- scope.merge! item.except(:where, :includes)
114
+ if scope_chain_item == refl.scope
115
+ scope.merge! item.except(:where, :includes, :bind)
102
116
  end
103
117
 
104
118
  if is_first_chain
@@ -113,12 +127,12 @@ module ActiveRecord
113
127
  scope
114
128
  end
115
129
 
116
- def alias_suffix
117
- reflection.name
130
+ def alias_suffix(refl)
131
+ refl.name
118
132
  end
119
133
 
120
- def table_name_for(reflection)
121
- if reflection == self.reflection
134
+ def table_name_for(reflection, klass, refl)
135
+ if reflection == refl
122
136
  # If this is a polymorphic belongs_to, we want to get the klass from the
123
137
  # association because it depends on the polymorphic_type attribute of
124
138
  # the owner
@@ -128,12 +142,8 @@ module ActiveRecord
128
142
  end
129
143
  end
130
144
 
131
- def eval_scope(klass, scope)
132
- if scope.is_a?(Relation)
133
- scope
134
- else
135
- klass.unscoped.instance_exec(owner, &scope)
136
- end
145
+ def eval_scope(klass, scope, owner)
146
+ klass.unscoped.instance_exec(owner, &scope)
137
147
  end
138
148
  end
139
149
  end
@@ -8,13 +8,16 @@ module ActiveRecord
8
8
  end
9
9
 
10
10
  def replace(record)
11
- raise_on_type_mismatch!(record) if record
12
-
13
- update_counters(record)
14
- replace_keys(record)
15
- set_inverse_instance(record)
16
-
17
- @updated = true if record
11
+ if record
12
+ raise_on_type_mismatch!(record)
13
+ update_counters(record)
14
+ replace_keys(record)
15
+ set_inverse_instance(record)
16
+ @updated = true
17
+ else
18
+ decrement_counters
19
+ remove_keys
20
+ end
18
21
 
19
22
  self.target = record
20
23
  end
@@ -34,35 +37,41 @@ module ActiveRecord
34
37
  !loaded? && foreign_key_present? && klass
35
38
  end
36
39
 
37
- def update_counters(record)
40
+ def with_cache_name
38
41
  counter_cache_name = reflection.counter_cache_column
42
+ return unless counter_cache_name && owner.persisted?
43
+ yield counter_cache_name
44
+ end
45
+
46
+ def update_counters(record)
47
+ with_cache_name do |name|
48
+ return unless different_target? record
49
+ record.class.increment_counter(name, record.id)
50
+ decrement_counter name
51
+ end
52
+ end
39
53
 
40
- if counter_cache_name && owner.persisted? && different_target?(record)
41
- if record
42
- record.class.increment_counter(counter_cache_name, record.id)
43
- end
54
+ def decrement_counters
55
+ with_cache_name { |name| decrement_counter name }
56
+ end
44
57
 
45
- if foreign_key_present?
46
- klass.decrement_counter(counter_cache_name, target_id)
47
- end
58
+ def decrement_counter counter_cache_name
59
+ if foreign_key_present?
60
+ klass.decrement_counter(counter_cache_name, target_id)
48
61
  end
49
62
  end
50
63
 
51
64
  # Checks whether record is different to the current target, without loading it
52
65
  def different_target?(record)
53
- if record.nil?
54
- owner[reflection.foreign_key]
55
- else
56
- record.id != owner[reflection.foreign_key]
57
- end
66
+ record.id != owner[reflection.foreign_key]
58
67
  end
59
68
 
60
69
  def replace_keys(record)
61
- if record
62
- owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
63
- else
64
- owner[reflection.foreign_key] = nil
65
- end
70
+ owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
71
+ end
72
+
73
+ def remove_keys
74
+ owner[reflection.foreign_key] = nil
66
75
  end
67
76
 
68
77
  def foreign_key_present?
@@ -11,7 +11,12 @@ module ActiveRecord
11
11
 
12
12
  def replace_keys(record)
13
13
  super
14
- owner[reflection.foreign_type] = record && record.class.base_class.name
14
+ owner[reflection.foreign_type] = record.class.base_class.name
15
+ end
16
+
17
+ def remove_keys
18
+ super
19
+ owner[reflection.foreign_type] = nil
15
20
  end
16
21
 
17
22
  def different_target?(record)