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
@@ -3,19 +3,17 @@ require 'thread'
3
3
  module ActiveRecord
4
4
  # See ActiveRecord::Transactions::ClassMethods for documentation.
5
5
  module Transactions
6
+ extend ActiveSupport::Concern
7
+
6
8
  class TransactionError < ActiveRecordError # :nodoc:
7
9
  end
8
10
 
9
- def self.included(base)
10
- base.extend(ClassMethods)
11
-
12
- base.class_eval do
13
- [:destroy, :save, :save!].each do |method|
14
- alias_method_chain method, :transactions
15
- end
16
- end
11
+ included do
12
+ define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
17
13
  end
18
14
 
15
+ # = Active Record Transactions
16
+ #
19
17
  # Transactions are protective blocks where SQL statements are only permanent
20
18
  # if they can all succeed as one atomic action. The classic example is a
21
19
  # transfer between two accounts where you can only have a deposit if the
@@ -23,17 +21,18 @@ module ActiveRecord
23
21
  # the database and guard the data against program errors or database
24
22
  # break-downs. So basically you should use transaction blocks whenever you
25
23
  # have a number of statements that must be executed together or not at all.
26
- # Example:
24
+ #
25
+ # For example:
27
26
  #
28
27
  # ActiveRecord::Base.transaction do
29
28
  # david.withdrawal(100)
30
29
  # mary.deposit(100)
31
30
  # end
32
31
  #
33
- # This example will only take money from David and give to Mary if neither
34
- # +withdrawal+ nor +deposit+ raises an exception. Exceptions will force a
35
- # ROLLBACK that returns the database to the state before the transaction was
36
- # begun. Be aware, though, that the objects will _not_ have their instance
32
+ # This example will only take money from David and give it to Mary if neither
33
+ # +withdrawal+ nor +deposit+ raise an exception. Exceptions will force a
34
+ # ROLLBACK that returns the database to the state before the transaction
35
+ # began. Be aware, though, that the objects will _not_ have their instance
37
36
  # data returned to their pre-transactional state.
38
37
  #
39
38
  # == Different Active Record classes in a single transaction
@@ -43,16 +42,16 @@ module ActiveRecord
43
42
  # that class. This is because transactions are per-database connection, not
44
43
  # per-model.
45
44
  #
46
- # In this example a <tt>Balance</tt> record is transactionally saved even
47
- # though <tt>transaction</tt> is called on the <tt>Account</tt> class:
45
+ # In this example a +balance+ record is transactionally saved even
46
+ # though +transaction+ is called on the +Account+ class:
48
47
  #
49
48
  # Account.transaction do
50
49
  # balance.save!
51
50
  # account.save!
52
51
  # end
53
52
  #
54
- # Note that the +transaction+ method is also available as a model instance
55
- # method. For example, you can also do this:
53
+ # The +transaction+ method is also available as a model instance method.
54
+ # For example, you can also do this:
56
55
  #
57
56
  # balance.transaction do
58
57
  # balance.save!
@@ -61,9 +60,9 @@ module ActiveRecord
61
60
  #
62
61
  # == Transactions are not distributed across database connections
63
62
  #
64
- # A transaction acts on a single database connection. If you have
63
+ # A transaction acts on a single database connection. If you have
65
64
  # multiple class-specific databases, the transaction will not protect
66
- # interaction among them. One workaround is to begin a transaction
65
+ # interaction among them. One workaround is to begin a transaction
67
66
  # on each class whose models you alter:
68
67
  #
69
68
  # Student.transaction do
@@ -73,16 +72,22 @@ module ActiveRecord
73
72
  # end
74
73
  # end
75
74
  #
76
- # This is a poor solution, but full distributed transactions are beyond
75
+ # This is a poor solution, but fully distributed transactions are beyond
77
76
  # the scope of Active Record.
78
77
  #
79
- # == Save and destroy are automatically wrapped in a transaction
78
+ # == +save+ and +destroy+ are automatically wrapped in a transaction
80
79
  #
