activerecord_csi 2.3.5.p6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (333) hide show
  1. data/CHANGELOG +5858 -0
  2. data/README +351 -0
  3. data/RUNNING_UNIT_TESTS +36 -0
  4. data/Rakefile +270 -0
  5. data/examples/associations.png +0 -0
  6. data/examples/performance.rb +162 -0
  7. data/install.rb +30 -0
  8. data/lib/active_record/aggregations.rb +261 -0
  9. data/lib/active_record/association_preload.rb +389 -0
  10. data/lib/active_record/associations/association_collection.rb +475 -0
  11. data/lib/active_record/associations/association_proxy.rb +278 -0
  12. data/lib/active_record/associations/belongs_to_association.rb +76 -0
  13. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +53 -0
  14. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +143 -0
  15. data/lib/active_record/associations/has_many_association.rb +122 -0
  16. data/lib/active_record/associations/has_many_through_association.rb +266 -0
  17. data/lib/active_record/associations/has_one_association.rb +133 -0
  18. data/lib/active_record/associations/has_one_through_association.rb +37 -0
  19. data/lib/active_record/associations.rb +2241 -0
  20. data/lib/active_record/attribute_methods.rb +388 -0
  21. data/lib/active_record/autosave_association.rb +364 -0
  22. data/lib/active_record/base.rb +3171 -0
  23. data/lib/active_record/batches.rb +81 -0
  24. data/lib/active_record/calculations.rb +311 -0
  25. data/lib/active_record/callbacks.rb +360 -0
  26. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +371 -0
  27. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +139 -0
  28. data/lib/active_record/connection_adapters/abstract/database_statements.rb +289 -0
  29. data/lib/active_record/connection_adapters/abstract/query_cache.rb +94 -0
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
  31. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +722 -0
  32. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +434 -0
  33. data/lib/active_record/connection_adapters/abstract_adapter.rb +241 -0
  34. data/lib/active_record/connection_adapters/mysql_adapter.rb +630 -0
  35. data/lib/active_record/connection_adapters/postgresql_adapter.rb +1113 -0
  36. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  37. data/lib/active_record/connection_adapters/sqlite_adapter.rb +453 -0
  38. data/lib/active_record/dirty.rb +183 -0
  39. data/lib/active_record/dynamic_finder_match.rb +41 -0
  40. data/lib/active_record/dynamic_scope_match.rb +25 -0
  41. data/lib/active_record/fixtures.rb +996 -0
  42. data/lib/active_record/i18n_interpolation_deprecation.rb +26 -0
  43. data/lib/active_record/locale/en.yml +58 -0
  44. data/lib/active_record/locking/optimistic.rb +148 -0
  45. data/lib/active_record/locking/pessimistic.rb +55 -0
  46. data/lib/active_record/migration.rb +566 -0
  47. data/lib/active_record/named_scope.rb +192 -0
  48. data/lib/active_record/nested_attributes.rb +392 -0
  49. data/lib/active_record/observer.rb +197 -0
  50. data/lib/active_record/query_cache.rb +33 -0
  51. data/lib/active_record/reflection.rb +320 -0
  52. data/lib/active_record/schema.rb +51 -0
  53. data/lib/active_record/schema_dumper.rb +182 -0
  54. data/lib/active_record/serialization.rb +101 -0
  55. data/lib/active_record/serializers/json_serializer.rb +91 -0
  56. data/lib/active_record/serializers/xml_serializer.rb +357 -0
  57. data/lib/active_record/session_store.rb +326 -0
  58. data/lib/active_record/test_case.rb +66 -0
  59. data/lib/active_record/timestamp.rb +71 -0
  60. data/lib/active_record/transactions.rb +235 -0
  61. data/lib/active_record/validations.rb +1135 -0
  62. data/lib/active_record/version.rb +9 -0
  63. data/lib/active_record.rb +84 -0
  64. data/lib/activerecord.rb +2 -0
  65. data/test/assets/example.log +1 -0
  66. data/test/assets/flowers.jpg +0 -0
  67. data/test/cases/aaa_create_tables_test.rb +24 -0
  68. data/test/cases/active_schema_test_mysql.rb +100 -0
  69. data/test/cases/active_schema_test_postgresql.rb +24 -0
  70. data/test/cases/adapter_test.rb +145 -0
  71. data/test/cases/aggregations_test.rb +167 -0
  72. data/test/cases/ar_schema_test.rb +32 -0
  73. data/test/cases/associations/belongs_to_associations_test.rb +425 -0
  74. data/test/cases/associations/callbacks_test.rb +161 -0
  75. data/test/cases/associations/cascaded_eager_loading_test.rb +131 -0
  76. data/test/cases/associations/eager_load_includes_full_sti_class_test.rb +36 -0
  77. data/test/cases/associations/eager_load_nested_include_test.rb +130 -0
  78. data/test/cases/associations/eager_singularization_test.rb +145 -0
  79. data/test/cases/associations/eager_test.rb +834 -0
  80. data/test/cases/associations/extension_test.rb +62 -0
  81. data/test/cases/associations/habtm_join_table_test.rb +56 -0
  82. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +822 -0
  83. data/test/cases/associations/has_many_associations_test.rb +1134 -0
  84. data/test/cases/associations/has_many_through_associations_test.rb +346 -0
  85. data/test/cases/associations/has_one_associations_test.rb +330 -0
  86. data/test/cases/associations/has_one_through_associations_test.rb +209 -0
  87. data/test/cases/associations/inner_join_association_test.rb +93 -0
  88. data/test/cases/associations/join_model_test.rb +712 -0
  89. data/test/cases/associations_test.rb +262 -0
  90. data/test/cases/attribute_methods_test.rb +305 -0
  91. data/test/cases/autosave_association_test.rb +1142 -0
  92. data/test/cases/base_test.rb +2154 -0
  93. data/test/cases/batches_test.rb +61 -0
  94. data/test/cases/binary_test.rb +30 -0
  95. data/test/cases/calculations_test.rb +348 -0
  96. data/test/cases/callbacks_observers_test.rb +38 -0
  97. data/test/cases/callbacks_test.rb +438 -0
  98. data/test/cases/class_inheritable_attributes_test.rb +32 -0
  99. data/test/cases/column_alias_test.rb +17 -0
  100. data/test/cases/column_definition_test.rb +70 -0
  101. data/test/cases/connection_pool_test.rb +25 -0
  102. data/test/cases/connection_test_firebird.rb +8 -0
  103. data/test/cases/connection_test_mysql.rb +64 -0
  104. data/test/cases/copy_table_test_sqlite.rb +80 -0
  105. data/test/cases/database_statements_test.rb +12 -0
  106. data/test/cases/datatype_test_postgresql.rb +204 -0
  107. data/test/cases/date_time_test.rb +37 -0
  108. data/test/cases/default_test_firebird.rb +16 -0
  109. data/test/cases/defaults_test.rb +111 -0
  110. data/test/cases/deprecated_finder_test.rb +30 -0
  111. data/test/cases/dirty_test.rb +316 -0
  112. data/test/cases/finder_respond_to_test.rb +76 -0
  113. data/test/cases/finder_test.rb +1066 -0
  114. data/test/cases/fixtures_test.rb +656 -0
  115. data/test/cases/helper.rb +68 -0
  116. data/test/cases/i18n_test.rb +46 -0
  117. data/test/cases/inheritance_test.rb +262 -0
  118. data/test/cases/invalid_date_test.rb +24 -0
  119. data/test/cases/json_serialization_test.rb +205 -0
  120. data/test/cases/lifecycle_test.rb +193 -0
  121. data/test/cases/locking_test.rb +304 -0
  122. data/test/cases/method_scoping_test.rb +704 -0
  123. data/test/cases/migration_test.rb +1523 -0
  124. data/test/cases/migration_test_firebird.rb +124 -0
  125. data/test/cases/mixin_test.rb +96 -0
  126. data/test/cases/modules_test.rb +81 -0
  127. data/test/cases/multiple_db_test.rb +85 -0
  128. data/test/cases/named_scope_test.rb +361 -0
  129. data/test/cases/nested_attributes_test.rb +581 -0
  130. data/test/cases/pk_test.rb +119 -0
  131. data/test/cases/pooled_connections_test.rb +103 -0
  132. data/test/cases/query_cache_test.rb +123 -0
  133. data/test/cases/readonly_test.rb +107 -0
  134. data/test/cases/reflection_test.rb +194 -0
  135. data/test/cases/reload_models_test.rb +22 -0
  136. data/test/cases/repair_helper.rb +50 -0
  137. data/test/cases/reserved_word_test_mysql.rb +176 -0
  138. data/test/cases/sanitize_test.rb +25 -0
  139. data/test/cases/schema_authorization_test_postgresql.rb +75 -0
  140. data/test/cases/schema_dumper_test.rb +211 -0
  141. data/test/cases/schema_test_postgresql.rb +178 -0
  142. data/test/cases/serialization_test.rb +47 -0
  143. data/test/cases/synonym_test_oracle.rb +17 -0
  144. data/test/cases/timestamp_test.rb +75 -0
  145. data/test/cases/transactions_test.rb +522 -0
  146. data/test/cases/unconnected_test.rb +32 -0
  147. data/test/cases/validations_i18n_test.rb +955 -0
  148. data/test/cases/validations_test.rb +1640 -0
  149. data/test/cases/xml_serialization_test.rb +240 -0
  150. data/test/config.rb +5 -0
  151. data/test/connections/jdbc_jdbcderby/connection.rb +18 -0
  152. data/test/connections/jdbc_jdbch2/connection.rb +18 -0
  153. data/test/connections/jdbc_jdbchsqldb/connection.rb +18 -0
  154. data/test/connections/jdbc_jdbcmysql/connection.rb +26 -0
  155. data/test/connections/jdbc_jdbcpostgresql/connection.rb +26 -0
  156. data/test/connections/jdbc_jdbcsqlite3/connection.rb +25 -0
  157. data/test/connections/native_db2/connection.rb +25 -0
  158. data/test/connections/native_firebird/connection.rb +26 -0
  159. data/test/connections/native_frontbase/connection.rb +27 -0
  160. data/test/connections/native_mysql/connection.rb +25 -0
  161. data/test/connections/native_openbase/connection.rb +21 -0
  162. data/test/connections/native_oracle/connection.rb +27 -0
  163. data/test/connections/native_postgresql/connection.rb +25 -0
  164. data/test/connections/native_sqlite/connection.rb +25 -0
  165. data/test/connections/native_sqlite3/connection.rb +25 -0
  166. data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
  167. data/test/connections/native_sybase/connection.rb +23 -0
  168. data/test/fixtures/accounts.yml +29 -0
  169. data/test/fixtures/all/developers.yml +0 -0
  170. data/test/fixtures/all/people.csv +0 -0
  171. data/test/fixtures/all/tasks.yml +0 -0
  172. data/test/fixtures/author_addresses.yml +5 -0
  173. data/test/fixtures/author_favorites.yml +4 -0
  174. data/test/fixtures/authors.yml +9 -0
  175. data/test/fixtures/binaries.yml +132 -0
  176. data/test/fixtures/books.yml +7 -0
  177. data/test/fixtures/categories/special_categories.yml +9 -0
  178. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  179. data/test/fixtures/categories.yml +14 -0
  180. data/test/fixtures/categories_ordered.yml +7 -0
  181. data/test/fixtures/categories_posts.yml +23 -0
  182. data/test/fixtures/categorizations.yml +17 -0
  183. data/test/fixtures/clubs.yml +6 -0
  184. data/test/fixtures/comments.yml +59 -0
  185. data/test/fixtures/companies.yml +56 -0
  186. data/test/fixtures/computers.yml +4 -0
  187. data/test/fixtures/courses.yml +7 -0
  188. data/test/fixtures/customers.yml +26 -0
  189. data/test/fixtures/developers.yml +21 -0
  190. data/test/fixtures/developers_projects.yml +17 -0
  191. data/test/fixtures/edges.yml +6 -0
  192. data/test/fixtures/entrants.yml +14 -0
  193. data/test/fixtures/fixture_database.sqlite3 +0 -0
  194. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  195. data/test/fixtures/fk_test_has_fk.yml +3 -0
  196. data/test/fixtures/fk_test_has_pk.yml +2 -0
  197. data/test/fixtures/funny_jokes.yml +10 -0
  198. data/test/fixtures/items.yml +4 -0
  199. data/test/fixtures/jobs.yml +7 -0
  200. data/test/fixtures/legacy_things.yml +3 -0
  201. data/test/fixtures/mateys.yml +4 -0
  202. data/test/fixtures/member_types.yml +6 -0
  203. data/test/fixtures/members.yml +6 -0
  204. data/test/fixtures/memberships.yml +20 -0
  205. data/test/fixtures/minimalistics.yml +2 -0
  206. data/test/fixtures/mixed_case_monkeys.yml +6 -0
  207. data/test/fixtures/mixins.yml +29 -0
  208. data/test/fixtures/movies.yml +7 -0
  209. data/test/fixtures/naked/csv/accounts.csv +1 -0
  210. data/test/fixtures/naked/yml/accounts.yml +1 -0
  211. data/test/fixtures/naked/yml/companies.yml +1 -0
  212. data/test/fixtures/naked/yml/courses.yml +1 -0
  213. data/test/fixtures/organizations.yml +5 -0
  214. data/test/fixtures/owners.yml +7 -0
  215. data/test/fixtures/parrots.yml +27 -0
  216. data/test/fixtures/parrots_pirates.yml +7 -0
  217. data/test/fixtures/people.yml +15 -0
  218. data/test/fixtures/pets.yml +14 -0
  219. data/test/fixtures/pirates.yml +9 -0
  220. data/test/fixtures/posts.yml +52 -0
  221. data/test/fixtures/price_estimates.yml +7 -0
  222. data/test/fixtures/projects.yml +7 -0
  223. data/test/fixtures/readers.yml +9 -0
  224. data/test/fixtures/references.yml +17 -0
  225. data/test/fixtures/reserved_words/distinct.yml +5 -0
  226. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  227. data/test/fixtures/reserved_words/group.yml +14 -0
  228. data/test/fixtures/reserved_words/select.yml +8 -0
  229. data/test/fixtures/reserved_words/values.yml +7 -0
  230. data/test/fixtures/ships.yml +5 -0
  231. data/test/fixtures/sponsors.yml +9 -0
  232. data/test/fixtures/subscribers.yml +7 -0
  233. data/test/fixtures/subscriptions.yml +12 -0
  234. data/test/fixtures/taggings.yml +28 -0
  235. data/test/fixtures/tags.yml +7 -0
  236. data/test/fixtures/tasks.yml +7 -0
  237. data/test/fixtures/topics.yml +42 -0
  238. data/test/fixtures/toys.yml +4 -0
  239. data/test/fixtures/treasures.yml +10 -0
  240. data/test/fixtures/vertices.yml +4 -0
  241. data/test/fixtures/warehouse-things.yml +3 -0
  242. data/test/migrations/broken/100_migration_that_raises_exception.rb +10 -0
  243. data/test/migrations/decimal/1_give_me_big_numbers.rb +15 -0
  244. data/test/migrations/duplicate/1_people_have_last_names.rb +9 -0
  245. data/test/migrations/duplicate/2_we_need_reminders.rb +12 -0
  246. data/test/migrations/duplicate/3_foo.rb +7 -0
  247. data/test/migrations/duplicate/3_innocent_jointable.rb +12 -0
  248. data/test/migrations/duplicate_names/20080507052938_chunky.rb +7 -0
  249. data/test/migrations/duplicate_names/20080507053028_chunky.rb +7 -0
  250. data/test/migrations/interleaved/pass_1/3_innocent_jointable.rb +12 -0
  251. data/test/migrations/interleaved/pass_2/1_people_have_last_names.rb +9 -0
  252. data/test/migrations/interleaved/pass_2/3_innocent_jointable.rb +12 -0
  253. data/test/migrations/interleaved/pass_3/1_people_have_last_names.rb +9 -0
  254. data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +8 -0
  255. data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +12 -0
  256. data/test/migrations/missing/1000_people_have_middle_names.rb +9 -0
  257. data/test/migrations/missing/1_people_have_last_names.rb +9 -0
  258. data/test/migrations/missing/3_we_need_reminders.rb +12 -0
  259. data/test/migrations/missing/4_innocent_jointable.rb +12 -0
  260. data/test/migrations/valid/1_people_have_last_names.rb +9 -0
  261. data/test/migrations/valid/2_we_need_reminders.rb +12 -0
  262. data/test/migrations/valid/3_innocent_jointable.rb +12 -0
  263. data/test/models/author.rb +146 -0
  264. data/test/models/auto_id.rb +4 -0
  265. data/test/models/binary.rb +2 -0
  266. data/test/models/bird.rb +3 -0
  267. data/test/models/book.rb +4 -0
  268. data/test/models/categorization.rb +5 -0
  269. data/test/models/category.rb +34 -0
  270. data/test/models/citation.rb +6 -0
  271. data/test/models/club.rb +13 -0
  272. data/test/models/column_name.rb +3 -0
  273. data/test/models/comment.rb +29 -0
  274. data/test/models/company.rb +171 -0
  275. data/test/models/company_in_module.rb +61 -0
  276. data/test/models/computer.rb +3 -0
  277. data/test/models/contact.rb +16 -0
  278. data/test/models/contract.rb +5 -0
  279. data/test/models/course.rb +3 -0
  280. data/test/models/customer.rb +73 -0
  281. data/test/models/default.rb +2 -0
  282. data/test/models/developer.rb +101 -0
  283. data/test/models/edge.rb +5 -0
  284. data/test/models/entrant.rb +3 -0
  285. data/test/models/essay.rb +3 -0
  286. data/test/models/event.rb +3 -0
  287. data/test/models/guid.rb +2 -0
  288. data/test/models/item.rb +7 -0
  289. data/test/models/job.rb +5 -0
  290. data/test/models/joke.rb +3 -0
  291. data/test/models/keyboard.rb +3 -0
  292. data/test/models/legacy_thing.rb +3 -0
  293. data/test/models/matey.rb +4 -0
  294. data/test/models/member.rb +12 -0
  295. data/test/models/member_detail.rb +5 -0
  296. data/test/models/member_type.rb +3 -0
  297. data/test/models/membership.rb +9 -0
  298. data/test/models/minimalistic.rb +2 -0
  299. data/test/models/mixed_case_monkey.rb +3 -0
  300. data/test/models/movie.rb +5 -0
  301. data/test/models/order.rb +4 -0
  302. data/test/models/organization.rb +6 -0
  303. data/test/models/owner.rb +5 -0
  304. data/test/models/parrot.rb +16 -0
  305. data/test/models/person.rb +16 -0
  306. data/test/models/pet.rb +5 -0
  307. data/test/models/pirate.rb +70 -0
  308. data/test/models/post.rb +100 -0
  309. data/test/models/price_estimate.rb +3 -0
  310. data/test/models/project.rb +30 -0
  311. data/test/models/reader.rb +4 -0
  312. data/test/models/reference.rb +4 -0
  313. data/test/models/reply.rb +46 -0
  314. data/test/models/ship.rb +10 -0
  315. data/test/models/ship_part.rb +5 -0
  316. data/test/models/sponsor.rb +4 -0
  317. data/test/models/subject.rb +4 -0
  318. data/test/models/subscriber.rb +8 -0
  319. data/test/models/subscription.rb +4 -0
  320. data/test/models/tag.rb +7 -0
  321. data/test/models/tagging.rb +10 -0
  322. data/test/models/task.rb +3 -0
  323. data/test/models/topic.rb +80 -0
  324. data/test/models/toy.rb +6 -0
  325. data/test/models/treasure.rb +8 -0
  326. data/test/models/vertex.rb +9 -0
  327. data/test/models/warehouse_thing.rb +5 -0
  328. data/test/schema/mysql_specific_schema.rb +24 -0
  329. data/test/schema/postgresql_specific_schema.rb +114 -0
  330. data/test/schema/schema.rb +493 -0
  331. data/test/schema/schema2.rb +6 -0
  332. data/test/schema/sqlite_specific_schema.rb +25 -0
  333. metadata +420 -0
