activerecord 1.0.0 → 4.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 (255) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2102 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +213 -0
  5. data/examples/performance.rb +172 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record/aggregations.rb +180 -84
  8. data/lib/active_record/associations/alias_tracker.rb +76 -0
  9. data/lib/active_record/associations/association.rb +248 -0
  10. data/lib/active_record/associations/association_scope.rb +135 -0
  11. data/lib/active_record/associations/belongs_to_association.rb +92 -0
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +35 -0
  13. data/lib/active_record/associations/builder/association.rb +108 -0
  14. data/lib/active_record/associations/builder/belongs_to.rb +98 -0
  15. data/lib/active_record/associations/builder/collection_association.rb +89 -0
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
  17. data/lib/active_record/associations/builder/has_many.rb +15 -0
  18. data/lib/active_record/associations/builder/has_one.rb +25 -0
  19. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  20. data/lib/active_record/associations/collection_association.rb +608 -0
  21. data/lib/active_record/associations/collection_proxy.rb +986 -0
  22. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +58 -39
  23. data/lib/active_record/associations/has_many_association.rb +116 -85
  24. data/lib/active_record/associations/has_many_through_association.rb +197 -0
  25. data/lib/active_record/associations/has_one_association.rb +102 -0
  26. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  27. data/lib/active_record/associations/join_dependency/join_association.rb +174 -0
  28. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  29. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  30. data/lib/active_record/associations/join_dependency.rb +235 -0
  31. data/lib/active_record/associations/join_helper.rb +45 -0
  32. data/lib/active_record/associations/preloader/association.rb +121 -0
  33. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  34. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  35. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  36. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  37. data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
  38. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  39. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  40. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  41. data/lib/active_record/associations/preloader/through_association.rb +63 -0
  42. data/lib/active_record/associations/preloader.rb +178 -0
  43. data/lib/active_record/associations/singular_association.rb +64 -0
  44. data/lib/active_record/associations/through_association.rb +87 -0
  45. data/lib/active_record/associations.rb +1437 -431
  46. data/lib/active_record/attribute_assignment.rb +201 -0
  47. data/lib/active_record/attribute_methods/before_type_cast.rb +70 -0
  48. data/lib/active_record/attribute_methods/dirty.rb +118 -0
  49. data/lib/active_record/attribute_methods/primary_key.rb +122 -0
  50. data/lib/active_record/attribute_methods/query.rb +40 -0
  51. data/lib/active_record/attribute_methods/read.rb +107 -0
  52. data/lib/active_record/attribute_methods/serialization.rb +162 -0
  53. data/lib/active_record/attribute_methods/time_zone_conversion.rb +59 -0
  54. data/lib/active_record/attribute_methods/write.rb +63 -0
  55. data/lib/active_record/attribute_methods.rb +393 -0
  56. data/lib/active_record/autosave_association.rb +426 -0
  57. data/lib/active_record/base.rb +268 -930
  58. data/lib/active_record/callbacks.rb +203 -230
  59. data/lib/active_record/coders/yaml_column.rb +38 -0
  60. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +638 -0
  61. data/lib/active_record/connection_adapters/abstract/database_limits.rb +67 -0
  62. data/lib/active_record/connection_adapters/abstract/database_statements.rb +390 -0
  63. data/lib/active_record/connection_adapters/abstract/query_cache.rb +95 -0
  64. data/lib/active_record/connection_adapters/abstract/quoting.rb +129 -0
  65. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +501 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
  67. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +873 -0
  68. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  69. data/lib/active_record/connection_adapters/abstract_adapter.rb +389 -275
  70. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
  71. data/lib/active_record/connection_adapters/column.rb +318 -0
  72. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +517 -90
  75. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  76. data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
  77. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  78. data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
  79. data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
  80. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  81. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
  82. data/lib/active_record/connection_adapters/postgresql_adapter.rb +911 -138
  83. data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
  84. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +624 -0
  85. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  86. data/lib/active_record/connection_handling.rb +98 -0
  87. data/lib/active_record/core.rb +463 -0
  88. data/lib/active_record/counter_cache.rb +122 -0
  89. data/lib/active_record/dynamic_matchers.rb +131 -0
  90. data/lib/active_record/errors.rb +213 -0
  91. data/lib/active_record/explain.rb +38 -0
  92. data/lib/active_record/explain_registry.rb +30 -0
  93. data/lib/active_record/explain_subscriber.rb +29 -0
  94. data/lib/active_record/fixture_set/file.rb +55 -0
  95. data/lib/active_record/fixtures.rb +892 -138
  96. data/lib/active_record/inheritance.rb +200 -0
  97. data/lib/active_record/integration.rb +60 -0
  98. data/lib/active_record/locale/en.yml +47 -0
  99. data/lib/active_record/locking/optimistic.rb +181 -0
  100. data/lib/active_record/locking/pessimistic.rb +77 -0
  101. data/lib/active_record/log_subscriber.rb +82 -0
  102. data/lib/active_record/migration/command_recorder.rb +164 -0
  103. data/lib/active_record/migration/join_table.rb +15 -0
  104. data/lib/active_record/migration.rb +1015 -0
  105. data/lib/active_record/model_schema.rb +345 -0
  106. data/lib/active_record/nested_attributes.rb +546 -0
  107. data/lib/active_record/null_relation.rb +65 -0
  108. data/lib/active_record/persistence.rb +509 -0
  109. data/lib/active_record/query_cache.rb +56 -0
  110. data/lib/active_record/querying.rb +62 -0
  111. data/lib/active_record/railtie.rb +205 -0
  112. data/lib/active_record/railties/console_sandbox.rb +5 -0
  113. data/lib/active_record/railties/controller_runtime.rb +50 -0
  114. data/lib/active_record/railties/databases.rake +402 -0
  115. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  116. data/lib/active_record/readonly_attributes.rb +30 -0
  117. data/lib/active_record/reflection.rb +544 -87
  118. data/lib/active_record/relation/batches.rb +93 -0
  119. data/lib/active_record/relation/calculations.rb +399 -0
  120. data/lib/active_record/relation/delegation.rb +125 -0
  121. data/lib/active_record/relation/finder_methods.rb +349 -0
  122. data/lib/active_record/relation/merger.rb +161 -0
  123. data/lib/active_record/relation/predicate_builder.rb +106 -0
  124. data/lib/active_record/relation/query_methods.rb +1044 -0
  125. data/lib/active_record/relation/spawn_methods.rb +73 -0
  126. data/lib/active_record/relation.rb +655 -0
  127. data/lib/active_record/result.rb +67 -0
  128. data/lib/active_record/runtime_registry.rb +17 -0
  129. data/lib/active_record/sanitization.rb +168 -0
  130. data/lib/active_record/schema.rb +65 -0
  131. data/lib/active_record/schema_dumper.rb +204 -0
  132. data/lib/active_record/schema_migration.rb +39 -0
  133. data/lib/active_record/scoping/default.rb +146 -0
  134. data/lib/active_record/scoping/named.rb +175 -0
  135. data/lib/active_record/scoping.rb +82 -0
  136. data/lib/active_record/serialization.rb +22 -0
  137. data/lib/active_record/serializers/xml_serializer.rb +197 -0
  138. data/lib/active_record/statement_cache.rb +26 -0
  139. data/lib/active_record/store.rb +156 -0
  140. data/lib/active_record/tasks/database_tasks.rb +203 -0
  141. data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
  142. data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
  143. data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
  144. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  145. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  146. data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
  147. data/lib/active_record/test_case.rb +96 -0
  148. data/lib/active_record/timestamp.rb +119 -0
  149. data/lib/active_record/transactions.rb +366 -69
  150. data/lib/active_record/translation.rb +22 -0
  151. data/lib/active_record/validations/associated.rb +49 -0
  152. data/lib/active_record/validations/presence.rb +65 -0
  153. data/lib/active_record/validations/uniqueness.rb +225 -0
  154. data/lib/active_record/validations.rb +64 -185
  155. data/lib/active_record/version.rb +11 -0
  156. data/lib/active_record.rb +149 -24
  157. data/lib/rails/generators/active_record/migration/migration_generator.rb +62 -0
  158. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  159. data/lib/rails/generators/active_record/migration/templates/migration.rb +39 -0
  160. data/lib/rails/generators/active_record/model/model_generator.rb +48 -0
  161. data/lib/rails/generators/active_record/model/templates/model.rb +10 -0
  162. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  163. data/lib/rails/generators/active_record.rb +23 -0
  164. metadata +261 -161
  165. data/CHANGELOG +0 -581
  166. data/README +0 -361
  167. data/RUNNING_UNIT_TESTS +0 -36
  168. data/dev-utils/eval_debugger.rb +0 -9
  169. data/examples/associations.png +0 -0
  170. data/examples/associations.rb +0 -87
  171. data/examples/shared_setup.rb +0 -15
  172. data/examples/validation.rb +0 -88
  173. data/install.rb +0 -60
  174. data/lib/active_record/associations/association_collection.rb +0 -70
  175. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -107
  176. data/lib/active_record/deprecated_associations.rb +0 -70
  177. data/lib/active_record/observer.rb +0 -71
  178. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  179. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  180. data/lib/active_record/support/clean_logger.rb +0 -10
  181. data/lib/active_record/support/inflector.rb +0 -70
  182. data/lib/active_record/vendor/mysql.rb +0 -1117
  183. data/lib/active_record/vendor/simple.rb +0 -702
  184. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  185. data/lib/active_record/wrappings.rb +0 -59
  186. data/rakefile +0 -122
  187. data/test/abstract_unit.rb +0 -16
  188. data/test/aggregations_test.rb +0 -34
  189. data/test/all.sh +0 -8
  190. data/test/associations_test.rb +0 -477
  191. data/test/base_test.rb +0 -513
  192. data/test/class_inheritable_attributes_test.rb +0 -33
  193. data/test/connections/native_mysql/connection.rb +0 -24
  194. data/test/connections/native_postgresql/connection.rb +0 -24
  195. data/test/connections/native_sqlite/connection.rb +0 -24
  196. data/test/deprecated_associations_test.rb +0 -336
  197. data/test/finder_test.rb +0 -67
  198. data/test/fixtures/accounts/signals37 +0 -3
  199. data/test/fixtures/accounts/unknown +0 -2
  200. data/test/fixtures/auto_id.rb +0 -4
  201. data/test/fixtures/column_name.rb +0 -3
  202. data/test/fixtures/companies/first_client +0 -6
  203. data/test/fixtures/companies/first_firm +0 -4
  204. data/test/fixtures/companies/second_client +0 -6
  205. data/test/fixtures/company.rb +0 -37
  206. data/test/fixtures/company_in_module.rb +0 -33
  207. data/test/fixtures/course.rb +0 -3
  208. data/test/fixtures/courses/java +0 -2
  209. data/test/fixtures/courses/ruby +0 -2
  210. data/test/fixtures/customer.rb +0 -30
  211. data/test/fixtures/customers/david +0 -6
  212. data/test/fixtures/db_definitions/mysql.sql +0 -96
  213. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  214. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  215. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  216. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  217. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  218. data/test/fixtures/default.rb +0 -2
  219. data/test/fixtures/developer.rb +0 -8
  220. data/test/fixtures/developers/david +0 -2
  221. data/test/fixtures/developers/jamis +0 -2
  222. data/test/fixtures/developers_projects/david_action_controller +0 -2
  223. data/test/fixtures/developers_projects/david_active_record +0 -2
  224. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  225. data/test/fixtures/entrant.rb +0 -3
  226. data/test/fixtures/entrants/first +0 -3
  227. data/test/fixtures/entrants/second +0 -3
  228. data/test/fixtures/entrants/third +0 -3
  229. data/test/fixtures/fixture_database.sqlite +0 -0
  230. data/test/fixtures/fixture_database_2.sqlite +0 -0
  231. data/test/fixtures/movie.rb +0 -5
  232. data/test/fixtures/movies/first +0 -2
  233. data/test/fixtures/movies/second +0 -2
  234. data/test/fixtures/project.rb +0 -3
  235. data/test/fixtures/projects/action_controller +0 -2
  236. data/test/fixtures/projects/active_record +0 -2
  237. data/test/fixtures/reply.rb +0 -21
  238. data/test/fixtures/subscriber.rb +0 -5
  239. data/test/fixtures/subscribers/first +0 -2
  240. data/test/fixtures/subscribers/second +0 -2
  241. data/test/fixtures/topic.rb +0 -20
  242. data/test/fixtures/topics/first +0 -9
  243. data/test/fixtures/topics/second +0 -8
  244. data/test/fixtures_test.rb +0 -20
  245. data/test/inflector_test.rb +0 -104
  246. data/test/inheritance_test.rb +0 -125
  247. data/test/lifecycle_test.rb +0 -110
  248. data/test/modules_test.rb +0 -21
  249. data/test/multiple_db_test.rb +0 -46
  250. data/test/pk_test.rb +0 -57
  251. data/test/reflection_test.rb +0 -78
  252. data/test/thread_safety_test.rb +0 -33
  253. data/test/transactions_test.rb +0 -83
  254. data/test/unconnected_test.rb +0 -24
  255. data/test/validations_test.rb +0 -126