81
- # Both Base#save and Base#destroy come wrapped in a transaction that ensures
82
- # that whatever you do in validations or callbacks will happen under the
83
- # protected cover of a transaction. So you can use validations to check for
84
- # values that the transaction depends on or you can raise exceptions in the
85
- # callbacks to rollback, including <tt>after_*</tt> callbacks.
80
+ # Both +save+ and +destroy+ come wrapped in a transaction that ensures
81
+ # that whatever you do in validations or callbacks will happen under its
82
+ # protected cover. So you can use validations to check for values that
83
+ # the transaction depends on or you can raise exceptions in the callbacks
84
+ # to rollback, including <tt>after_*</tt> callbacks.
85
+ #
86
+ # As a consequence changes to the database are not seen outside your connection
87
+ # until the operation is complete. For example, if you try to update the index
88
+ # of a search engine in +after_save+ the indexer won't see the updated record.
89
+ # The +after_commit+ callback is the only one that is triggered once the update
90
+ # is committed. See below.
86
91
  #
87
92
  # == Exception handling and rolling back
88
93
  #
@@ -90,14 +95,14 @@ module ActiveRecord
90
95
  # be propagated (after triggering the ROLLBACK), so you should be ready to
91
96
  # catch those in your application code.
92
97
  #
93
- # One exception is the ActiveRecord::Rollback exception, which will trigger
98
+ # One exception is the <tt>ActiveRecord::Rollback</tt> exception, which will trigger
94
99
  # a ROLLBACK when raised, but not be re-raised by the transaction block.
95
100
  #
96
- # *Warning*: one should not catch ActiveRecord::StatementInvalid exceptions
97
- # inside a transaction block. StatementInvalid exceptions indicate that an
101
+ # *Warning*: one should not catch <tt>ActiveRecord::StatementInvalid</tt> exceptions
102
+ # inside a transaction block. <tt>ActiveRecord::StatementInvalid</tt> exceptions indicate that an
98
103
  # error occurred at the database level, for example when a unique constraint
99
104
  # is violated. On some database systems, such as PostgreSQL, database errors
100
- # inside a transaction causes the entire transaction to become unusable
105
+ # inside a transaction cause the entire transaction to become unusable
101
106
  # until it's restarted from the beginning. Here is an example which
102
107
  # demonstrates the problem:
103
108
  #
@@ -110,7 +115,7 @@ module ActiveRecord
110
115
  # rescue ActiveRecord::StatementInvalid
111
116
  # # ...which we ignore.
112
117
  # end
113
- #
118
+ #
114
119
  # # On PostgreSQL, the transaction is now unusable. The following
115
120
  # # statement will cause a PostgreSQL error, even though the unique
116
121
  # # constraint is no longer violated:
@@ -119,13 +124,14 @@ module ActiveRecord
119
124
  # # ignored until end of transaction block"
120
125
  # end
121
126
  #
122
- # One should restart the entire transaction if a StatementError occurred.
127
+ # One should restart the entire transaction if an
128
+ # <tt>ActiveRecord::StatementInvalid</tt> occurred.
123
129
  #
124
130
  # == Nested transactions
125
131
  #
126
- # #transaction calls can be nested. By default, this makes all database
132
+ # +transaction+ calls can be nested. By default, this makes all database
127
133
  # statements in the nested transaction block become part of the parent
128
- # transaction. For example:
134
+ # transaction. For example, the following behavior may be surprising:
129
135
  #
130
136
  # User.transaction do
131
137
  # User.create(:username => 'Kotori')
@@ -134,13 +140,16 @@ module ActiveRecord
134
140
  # raise ActiveRecord::Rollback
135
141
  # end
136
142
  # end
137
- #
138
- # User.find(:all) # => empty
139
143
  #
