activerecord 1.0.0 → 3.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 (178) hide show
  1. data/CHANGELOG +5518 -76
  2. data/README.rdoc +222 -0
  3. data/examples/performance.rb +162 -0
  4. data/examples/simple.rb +14 -0
  5. data/lib/active_record/aggregations.rb +192 -80
  6. data/lib/active_record/association_preload.rb +403 -0
  7. data/lib/active_record/associations/association_collection.rb +545 -53
  8. data/lib/active_record/associations/association_proxy.rb +295 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +91 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +78 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +127 -36
  12. data/lib/active_record/associations/has_many_association.rb +108 -84
  13. data/lib/active_record/associations/has_many_through_association.rb +116 -0
  14. data/lib/active_record/associations/has_one_association.rb +143 -0
  15. data/lib/active_record/associations/has_one_through_association.rb +40 -0
  16. data/lib/active_record/associations/through_association_scope.rb +154 -0
  17. data/lib/active_record/associations.rb +2086 -368
  18. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  19. data/lib/active_record/attribute_methods/dirty.rb +95 -0
  20. data/lib/active_record/attribute_methods/primary_key.rb +50 -0
  21. data/lib/active_record/attribute_methods/query.rb +39 -0
  22. data/lib/active_record/attribute_methods/read.rb +116 -0
  23. data/lib/active_record/attribute_methods/time_zone_conversion.rb +61 -0
  24. data/lib/active_record/attribute_methods/write.rb +37 -0
  25. data/lib/active_record/attribute_methods.rb +60 -0
  26. data/lib/active_record/autosave_association.rb +369 -0
  27. data/lib/active_record/base.rb +1603 -721
  28. data/lib/active_record/callbacks.rb +176 -225
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +365 -0
  30. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +113 -0
  31. data/lib/active_record/connection_adapters/abstract/database_limits.rb +57 -0
  32. data/lib/active_record/connection_adapters/abstract/database_statements.rb +329 -0
  33. data/lib/active_record/connection_adapters/abstract/query_cache.rb +81 -0
  34. data/lib/active_record/connection_adapters/abstract/quoting.rb +72 -0
  35. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +739 -0
  36. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +543 -0
  37. data/lib/active_record/connection_adapters/abstract_adapter.rb +165 -279
  38. data/lib/active_record/connection_adapters/mysql_adapter.rb +594 -82
  39. data/lib/active_record/connection_adapters/postgresql_adapter.rb +988 -135
  40. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +53 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +365 -71
  42. data/lib/active_record/counter_cache.rb +115 -0
  43. data/lib/active_record/dynamic_finder_match.rb +53 -0
  44. data/lib/active_record/dynamic_scope_match.rb +32 -0
  45. data/lib/active_record/errors.rb +172 -0
  46. data/lib/active_record/fixtures.rb +941 -105
  47. data/lib/active_record/locale/en.yml +40 -0
  48. data/lib/active_record/locking/optimistic.rb +172 -0
  49. data/lib/active_record/locking/pessimistic.rb +55 -0
  50. data/lib/active_record/log_subscriber.rb +48 -0
  51. data/lib/active_record/migration.rb +617 -0
  52. data/lib/active_record/named_scope.rb +138 -0
  53. data/lib/active_record/nested_attributes.rb +417 -0
  54. data/lib/active_record/observer.rb +105 -36
  55. data/lib/active_record/persistence.rb +291 -0
  56. data/lib/active_record/query_cache.rb +36 -0
  57. data/lib/active_record/railtie.rb +91 -0
  58. data/lib/active_record/railties/controller_runtime.rb +38 -0
  59. data/lib/active_record/railties/databases.rake +512 -0
  60. data/lib/active_record/reflection.rb +364 -87
  61. data/lib/active_record/relation/batches.rb +89 -0
  62. data/lib/active_record/relation/calculations.rb +286 -0
  63. data/lib/active_record/relation/finder_methods.rb +355 -0
  64. data/lib/active_record/relation/predicate_builder.rb +41 -0
  65. data/lib/active_record/relation/query_methods.rb +261 -0
  66. data/lib/active_record/relation/spawn_methods.rb +112 -0
  67. data/lib/active_record/relation.rb +393 -0
  68. data/lib/active_record/schema.rb +59 -0
  69. data/lib/active_record/schema_dumper.rb +195 -0
  70. data/lib/active_record/serialization.rb +60 -0
  71. data/lib/active_record/serializers/xml_serializer.rb +244 -0
  72. data/lib/active_record/session_store.rb +340 -0
  73. data/lib/active_record/test_case.rb +67 -0
  74. data/lib/active_record/timestamp.rb +88 -0
  75. data/lib/active_record/transactions.rb +329 -75
  76. data/lib/active_record/validations/associated.rb +48 -0
  77. data/lib/active_record/validations/uniqueness.rb +185 -0
  78. data/lib/active_record/validations.rb +58 -179
  79. data/lib/active_record/version.rb +9 -0
  80. data/lib/active_record.rb +100 -24
  81. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  82. data/lib/rails/generators/active_record/migration/templates/migration.rb +17 -0
  83. data/lib/rails/generators/active_record/model/model_generator.rb +38 -0
  84. data/lib/rails/generators/active_record/model/templates/migration.rb +16 -0
  85. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  86. data/lib/rails/generators/active_record/model/templates/module.rb +5 -0
  87. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  88. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  89. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  90. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +16 -0
  91. data/lib/rails/generators/active_record.rb +27 -0
  92. metadata +216 -158
  93. data/README +0 -361
  94. data/RUNNING_UNIT_TESTS +0 -36
  95. data/dev-utils/eval_debugger.rb +0 -9
  96. data/examples/associations.rb +0 -87
  97. data/examples/shared_setup.rb +0 -15
  98. data/examples/validation.rb +0 -88
  99. data/install.rb +0 -60
  100. data/lib/active_record/deprecated_associations.rb +0 -70
  101. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  102. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  103. data/lib/active_record/support/clean_logger.rb +0 -10
  104. data/lib/active_record/support/inflector.rb +0 -70
  105. data/lib/active_record/vendor/mysql.rb +0 -1117
  106. data/lib/active_record/vendor/simple.rb +0 -702
  107. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  108. data/lib/active_record/wrappings.rb +0 -59
  109. data/rakefile +0 -122
  110. data/test/abstract_unit.rb +0 -16
  111. data/test/aggregations_test.rb +0 -34
  112. data/test/all.sh +0 -8
  113. data/test/associations_test.rb +0 -477
  114. data/test/base_test.rb +0 -513
  115. data/test/class_inheritable_attributes_test.rb +0 -33
  116. data/test/connections/native_mysql/connection.rb +0 -24
  117. data/test/connections/native_postgresql/connection.rb +0 -24
  118. data/test/connections/native_sqlite/connection.rb +0 -24
  119. data/test/deprecated_associations_test.rb +0 -336
  120. data/test/finder_test.rb +0 -67
  121. data/test/fixtures/accounts/signals37 +0 -3
  122. data/test/fixtures/accounts/unknown +0 -2
  123. data/test/fixtures/auto_id.rb +0 -4
  124. data/test/fixtures/column_name.rb +0 -3
  125. data/test/fixtures/companies/first_client +0 -6
  126. data/test/fixtures/companies/first_firm +0 -4
  127. data/test/fixtures/companies/second_client +0 -6
  128. data/test/fixtures/company.rb +0 -37
  129. data/test/fixtures/company_in_module.rb +0 -33
  130. data/test/fixtures/course.rb +0 -3
  131. data/test/fixtures/courses/java +0 -2
  132. data/test/fixtures/courses/ruby +0 -2
  133. data/test/fixtures/customer.rb +0 -30
  134. data/test/fixtures/customers/david +0 -6
  135. data/test/fixtures/db_definitions/mysql.sql +0 -96
  136. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  137. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  138. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  139. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  140. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  141. data/test/fixtures/default.rb +0 -2
  142. data/test/fixtures/developer.rb +0 -8
  143. data/test/fixtures/developers/david +0 -2
  144. data/test/fixtures/developers/jamis +0 -2
  145. data/test/fixtures/developers_projects/david_action_controller +0 -2
  146. data/test/fixtures/developers_projects/david_active_record +0 -2
  147. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  148. data/test/fixtures/entrant.rb +0 -3
  149. data/test/fixtures/entrants/first +0 -3
  150. data/test/fixtures/entrants/second +0 -3
  151. data/test/fixtures/entrants/third +0 -3
  152. data/test/fixtures/fixture_database.sqlite +0 -0
  153. data/test/fixtures/fixture_database_2.sqlite +0 -0
  154. data/test/fixtures/movie.rb +0 -5
  155. data/test/fixtures/movies/first +0 -2
  156. data/test/fixtures/movies/second +0 -2
  157. data/test/fixtures/project.rb +0 -3
  158. data/test/fixtures/projects/action_controller +0 -2
  159. data/test/fixtures/projects/active_record +0 -2
  160. data/test/fixtures/reply.rb +0 -21
  161. data/test/fixtures/subscriber.rb +0 -5
  162. data/test/fixtures/subscribers/first +0 -2
  163. data/test/fixtures/subscribers/second +0 -2
  164. data/test/fixtures/topic.rb +0 -20
  165. data/test/fixtures/topics/first +0 -9
  166. data/test/fixtures/topics/second +0 -8
  167. data/test/fixtures_test.rb +0 -20
  168. data/test/inflector_test.rb +0 -104
  169. data/test/inheritance_test.rb +0 -125
  170. data/test/lifecycle_test.rb +0 -110
  171. data/test/modules_test.rb +0 -21
  172. data/test/multiple_db_test.rb +0 -46
  173. data/test/pk_test.rb +0 -57
  174. data/test/reflection_test.rb +0 -78
  175. data/test/thread_safety_test.rb +0 -33
  176. data/test/transactions_test.rb +0 -83
  177. data/test/unconnected_test.rb +0 -24
  178. data/test/validations_test.rb +0 -126