@@ -1,46 +1,65 @@
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}"
16
- 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?
25
- 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?
4
+ class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
5
+ attr_reader :join_table
6
+
7
+ def initialize(owner, reflection)
8
+ @join_table = Arel::Table.new(reflection.join_table)
9
+ super
33
10
  end
34
-
35
- protected
36
- def find_all_records
37
- @association_class.find_by_sql(@finder_sql)
11
+
12
+ def insert_record(record, validate = true, raise = false)
13
+ if record.new_record?
14
+ if raise
15
+ record.save!(:validate => validate)
16
+ else
17
+ return unless record.save(:validate => validate)
18
+ end
38
19
  end
39
-
40
- def count_records
41
- load_collection_to_array
42
- @collection_array.size
20
+
21
+ if options[:insert_sql]
22
+ owner.connection.insert(interpolate(options[:insert_sql], record))
23
+ else
24
+ stmt = join_table.compile_insert(
25
+ join_table[reflection.foreign_key] => owner.id,
26
+ join_table[reflection.association_foreign_key] => record.id
27
+ )
28
+
29
+ owner.class.connection.insert stmt
43
30
  end
31
+
32
+ record
44
33
  end
34
+
35
+ private
36
+
37
+ def count_records
38
+ load_target.size
39
+ end
40
+
41
+ def delete_records(records, method)
42
+ if sql = options[:delete_sql]
43
+ records = load_target if records == :all
44
+ records.each { |record| owner.class.connection.delete(interpolate(sql, record)) }
45
+ else
46
+ relation = join_table
47
+ condition = relation[reflection.foreign_key].eq(owner.id)
48
+
49
+ unless records == :all
50
+ condition = condition.and(
51
+ relation[reflection.association_foreign_key]
52
+ .in(records.map { |x| x.id }.compact)
53
+ )
54
+ end
55
+
56
+ owner.class.connection.delete(relation.where(condition).compile_delete)
57
+ end
58
+ end
59
+
60
+ def invertible_for?(record)
61
+ false
62
+ end
63
+ end
45
64
  end