140
- # It is also possible to requires a sub-transaction by passing
141
- # <tt>:requires_new => true</tt>. If anything goes wrong, the
142
- # database rolls back to the beginning of the sub-transaction
143
- # without rolling back the parent transaction. For example:
144
+ # creates both "Kotori" and "Nemu". Reason is the <tt>ActiveRecord::Rollback</tt>
145
+ # exception in the nested block does not issue a ROLLBACK. Since these exceptions
146
+ # are captured in transaction blocks, the parent block does not see it and the
147
+ # real transaction is committed.
148
+ #
149
+ # In order to get a ROLLBACK for the nested transaction you may ask for a real
150
+ # sub-transaction by passing <tt>:requires_new => true</tt>. If anything goes wrong,
151
+ # the database rolls back to the beginning of the sub-transaction without rolling
152
+ # back the parent transaction. If we add it to the previous example:
144
153
  #
145
154
  # User.transaction do
146
155
  # User.create(:username => 'Kotori')
@@ -149,72 +158,146 @@ module ActiveRecord
149
158
  # raise ActiveRecord::Rollback
150
159
  # end
151
160
  # end
152
- #
153
- # User.find(:all) # => Returns only Kotori
161
+ #
162
+ # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.)
154
163
  #
155
164
  # Most databases don't support true nested transactions. At the time of
156
165
  # writing, the only database that we're aware of that supports true nested
157
166
  # transactions, is MS-SQL. Because of this, Active Record emulates nested
158
- # transactions by using savepoints. See
159
- # http://dev.mysql.com/doc/refman/5.0/en/savepoints.html
167
+ # transactions by using savepoints on MySQL and PostgreSQL. See
168
+ # http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
160
169
  # for more information about savepoints.
161
170
  #
171
+ # === Callbacks
172
+ #
173
+ # There are two types of callbacks associated with committing and rolling back transactions:
174
+ # +after_commit+ and +after_rollback+.
175
+ #
176
+ # +after_commit+ callbacks are called on every record saved or destroyed within a
177
+ # transaction immediately after the transaction is committed. +after_rollback+ callbacks
178
+ # are called on every record saved or destroyed within a transaction immediately after the
179
+ # transaction or savepoint is rolled back.
180
+ #
181
+ # These callbacks are useful for interacting with other systems since you will be guaranteed
182
+ # that the callback is only executed when the database is in a permanent state. For example,
183
+ # +after_commit+ is a good spot to put in a hook to clearing a cache since clearing it from
184
+ # within a transaction could trigger the cache to be regenerated before the database is updated.
185
+ #
162
186
  # === Caveats
163
187
  #
164
188
  # If you're on MySQL, then do not use DDL operations in nested transactions
165
189
  # blocks that are emulated with savepoints. That is, do not execute statements
166
190
  # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically
167
- # releases all savepoints upon executing a DDL operation. When #transaction
191
+ # releases all savepoints upon executing a DDL operation. When +transaction+
168
192
  # is finished and tries to release the savepoint it created earlier, a
169
193
  # database error will occur because the savepoint has already been
170
194
  # automatically released. The following example demonstrates the problem:
171
- #
195
+ #
172
196
  # Model.connection.transaction do # BEGIN
173
197
  # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
174
198
  # Model.connection.create_table(...) # active_record_1 now automatically released
175
199
  # end # RELEASE savepoint active_record_1
176
200
  # # ^^^^ BOOM! database error!
177
201
  # end
202
+ #
203
+ # Note that "TRUNCATE" is also a MySQL DDL statement!
178
204
  module ClassMethods
179
205
  # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
180
206
  def transaction(options = {}, &block)
181
207
  # See the ConnectionAdapters::DatabaseStatements#transaction API docs.
182
208
  connection.transaction(options, &block)
183
209
  end
