activerecord 2.3.18 → 3.2.22

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 (454) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1014 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +222 -0
  5. data/examples/performance.rb +100 -126
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record/aggregations.rb +93 -99
  8. data/lib/active_record/associations/alias_tracker.rb +76 -0
  9. data/lib/active_record/associations/association.rb +247 -0
  10. data/lib/active_record/associations/association_scope.rb +134 -0
  11. data/lib/active_record/associations/belongs_to_association.rb +54 -61
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +17 -59
  13. data/lib/active_record/associations/builder/association.rb +55 -0
  14. data/lib/active_record/associations/builder/belongs_to.rb +88 -0
  15. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +57 -0
  17. data/lib/active_record/associations/builder/has_many.rb +71 -0
  18. data/lib/active_record/associations/builder/has_one.rb +62 -0
  19. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  20. data/lib/active_record/associations/collection_association.rb +580 -0
  21. data/lib/active_record/associations/collection_proxy.rb +133 -0
  22. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +39 -119
  23. data/lib/active_record/associations/has_many_association.rb +60 -79
  24. data/lib/active_record/associations/has_many_through_association.rb +127 -206
  25. data/lib/active_record/associations/has_one_association.rb +55 -114
  26. data/lib/active_record/associations/has_one_through_association.rb +25 -26
  27. data/lib/active_record/associations/join_dependency/join_association.rb +159 -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 +214 -0
  31. data/lib/active_record/associations/join_helper.rb +55 -0
  32. data/lib/active_record/associations/preloader/association.rb +125 -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 +15 -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 +67 -0
  42. data/lib/active_record/associations/preloader.rb +181 -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 +693 -1337
  46. data/lib/active_record/attribute_assignment.rb +221 -0
  47. data/lib/active_record/attribute_methods/before_type_cast.rb +31 -0
  48. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  49. data/lib/active_record/attribute_methods/dirty.rb +111 -0
  50. data/lib/active_record/attribute_methods/primary_key.rb +114 -0
  51. data/lib/active_record/attribute_methods/query.rb +39 -0
  52. data/lib/active_record/attribute_methods/read.rb +136 -0
  53. data/lib/active_record/attribute_methods/serialization.rb +120 -0
  54. data/lib/active_record/attribute_methods/time_zone_conversion.rb +65 -0
  55. data/lib/active_record/attribute_methods/write.rb +70 -0
  56. data/lib/active_record/attribute_methods.rb +211 -339
  57. data/lib/active_record/autosave_association.rb +179 -149
  58. data/lib/active_record/base.rb +401 -2907
  59. data/lib/active_record/callbacks.rb +91 -176
  60. data/lib/active_record/coders/yaml_column.rb +41 -0
  61. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +236 -119
  62. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +110 -58
  63. data/lib/active_record/connection_adapters/abstract/database_limits.rb +12 -11
  64. data/lib/active_record/connection_adapters/abstract/database_statements.rb +175 -74
  65. data/lib/active_record/connection_adapters/abstract/query_cache.rb +31 -35
  66. data/lib/active_record/connection_adapters/abstract/quoting.rb +71 -21
  67. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +81 -311
  68. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +194 -78
  69. data/lib/active_record/connection_adapters/abstract_adapter.rb +130 -83
  70. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +676 -0
  71. data/lib/active_record/connection_adapters/column.rb +296 -0
  72. data/lib/active_record/connection_adapters/mysql2_adapter.rb +280 -0
  73. data/lib/active_record/connection_adapters/mysql_adapter.rb +272 -493
  74. data/lib/active_record/connection_adapters/postgresql_adapter.rb +650 -405
  75. data/lib/active_record/connection_adapters/schema_cache.rb +69 -0
  76. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +30 -9
  77. data/lib/active_record/connection_adapters/sqlite_adapter.rb +276 -147
  78. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  79. data/lib/active_record/counter_cache.rb +123 -0
  80. data/lib/active_record/dynamic_finder_match.rb +41 -14
  81. data/lib/active_record/dynamic_matchers.rb +84 -0
  82. data/lib/active_record/dynamic_scope_match.rb +13 -15
  83. data/lib/active_record/errors.rb +195 -0
  84. data/lib/active_record/explain.rb +86 -0
  85. data/lib/active_record/explain_subscriber.rb +25 -0
  86. data/lib/active_record/fixtures/file.rb +65 -0
  87. data/lib/active_record/fixtures.rb +695 -770
  88. data/lib/active_record/identity_map.rb +162 -0
  89. data/lib/active_record/inheritance.rb +174 -0
  90. data/lib/active_record/integration.rb +60 -0
  91. data/lib/active_record/locale/en.yml +9 -27
  92. data/lib/active_record/locking/optimistic.rb +76 -73
  93. data/lib/active_record/locking/pessimistic.rb +32 -10
  94. data/lib/active_record/log_subscriber.rb +72 -0
  95. data/lib/active_record/migration/command_recorder.rb +105 -0
  96. data/lib/active_record/migration.rb +415 -205
  97. data/lib/active_record/model_schema.rb +368 -0
  98. data/lib/active_record/nested_attributes.rb +153 -63
  99. data/lib/active_record/observer.rb +27 -103
  100. data/lib/active_record/persistence.rb +376 -0
  101. data/lib/active_record/query_cache.rb +49 -8
  102. data/lib/active_record/querying.rb +58 -0
  103. data/lib/active_record/railtie.rb +131 -0
  104. data/lib/active_record/railties/console_sandbox.rb +6 -0
  105. data/lib/active_record/railties/controller_runtime.rb +49 -0
  106. data/lib/active_record/railties/databases.rake +659 -0
  107. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  108. data/lib/active_record/readonly_attributes.rb +26 -0
  109. data/lib/active_record/reflection.rb +269 -120
  110. data/lib/active_record/relation/batches.rb +90 -0
  111. data/lib/active_record/relation/calculations.rb +372 -0
  112. data/lib/active_record/relation/delegation.rb +49 -0
  113. data/lib/active_record/relation/finder_methods.rb +402 -0
  114. data/lib/active_record/relation/predicate_builder.rb +63 -0
  115. data/lib/active_record/relation/query_methods.rb +417 -0
  116. data/lib/active_record/relation/spawn_methods.rb +180 -0
  117. data/lib/active_record/relation.rb +537 -0
  118. data/lib/active_record/result.rb +40 -0
  119. data/lib/active_record/sanitization.rb +194 -0
  120. data/lib/active_record/schema.rb +9 -6
  121. data/lib/active_record/schema_dumper.rb +55 -32
  122. data/lib/active_record/scoping/default.rb +142 -0
  123. data/lib/active_record/scoping/named.rb +200 -0
  124. data/lib/active_record/scoping.rb +152 -0
  125. data/lib/active_record/serialization.rb +8 -91
  126. data/lib/active_record/serializers/xml_serializer.rb +43 -197
  127. data/lib/active_record/session_store.rb +129 -103
  128. data/lib/active_record/store.rb +52 -0
  129. data/lib/active_record/test_case.rb +30 -23
  130. data/lib/active_record/timestamp.rb +95 -52
  131. data/lib/active_record/transactions.rb +212 -66
  132. data/lib/active_record/translation.rb +22 -0
  133. data/lib/active_record/validations/associated.rb +43 -0
  134. data/lib/active_record/validations/uniqueness.rb +180 -0
  135. data/lib/active_record/validations.rb +43 -1106
  136. data/lib/active_record/version.rb +5 -4
  137. data/lib/active_record.rb +121 -48
  138. data/lib/rails/generators/active_record/migration/migration_generator.rb +25 -0
  139. data/lib/rails/generators/active_record/migration/templates/migration.rb +34 -0
  140. data/lib/rails/generators/active_record/migration.rb +15 -0
  141. data/lib/rails/generators/active_record/model/model_generator.rb +47 -0
  142. data/lib/rails/generators/active_record/model/templates/migration.rb +15 -0
  143. data/lib/rails/generators/active_record/model/templates/model.rb +12 -0
  144. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  145. data/lib/rails/generators/active_record/observer/observer_generator.rb +15 -0
  146. data/lib/rails/generators/active_record/observer/templates/observer.rb +4 -0
  147. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +25 -0
  148. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +12 -0
  149. data/lib/rails/generators/active_record.rb +25 -0
  150. metadata +187 -363
  151. data/CHANGELOG +0 -5904
  152. data/README +0 -351
  153. data/RUNNING_UNIT_TESTS +0 -36
  154. data/Rakefile +0 -268
  155. data/install.rb +0 -30
  156. data/lib/active_record/association_preload.rb +0 -406
  157. data/lib/active_record/associations/association_collection.rb +0 -533
  158. data/lib/active_record/associations/association_proxy.rb +0 -288
  159. data/lib/active_record/batches.rb +0 -85
  160. data/lib/active_record/calculations.rb +0 -321
  161. data/lib/active_record/dirty.rb +0 -183
  162. data/lib/active_record/named_scope.rb +0 -197
  163. data/lib/active_record/serializers/json_serializer.rb +0 -91
  164. data/lib/activerecord.rb +0 -2
  165. data/test/assets/example.log +0 -1
  166. data/test/assets/flowers.jpg +0 -0
  167. data/test/cases/aaa_create_tables_test.rb +0 -24
  168. data/test/cases/active_schema_test_mysql.rb +0 -122
  169. data/test/cases/active_schema_test_postgresql.rb +0 -24
  170. data/test/cases/adapter_test.rb +0 -144
  171. data/test/cases/aggregations_test.rb +0 -167
  172. data/test/cases/ar_schema_test.rb +0 -32
  173. data/test/cases/associations/belongs_to_associations_test.rb +0 -438
  174. data/test/cases/associations/callbacks_test.rb +0 -161
  175. data/test/cases/associations/cascaded_eager_loading_test.rb +0 -131
  176. data/test/cases/associations/eager_load_includes_full_sti_class_test.rb +0 -36
  177. data/test/cases/associations/eager_load_nested_include_test.rb +0 -131
  178. data/test/cases/associations/eager_load_nested_polymorphic_include.rb +0 -19
  179. data/test/cases/associations/eager_singularization_test.rb +0 -145
  180. data/test/cases/associations/eager_test.rb +0 -852
  181. data/test/cases/associations/extension_test.rb +0 -62
  182. data/test/cases/associations/habtm_join_table_test.rb +0 -56
  183. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +0 -827
  184. data/test/cases/associations/has_many_associations_test.rb +0 -1273
  185. data/test/cases/associations/has_many_through_associations_test.rb +0 -360
  186. data/test/cases/associations/has_one_associations_test.rb +0 -330
  187. data/test/cases/associations/has_one_through_associations_test.rb +0 -209
  188. data/test/cases/associations/inner_join_association_test.rb +0 -93
  189. data/test/cases/associations/inverse_associations_test.rb +0 -566
  190. data/test/cases/associations/join_model_test.rb +0 -712
  191. data/test/cases/associations_test.rb +0 -282
  192. data/test/cases/attribute_methods_test.rb +0 -305
  193. data/test/cases/autosave_association_test.rb +0 -1218
  194. data/test/cases/base_test.rb +0 -2166
  195. data/test/cases/batches_test.rb +0 -81
  196. data/test/cases/binary_test.rb +0 -30
  197. data/test/cases/calculations_test.rb +0 -360
  198. data/test/cases/callbacks_observers_test.rb +0 -38
  199. data/test/cases/callbacks_test.rb +0 -438
  200. data/test/cases/class_inheritable_attributes_test.rb +0 -32
  201. data/test/cases/column_alias_test.rb +0 -17
  202. data/test/cases/column_definition_test.rb +0 -70
  203. data/test/cases/connection_pool_test.rb +0 -25
  204. data/test/cases/connection_test_firebird.rb +0 -8
  205. data/test/cases/connection_test_mysql.rb +0 -65
  206. data/test/cases/copy_table_test_sqlite.rb +0 -80
  207. data/test/cases/counter_cache_test.rb +0 -84
  208. data/test/cases/database_statements_test.rb +0 -12
  209. data/test/cases/datatype_test_postgresql.rb +0 -204
  210. data/test/cases/date_time_test.rb +0 -37
  211. data/test/cases/default_test_firebird.rb +0 -16
  212. data/test/cases/defaults_test.rb +0 -111
  213. data/test/cases/deprecated_finder_test.rb +0 -30
  214. data/test/cases/dirty_test.rb +0 -316
  215. data/test/cases/finder_respond_to_test.rb +0 -76
  216. data/test/cases/finder_test.rb +0 -1098
  217. data/test/cases/fixtures_test.rb +0 -661
  218. data/test/cases/helper.rb +0 -68
  219. data/test/cases/i18n_test.rb +0 -46
  220. data/test/cases/inheritance_test.rb +0 -262
  221. data/test/cases/invalid_date_test.rb +0 -24
  222. data/test/cases/json_serialization_test.rb +0 -219
  223. data/test/cases/lifecycle_test.rb +0 -193
  224. data/test/cases/locking_test.rb +0 -350
  225. data/test/cases/method_scoping_test.rb +0 -704
  226. data/test/cases/migration_test.rb +0 -1649
  227. data/test/cases/migration_test_firebird.rb +0 -124
  228. data/test/cases/mixin_test.rb +0 -96
  229. data/test/cases/modules_test.rb +0 -109
  230. data/test/cases/multiple_db_test.rb +0 -85
  231. data/test/cases/named_scope_test.rb +0 -372
  232. data/test/cases/nested_attributes_test.rb +0 -840
  233. data/test/cases/pk_test.rb +0 -119
  234. data/test/cases/pooled_connections_test.rb +0 -103
  235. data/test/cases/query_cache_test.rb +0 -129
  236. data/test/cases/readonly_test.rb +0 -107
  237. data/test/cases/reflection_test.rb +0 -234
  238. data/test/cases/reload_models_test.rb +0 -22
  239. data/test/cases/repair_helper.rb +0 -50
  240. data/test/cases/reserved_word_test_mysql.rb +0 -176
  241. data/test/cases/sanitize_test.rb +0 -25
  242. data/test/cases/schema_authorization_test_postgresql.rb +0 -75
  243. data/test/cases/schema_dumper_test.rb +0 -211
  244. data/test/cases/schema_test_postgresql.rb +0 -178
  245. data/test/cases/serialization_test.rb +0 -47
  246. data/test/cases/sp_test_mysql.rb +0 -16
  247. data/test/cases/synonym_test_oracle.rb +0 -17
  248. data/test/cases/timestamp_test.rb +0 -75
  249. data/test/cases/transactions_test.rb +0 -543
  250. data/test/cases/unconnected_test.rb +0 -32
  251. data/test/cases/validations_i18n_test.rb +0 -925
  252. data/test/cases/validations_test.rb +0 -1684
  253. data/test/cases/xml_serialization_test.rb +0 -240
  254. data/test/cases/yaml_serialization_test.rb +0 -11
  255. data/test/config.rb +0 -5
  256. data/test/connections/jdbc_jdbcderby/connection.rb +0 -18
  257. data/test/connections/jdbc_jdbch2/connection.rb +0 -18
  258. data/test/connections/jdbc_jdbchsqldb/connection.rb +0 -18
  259. data/test/connections/jdbc_jdbcmysql/connection.rb +0 -26
  260. data/test/connections/jdbc_jdbcpostgresql/connection.rb +0 -26
  261. data/test/connections/jdbc_jdbcsqlite3/connection.rb +0 -25
  262. data/test/connections/native_db2/connection.rb +0 -25
  263. data/test/connections/native_firebird/connection.rb +0 -26
  264. data/test/connections/native_frontbase/connection.rb +0 -27
  265. data/test/connections/native_mysql/connection.rb +0 -25
  266. data/test/connections/native_openbase/connection.rb +0 -21
  267. data/test/connections/native_oracle/connection.rb +0 -27
  268. data/test/connections/native_postgresql/connection.rb +0 -21
  269. data/test/connections/native_sqlite/connection.rb +0 -25
  270. data/test/connections/native_sqlite3/connection.rb +0 -25
  271. data/test/connections/native_sqlite3/in_memory_connection.rb +0 -18
  272. data/test/connections/native_sybase/connection.rb +0 -23
  273. data/test/fixtures/accounts.yml +0 -29
  274. data/test/fixtures/all/developers.yml +0 -0
  275. data/test/fixtures/all/people.csv +0 -0
  276. data/test/fixtures/all/tasks.yml +0 -0
  277. data/test/fixtures/author_addresses.yml +0 -5
  278. data/test/fixtures/author_favorites.yml +0 -4
  279. data/test/fixtures/authors.yml +0 -9
  280. data/test/fixtures/binaries.yml +0 -132
  281. data/test/fixtures/books.yml +0 -7
  282. data/test/fixtures/categories/special_categories.yml +0 -9
  283. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +0 -4
  284. data/test/fixtures/categories.yml +0 -14
  285. data/test/fixtures/categories_ordered.yml +0 -7
  286. data/test/fixtures/categories_posts.yml +0 -23
  287. data/test/fixtures/categorizations.yml +0 -17
  288. data/test/fixtures/clubs.yml +0 -6
  289. data/test/fixtures/comments.yml +0 -59
  290. data/test/fixtures/companies.yml +0 -56
  291. data/test/fixtures/computers.yml +0 -4
  292. data/test/fixtures/courses.yml +0 -7
  293. data/test/fixtures/customers.yml +0 -26
  294. data/test/fixtures/developers.yml +0 -21
  295. data/test/fixtures/developers_projects.yml +0 -17
  296. data/test/fixtures/edges.yml +0 -6
  297. data/test/fixtures/entrants.yml +0 -14
  298. data/test/fixtures/faces.yml +0 -11
  299. data/test/fixtures/fk_test_has_fk.yml +0 -3
  300. data/test/fixtures/fk_test_has_pk.yml +0 -2
  301. data/test/fixtures/funny_jokes.yml +0 -10
  302. data/test/fixtures/interests.yml +0 -33
  303. data/test/fixtures/items.yml +0 -4
  304. data/test/fixtures/jobs.yml +0 -7
  305. data/test/fixtures/legacy_things.yml +0 -3
  306. data/test/fixtures/mateys.yml +0 -4
  307. data/test/fixtures/member_types.yml +0 -6
  308. data/test/fixtures/members.yml +0 -6
  309. data/test/fixtures/memberships.yml +0 -20
  310. data/test/fixtures/men.yml +0 -5
  311. data/test/fixtures/minimalistics.yml +0 -2
  312. data/test/fixtures/mixed_case_monkeys.yml +0 -6
  313. data/test/fixtures/mixins.yml +0 -29
  314. data/test/fixtures/movies.yml +0 -7
  315. data/test/fixtures/naked/csv/accounts.csv +0 -1
  316. data/test/fixtures/naked/yml/accounts.yml +0 -1
  317. data/test/fixtures/naked/yml/companies.yml +0 -1
  318. data/test/fixtures/naked/yml/courses.yml +0 -1
  319. data/test/fixtures/organizations.yml +0 -5
  320. data/test/fixtures/owners.yml +0 -7
  321. data/test/fixtures/parrots.yml +0 -27
  322. data/test/fixtures/parrots_pirates.yml +0 -7
  323. data/test/fixtures/people.yml +0 -15
  324. data/test/fixtures/pets.yml +0 -14
  325. data/test/fixtures/pirates.yml +0 -9
  326. data/test/fixtures/polymorphic_designs.yml +0 -19
  327. data/test/fixtures/polymorphic_prices.yml +0 -19
  328. data/test/fixtures/posts.yml +0 -52
  329. data/test/fixtures/price_estimates.yml +0 -7
  330. data/test/fixtures/projects.yml +0 -7
  331. data/test/fixtures/readers.yml +0 -9
  332. data/test/fixtures/references.yml +0 -17
  333. data/test/fixtures/reserved_words/distinct.yml +0 -5
  334. data/test/fixtures/reserved_words/distincts_selects.yml +0 -11
  335. data/test/fixtures/reserved_words/group.yml +0 -14
  336. data/test/fixtures/reserved_words/select.yml +0 -8
  337. data/test/fixtures/reserved_words/values.yml +0 -7
  338. data/test/fixtures/ships.yml +0 -5
  339. data/test/fixtures/sponsors.yml +0 -9
  340. data/test/fixtures/subscribers.yml +0 -7
  341. data/test/fixtures/subscriptions.yml +0 -12
  342. data/test/fixtures/taggings.yml +0 -28
  343. data/test/fixtures/tags.yml +0 -7
  344. data/test/fixtures/tasks.yml +0 -7
  345. data/test/fixtures/tees.yml +0 -4
  346. data/test/fixtures/ties.yml +0 -4
  347. data/test/fixtures/topics.yml +0 -42
  348. data/test/fixtures/toys.yml +0 -4
  349. data/test/fixtures/treasures.yml +0 -10
  350. data/test/fixtures/vertices.yml +0 -4
  351. data/test/fixtures/warehouse-things.yml +0 -3
  352. data/test/fixtures/zines.yml +0 -5
  353. data/test/migrations/broken/100_migration_that_raises_exception.rb +0 -10
  354. data/test/migrations/decimal/1_give_me_big_numbers.rb +0 -15
  355. data/test/migrations/duplicate/1_people_have_last_names.rb +0 -9
  356. data/test/migrations/duplicate/2_we_need_reminders.rb +0 -12
  357. data/test/migrations/duplicate/3_foo.rb +0 -7
  358. data/test/migrations/duplicate/3_innocent_jointable.rb +0 -12
  359. data/test/migrations/duplicate_names/20080507052938_chunky.rb +0 -7
  360. data/test/migrations/duplicate_names/20080507053028_chunky.rb +0 -7
  361. data/test/migrations/interleaved/pass_1/3_innocent_jointable.rb +0 -12
  362. data/test/migrations/interleaved/pass_2/1_people_have_last_names.rb +0 -9
  363. data/test/migrations/interleaved/pass_2/3_innocent_jointable.rb +0 -12
  364. data/test/migrations/interleaved/pass_3/1_people_have_last_names.rb +0 -9
  365. data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +0 -8
  366. data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +0 -12
  367. data/test/migrations/missing/1000_people_have_middle_names.rb +0 -9
  368. data/test/migrations/missing/1_people_have_last_names.rb +0 -9
  369. data/test/migrations/missing/3_we_need_reminders.rb +0 -12
  370. data/test/migrations/missing/4_innocent_jointable.rb +0 -12
  371. data/test/migrations/valid/1_people_have_last_names.rb +0 -9
  372. data/test/migrations/valid/2_we_need_reminders.rb +0 -12
  373. data/test/migrations/valid/3_innocent_jointable.rb +0 -12
  374. data/test/models/author.rb +0 -151
  375. data/test/models/auto_id.rb +0 -4
  376. data/test/models/binary.rb +0 -2
  377. data/test/models/bird.rb +0 -9
  378. data/test/models/book.rb +0 -4
  379. data/test/models/categorization.rb +0 -5
  380. data/test/models/category.rb +0 -34
  381. data/test/models/citation.rb +0 -6
  382. data/test/models/club.rb +0 -13
  383. data/test/models/column_name.rb +0 -3
  384. data/test/models/comment.rb +0 -29
  385. data/test/models/company.rb +0 -173
  386. data/test/models/company_in_module.rb +0 -78
  387. data/test/models/computer.rb +0 -3
  388. data/test/models/contact.rb +0 -16
  389. data/test/models/contract.rb +0 -5
  390. data/test/models/course.rb +0 -3
  391. data/test/models/customer.rb +0 -73
  392. data/test/models/default.rb +0 -2
  393. data/test/models/developer.rb +0 -101
  394. data/test/models/edge.rb +0 -5
  395. data/test/models/entrant.rb +0 -3
  396. data/test/models/essay.rb +0 -3
  397. data/test/models/event.rb +0 -3
  398. data/test/models/event_author.rb +0 -8
  399. data/test/models/face.rb +0 -7
  400. data/test/models/guid.rb +0 -2
  401. data/test/models/interest.rb +0 -5
  402. data/test/models/invoice.rb +0 -4
  403. data/test/models/item.rb +0 -7
  404. data/test/models/job.rb +0 -5
  405. data/test/models/joke.rb +0 -3
  406. data/test/models/keyboard.rb +0 -3
  407. data/test/models/legacy_thing.rb +0 -3
  408. data/test/models/line_item.rb +0 -3
  409. data/test/models/man.rb +0 -9
  410. data/test/models/matey.rb +0 -4
  411. data/test/models/member.rb +0 -12
  412. data/test/models/member_detail.rb +0 -5
  413. data/test/models/member_type.rb +0 -3
  414. data/test/models/membership.rb +0 -9
  415. data/test/models/minimalistic.rb +0 -2
  416. data/test/models/mixed_case_monkey.rb +0 -3
  417. data/test/models/movie.rb +0 -5
  418. data/test/models/order.rb +0 -4
  419. data/test/models/organization.rb +0 -6
  420. data/test/models/owner.rb +0 -5
  421. data/test/models/parrot.rb +0 -22
  422. data/test/models/person.rb +0 -16
  423. data/test/models/pet.rb +0 -5
  424. data/test/models/pirate.rb +0 -80
  425. data/test/models/polymorphic_design.rb +0 -3
  426. data/test/models/polymorphic_price.rb +0 -3
  427. data/test/models/post.rb +0 -102
  428. data/test/models/price_estimate.rb +0 -3
  429. data/test/models/project.rb +0 -30
  430. data/test/models/reader.rb +0 -4
  431. data/test/models/reference.rb +0 -4
  432. data/test/models/reply.rb +0 -46
  433. data/test/models/ship.rb +0 -19
  434. data/test/models/ship_part.rb +0 -7
  435. data/test/models/sponsor.rb +0 -4
  436. data/test/models/subject.rb +0 -4
  437. data/test/models/subscriber.rb +0 -8
  438. data/test/models/subscription.rb +0 -4
  439. data/test/models/tag.rb +0 -7
  440. data/test/models/tagging.rb +0 -10
  441. data/test/models/task.rb +0 -3
  442. data/test/models/tee.rb +0 -4
  443. data/test/models/tie.rb +0 -4
  444. data/test/models/topic.rb +0 -80
  445. data/test/models/toy.rb +0 -6
  446. data/test/models/treasure.rb +0 -8
  447. data/test/models/vertex.rb +0 -9
  448. data/test/models/warehouse_thing.rb +0 -5
  449. data/test/models/zine.rb +0 -3
  450. data/test/schema/mysql_specific_schema.rb +0 -31
  451. data/test/schema/postgresql_specific_schema.rb +0 -114
  452. data/test/schema/schema.rb +0 -550
  453. data/test/schema/schema2.rb +0 -6
  454. data/test/schema/sqlite_specific_schema.rb +0 -25