46
- end
65
+ end
@@ -1,104 +1,135 @@
1
1
  module ActiveRecord
2
+ # = Active Record Has Many Association
2
3
  module Associations
3
- class HasManyAssociation < AssociationCollection #:nodoc:
4
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
- @conditions = options[:conditions]
7
-
8
- if options[:finder_sql]
9
- @counter_sql = options[:finder_sql].gsub(/SELECT (.*) FROM/, "SELECT COUNT(*) FROM")
10
- @finder_sql = options[:finder_sql]
11
- else
12
- @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + @conditions : ""}"
13
- @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
14
- end
15
- end
16
-
17
- def <<(record)
18
- raise ActiveRecord::AssociationTypeMismatch unless @association_class === record
19
- record.send(@association_class_primary_key_name + "=", @owner.id)
20
- record.save(false)
21
- @collection_array << record unless @collection_array.nil?
22
- end
23
-
24
- def delete(records)
25
- duplicated_records_array(records).each do |record|
26
- next if record.send(@association_class_primary_key_name) != @owner.id
27
- record.send(@association_class_primary_key_name + "=", nil)
28
- record.save(false)
29
- @collection_array.delete(record) unless @collection_array.nil?
30
- end
31
- end
32
-
33
- def create(attributes = {})
34
- # We can't use the regular Base.create method as the foreign key might be a protected attribute, hence the repetion
35
- record = @association_class.new(attributes || {})
36
- record.send(@association_class_primary_key_name + "=", @owner.id)
37
- record.save
38
-
39
- @collection_array << record unless @collection_array.nil?
40
-
41
- return record
42
- end
4
+ # This is the proxy that handles a has many association.
5
+ #
6
+ # If the association has a <tt>:through</tt> option further specialization
7
+ # is provided by its child HasManyThroughAssociation.
8
+ class HasManyAssociation < CollectionAssociation #:nodoc:
9
+
10
+ def handle_dependency
11
+ case options[:dependent]
12
+ when :restrict, :restrict_with_exception
13
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
14
+
15
+ when :restrict_with_error
16
+ unless empty?
17
+ record = klass.human_attribute_name(reflection.name).downcase
18
+ owner.errors.add(:base, :"restrict_dependent_destroy.many", record: record)
19
+ false
20
+ end
43
21
 