210
+
211
+ # This callback is called after a record has been created, updated, or destroyed.
212
+ #
213
+ # You can specify that the callback should only be fired by a certain action with
214
+ # the +:on+ option:
215
+ #
216
+ # after_commit :do_foo, :on => :create
217
+ # after_commit :do_bar, :on => :update
218
+ # after_commit :do_baz, :on => :destroy
219
+ #
220
+ # Also, to have the callback fired on create and update, but not on destroy:
221
+ #
222
+ # after_commit :do_zoo, :if => :persisted?
223
+ #
224
+ # Note that transactional fixtures do not play well with this feature. Please
225
+ # use the +test_after_commit+ gem to have these hooks fired in tests.
226
+ def after_commit(*args, &block)
227
+ options = args.last
228
+ if options.is_a?(Hash) && options[:on]
229
+ options[:if] = Array.wrap(options[:if])
230
+ options[:if] << "transaction_include_action?(:#{options[:on]})"
231
+ end
232
+ set_callback(:commit, :after, *args, &block)
233
+ end
234
+
235
+ # This callback is called after a create, update, or destroy are rolled back.
236
+ #
237
+ # Please check the documentation of +after_commit+ for options.
238
+ def after_rollback(*args, &block)
239
+ options = args.last
240
+ if options.is_a?(Hash) && options[:on]
241
+ options[:if] = Array.wrap(options[:if])
242
+ options[:if] << "transaction_include_action?(:#{options[:on]})"
243
+ end
244
+ set_callback(:rollback, :after, *args, &block)
245
+ end
184
246
  end
185
247
 
186
248
  # See ActiveRecord::Transactions::ClassMethods for detailed documentation.
187
- def transaction(&block)
188
- self.class.transaction(&block)
249
+ def transaction(options = {}, &block)
250
+ self.class.transaction(options, &block)
189
251
  end
190
252
 
191
- def destroy_with_transactions #:nodoc:
192
- with_transaction_returning_status(:destroy_without_transactions)
253
+ def destroy #:nodoc:
254
+ with_transaction_returning_status { super }
193
255
  end
194
256
 
195
- def save_with_transactions(perform_validation = true) #:nodoc:
196
- rollback_active_record_state! { with_transaction_returning_status(:save_without_transactions, perform_validation) }
257
+ def save(*) #:nodoc:
258
+ rollback_active_record_state! do
259
+ with_transaction_returning_status { super }
260
+ end
197
261
  end
198
262
 
199
- def save_with_transactions! #:nodoc:
200
- rollback_active_record_state! { self.class.transaction { save_without_transactions! } }
263
+ def save!(*) #:nodoc:
264
+ with_transaction_returning_status { super }
201
265
  end
202
266
 
203
267
  # Reset id and @new_record if the transaction rolls back.
204
268
  def rollback_active_record_state!
205
- id_present = has_attribute?(self.class.primary_key)
206
- previous_id = id
207
- previous_new_record = new_record?
269
+ remember_transaction_record_state
208
270
  yield
209
271
  rescue Exception
210
- @new_record = previous_new_record
211
- if id_present
212
- self.id = previous_id
213
- else
214
- @attributes.delete(self.class.primary_key)
215
- @attributes_cache.delete(self.class.primary_key)
216
- end
272
+ IdentityMap.remove(self) if IdentityMap.enabled?
273
+ restore_transaction_record_state
217
274
  raise
275
+ ensure
276
+ clear_transaction_record_state
277
+ end
278
+
279
+ # Call the after_commit callbacks
280
+ def committed! #:nodoc:
281
+ run_callbacks :commit
282
+ ensure
283
+ clear_transaction_record_state
284
+ end
285
+
286
+ # Call the after rollback callbacks. The restore_state argument indicates if the record
287
+ # state should be rolled back to the beginning or just to the last savepoint.
288
+ def rolledback!(force_restore_state = false) #:nodoc:
289
+ run_callbacks :rollback
290
+ ensure
291
+ IdentityMap.remove(self) if IdentityMap.enabled?
292
+ restore_transaction_record_state(force_restore_state)
293
+ end
294
+
295
+ # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
296
+ # can be called.
297
+ def add_to_transaction
298
+ if self.class.connection.add_transaction_record(self)
299
+ remember_transaction_record_state
300
+ end
218
301
  end
219
302
 
220
303
  # Executes +method+ within a transaction and captures its return value as a
@@ -223,13 +306,76 @@ module ActiveRecord
223
306
  #
224
307
  # This method is available within the context of an ActiveRecord::Base
225
308
  # instance.
226
- def with_transaction_returning_status(method, *args)
309
+ def with_transaction_returning_status
227
310
  status = nil