@@ -0,0 +1,580 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveRecord
4
+ module Associations
5
+ # = Active Record Association Collection
6
+ #
7
+ # CollectionAssociation is an abstract class that provides common stuff to
8
+ # ease the implementation of association proxies that represent
9
+ # collections. See the class hierarchy in AssociationProxy.
10
+ #
11
+ # You need to be careful with assumptions regarding the target: The proxy
12
+ # does not fetch records from the database until it needs them, but new
13
+ # ones created with +build+ are added to the target. So, the target may be
14
+ # non-empty and still lack children waiting to be read from the database.
15
+ # If you look directly to the database you cannot assume that's the entire
16
+ # collection because new records may have been added to the target, etc.
17
+ #
18
+ # If you need to work on all current children, new and existing records,
19
+ # +load_target+ and the +loaded+ flag are your friends.
20
+ class CollectionAssociation < Association #:nodoc:
21
+ attr_reader :proxy
22
+
23
+ def initialize(owner, reflection)
24
+ super
25
+ @proxy = CollectionProxy.new(self)
26
+ end
27
+
28
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
29
+ def reader(force_reload = false)
30
+ if force_reload
31
+ klass.uncached { reload }
32
+ elsif stale_target?
33
+ reload
34
+ end
35
+
36
+ proxy
37
+ end
38
+
39
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
40
+ def writer(records)
41
+ replace(records)
42
+ end
43
+
44
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
45
+ def ids_reader
46
+ if owner.new_record? || loaded? || options[:finder_sql]
47
+ load_target.map do |record|
48
+ record.send(reflection.association_primary_key)
49
+ end
50
+ else
51
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
52
+ relation = scoped
53
+
54
+ including = (relation.eager_load_values + relation.includes_values).uniq
55
+
56
+ if including.any?
57
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(reflection.klass, including, [])
58
+ relation = join_dependency.join_associations.inject(relation) do |r, association|
59
+ association.join_relation(r)
60
+ end
61
+ end
62
+
63
+ relation.pluck(column)
64
+ end
65
+ end
66
+
67
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
68
+ def ids_writer(ids)
69
+ pk_column = reflection.primary_key_column
70
+ ids = Array.wrap(ids).reject { |id| id.blank? }
71
+ ids.map! { |i| pk_column.type_cast(i) }
72
+ replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
73
+ end
74
+
75
+ def reset
76
+ @loaded = false
77
+ @target = []
78
+ end
79
+
80
+ def select(select = nil)
81
+ if block_given?
82
+ load_target.select.each { |e| yield e }
83
+ else
84
+ scoped.select(select)
85
+ end
86
+ end
87
+
88
+ def find(*args)
89
+ if block_given?
90
+ load_target.find(*args) { |*block_args| yield(*block_args) }
91
+ else
92
+ if options[:finder_sql]
93
+ find_by_scan(*args)
94
+ else
95
+ scoped.find(*args)
96
+ end
97
+ end
98
+ end
99
+
100
+ def first(*args)
101
+ first_or_last(:first, *args)
102
+ end
103
+
104
+ def last(*args)
105
+ first_or_last(:last, *args)
106
+ end
107
+
108
+ def build(attributes = {}, options = {}, &block)
109
+ if attributes.is_a?(Array)
110
+ attributes.collect { |attr| build(attr, options, &block) }
111
+ else
112
+ add_to_target(build_record(attributes, options)) do |record|
113
+ yield(record) if block_given?
114
+ end
115
+ end
116
+ end
117
+
118
+ def create(attributes = {}, options = {}, &block)
119
+ create_record(attributes, options, &block)
120
+ end
121
+
122
+ def create!(attributes = {}, options = {}, &block)
123
+ create_record(attributes, options, true, &block)
124
+ end
125
+
126
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
127
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
128
+ def concat(*records)
129
+ load_target if owner.new_record?
130
+
131
+ if owner.new_record?
132
+ concat_records(records)
133
+ else
134
+ transaction { concat_records(records) }
135
+ end
136
+ end
137
+
138
+ # Starts a transaction in the association class's database connection.
139
+ #
140
+ # class Author < ActiveRecord::Base
141
+ # has_many :books
142
+ # end
143
+ #
144
+ # Author.first.books.transaction do
145
+ # # same effect as calling Book.transaction
146
+ # end
147
+ def transaction(*args)
148
+ reflection.klass.transaction(*args) do
149
+ yield
150
+ end
151
+ end
152
+
153
+ # Remove all records from this association
154
+ #
155
+ # See delete for more info.
156
+ def delete_all
157
+ delete(load_target).tap do
158
+ reset
159
+ loaded!
160
+ end
161
+ end
162
+
163
+ # Called when the association is declared as :dependent => :delete_all. This is
164
+ # an optimised version which avoids loading the records into memory. Not really
165
+ # for public consumption.
166
+ def delete_all_on_destroy
167
+ scoped.delete_all
168
+ end
169
+
170
+ # Destroy all the records from this association.
171
+ #
172
+ # See destroy for more info.
173
+ def destroy_all
174
+ destroy(load_target).tap do
175
+ reset
176
+ loaded!
177
+ end
178
+ end
179
+
180
+ # Calculate sum using SQL, not Enumerable
181
+ def sum(*args)
182
+ if block_given?
183
+ scoped.sum(*args) { |*block_args| yield(*block_args) }
184
+ else
185
+ scoped.sum(*args)
186
+ end
187
+ end
188
+
189
+ # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
190
+ # association, it will be used for the query. Otherwise, construct options and pass them with
191
+ # scope to the target class's +count+.
192
+ def count(column_name = nil, count_options = {})
193
+ return 0 if owner.new_record?
194
+
195
+ column_name, count_options = nil, column_name if column_name.is_a?(Hash)
196
+
197
+ if options[:counter_sql] || options[:finder_sql]
198
+ unless count_options.blank?
199
+ raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
200
+ end
201
+
202
+ reflection.klass.count_by_sql(custom_counter_sql)
203
+ else
204
+ if options[:uniq]
205
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
206
+ column_name ||= reflection.klass.primary_key
207
+ count_options.merge!(:distinct => true)
208
+ end
209
+
210
+ value = scoped.count(column_name, count_options)
211
+
212
+ limit = options[:limit]
213
+ offset = options[:offset]
214
+
215
+ if limit || offset
216
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
217
+ else
218
+ value
219
+ end
220
+ end
221
+ end
222
+
223
+ # Removes +records+ from this association calling +before_remove+ and
224
+ # +after_remove+ callbacks.
225
+ #
226
+ # This method is abstract in the sense that +delete_records+ has to be
227
+ # provided by descendants. Note this method does not imply the records
228
+ # are actually removed from the database, that depends precisely on
229
+ # +delete_records+. They are in any case removed from the collection.
230
+ def delete(*records)
231
+ delete_or_destroy(records, options[:dependent])
232
+ end
233
+
234
+ # Destroy +records+ and remove them from this association calling
235
+ # +before_remove+ and +after_remove+ callbacks.
236
+ #
237
+ # Note that this method will _always_ remove records from the database
238
+ # ignoring the +:dependent+ option.
239
+ def destroy(*records)
240
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
241
+ delete_or_destroy(records, :destroy)
242
+ end
243
+
244
+ # Returns the size of the collection by executing a SELECT COUNT(*)
245
+ # query if the collection hasn't been loaded, and calling
246
+ # <tt>collection.size</tt> if it has.
247
+ #
248
+ # If the collection has been already loaded +size+ and +length+ are
249
+ # equivalent. If not and you are going to need the records anyway
250
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
251
+ #
252
+ # This method is abstract in the sense that it relies on
253
+ # +count_records+, which is a method descendants have to provide.
254
+ def size
255
+ if !find_target? || (loaded? && !options[:uniq])
256
+ target.size
257
+ elsif !loaded? && options[:group]
258
+ load_target.size
259
+ elsif !loaded? && !options[:uniq] && target.is_a?(Array)
260
+ unsaved_records = target.select { |r| r.new_record? }
261
+ unsaved_records.size + count_records
262
+ else
263
+ count_records
264
+ end
265
+ end
266
+
267
+ # Returns the size of the collection calling +size+ on the target.
268
+ #
269
+ # If the collection has been already loaded +length+ and +size+ are
270
+ # equivalent. If not and you are going to need the records anyway this
271
+ # method will take one less query. Otherwise +size+ is more efficient.
272
+ def length
273
+ load_target.size
274
+ end
275
+
276
+ # Equivalent to <tt>collection.size.zero?</tt>. If the collection has
277
+ # not been already loaded and you are going to fetch the records anyway
278
+ # it is better to check <tt>collection.length.zero?</tt>.
279
+ def empty?
280
+ size.zero?
281
+ end
282
+
283
+ def any?
284
+ if block_given?
285
+ load_target.any? { |*block_args| yield(*block_args) }
286
+ else
287
+ !empty?
288
+ end
289
+ end
290
+
291
+ # Returns true if the collection has more than 1 record. Equivalent to collection.size > 1.
292
+ def many?
293
+ if block_given?
294
+ load_target.many? { |*block_args| yield(*block_args) }
295
+ else
296
+ size > 1
297
+ end
298
+ end
299
+
300
+ def uniq(collection = load_target)
301
+ seen = {}
302
+ collection.find_all do |record|
303
+ seen[record.id] = true unless seen.key?(record.id)
304
+ end
305
+ end
306
+
307
+ # Replace this collection with +other_array+
308
+ # This will perform a diff and delete/add only records that have changed.
309
+ def replace(other_array)
310
+ other_array.each { |val| raise_on_type_mismatch(val) }
311
+ original_target = load_target.dup
312
+
313
+ if owner.new_record?
314
+ replace_records(other_array, original_target)
315
+ else
316
+ transaction { replace_records(other_array, original_target) }
317
+ end
318
+ end
319
+
320
+ def include?(record)
321
+ if record.is_a?(reflection.klass)
322
+ if record.new_record?
323
+ include_in_memory?(record)
324
+ else
325
+ load_target if options[:finder_sql]
326
+ loaded? ? target.include?(record) : scoped.exists?(record)
327
+ end
328
+ else
329
+ false
330
+ end
331
+ end
332
+
333
+ def load_target
334
+ if find_target?
335
+ @target = merge_target_lists(find_target, target)
336
+ end
337
+
338
+ loaded!
339
+ target
340
+ end
341
+
342
+ def add_to_target(record)
343
+ callback(:before_add, record)
344
+ yield(record) if block_given?
345
+
346
+ if options[:uniq] && index = @target.index(record)
347
+ @target[index] = record
348
+ else
349
+ @target << record
350
+ end
351
+
352
+ callback(:after_add, record)
353
+ set_inverse_instance(record)
354
+
355
+ record
356
+ end
357
+
358
+ private
359
+
360
+ def custom_counter_sql
361
+ if options[:counter_sql]
362
+ interpolate(options[:counter_sql])
363
+ else
364
+ # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
365
+ interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
366
+ count_with = $2.to_s
367
+ count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
368
+ "SELECT #{$1}COUNT(#{count_with}) FROM"
369
+ end
370
+ end
371
+ end
372
+
373
+ def custom_finder_sql
374
+ interpolate(options[:finder_sql])
375
+ end
376
+
377
+ def find_target
378
+ records =
379
+ if options[:finder_sql]
380
+ reflection.klass.find_by_sql(custom_finder_sql)
381
+ else
382
+ scoped.all
383
+ end
384
+
385
+ records = options[:uniq] ? uniq(records) : records
386
+ records.each { |record| set_inverse_instance(record) }
387
+ records
388
+ end
389
+
390
+ # We have some records loaded from the database (persisted) and some that are
391
+ # in-memory (memory). The same record may be represented in the persisted array
392
+ # and in the memory array.
393
+ #
394
+ # So the task of this method is to merge them according to the following rules:
395
+ #
396
+ # * The final array must not have duplicates
397
+ # * The order of the persisted array is to be preserved
398
+ # * Any changes made to attributes on objects in the memory array are to be preserved
399
+ # * Otherwise, attributes should have the value found in the database
400
+ def merge_target_lists(persisted, memory)
401
+ return persisted if memory.empty?
402
+ return memory if persisted.empty?
403
+
404
+ persisted.map! do |record|
405
+ # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns
406
+ # record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9.
407
+ mem_index = memory.index(record)
408
+
409
+ if mem_index
410
+ mem_record = memory.delete_at(mem_index)
411
+
412
+ ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name|
413
+ mem_record[name] = record[name]
414
+ end
415
+
416
+ mem_record
417
+ else
418
+ record
419
+ end
420
+ end
421
+
422
+ persisted + memory
423
+ end
424
+
425
+ def create_record(attributes, options, raise = false, &block)
426
+ unless owner.persisted?
427
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
428
+ end
429
+
430
+ if attributes.is_a?(Array)
431
+ attributes.collect { |attr| create_record(attr, options, raise, &block) }
432
+ else
433
+ transaction do
434
+ add_to_target(build_record(attributes, options)) do |record|
435
+ yield(record) if block_given?
436
+ insert_record(record, true, raise)
437
+ end
438
+ end
439
+ end
440
+ end
441
+
442
+ # Do the relevant stuff to insert the given record into the association collection.
443
+ def insert_record(record, validate = true, raise = false)
444
+ raise NotImplementedError
445
+ end
446
+
447
+ def create_scope
448
+ scoped.scope_for_create.stringify_keys
449
+ end
450
+
451
+ def delete_or_destroy(records, method)
452
+ records = records.flatten
453
+ records.each { |record| raise_on_type_mismatch(record) }
454
+ existing_records = records.reject { |r| r.new_record? }
455
+
456
+ if existing_records.empty?
457
+ remove_records(existing_records, records, method)
458
+ else
459
+ transaction { remove_records(existing_records, records, method) }
460
+ end
461
+ end
462
+
463
+ def remove_records(existing_records, records, method)
464
+ records.each { |record| callback(:before_remove, record) }
465
+
466
+ delete_records(existing_records, method) if existing_records.any?
467
+ records.each { |record| target.delete(record) }
468
+
469
+ records.each { |record| callback(:after_remove, record) }
470
+ end
471
+
472
+ # Delete the given records from the association, using one of the methods :destroy,
473
+ # :delete_all or :nullify (or nil, in which case a default is used).
474
+ def delete_records(records, method)
475
+ raise NotImplementedError
476
+ end
477
+
478
+ def replace_records(new_target, original_target)
479
+ delete(target - new_target)
480
+
481
+ unless concat(new_target - target)
482
+ @target = original_target
483
+ raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
484
+ "new records could not be saved."
485
+ end
486
+
487
+ target
488
+ end
489
+
490
+ def concat_records(records)
491
+ result = true
492
+
493
+ records.flatten.each do |record|
494
+ raise_on_type_mismatch(record)
495
+ add_to_target(record) do |r|
496
+ result &&= insert_record(record) unless owner.new_record?
497
+ end
498
+ end
499
+
500
+ result && records
501
+ end
502
+
503
+ def callback(method, record)
504
+ callbacks_for(method).each do |callback|
505
+ case callback
506
+ when Symbol
507
+ owner.send(callback, record)
508
+ when Proc
509
+ callback.call(owner, record)
510
+ else
511
+ callback.send(method, owner, record)
512
+ end
513
+ end
514
+ end
515
+
516
+ def callbacks_for(callback_name)
517
+ full_callback_name = "#{callback_name}_for_#{reflection.name}"
518
+ owner.class.send(full_callback_name.to_sym) || []
519
+ end
520
+
521
+ # Should we deal with assoc.first or assoc.last by issuing an independent query to
522
+ # the database, or by getting the target, and then taking the first/last item from that?
523
+ #
524
+ # If the args is just a non-empty options hash, go to the database.
525
+ #
526
+ # Otherwise, go to the database only if none of the following are true:
527
+ # * target already loaded
528
+ # * owner is new record
529
+ # * custom :finder_sql exists
530
+ # * target contains new or changed record(s)
531
+ # * the first arg is an integer (which indicates the number of records to be returned)
532
+ def fetch_first_or_last_using_find?(args)
533
+ if args.first.is_a?(Hash)
534
+ true
535
+ else
536
+ !(loaded? ||
537
+ owner.new_record? ||
538
+ options[:finder_sql] ||
539
+ target.any? { |record| record.new_record? || record.changed? } ||
540
+ args.first.kind_of?(Integer))
541
+ end
542
+ end
543
+
544
+ def include_in_memory?(record)
545
+ if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
546
+ owner.send(reflection.through_reflection.name).any? { |source|
547
+ target = source.send(reflection.source_reflection.name)
548
+ target.respond_to?(:include?) ? target.include?(record) : target == record
549
+ } || target.include?(record)
550
+ else
551
+ target.include?(record)
552
+ end
553
+ end
554
+
555
+ # If using a custom finder_sql, #find scans the entire collection.
556
+ def find_by_scan(*args)
557
+ expects_array = args.first.kind_of?(Array)
558
+ ids = args.flatten.compact.uniq.map { |arg| arg.to_i }
559
+
560
+ if ids.size == 1
561
+ id = ids.first
562
+ record = load_target.detect { |r| id == r.id }
563
+ expects_array ? [ record ] : record
564
+ else
565
+ load_target.select { |r| ids.include?(r.id) }
566
+ end
567
+ end
568
+
569
+ # Fetches the first/last using SQL if possible, otherwise from the target array.
570
+ def first_or_last(type, *args)
571
+ args.shift if args.first.is_a?(Hash) && args.first.empty?
572
+
573
+ collection = fetch_first_or_last_using_find?(args) ? scoped : load_target
574
+ collection.send(type, *args).tap do |record|
575
+ set_inverse_instance record if record.is_a? ActiveRecord::Base
576
+ end
577
+ end
578
+ end
579
+ end
580
+ end
@@ -0,0 +1,133 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ # Association proxies in Active Record are middlemen between the object that
4
+ # holds the association, known as the <tt>@owner</tt>, and the actual associated
5
+ # object, known as the <tt>@target</tt>. The kind of association any proxy is
6
+ # about is available in <tt>@reflection</tt>. That's an instance of the class
7
+ # ActiveRecord::Reflection::AssociationReflection.
8
+ #
9
+ # For example, given
10
+ #
11
+ # class Blog < ActiveRecord::Base
12
+ # has_many :posts
13
+ # end
14
+ #
15
+ # blog = Blog.first
16
+ #
17
+ # the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
18
+ # <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
19
+ # the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
20
+ #
21
+ # This class has most of the basic instance methods removed, and delegates
22
+ # unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
23
+ # corner case, it even removes the +class+ method and that's why you get
24
+ #
25
+ # blog.posts.class # => Array
26
+ #
27
+ # though the object behind <tt>blog.posts</tt> is not an Array, but an
28
+ # ActiveRecord::Associations::HasManyAssociation.
29
+ #
30
+ # The <tt>@target</tt> object is not \loaded until needed. For example,
31
+ #
32
+ # blog.posts.count
33
+ #
34
+ # is computed directly through SQL and does not trigger by itself the
35
+ # instantiation of the actual post records.
36
+ class CollectionProxy # :nodoc:
37
+ alias :proxy_extend :extend
38
+
39
+ instance_methods.each { |m| undef_method m unless m.to_s =~ /^(?:nil\?|send|object_id|to_a)$|^__|^respond_to|proxy_/ }
40
+
41
+ delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :includes, :from,
42
+ :lock, :readonly, :having, :pluck, :to => :scoped
43
+
44
+ delegate :target, :load_target, :loaded?, :to => :@association
45
+
46
+ delegate :select, :find, :first, :last,
47
+ :build, :create, :create!,
48
+ :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq,
49
+ :sum, :count, :size, :length, :empty?,
50
+ :any?, :many?, :include?,
51
+ :to => :@association
52
+
53
+ def initialize(association)
54
+ @association = association
55
+ Array.wrap(association.options[:extend]).each { |ext| proxy_extend(ext) }
56
+ end
57
+
58
+ alias_method :new, :build
59
+
60
+ def proxy_association
61
+ @association
62
+ end
63
+
64
+ def scoped
65
+ association = @association
66
+ association.scoped.extending do
67
+ define_method(:proxy_association) { association }
68
+ end
69
+ end
70
+
71
+ def respond_to?(name, include_private = false)
72
+ super ||
73
+ (load_target && target.respond_to?(name, include_private)) ||
74
+ proxy_association.klass.respond_to?(name, include_private)
75
+ end
76
+
77
+ def method_missing(method, *args, &block)
78
+ match = DynamicFinderMatch.match(method)
79
+ if match && match.instantiator?
80
+ send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |record|
81
+ proxy_association.send :set_owner_attributes, record
82
+ proxy_association.send :add_to_target, record
83
+ yield(record) if block_given?
84
+ end.tap do |record|
85
+ proxy_association.send :set_inverse_instance, record
86
+ end
87
+
88
+ elsif target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method))
89
+ if load_target
90
+ if target.respond_to?(method)
91
+ target.send(method, *args, &block)
92
+ else
93
+ begin
94
+ super
95
+ rescue NoMethodError => e
96
+ raise e, e.message.sub(/ for #<.*$/, " via proxy for #{target}")
97
+ end
98
+ end
99
+ end
100
+
101
+ else
102
+ scoped.readonly(nil).send(method, *args, &block)
103
+ end
104
+ end
105
+
106
+ # Forwards <tt>===</tt> explicitly to the \target because the instance method
107
+ # removal above doesn't catch it. Loads the \target if needed.
108
+ def ===(other)
109
+ other === load_target
110
+ end
111
+
112
+ def to_ary
113
+ load_target.dup
114
+ end
115
+ alias_method :to_a, :to_ary
116
+
117
+ def <<(*records)
118
+ proxy_association.concat(records) && self
119
+ end
120
+ alias_method :push, :<<
121
+
122
+ def clear
123
+ delete_all
124
+ self
125
+ end
126
+
127
+ def reload
128
+ proxy_association.reload
129
+ self
130
+ end
131
+ end
132
+ end
133
+ end