44
- def build(attributes = {})
45
- association = @association_class.new
46
- association.attributes = attributes.merge({ "#{@association_class_primary_key_name}" => @owner.id})
47
- association
48
- end
49
-
50
- def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
51
- if block_given? || @options[:finder_sql]
52
- load_collection_to_array
53
- @collection_array.send(:find_all, &block)
54
22
  else
55
- @association_class.find_all(
56
- "#{@association_class_primary_key_name} = '#{@owner.id}' " +
57
- "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + runtime_conditions : ""}",
58
- orderings,
59
- limit,
60
- joins
61
- )
23
+ if options[:dependent] == :destroy
24
+ # No point in executing the counter update since we're going to destroy the parent anyway
25
+ load_target.each { |t| t.destroyed_by_association = reflection }
26
+ destroy_all
27
+ else
28
+ delete_all
29
+ end
62
30
  end
63
31
  end
64
32
 
65
- def find(association_id = nil, &block)
66
- if block_given? || @options[:finder_sql]
67
- load_collection_to_array
68
- return @collection_array.send(:find, &block)
33
+ def insert_record(record, validate = true, raise = false)
34
+ set_owner_attributes(record)
35
+
36
+ if raise
37
+ record.save!(:validate => validate)
69
38
  else
70
- @association_class.find_on_conditions(
71
- association_id, "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
72
- )
39
+ record.save(:validate => validate)
73
40
  end