228
311
  self.class.transaction do
229
- status = send(method, *args)
312
+ add_to_transaction
313
+ status = yield
230
314
  raise ActiveRecord::Rollback unless status
231
315
  end
232
316
  status
233
317
  end
318
+
319
+ protected
320
+
321
+ # Save the new record state and id of a record so it can be restored later if a transaction fails.
322
+ def remember_transaction_record_state #:nodoc:
323
+ @_start_transaction_state ||= {}
324
+ @_start_transaction_state[:id] = id if has_attribute?(self.class.primary_key)
325
+ unless @_start_transaction_state.include?(:new_record)
326
+ @_start_transaction_state[:new_record] = @new_record
327
+ end
328
+ unless @_start_transaction_state.include?(:destroyed)
329
+ @_start_transaction_state[:destroyed] = @destroyed
330
+ end
331
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
332
+ @_start_transaction_state[:frozen?] = @attributes.frozen?
333
+ end
334
+
335
+ # Clear the new record state and id of a record.
336
+ def clear_transaction_record_state #:nodoc:
337
+ if defined?(@_start_transaction_state)
338
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
339
+ remove_instance_variable(:@_start_transaction_state) if @_start_transaction_state[:level] < 1
340
+ end
341
+ end
342
+
343
+ # Restore the new record state and id of a record that was previously saved by a call to save_record_state.
344
+ def restore_transaction_record_state(force = false) #:nodoc:
345
+ if defined?(@_start_transaction_state)
346
+ @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
347
+ if @_start_transaction_state[:level] < 1 || force
348
+ restore_state = remove_instance_variable(:@_start_transaction_state)
349
+ was_frozen = restore_state[:frozen?]
350
+ @attributes = @attributes.dup if @attributes.frozen?
351
+ @new_record = restore_state[:new_record]
352
+ @destroyed = restore_state[:destroyed]
353
+ if restore_state.has_key?(:id)
354
+ self.id = restore_state[:id]
355
+ else
356
+ @attributes.delete(self.class.primary_key)
357
+ @attributes_cache.delete(self.class.primary_key)
358
+ end
359
+ @attributes.freeze if was_frozen
360
+ end
361
+ end
362
+ end
363
+
364
+ # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.
365
+ def transaction_record_state(state) #:nodoc:
366
+ @_start_transaction_state[state] if defined?(@_start_transaction_state)
367
+ end
368
+
369
+ # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
370
+ def transaction_include_action?(action) #:nodoc:
371
+ case action
372
+ when :create
373
+ transaction_record_state(:new_record)
374
+ when :destroy
375
+ destroyed?
376
+ when :update
377
+ !(transaction_record_state(:new_record) || destroyed?)
378
+ end
379
+ end
234
380
  end
235
381
  end