@@ -0,0 +1,295 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ # = Active Record Associations
6
+ #
7
+ # This is the root class of all association proxies:
8
+ #
9
+ # AssociationProxy
10
+ # BelongsToAssociation
11
+ # HasOneAssociation
12
+ # BelongsToPolymorphicAssociation
13
+ # AssociationCollection
14
+ # HasAndBelongsToManyAssociation
15
+ # HasManyAssociation
16
+ # HasManyThroughAssociation
17
+ # HasOneThroughAssociation
18
+ #
19
+ # Association proxies in Active Record are middlemen between the object that
20
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
21
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
22
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
23
+ # ActiveRecord::Reflection::AssociationReflection.
24
+ #
25
+ # For example, given
26
+ #
27
+ # class Blog < ActiveRecord::Base
28
+ # has_many :posts
29
+ # end
30
+ #
31
+ # blog = Blog.find(:first)
32
+ #
33
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
34
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
35
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
36
+ #
37
+ # This class has most of the basic instance methods removed, and delegates
38
+ # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
39
+ # corner case, it even removes the +class+ method and that's why you get
40
+ #
41
+ # blog.posts.class # => Array
42
+ #
43
+ # though the object behind <tt>blog.posts</tt> is not an Array, but an
44
+ # ActiveRecord::Associations::HasManyAssociation.
45
+ #
46
+ # The <tt>@target</tt> object is not \loaded until needed. For example,
47
+ #
48
+ # blog.posts.count
49
+ #
50
+ # is computed directly through SQL and does not trigger by itself the
51
+ # instantiation of the actual post records.
52
+ class AssociationProxy #:nodoc:
53
+ alias_method :proxy_respond_to?, :respond_to?
54
+ alias_method :proxy_extend, :extend
55
+ delegate :to_param, :to => :proxy_target
56
+ instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|proxy_/ }
57
+
58
+ def initialize(owner, reflection)
59
+ @owner, @reflection = owner, reflection
60
+ @updated = false
61
+ reflection.check_validity!
62
+ Array.wrap(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
63
+ reset
64
+ end
65
+
66
+ # Returns the owner of the proxy.
67
+ def proxy_owner
68
+ @owner
69
+ end
70
+
71
+ # Returns the reflection object that represents the association handled
72
+ # by the proxy.
73
+ def proxy_reflection
74
+ @reflection
75
+ end
76
+
77
+ # Returns the \target of the proxy, same as +target+.
78
+ def proxy_target
79
+ @target
80
+ end
81
+
82
+ # Does the proxy or its \target respond to +symbol+?
83
+ def respond_to?(*args)
84
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
85
+ end
86
+
87
+ # Forwards <tt>===</tt> explicitly to the \target because the instance method
88
+ # removal above doesn't catch it. Loads the \target if needed.
89
+ def ===(other)
90
+ load_target
91
+ other === @target
92
+ end
93
+
94
+ # Returns the name of the table of the related class:
95
+ #
96
+ # post.comments.aliased_table_name # => "comments"
97
+ #
98
+ def aliased_table_name
99
+ @reflection.klass.table_name
100
+ end
101
+
102
+ # Returns the SQL string that corresponds to the <tt>:conditions</tt>
103
+ # option of the macro, if given, or +nil+ otherwise.
104
+ def conditions
105
+ @conditions ||= interpolate_sql(@reflection.sanitized_conditions) if @reflection.sanitized_conditions
106
+ end
107
+ alias :sql_conditions :conditions
108
+
109
+ # Resets the \loaded flag to +false+ and sets the \target to +nil+.
110
+ def reset
111
+ @loaded = false
112
+ @target = nil
113
+ end
114
+
115
+ # Reloads the \target and returns +self+ on success.
116
+ def reload
117
+ reset
118
+ load_target
119
+ self unless @target.nil?
120
+ end
121
+
122
+ # Has the \target been already \loaded?
123
+ def loaded?
124
+ @loaded
125
+ end
126
+
127
+ # Asserts the \target has been loaded setting the \loaded flag to +true+.
128
+ def loaded
129
+ @loaded = true
130
+ end
131
+
132
+ # Returns the target of this proxy, same as +proxy_target+.
133
+ def target
134
+ @target
135
+ end
136
+
137
+ # Sets the target of this proxy to <tt>\target</tt>, and the \loaded flag to +true+.
138
+ def target=(target)
139
+ @target = target
140
+ loaded
141
+ end
142
+
143
+ # Forwards the call to the target. Loads the \target if needed.
144
+ def inspect
145
+ load_target
146
+ @target.inspect
147
+ end
148
+
149
+ def send(method, *args)
150
+ if proxy_respond_to?(method)
151
+ super
152
+ else
153
+ load_target
154
+ @target.send(method, *args)
155
+ end
156
+ end
157
+
158
+ protected
159
+ # Does the association have a <tt>:dependent</tt> option?
160
+ def dependent?
161
+ @reflection.options[:dependent]
162
+ end
163
+
164
+ def interpolate_sql(sql, record = nil)
165
+ @owner.send(:interpolate_sql, sql, record)
166
+ end
167
+
168
+ # Forwards the call to the reflection class.
169
+ def sanitize_sql(sql, table_name = @reflection.klass.table_name)
170
+ @reflection.klass.send(:sanitize_sql, sql, table_name)
171
+ end
172
+
173
+ # Assigns the ID of the owner to the corresponding foreign key in +record+.
174
+ # If the association is polymorphic the type of the owner is also set.
175
+ def set_belongs_to_association_for(record)
176
+ if @reflection.options[:as]
177
+ record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
178
+ record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
179
+ else
180
+ unless @owner.new_record?
181
+ primary_key = @reflection.options[:primary_key] || :id
182
+ record[@reflection.primary_key_name] = @owner.send(primary_key)
183
+ end
184
+ end
185
+ end
186
+
187
+ # Merges into +options+ the ones coming from the reflection.
188
+ def merge_options_from_reflection!(options)
189
+ options.reverse_merge!(
190
+ :group => @reflection.options[:group],
191
+ :having => @reflection.options[:having],
192
+ :limit => @reflection.options[:limit],
193
+ :offset => @reflection.options[:offset],
194
+ :joins => @reflection.options[:joins],
195
+ :include => @reflection.options[:include],
196
+ :select => @reflection.options[:select],
197
+ :readonly => @reflection.options[:readonly]
198
+ )
199
+ end
200
+
201
+ # Forwards +with_scope+ to the reflection.
202
+ def with_scope(*args, &block)
203
+ @reflection.klass.send :with_scope, *args, &block
204
+ end
205
+
206
+ private
207
+ # Forwards any missing method call to the \target.
208
+ def method_missing(method, *args)
209
+ if load_target
210
+ unless @target.respond_to?(method)
211
+ message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
212
+ raise NoMethodError, message
213
+ end
214
+
215
+ if block_given?
216
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
217
+ else
218
+ @target.send(method, *args)
219
+ end
220
+ end
221
+ end
222
+
223
+ # Loads the \target if needed and returns it.
224
+ #
225
+ # This method is abstract in the sense that it relies on +find_target+,
226
+ # which is expected to be provided by descendants.
227
+ #
228
+ # If the \target is already \loaded it is just returned. Thus, you can call
229
+ # +load_target+ unconditionally to get the \target.
230
+ #
231
+ # ActiveRecord::RecordNotFound is rescued within the method, and it is
232
+ # not reraised. The proxy is \reset and +nil+ is the return value.
233
+ def load_target
234
+ return nil unless defined?(@loaded)
235
+
236
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
237
+ @target = find_target
238
+ end
239
+
240
+ @loaded = true
241
+ @target
242
+ rescue ActiveRecord::RecordNotFound
243
+ reset
244
+ end
245
+
246
+ # Can be overwritten by associations that might have the foreign key
247
+ # available for an association without having the object itself (and
248
+ # still being a new record). Currently, only +belongs_to+ presents
249
+ # this scenario (both vanilla and polymorphic).
250
+ def foreign_key_present
251
+ false
252
+ end
253
+
254
+ # Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
255
+ # the kind of the class of the associated objects. Meant to be used as
256
+ # a sanity check when you are about to assign an associated record.
257
+ def raise_on_type_mismatch(record)
258
+ unless record.is_a?(@reflection.klass) || record.is_a?(@reflection.class_name.constantize)
259
+ message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
260
+ raise ActiveRecord::AssociationTypeMismatch, message
261
+ end
262
+ end
263
+
264
+ if RUBY_VERSION < '1.9.2'
265
+ # Array#flatten has problems with recursive arrays before Ruby 1.9.2.
266
+ # Going one level deeper solves the majority of the problems.
267
+ def flatten_deeper(array)
268
+ array.collect { |element| (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element }.flatten
269
+ end
270
+ else
271
+ def flatten_deeper(array)
272
+ array.flatten
273
+ end
274
+ end
275
+
276
+ # Returns the ID of the owner, quoted if needed.
277
+ def owner_quoted_id
278
+ @owner.quoted_id
279
+ end
280
+
281
+ def set_inverse_instance(record, instance)
282
+ return if record.nil? || !we_can_set_the_inverse_on_this?(record)
283
+ inverse_relationship = @reflection.inverse_of
284
+ unless inverse_relationship.nil?
285
+ record.send(:"set_#{inverse_relationship.name}_target", instance)
286
+ end
287
+ end
288
+
289
+ # Override in subclasses
290
+ def we_can_set_the_inverse_on_this?(record)
291
+ false
292
+ end
293
+ end
294
+ end
295
+ end
@@ -0,0 +1,91 @@
1
+ module ActiveRecord
2
+ # = Active Record Belongs To Associations
3
+ module Associations
4
+ class BelongsToAssociation < AssociationProxy #:nodoc:
5
+ def create(attributes = {})
6
+ replace(@reflection.create_association(attributes))
7
+ end
8
+
9
+ def build(attributes = {})
10
+ replace(@reflection.build_association(attributes))
11
+ end
12
+
13
+ def replace(record)
14
+ counter_cache_name = @reflection.counter_cache_column
15
+
16
+ if record.nil?
17
+ if counter_cache_name && !@owner.new_record?
18
+ @reflection.klass.decrement_counter(counter_cache_name, previous_record_id) if @owner[@reflection.primary_key_name]
19
+ end
20
+
21
+ @target = @owner[@reflection.primary_key_name] = nil
22
+ else
23
+ raise_on_type_mismatch(record)
24
+
25
+ if counter_cache_name && !@owner.new_record? && record.id != @owner[@reflection.primary_key_name]
26
+ @reflection.klass.increment_counter(counter_cache_name, record.id)
27
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
28
+ end
29
+
30
+ @target = (AssociationProxy === record ? record.target : record)
31
+ @owner[@reflection.primary_key_name] = record_id(record) unless record.new_record?
32
+ @updated = true
33
+ end
34
+
35
+ set_inverse_instance(record, @owner)
36
+
37
+ loaded
38
+ record
39
+ end
40
+
41
+ def updated?
42
+ @updated
43
+ end
44
+
45
+ private
46
+ def find_target
47
+ find_method = if @reflection.options[:primary_key]
48
+ "find_by_#{@reflection.options[:primary_key]}"
49
+ else
50
+ "find"
51
+ end
52
+
53
+ options = @reflection.options.dup
54
+ (options.keys - [:select, :include, :readonly]).each do |key|
55
+ options.delete key
56
+ end
57
+ options[:conditions] = conditions
58
+
59
+ the_target = @reflection.klass.send(find_method,
60
+ @owner[@reflection.primary_key_name],
61
+ options
62
+ ) if @owner[@reflection.primary_key_name]
63
+ set_inverse_instance(the_target, @owner)
64
+ the_target
65
+ end
66
+
67
+ def foreign_key_present
68
+ !@owner[@reflection.primary_key_name].nil?
69
+ end
70
+
71
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
72
+ # has_one associations.
73
+ def we_can_set_the_inverse_on_this?(record)
74
+ @reflection.has_inverse? && @reflection.inverse_of.macro == :has_one
75
+ end
76
+
77
+ def record_id(record)
78
+ record.send(@reflection.options[:primary_key] || :id)
79
+ end
80
+
81
+ def previous_record_id
82
+ @previous_record_id ||= if @reflection.options[:primary_key]
83
+ previous_record = @owner.send(@reflection.name)
84
+ previous_record.nil? ? nil : previous_record.id
85
+ else
86
+ @owner[@reflection.primary_key_name]
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,78 @@
1
+ module ActiveRecord
2
+ # = Active Record Belongs To Polymorphic Association
3
+ module Associations
4
+ class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
5
+ def replace(record)
6
+ if record.nil?
7
+ @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
8
+ else
9
+ @target = (AssociationProxy === record ? record.target : record)
10
+
11
+ @owner[@reflection.primary_key_name] = record_id(record)
12
+ @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
13
+
14
+ @updated = true
15
+ end
16
+
17
+ set_inverse_instance(record, @owner)
18
+ loaded
19
+ record
20
+ end
21
+
22
+ def updated?
23
+ @updated
24
+ end
25
+
26
+ private
27
+
28
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
29
+ # has_one associations.
30
+ def we_can_set_the_inverse_on_this?(record)
31
+ if @reflection.has_inverse?
32
+ inverse_association = @reflection.polymorphic_inverse_of(record.class)
33
+ inverse_association && inverse_association.macro == :has_one
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ def set_inverse_instance(record, instance)
40
+ return if record.nil? || !we_can_set_the_inverse_on_this?(record)
41
+ inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
42
+ unless inverse_relationship.nil?
43
+ record.send(:"set_#{inverse_relationship.name}_target", instance)
44
+ end
45
+ end
46
+
47
+ def find_target
48
+ return nil if association_class.nil?
49
+
50
+ target =
51
+ if @reflection.options[:conditions]
52
+ association_class.find(
53
+ @owner[@reflection.primary_key_name],
54
+ :select => @reflection.options[:select],
55
+ :conditions => conditions,
56
+ :include => @reflection.options[:include]
57
+ )
58
+ else
59
+ association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
60
+ end
61
+ set_inverse_instance(target, @owner)
62
+ target
63
+ end
64
+
65
+ def foreign_key_present
66
+ !@owner[@reflection.primary_key_name].nil?
67
+ end
68
+
69
+ def record_id(record)
70
+ record.send(@reflection.options[:primary_key] || :id)
71
+ end
72
+
73
+ def association_class
74
+ @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,46 +1,137 @@
1
1
  module ActiveRecord
2
+ # = Active Record Has And Belongs To Many Association
2
3
  module Associations
3
- class HasAndBelongsToManyCollection < AssociationCollection #:nodoc:
4
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
5
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
-
7
- @association_foreign_key = options[:association_foreign_key] || association_class_name.downcase + "_id"
8
- association_table_name = options[:table_name] || @association_class.table_name(association_class_name)
9
- @join_table = join_table
10
- @order = options[:order] || "t.#{@owner.class.primary_key}"
11
-
12
- @finder_sql = options[:finder_sql] ||
13
- "SELECT t.* FROM #{association_table_name} t, #{@join_table} j " +
14
- "WHERE t.#{@owner.class.primary_key} = j.#{@association_foreign_key} AND " +
15
- "j.#{association_class_primary_key_name} = '#{@owner.id}' ORDER BY #{@order}"
4
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
5
+ def create(attributes = {})
6
+ create_record(attributes) { |record| insert_record(record) }
16
7
  end
17
-
18
- def <<(record)
19
- raise ActiveRecord::AssociationTypeMismatch unless @association_class === record
20
- sql = @options[:insert_sql] ||
21
- "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) " +
22
- "VALUES ('#{@owner.id}', '#{record.id}')"
23
- @owner.connection.execute(sql)
24
- @collection_array << record unless @collection_array.nil?
8
+
9
+ def create!(attributes = {})
10
+ create_record(attributes) { |record| insert_record(record, true) }
11
+ end
12
+
13
+ def columns
14
+ @reflection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
15
+ end
16
+
17
+ def reset_column_information
18
+ @reflection.reset_column_information
25
19
  end
26
-
27
- def delete(records)
28
- records = duplicated_records_array(records)
29
- sql = @options[:delete_sql] || "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'"
30
- ids = records.map { |record| "'" + record.id.to_s + "'" }.join(',')
31
- @owner.connection.delete "#{sql} AND #{@association_foreign_key} in (#{ids})"
32
- records.each {|record| @collection_array.delete(record) } unless @collection_array.nil?
20
+
21
+ def has_primary_key?
22
+ @has_primary_key ||= @owner.connection.supports_primary_key? && @owner.connection.primary_key(@reflection.options[:join_table])
33
23
  end
34
-
24
+
35
25
  protected
36
- def find_all_records
37
- @association_class.find_by_sql(@finder_sql)
26
+ def construct_find_options!(options)
27
+ options[:joins] = Arel::SqlLiteral.new @join_sql
28
+ options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
29
+ options[:select] ||= (@reflection.options[:select] || Arel::SqlLiteral.new('*'))
38
30
  end
39
-
31
+
40
32
  def count_records
41
- load_collection_to_array
42
- @collection_array.size
33
+ load_target.size
43
34
  end
44
- end
35
+
36
+ def insert_record(record, force = true, validate = true)
37
+ if record.new_record?
38
+ if force
39
+ record.save!
40
+ else
41
+ return false unless record.save(:validate => validate)
42
+ end
43
+ end
44
+
45
+ if @reflection.options[:insert_sql]
46
+ @owner.connection.insert(interpolate_sql(@reflection.options[:insert_sql], record))
47
+ else
48
+ relation = Arel::Table.new(@reflection.options[:join_table])
49
+ timestamps = record_timestamp_columns(record)
50
+ timezone = record.send(:current_time_from_proper_timezone) if timestamps.any?
51
+
52
+ attributes = columns.inject({}) do |attrs, column|
53
+ name = column.name
54
+ case name.to_s
55
+ when @reflection.primary_key_name.to_s
56
+ attrs[relation[name]] = @owner.id
57
+ when @reflection.association_foreign_key.to_s
58
+ attrs[relation[name]] = record.id
59
+ when *timestamps
60
+ attrs[relation[name]] = timezone
61
+ else
62
+ if record.has_attribute?(name)
63
+ value = @owner.send(:quote_value, record[name], column)
64
+ attrs[relation[name]] = value unless value.nil?
65
+ end
66
+ end
67
+ attrs
68
+ end
69
+
70
+ relation.insert(attributes)
71
+ end
72
+
73
+ return true
74
+ end
75
+
76
+ def delete_records(records)
77
+ if sql = @reflection.options[:delete_sql]
78
+ records.each { |record| @owner.connection.delete(interpolate_sql(sql, record)) }
79
+ else
80
+ relation = Arel::Table.new(@reflection.options[:join_table])
81
+ relation.where(relation[@reflection.primary_key_name].eq(@owner.id).
82
+ and(relation[@reflection.association_foreign_key].in(records.map { |x| x.id }))
83
+ ).delete
84
+ end
85
+ end
86
+
87
+ def construct_sql
88
+ if @reflection.options[:finder_sql]
89
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
90
+ else
91
+ @finder_sql = "#{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{owner_quoted_id} "
92
+ @finder_sql << " AND (#{conditions})" if conditions
93
+ end
94
+
95
+ @join_sql = "INNER JOIN #{@owner.connection.quote_table_name @reflection.options[:join_table]} ON #{@reflection.quoted_table_name}.#{@reflection.klass.primary_key} = #{@owner.connection.quote_table_name @reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
96
+
97
+ construct_counter_sql
98
+ end
99
+
100
+ def construct_scope
101
+ { :find => { :conditions => @finder_sql,
102
+ :joins => @join_sql,
103
+ :readonly => false,
104
+ :order => @reflection.options[:order],
105
+ :include => @reflection.options[:include],
106
+ :limit => @reflection.options[:limit] } }
107
+ end
108
+
109
+ # Join tables with additional columns on top of the two foreign keys must be considered
110
+ # ambiguous unless a select clause has been explicitly defined. Otherwise you can get
111
+ # broken records back, if, for example, the join column also has an id column. This will
112
+ # then overwrite the id column of the records coming back.
113
+ def finding_with_ambiguous_select?(select_clause)
114
+ !select_clause && columns.size != 2
115
+ end
116
+
117
+ private
118
+ def create_record(attributes, &block)
119
+ # Can't use Base.create because the foreign key may be a protected attribute.
120
+ ensure_owner_is_not_new
121
+ if attributes.is_a?(Array)
122
+ attributes.collect { |attr| create(attr) }
123
+ else
124
+ build_record(attributes, &block)
125
+ end
126
+ end
127
+
128
+ def record_timestamp_columns(record)
129
+ if record.record_timestamps
130
+ record.send(:all_timestamp_attributes).map { |x| x.to_s }
131
+ else
132
+ []
133
+ end
134
+ end
135
+ end
45
136
  end
46
- end
137
+ end