74
41
  end
75
-
76
- protected
77
- def find_all_records
78
- if @options[:finder_sql]
79
- @association_class.find_by_sql(@finder_sql)
42
+
43
+ private
44
+
45
+ # Returns the number of records in this collection.
46
+ #
47
+ # If the association has a counter cache it gets that value. Otherwise
48
+ # it will attempt to do a count via SQL, bounded to <tt>:limit</tt> if
49
+ # there's one. Some configuration options like :group make it impossible
50
+ # to do an SQL count, in those cases the array count will be used.
51
+ #
52
+ # That does not depend on whether the collection has already been loaded
53
+ # or not. The +size+ method is the one that takes the loaded flag into
54
+ # account and delegates to +count_records+ if needed.
55
+ #
56
+ # If the collection is empty the target is set to an empty array and
57
+ # the loaded flag is set to true as well.
58
+ def count_records
59
+ count = if has_cached_counter?
60
+ owner.send(:read_attribute, cached_counter_attribute_name)
61
+ elsif options[:counter_sql] || options[:finder_sql]
62
+ reflection.klass.count_by_sql(custom_counter_sql)
80
63
  else
81
- @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
64
+ scope.count
82
65
  end
66
+
67
+ # If there's nothing in the database and @target has no new records
68
+ # we are certain the current target is an empty array. This is a
69
+ # documented side-effect of the method that may avoid an extra SELECT.
70
+ @target ||= [] and loaded! if count == 0
71
+
72
+ [association_scope.limit_value, count].compact.min
83
73
  end