@@ -0,0 +1,22 @@
1
+ module ActiveRecord
2
+ module Translation
3
+ include ActiveModel::Translation
4
+
5
+ # Set the lookup ancestors for ActiveModel.
6
+ def lookup_ancestors #:nodoc:
7
+ klass = self
8
+ classes = [klass]
9
+ return classes if klass == ActiveRecord::Base
10
+
11
+ while klass != klass.base_class
12
+ classes << klass = klass.superclass
13
+ end
14
+ classes
15
+ end
16
+
17
+ # Set the i18n scope to overwrite ActiveModel.
18
+ def i18n_scope #:nodoc:
19
+ :activerecord
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveRecord
2
+ module Validations
3
+ class AssociatedValidator < ActiveModel::EachValidator
4
+ def validate_each(record, attribute, value)
5
+ if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
6
+ record.errors.add(attribute, :invalid, options.merge(:value => value))
7
+ end
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ # Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
13
+ #
14
+ # class Book < ActiveRecord::Base
15
+ # has_many :pages
16
+ # belongs_to :library
17
+ #
18
+ # validates_associated :pages, :library
19
+ # end
20
+ #
21
+ # WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion.
22
+ #
23
+ # NOTE: This validation will not fail if the association hasn't been assigned. If you want to
24
+ # ensure that the association is both present and guaranteed to be valid, you also need to
25
+ # use +validates_presence_of+.
26
+ #
27
+ # Configuration options:
28
+ # * <tt>:message</tt> - A custom error message (default is: "is invalid")
29
+ # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
30
+ # validation contexts by default (+nil+), other options are <tt>:create</tt>
31
+ # and <tt>:update</tt>.
32
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
33
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
34
+ # method, proc or string should return or evaluate to a true or false value.
35
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
36
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
37
+ # method, proc or string should return or evaluate to a true or false value.
38
+ def validates_associated(*attr_names)
39
+ validates_with AssociatedValidator, _merge_attributes(attr_names)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,180 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module ActiveRecord
4
+ module Validations
5
+ class UniquenessValidator < ActiveModel::EachValidator
6
+ def initialize(options)
7
+ super(options.reverse_merge(:case_sensitive => true))
8
+ end
9
+
10
+ # Unfortunately, we have to tie Uniqueness validators to a class.
11
+ def setup(klass)
12
+ @klass = klass
13
+ end
14
+
15
+ def validate_each(record, attribute, value)
16
+ finder_class = find_finder_class_for(record)
17
+ table = finder_class.arel_table
18
+
19
+ coder = record.class.serialized_attributes[attribute.to_s]
20
+
21
+ if value && coder
22
+ value = coder.dump value
23
+ end
24
+
25
+ relation = build_relation(finder_class, table, attribute, value)
26
+ relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted?
27
+
28
+ Array.wrap(options[:scope]).each do |scope_item|
29
+ scope_value = record.read_attribute(scope_item)
30
+ relation = relation.and(table[scope_item].eq(scope_value))
31
+ end
32
+
33
+ if finder_class.unscoped.where(relation).exists?
34
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ # The check for an existing value should be run from a class that
41
+ # isn't abstract. This means working down from the current class
42
+ # (self), to the first non-abstract class. Since classes don't know
43
+ # their subclasses, we have to build the hierarchy between self and
44
+ # the record's class.
45
+ def find_finder_class_for(record) #:nodoc:
46
+ class_hierarchy = [record.class]
47
+
48
+ while class_hierarchy.first != @klass
49
+ class_hierarchy.insert(0, class_hierarchy.first.superclass)
50
+ end
51
+
52
+ class_hierarchy.detect { |klass| !klass.abstract_class? }
53
+ end
54
+
55
+ def build_relation(klass, table, attribute, value) #:nodoc:
56
+ column = klass.columns_hash[attribute.to_s]
57
+ value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if value && column.text?
58
+
59
+ if !options[:case_sensitive] && value && column.text?
60
+ # will use SQL LOWER function before comparison, unless it detects a case insensitive collation
61
+ relation = klass.connection.case_insensitive_comparison(table, attribute, column, value)
62
+ else
63
+ value = klass.connection.case_sensitive_modifier(value) if value
64
+ relation = table[attribute].eq(value)
65
+ end
66
+
67
+ relation
68
+ end
69
+ end
70
+
71
+ module ClassMethods
72
+ # Validates whether the value of the specified attributes are unique across the system.
73
+ # Useful for making sure that only one user
74
+ # can be named "davidhh".
75
+ #
76
+ # class Person < ActiveRecord::Base
77
+ # validates_uniqueness_of :user_name
78
+ # end
79
+ #
80
+ # It can also validate whether the value of the specified attributes are unique based on a scope parameter:
81
+ #
82
+ # class Person < ActiveRecord::Base
83
+ # validates_uniqueness_of :user_name, :scope => :account_id
84
+ # end
85
+ #
86
+ # Or even multiple scope parameters. For example, making sure that a teacher can only be on the schedule once
87
+ # per semester for a particular class.
88
+ #
89
+ # class TeacherSchedule < ActiveRecord::Base
90
+ # validates_uniqueness_of :teacher_id, :scope => [:semester_id, :class_id]
91
+ # end
92
+ #
93
+ # When the record is created, a check is performed to make sure that no record exists in the database
94
+ # with the given value for the specified attribute (that maps to a column). When the record is updated,
95
+ # the same check is made but disregarding the record itself.
96
+ #
97
+ # Configuration options:
98
+ # * <tt>:message</tt> - Specifies a custom error message (default is: "has already been taken").
99
+ # * <tt>:scope</tt> - One or more columns by which to limit the scope of the uniqueness constraint.
100
+ # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by non-text columns (+true+ by default).
101
+ # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+).
102
+ # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+).
103
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
104
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>).
105
+ # The method, proc or string should return or evaluate to a true or false value.
106
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
107
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or
108
+ # <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The method, proc or string should
109
+ # return or evaluate to a true or false value.
110
+ #
111
+ # === Concurrency and integrity
112
+ #
113
+ # Using this validation method in conjunction with ActiveRecord::Base#save
114
+ # does not guarantee the absence of duplicate record insertions, because
115
+ # uniqueness checks on the application level are inherently prone to race
116
+ # conditions. For example, suppose that two users try to post a Comment at
117
+ # the same time, and a Comment's title must be unique. At the database-level,
118
+ # the actions performed by these users could be interleaved in the following manner:
119
+ #
120
+ # User 1 | User 2
121
+ # ------------------------------------+--------------------------------------
122
+ # # User 1 checks whether there's |
123
+ # # already a comment with the title |
124
+ # # 'My Post'. This is not the case. |
125
+ # SELECT * FROM comments |
126
+ # WHERE title = 'My Post' |
127
+ # |
128
+ # | # User 2 does the same thing and also
129
+ # | # infers that his title is unique.
130
+ # | SELECT * FROM comments
131
+ # | WHERE title = 'My Post'
132
+ # |
133
+ # # User 1 inserts his comment. |
134
+ # INSERT INTO comments |
135
+ # (title, content) VALUES |
136
+ # ('My Post', 'hi!') |
137
+ # |
138
+ # | # User 2 does the same thing.
139
+ # | INSERT INTO comments
140
+ # | (title, content) VALUES
141
+ # | ('My Post', 'hello!')
142
+ # |
143
+ # | # ^^^^^^
144
+ # | # Boom! We now have a duplicate
145
+ # | # title!
146
+ #
147
+ # This could even happen if you use transactions with the 'serializable'
148
+ # isolation level. The best way to work around this problem is to add a unique
149
+ # index to the database table using
150
+ # ActiveRecord::ConnectionAdapters::SchemaStatements#add_index. In the
151
+ # rare case that a race condition occurs, the database will guarantee
152
+ # the field's uniqueness.
153
+ #
154
+ # When the database catches such a duplicate insertion,
155
+ # ActiveRecord::Base#save will raise an ActiveRecord::StatementInvalid
156
+ # exception. You can either choose to let this error propagate (which
157
+ # will result in the default Rails exception page being shown), or you
158
+ # can catch it and restart the transaction (e.g. by telling the user
159
+ # that the title already exists, and asking him to re-enter the title).
160
+ # This technique is also known as optimistic concurrency control:
161
+ # http://en.wikipedia.org/wiki/Optimistic_concurrency_control
162
+ #
163
+ # The bundled ActiveRecord::ConnectionAdapters distinguish unique index
164
+ # constraint errors from other types of database errors by throwing an
165
+ # ActiveRecord::RecordNotUnique exception.
166
+ # For other adapters you will have to parse the (database-specific) exception
167
+ # message to detect such a case.
168
+ # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception:
169
+ # * ActiveRecord::ConnectionAdapters::MysqlAdapter
170
+ # * ActiveRecord::ConnectionAdapters::Mysql2Adapter
171
+ # * ActiveRecord::ConnectionAdapters::SQLiteAdapter
172
+ # * ActiveRecord::ConnectionAdapters::SQLite3Adapter
173
+ # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
174
+ #
175
+ def validates_uniqueness_of(*attr_names)
176
+ validates_with UniquenessValidator, _merge_attributes(attr_names)
177
+ end
178
+ end
179
+ end
180
+ end