@@ -0,0 +1,475 @@
1
+ require 'set'
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ # AssociationCollection is an abstract class that provides common stuff to
6
+ # ease the implementation of association proxies that represent
7
+ # collections. See the class hierarchy in AssociationProxy.
8
+ #
9
+ # You need to be careful with assumptions regarding the target: The proxy
10
+ # does not fetch records from the database until it needs them, but new
11
+ # ones created with +build+ are added to the target. So, the target may be
12
+ # non-empty and still lack children waiting to be read from the database.
13
+ # If you look directly to the database you cannot assume that's the entire
14
+ # collection because new records may have beed added to the target, etc.
15
+ #
16
+ # If you need to work on all current children, new and existing records,
17
+ # +load_target+ and the +loaded+ flag are your friends.
18
+ class AssociationCollection < AssociationProxy #:nodoc:
19
+ def initialize(owner, reflection)
20
+ super
21
+ construct_sql
22
+ end
23
+
24
+ def find(*args)
25
+ options = args.extract_options!
26
+
27
+ # If using a custom finder_sql, scan the entire collection.
28
+ if @reflection.options[:finder_sql]
29
+ expects_array = args.first.kind_of?(Array)
30
+ ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
31
+
32
+ if ids.size == 1
33
+ id = ids.first
34
+ record = load_target.detect { |r| id == r.id }
35
+ expects_array ? [ record ] : record
36
+ else
37
+ load_target.select { |r| ids.include?(r.id) }
38
+ end
39
+ else
40
+ conditions = "#{@finder_sql}"
41
+ if sanitized_conditions = sanitize_sql(options[:conditions])
42
+ conditions << " AND (#{sanitized_conditions})"
43
+ end
44
+
45
+ options[:conditions] = conditions
46
+
47
+ if options[:order] && @reflection.options[:order]
48
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
49
+ elsif @reflection.options[:order]
50
+ options[:order] = @reflection.options[:order]
51
+ end
52
+
53
+ # Build options specific to association
54
+ construct_find_options!(options)
55
+
56
+ merge_options_from_reflection!(options)
57
+
58
+ # Pass through args exactly as we received them.
59
+ args << options
60
+ @reflection.klass.find(*args)
61
+ end
62
+ end
63
+
64
+ # Fetches the first one using SQL if possible.
65
+ def first(*args)
66
+ if fetch_first_or_last_using_find?(args)
67
+ find(:first, *args)
68
+ else
69
+ load_target unless loaded?
70
+ @target.first(*args)
71
+ end
72
+ end
73
+
74
+ # Fetches the last one using SQL if possible.
75
+ def last(*args)
76
+ if fetch_first_or_last_using_find?(args)
77
+ find(:last, *args)
78
+ else
79
+ load_target unless loaded?
80
+ @target.last(*args)
81
+ end
82
+ end
83
+
84
+ def to_ary
85
+ load_target
86
+ if @target.is_a?(Array)
87
+ @target.to_ary
88
+ else
89
+ Array(@target)
90
+ end
91
+ end
92
+
93
+ def reset
94
+ reset_target!
95
+ @loaded = false
96
+ end
97
+
98
+ def build(attributes = {}, &block)
99
+ if attributes.is_a?(Array)
100
+ attributes.collect { |attr| build(attr, &block) }
101
+ else
102
+ build_record(attributes) do |record|
103
+ block.call(record) if block_given?
104
+ set_belongs_to_association_for(record)
105
+ end
106
+ end
107
+ end
108
+
109
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
110
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
111
+ def <<(*records)
112
+ result = true
113
+ load_target if @owner.new_record?
114
+
115
+ transaction do
116
+ flatten_deeper(records).each do |record|
117
+ raise_on_type_mismatch(record)
118
+ add_record_to_target_with_callbacks(record) do |r|
119
+ result &&= insert_record(record) unless @owner.new_record?
120
+ end
121
+ end
122
+ end
123
+
124
+ result && self
125
+ end
126
+
127
+ alias_method :push, :<<
128
+ alias_method :concat, :<<
129
+
130
+ # Starts a transaction in the association class's database connection.
131
+ #
132
+ # class Author < ActiveRecord::Base
133
+ # has_many :books
134
+ # end
135
+ #
136
+ # Author.find(:first).books.transaction do
137
+ # # same effect as calling Book.transaction
138
+ # end
139
+ def transaction(*args)
140
+ @reflection.klass.transaction(*args) do
141
+ yield
142
+ end
143
+ end
144
+
145
+ # Remove all records from this association
146
+ #
147
+ # See delete for more info.
148
+ def delete_all
149
+ load_target
150
+ delete(@target)
151
+ reset_target!
152
+ end
153
+
154
+ # Calculate sum using SQL, not Enumerable
155
+ def sum(*args)
156
+ if block_given?
157
+ calculate(:sum, *args) { |*block_args| yield(*block_args) }
158
+ else
159
+ calculate(:sum, *args)
160
+ end
161
+ end
162
+
163
+ # Count all records using SQL. If the +:counter_sql+ option is set for the association, it will
164
+ # be used for the query. If no +:counter_sql+ was supplied, but +:finder_sql+ was set, the
165
+ # descendant's +construct_sql+ method will have set :counter_sql automatically.
166
+ # Otherwise, construct options and pass them with scope to the target class's +count+.
167
+ def count(*args)
168
+ if @reflection.options[:counter_sql]
169
+ @reflection.klass.count_by_sql(@counter_sql)
170
+ else
171
+ column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
172
+ if @reflection.options[:uniq]
173
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
174
+ column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
175
+ options.merge!(:distinct => true)
176
+ end
177
+
178
+ value = @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.count(column_name, options) }
179
+
180
+ limit = @reflection.options[:limit]
181
+ offset = @reflection.options[:offset]
182
+
183
+ if limit || offset
184
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
185
+ else
186
+ value
187
+ end
188
+ end
189
+ end
190
+
191
+ # Removes +records+ from this association calling +before_remove+ and
192
+ # +after_remove+ callbacks.
193
+ #
194
+ # This method is abstract in the sense that +delete_records+ has to be
195
+ # provided by descendants. Note this method does not imply the records
196
+ # are actually removed from the database, that depends precisely on
197
+ # +delete_records+. They are in any case removed from the collection.
198
+ def delete(*records)
199
+ remove_records(records) do |records, old_records|
200
+ delete_records(old_records) if old_records.any?
201
+ records.each { |record| @target.delete(record) }
202
+ end
203
+ end
204
+
205
+ # Destroy +records+ and remove them from this association calling
206
+ # +before_remove+ and +after_remove+ callbacks.
207
+ #
208
+ # Note that this method will _always_ remove records from the database
209
+ # ignoring the +:dependent+ option.
210
+ def destroy(*records)
211
+ records = find(records) if records.any? {|record| record.kind_of?(Fixnum) || record.kind_of?(String)}
212
+ remove_records(records) do |records, old_records|
213
+ old_records.each { |record| record.destroy }
214
+ end
215
+
216
+ load_target
217
+ end
218
+
219
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
220
+ def clear
221
+ return self if length.zero? # forces load_target if it hasn't happened already
222
+
223
+ if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
224
+ destroy_all
225
+ else
226
+ delete_all
227
+ end
228
+
229
+ self
230
+ end
231
+
232
+ # Destory all the records from this association.
233
+ #
234
+ # See destroy for more info.
235
+ def destroy_all
236
+ load_target
237
+ destroy(@target)
238
+ reset_target!
239
+ end
240
+
241
+ def create(attrs = {})
242
+ if attrs.is_a?(Array)
243
+ attrs.collect { |attr| create(attr) }
244
+ else
245
+ create_record(attrs) do |record|
246
+ yield(record) if block_given?
247
+ record.save
248
+ end
249
+ end
250
+ end
251
+
252
+ def create!(attrs = {})
253
+ create_record(attrs) do |record|
254
+ yield(record) if block_given?
255
+ record.save!
256
+ end
257
+ end
258
+
259
+ # Returns the size of the collection by executing a SELECT COUNT(*)
260
+ # query if the collection hasn't been loaded, and calling
261
+ # <tt>collection.size</tt> if it has.
262
+ #
263
+ # If the collection has been already loaded +size+ and +length+ are
264
+ # equivalent. If not and you are going to need the records anyway
265
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
266
+ #
267
+ # This method is abstract in the sense that it relies on
268
+ # +count_records+, which is a method descendants have to provide.
269
+ def size
270
+ if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
271
+ @target.size
272
+ elsif !loaded? && @reflection.options[:group]
273
+ load_target.size
274
+ elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
275
+ unsaved_records = @target.select { |r| r.new_record? }
276
+ unsaved_records.size + count_records
277
+ else
278
+ count_records
279
+ end
280
+ end
281
+
282
+ # Returns the size of the collection calling +size+ on the target.
283
+ #
284
+ # If the collection has been already loaded +length+ and +size+ are
285
+ # equivalent. If not and you are going to need the records anyway this
286
+ # method will take one less query. Otherwise +size+ is more efficient.
287
+ def length
288
+ load_target.size
289
+ end
290
+
291
+ # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
292
+ # not been already loaded and you are going to fetch the records anyway
293
+ # it is better to check <tt>collection.length.zero?</tt>.
294
+ def empty?
295
+ size.zero?
296
+ end
297
+
298
+ def any?
299
+ if block_given?
300
+ method_missing(:any?) { |*block_args| yield(*block_args) }
301
+ else
302
+ !empty?
303
+ end
304
+ end
305
+
306
+ def uniq(collection = self)
307
+ seen = Set.new
308
+ collection.inject([]) do |kept, record|
309
+ unless seen.include?(record.id)
310
+ kept << record
311
+ seen << record.id
312
+ end
313
+ kept
314
+ end
315
+ end
316
+
317
+ # Replace this collection with +other_array+
318
+ # This will perform a diff and delete/add only records that have changed.
319
+ def replace(other_array)
320
+ other_array.each { |val| raise_on_type_mismatch(val) }
321
+
322
+ load_target
323
+ other = other_array.size < 100 ? other_array : other_array.to_set
324
+ current = @target.size < 100 ? @target : @target.to_set
325
+
326
+ transaction do
327
+ delete(@target.select { |v| !other.include?(v) })
328
+ concat(other_array.select { |v| !current.include?(v) })
329
+ end
330
+ end
331
+
332
+ def include?(record)
333
+ return false unless record.is_a?(@reflection.klass)
334
+ load_target if @reflection.options[:finder_sql] && !loaded?
335
+ return @target.include?(record) if loaded?
336
+ exists?(record)
337
+ end
338
+
339
+ def proxy_respond_to?(method, include_private = false)
340
+ super || @reflection.klass.respond_to?(method, include_private)
341
+ end
342
+
343
+ protected
344
+ def construct_find_options!(options)
345
+ end
346
+
347
+ def load_target
348
+ if !@owner.new_record? || foreign_key_present
349
+ begin
350
+ if !loaded?
351
+ if @target.is_a?(Array) && @target.any?
352
+ @target = find_target + @target.find_all {|t| t.new_record? }
353
+ else
354
+ @target = find_target
355
+ end
356
+ end
357
+ rescue ActiveRecord::RecordNotFound
358
+ reset
359
+ end
360
+ end
361
+
362
+ loaded if target
363
+ target
364
+ end
365
+
366
+ def method_missing(method, *args)
367
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
368
+ if block_given?
369
+ super { |*block_args| yield(*block_args) }
370
+ else
371
+ super
372
+ end
373
+ elsif @reflection.klass.scopes.include?(method)
374
+ @reflection.klass.scopes[method].call(self, *args)
375
+ else
376
+ with_scope(construct_scope) do
377
+ if block_given?
378
+ @reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
379
+ else
380
+ @reflection.klass.send(method, *args)
381
+ end
382
+ end
383
+ end
384
+ end
385
+
386
+ # overloaded in derived Association classes to provide useful scoping depending on association type.
387
+ def construct_scope
388
+ {}
389
+ end
390
+
391
+ def reset_target!
392
+ @target = Array.new
393
+ end
394
+
395
+ def find_target
396
+ records =
397
+ if @reflection.options[:finder_sql]
398
+ @reflection.klass.find_by_sql(@finder_sql)
399
+ else
400
+ find(:all)
401
+ end
402
+
403
+ @reflection.options[:uniq] ? uniq(records) : records
404
+ end
405
+
406
+ private
407
+
408
+ def create_record(attrs)
409
+ attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
410
+ ensure_owner_is_not_new
411
+ record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) do
412
+ @reflection.build_association(attrs)
413
+ end
414
+ if block_given?
415
+ add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
416
+ else
417
+ add_record_to_target_with_callbacks(record)
418
+ end
419
+ end
420
+
421
+ def build_record(attrs)
422
+ attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
423
+ record = @reflection.build_association(attrs)
424
+ if block_given?
425
+ add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
426
+ else
427
+ add_record_to_target_with_callbacks(record)
428
+ end
429
+ end
430
+
431
+ def add_record_to_target_with_callbacks(record)
432
+ callback(:before_add, record)
433
+ yield(record) if block_given?
434
+ @target ||= [] unless loaded?
435
+ @target << record unless @reflection.options[:uniq] && @target.include?(record)
436
+ callback(:after_add, record)
437
+ record
438
+ end
439
+
440
+ def remove_records(*records)
441
+ records = flatten_deeper(records)
442
+ records.each { |record| raise_on_type_mismatch(record) }
443
+
444
+ transaction do
445
+ records.each { |record| callback(:before_remove, record) }
446
+ old_records = records.reject { |r| r.new_record? }
447
+ yield(records, old_records)
448
+ records.each { |record| callback(:after_remove, record) }
449
+ end
450
+ end
451
+
452
+ def callback(method, record)
453
+ callbacks_for(method).each do |callback|
454
+ ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
455
+ end
456
+ end
457
+
458
+ def callbacks_for(callback_name)
459
+ full_callback_name = "#{callback_name}_for_#{@reflection.name}"
460
+ @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
461
+ end
462
+
463
+ def ensure_owner_is_not_new
464
+ if @owner.new_record?
465
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
466
+ end
467
+ end
468
+
469
+ def fetch_first_or_last_using_find?(args)
470
+ args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || @reflection.options[:finder_sql] ||
471
+ @target.any? { |record| record.new_record? } || args.first.kind_of?(Integer))
472
+ end
473
+ end
474
+ end
475
+ end