84
-
85
- def count_records
86
- if has_cached_counter?
87
- @owner.send(:read_attribute, cached_counter_attribute_name)
88
- elsif @options[:finder_sql]
89
- @association_class.count_by_sql(@counter_sql)
90
- else
91
- @association_class.count(@counter_sql)
74
+
75
+ def has_cached_counter?(reflection = reflection)
76
+ owner.attribute_present?(cached_counter_attribute_name(reflection))
77
+ end
78
+
79
+ def cached_counter_attribute_name(reflection = reflection)
80
+ options[:counter_cache] || "#{reflection.name}_count"
81
+ end
82
+
83
+ def update_counter(difference, reflection = reflection)
84
+ if has_cached_counter?(reflection)
85
+ counter = cached_counter_attribute_name(reflection)
86
+ owner.class.update_counters(owner.id, counter => difference)
87
+ owner[counter] += difference
88
+ owner.changed_attributes.delete(counter) # eww
92
89
  end
93
90
  end
94
-
95
- def has_cached_counter?
96
- @owner.attribute_present?(cached_counter_attribute_name)
91
+
92
+ # This shit is nasty. We need to avoid the following situation:
93
+ #
94
+ # * An associated record is deleted via record.destroy
95
+ # * Hence the callbacks run, and they find a belongs_to on the record with a
96
+ # :counter_cache options which points back at our owner. So they update the
97
+ # counter cache.
98
+ # * In which case, we must make sure to *not* update the counter cache, or else
99
+ # it will be decremented twice.
100
+ #
101
+ # Hence this method.
102
+ def inverse_updates_counter_cache?(reflection = reflection)
103
+ counter_name = cached_counter_attribute_name(reflection)
104
+ reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
105
+ inverse_reflection.counter_cache_column == counter_name
106
+ }
107
+ end
108
+
109
+ # Deletes the records according to the <tt>:dependent</tt> option.
110
+ def delete_records(records, method)
111
+ if method == :destroy
112
+ records.each { |r| r.destroy }
113
+ update_counter(-records.length) unless inverse_updates_counter_cache?
114
+ else
115
+ if records == :all
116
+ scope = self.scope
117
+ else
118
+ keys = records.map { |r| r[reflection.association_primary_key] }
119
+ scope = self.scope.where(reflection.association_primary_key => keys)
120
+ end
121
+
122
+ if method == :delete_all
123
+ update_counter(-scope.delete_all)
124
+ else
125
+ update_counter(-scope.update_all(reflection.foreign_key => nil))
126
+ end
127
+ end
97
128
  end
98
-
99
- def cached_counter_attribute_name
100
- @association_name + "_count"
129
+
130
+ def foreign_key_present?
131
+ owner.attribute_present?(reflection.association_primary_key)
101
132
  end
102
133
  end
103
134
  end
104
- end
135
+ end
@@ -0,0 +1,197 @@
1
+
2
+ module ActiveRecord
3
+ # = Active Record Has Many Through Association
4
+ module Associations
5
+ class HasManyThroughAssociation < HasManyAssociation #:nodoc:
6
+ include ThroughAssociation
7
+
8
+ def initialize(owner, reflection)
9
+ super
10
+
11
+ @through_records = {}
12
+ @through_association = nil
13
+ end
14
+
15
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
16
+ # loaded and calling collection.size if it has. If it's more likely than not that the collection does
17
+ # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
18
+ # SELECT query if you use #length.
19
+ def size
20
+ if has_cached_counter?
21
+ owner.send(:read_attribute, cached_counter_attribute_name)
22
+ elsif loaded?
23
+ target.size
24
+ else
25
+ count
26
+ end
27
+ end
28
+
29
+ def concat(*records)
30
+ unless owner.new_record?
31
+ records.flatten.each do |record|
32
+ raise_on_type_mismatch!(record)
33
+ record.save! if record.new_record?
34
+ end
35
+ end
36
+
37
+ super
38
+ end
39
+
40
+ def concat_records(records)
41
+ ensure_not_nested
42
+
43
+ records = super
44
+
45
+ if owner.new_record? && records
46
+ records.flatten.each do |record|
47
+ build_through_record(record)
48
+ end
49
+ end
50
+
51
+ records
52
+ end
53
+
54
+ def insert_record(record, validate = true, raise = false)
55
+ ensure_not_nested
56
+
57
+ if record.new_record?
58
+ if raise
59
+ record.save!(:validate => validate)
60
+ else
61
+ return unless record.save(:validate => validate)
62
+ end
63
+ end
64
+
65
+ save_through_record(record)
66
+ update_counter(1)
67
+ record
68
+ end
69
+
70
+ private
71
+
72
+ def through_association
73
+ @through_association ||= owner.association(through_reflection.name)
74
+ end
75
+
76
+ # We temporarily cache through record that has been build, because if we build a
77
+ # through record in build_record and then subsequently call insert_record, then we
78
+ # want to use the exact same object.
79
+ #
80
+ # However, after insert_record has been called, we clear the cache entry because
81
+ # we want it to be possible to have multiple instances of the same record in an
82
+ # association
83
+ def build_through_record(record)
84
+ @through_records[record.object_id] ||= begin
85
+ ensure_mutable
86
+
87
+ through_record = through_association.build
88
+ through_record.send("#{source_reflection.name}=", record)
89
+ through_record
90
+ end
91
+ end
92
+
93
+ def save_through_record(record)
94
+ build_through_record(record).save!
95
+ ensure
96
+ @through_records.delete(record.object_id)
97
+ end
98
+
99
+ def build_record(attributes)
100
+ ensure_not_nested
101
+
102
+ record = super(attributes)
103
+
104
+ inverse = source_reflection.inverse_of
105
+ if inverse
106
+ if inverse.macro == :has_many
107
+ record.send(inverse.name) << build_through_record(record)
108
+ elsif inverse.macro == :has_one
109
+ record.send("#{inverse.name}=", build_through_record(record))
110
+ end
111
+ end
112
+
113
+ record
114
+ end
115
+
116
+ def target_reflection_has_associated_record?
117
+ !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?)
118
+ end
119
+
120
+ def update_through_counter?(method)
121
+ case method
122
+ when :destroy
123
+ !inverse_updates_counter_cache?(through_reflection)
124
+ when :nullify
125
+ false
126
+ else
127
+ true
128
+ end
129
+ end
130
+
131
+ def delete_records(records, method)
132
+ ensure_not_nested
133
+
134
+ # This is unoptimised; it will load all the target records
135
+ # even when we just want to delete everything.
136
+ records = load_target if records == :all
137
+
138
+ scope = through_association.scope
139
+ scope.where! construct_join_attributes(*records)
140
+
141
+ case method
142
+ when :destroy
143
+ count = scope.destroy_all.length
144
+ when :nullify
145
+ count = scope.update_all(source_reflection.foreign_key => nil)
146
+ else
147
+ count = scope.delete_all
148
+ end
149
+
150
+ delete_through_records(records)
151
+
152
+ if source_reflection.options[:counter_cache]
153
+ counter = source_reflection.counter_cache_column
154
+ klass.decrement_counter counter, records.map(&:id)
155
+ end
156
+
157
+ if through_reflection.macro == :has_many && update_through_counter?(method)
158
+ update_counter(-count, through_reflection)
159
+ end
160
+
161
+ update_counter(-count)
162
+ end
163
+
164
+ def through_records_for(record)
165
+ attributes = construct_join_attributes(record)
166
+ candidates = Array.wrap(through_association.target)
167
+ candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
168
+ end
169
+
170
+ def delete_through_records(records)
171
+ records.each do |record|
172
+ through_records = through_records_for(record)
173
+
174
+ if through_reflection.macro == :has_many
175
+ through_records.each { |r| through_association.target.delete(r) }
176
+ else
177
+ if through_records.include?(through_association.target)
178
+ through_association.target = nil
179
+ end
180
+ end
181
+
182
+ @through_records.delete(record.object_id)
183
+ end
184
+ end
185
+
186
+ def find_target
187
+ return [] unless target_reflection_has_associated_record?
188
+ scope.to_a
189
+ end
190
+
191
+ # NOTE - not sure that we can actually cope with inverses here
192
+ def invertible_for?(record)
193
+ false
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,102 @@
1
+
2
+ module ActiveRecord
3
+ # = Active Record Belongs To Has One Association
4
+ module Associations
5
+ class HasOneAssociation < SingularAssociation #:nodoc:
6
+
7
+ def handle_dependency
8
+ case options[:dependent]
9
+ when :restrict, :restrict_with_exception
10
+ raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target
11
+
12
+ when :restrict_with_error
13
+ if load_target
14
+ record = klass.human_attribute_name(reflection.name).downcase
15
+ owner.errors.add(:base, :"restrict_dependent_destroy.one", record: record)
16
+ false
17
+ end
18
+
19
+ else
20
+ delete
21
+ end
22
+ end
23
+
24
+ def replace(record, save = true)
25
+ raise_on_type_mismatch!(record) if record
26
+ load_target
27
+
28
+ return self.target if !(target || record)
29
+ if (target != record) || record.changed?
30
+ transaction_if(save) do
31
+ remove_target!(options[:dependent]) if target && !target.destroyed?
32
+
33
+ if record
34
+ set_owner_attributes(record)
35
+ set_inverse_instance(record)
36
+
37
+ if owner.persisted? && save && !record.save
38
+ nullify_owner_attributes(record)
39
+ set_owner_attributes(target) if target
40
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ self.target = record
47
+ end
48
+
49
+ def delete(method = options[:dependent])
50
+ if load_target
51
+ case method
52
+ when :delete
53
+ target.delete
54
+ when :destroy
55
+ target.destroy
56
+ when :nullify
57
+ target.update_columns(reflection.foreign_key => nil)
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # The reason that the save param for replace is false, if for create (not just build),
65
+ # is because the setting of the foreign keys is actually handled by the scoping when
66
+ # the record is instantiated, and so they are set straight away and do not need to be
67
+ # updated within replace.
68
+ def set_new_record(record)
69
+ replace(record, false)
70
+ end
71
+
72
+ def remove_target!(method)
73
+ case method
74
+ when :delete
75
+ target.delete
76
+ when :destroy
77
+ target.destroy
78
+ else
79
+ nullify_owner_attributes(target)
80
+
81
+ if target.persisted? && owner.persisted? && !target.save
82
+ set_owner_attributes(target)
83
+ raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " +
84
+ "The record failed to save after its foreign key was set to nil."
85
+ end
86
+ end
87
+ end
88
+
89
+ def nullify_owner_attributes(record)
90
+ record[reflection.foreign_key] = nil
91
+ end
92
+
93
+ def transaction_if(value)
94
+ if value
95
+ reflection.klass.transaction { yield }
96
+ else
97
+ yield
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,36 @@
1
+ module ActiveRecord
2
+ # = Active Record Has One Through Association
3
+ module Associations
4
+ class HasOneThroughAssociation < HasOneAssociation #:nodoc:
5
+ include ThroughAssociation
6
+
7
+ def replace(record)
8
+ create_through_record(record)
9
+ self.target = record
10
+ end
11
+
12
+ private
13
+
14
+ def create_through_record(record)
15
+ ensure_not_nested
16
+
17
+ through_proxy = owner.association(through_reflection.name)
18
+ through_record = through_proxy.send(:load_target)
19
+
20
+ if through_record && !record
21
+ through_record.destroy
22
+ elsif record
23
+ attributes = construct_join_attributes(record)
24
+
25
+ if through_record
26
+ through_record.update(attributes)
27
+ elsif owner.new_record?
28
+ through_proxy.build(attributes)
29
+ else
30
+ through_proxy.create(attributes)
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end