activerecord 2.3.18 → 3.0.0.beta

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 (378) hide show
  1. data/CHANGELOG +105 -34
  2. data/examples/performance.rb +3 -39
  3. data/examples/simple.rb +14 -0
  4. data/lib/active_record.rb +81 -47
  5. data/lib/active_record/aggregations.rb +1 -3
  6. data/lib/active_record/association_preload.rb +39 -54
  7. data/lib/active_record/associations.rb +262 -419
  8. data/lib/active_record/associations/association_collection.rb +85 -100
  9. data/lib/active_record/associations/association_proxy.rb +20 -18
  10. data/lib/active_record/associations/belongs_to_association.rb +8 -8
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +13 -35
  12. data/lib/active_record/associations/has_many_association.rb +11 -19
  13. data/lib/active_record/associations/has_many_through_association.rb +30 -183
  14. data/lib/active_record/associations/has_one_association.rb +10 -10
  15. data/lib/active_record/associations/has_one_through_association.rb +13 -11
  16. data/lib/active_record/associations/through_association_scope.rb +153 -0
  17. data/lib/active_record/attribute_methods.rb +17 -370
  18. data/lib/active_record/attribute_methods/before_type_cast.rb +33 -0
  19. data/lib/active_record/attribute_methods/dirty.rb +87 -0
  20. data/lib/active_record/attribute_methods/primary_key.rb +44 -0
  21. data/lib/active_record/attribute_methods/query.rb +37 -0
  22. data/lib/active_record/attribute_methods/read.rb +116 -0
  23. data/lib/active_record/attribute_methods/time_zone_conversion.rb +60 -0
  24. data/lib/active_record/attribute_methods/write.rb +37 -0
  25. data/lib/active_record/autosave_association.rb +20 -41
  26. data/lib/active_record/base.rb +357 -1180
  27. data/lib/active_record/batches.rb +10 -16
  28. data/lib/active_record/callbacks.rb +66 -126
  29. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +17 -13
  30. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +5 -25
  31. data/lib/active_record/connection_adapters/abstract/database_statements.rb +4 -43
  32. data/lib/active_record/connection_adapters/abstract/query_cache.rb +3 -2
  33. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -4
  34. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  35. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +18 -72
  36. data/lib/active_record/connection_adapters/abstract_adapter.rb +16 -49
  37. data/lib/active_record/connection_adapters/mysql_adapter.rb +15 -27
  38. data/lib/active_record/connection_adapters/postgresql_adapter.rb +84 -46
  39. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +9 -3
  40. data/lib/active_record/connection_adapters/sqlite_adapter.rb +34 -65
  41. data/lib/active_record/fixtures.rb +21 -25
  42. data/lib/active_record/locale/en.yml +9 -27
  43. data/lib/active_record/locking/optimistic.rb +16 -48
  44. data/lib/active_record/migration.rb +59 -46
  45. data/lib/active_record/named_scope.rb +85 -92
  46. data/lib/active_record/nested_attributes.rb +18 -24
  47. data/lib/active_record/observer.rb +18 -94
  48. data/lib/active_record/railtie.rb +83 -0
  49. data/lib/active_record/railties/controller_runtime.rb +38 -0
  50. data/lib/active_record/railties/databases.rake +477 -0
  51. data/lib/active_record/railties/subscriber.rb +27 -0
  52. data/lib/active_record/reflection.rb +2 -15
  53. data/lib/active_record/relation.rb +339 -0
  54. data/lib/active_record/relation/calculations.rb +259 -0
  55. data/lib/active_record/relation/finder_methods.rb +315 -0
  56. data/lib/active_record/relation/predicate_builder.rb +46 -0
  57. data/lib/active_record/relation/query_methods.rb +218 -0
  58. data/lib/active_record/relation/spawn_methods.rb +102 -0
  59. data/lib/active_record/schema_dumper.rb +10 -6
  60. data/lib/active_record/serialization.rb +31 -74
  61. data/lib/active_record/serializers/xml_serializer.rb +33 -121
  62. data/lib/active_record/session_store.rb +1 -9
  63. data/lib/active_record/test_case.rb +1 -3
  64. data/lib/active_record/timestamp.rb +7 -5
  65. data/lib/active_record/transactions.rb +9 -9
  66. data/lib/active_record/validations.rb +51 -1100
  67. data/lib/active_record/validations/associated.rb +47 -0
  68. data/lib/active_record/validations/uniqueness.rb +181 -0
  69. data/lib/active_record/version.rb +3 -3
  70. data/lib/generators/active_record.rb +30 -0
  71. data/lib/generators/active_record/migration/migration_generator.rb +25 -0
  72. data/lib/generators/active_record/migration/templates/migration.rb +11 -0
  73. data/lib/generators/active_record/model/model_generator.rb +33 -0
  74. data/lib/generators/active_record/model/templates/migration.rb +16 -0
  75. data/lib/generators/active_record/model/templates/model.rb +5 -0
  76. data/lib/generators/active_record/observer/observer_generator.rb +15 -0
  77. data/lib/generators/active_record/observer/templates/observer.rb +2 -0
  78. data/lib/generators/active_record/session_migration/session_migration_generator.rb +24 -0
  79. data/lib/generators/active_record/session_migration/templates/migration.rb +16 -0
  80. metadata +67 -325
  81. data/RUNNING_UNIT_TESTS +0 -36
  82. data/Rakefile +0 -268
  83. data/install.rb +0 -30
  84. data/lib/active_record/calculations.rb +0 -321
  85. data/lib/active_record/connection_adapters/abstract/database_limits.rb +0 -57
  86. data/lib/active_record/dirty.rb +0 -183
  87. data/lib/active_record/serializers/json_serializer.rb +0 -91
  88. data/lib/activerecord.rb +0 -2
  89. data/test/assets/example.log +0 -1
  90. data/test/assets/flowers.jpg +0 -0
  91. data/test/cases/aaa_create_tables_test.rb +0 -24
  92. data/test/cases/active_schema_test_mysql.rb +0 -122
  93. data/test/cases/active_schema_test_postgresql.rb +0 -24
  94. data/test/cases/adapter_test.rb +0 -144
  95. data/test/cases/aggregations_test.rb +0 -167
  96. data/test/cases/ar_schema_test.rb +0 -32
  97. data/test/cases/associations/belongs_to_associations_test.rb +0 -438
  98. data/test/cases/associations/callbacks_test.rb +0 -161
  99. data/test/cases/associations/cascaded_eager_loading_test.rb +0 -131
  100. data/test/cases/associations/eager_load_includes_full_sti_class_test.rb +0 -36
  101. data/test/cases/associations/eager_load_nested_include_test.rb +0 -131
  102. data/test/cases/associations/eager_load_nested_polymorphic_include.rb +0 -19
  103. data/test/cases/associations/eager_singularization_test.rb +0 -145
  104. data/test/cases/associations/eager_test.rb +0 -852
  105. data/test/cases/associations/extension_test.rb +0 -62
  106. data/test/cases/associations/habtm_join_table_test.rb +0 -56
  107. data/test/cases/associations/has_and_belongs_to_many_associations_test.rb +0 -827
  108. data/test/cases/associations/has_many_associations_test.rb +0 -1273
  109. data/test/cases/associations/has_many_through_associations_test.rb +0 -360
  110. data/test/cases/associations/has_one_associations_test.rb +0 -330
  111. data/test/cases/associations/has_one_through_associations_test.rb +0 -209
  112. data/test/cases/associations/inner_join_association_test.rb +0 -93
  113. data/test/cases/associations/inverse_associations_test.rb +0 -566
  114. data/test/cases/associations/join_model_test.rb +0 -712
  115. data/test/cases/associations_test.rb +0 -282
  116. data/test/cases/attribute_methods_test.rb +0 -305
  117. data/test/cases/autosave_association_test.rb +0 -1218
  118. data/test/cases/base_test.rb +0 -2166
  119. data/test/cases/batches_test.rb +0 -81
  120. data/test/cases/binary_test.rb +0 -30
  121. data/test/cases/calculations_test.rb +0 -360
  122. data/test/cases/callbacks_observers_test.rb +0 -38
  123. data/test/cases/callbacks_test.rb +0 -438
  124. data/test/cases/class_inheritable_attributes_test.rb +0 -32
  125. data/test/cases/column_alias_test.rb +0 -17
  126. data/test/cases/column_definition_test.rb +0 -70
  127. data/test/cases/connection_pool_test.rb +0 -25
  128. data/test/cases/connection_test_firebird.rb +0 -8
  129. data/test/cases/connection_test_mysql.rb +0 -65
  130. data/test/cases/copy_table_test_sqlite.rb +0 -80
  131. data/test/cases/counter_cache_test.rb +0 -84
  132. data/test/cases/database_statements_test.rb +0 -12
  133. data/test/cases/datatype_test_postgresql.rb +0 -204
  134. data/test/cases/date_time_test.rb +0 -37
  135. data/test/cases/default_test_firebird.rb +0 -16
  136. data/test/cases/defaults_test.rb +0 -111
  137. data/test/cases/deprecated_finder_test.rb +0 -30
  138. data/test/cases/dirty_test.rb +0 -316
  139. data/test/cases/finder_respond_to_test.rb +0 -76
  140. data/test/cases/finder_test.rb +0 -1098
  141. data/test/cases/fixtures_test.rb +0 -661
  142. data/test/cases/helper.rb +0 -68
  143. data/test/cases/i18n_test.rb +0 -46
  144. data/test/cases/inheritance_test.rb +0 -262
  145. data/test/cases/invalid_date_test.rb +0 -24
  146. data/test/cases/json_serialization_test.rb +0 -219
  147. data/test/cases/lifecycle_test.rb +0 -193
  148. data/test/cases/locking_test.rb +0 -350
  149. data/test/cases/method_scoping_test.rb +0 -704
  150. data/test/cases/migration_test.rb +0 -1649
  151. data/test/cases/migration_test_firebird.rb +0 -124
  152. data/test/cases/mixin_test.rb +0 -96
  153. data/test/cases/modules_test.rb +0 -109
  154. data/test/cases/multiple_db_test.rb +0 -85
  155. data/test/cases/named_scope_test.rb +0 -372
  156. data/test/cases/nested_attributes_test.rb +0 -840
  157. data/test/cases/pk_test.rb +0 -119
  158. data/test/cases/pooled_connections_test.rb +0 -103
  159. data/test/cases/query_cache_test.rb +0 -129
  160. data/test/cases/readonly_test.rb +0 -107
  161. data/test/cases/reflection_test.rb +0 -234
  162. data/test/cases/reload_models_test.rb +0 -22
  163. data/test/cases/repair_helper.rb +0 -50
  164. data/test/cases/reserved_word_test_mysql.rb +0 -176
  165. data/test/cases/sanitize_test.rb +0 -25
  166. data/test/cases/schema_authorization_test_postgresql.rb +0 -75
  167. data/test/cases/schema_dumper_test.rb +0 -211
  168. data/test/cases/schema_test_postgresql.rb +0 -178
  169. data/test/cases/serialization_test.rb +0 -47
  170. data/test/cases/sp_test_mysql.rb +0 -16
  171. data/test/cases/synonym_test_oracle.rb +0 -17
  172. data/test/cases/timestamp_test.rb +0 -75
  173. data/test/cases/transactions_test.rb +0 -543
  174. data/test/cases/unconnected_test.rb +0 -32
  175. data/test/cases/validations_i18n_test.rb +0 -925
  176. data/test/cases/validations_test.rb +0 -1684
  177. data/test/cases/xml_serialization_test.rb +0 -240
  178. data/test/cases/yaml_serialization_test.rb +0 -11
  179. data/test/config.rb +0 -5
  180. data/test/connections/jdbc_jdbcderby/connection.rb +0 -18
  181. data/test/connections/jdbc_jdbch2/connection.rb +0 -18
  182. data/test/connections/jdbc_jdbchsqldb/connection.rb +0 -18
  183. data/test/connections/jdbc_jdbcmysql/connection.rb +0 -26
  184. data/test/connections/jdbc_jdbcpostgresql/connection.rb +0 -26
  185. data/test/connections/jdbc_jdbcsqlite3/connection.rb +0 -25
  186. data/test/connections/native_db2/connection.rb +0 -25
  187. data/test/connections/native_firebird/connection.rb +0 -26
  188. data/test/connections/native_frontbase/connection.rb +0 -27
  189. data/test/connections/native_mysql/connection.rb +0 -25
  190. data/test/connections/native_openbase/connection.rb +0 -21
  191. data/test/connections/native_oracle/connection.rb +0 -27
  192. data/test/connections/native_postgresql/connection.rb +0 -21
  193. data/test/connections/native_sqlite/connection.rb +0 -25
  194. data/test/connections/native_sqlite3/connection.rb +0 -25
  195. data/test/connections/native_sqlite3/in_memory_connection.rb +0 -18
  196. data/test/connections/native_sybase/connection.rb +0 -23
  197. data/test/fixtures/accounts.yml +0 -29
  198. data/test/fixtures/all/developers.yml +0 -0
  199. data/test/fixtures/all/people.csv +0 -0
  200. data/test/fixtures/all/tasks.yml +0 -0
  201. data/test/fixtures/author_addresses.yml +0 -5
  202. data/test/fixtures/author_favorites.yml +0 -4
  203. data/test/fixtures/authors.yml +0 -9
  204. data/test/fixtures/binaries.yml +0 -132
  205. data/test/fixtures/books.yml +0 -7
  206. data/test/fixtures/categories.yml +0 -14
  207. data/test/fixtures/categories/special_categories.yml +0 -9
  208. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +0 -4
  209. data/test/fixtures/categories_ordered.yml +0 -7
  210. data/test/fixtures/categories_posts.yml +0 -23
  211. data/test/fixtures/categorizations.yml +0 -17
  212. data/test/fixtures/clubs.yml +0 -6
  213. data/test/fixtures/comments.yml +0 -59
  214. data/test/fixtures/companies.yml +0 -56
  215. data/test/fixtures/computers.yml +0 -4
  216. data/test/fixtures/courses.yml +0 -7
  217. data/test/fixtures/customers.yml +0 -26
  218. data/test/fixtures/developers.yml +0 -21
  219. data/test/fixtures/developers_projects.yml +0 -17
  220. data/test/fixtures/edges.yml +0 -6
  221. data/test/fixtures/entrants.yml +0 -14
  222. data/test/fixtures/faces.yml +0 -11
  223. data/test/fixtures/fk_test_has_fk.yml +0 -3
  224. data/test/fixtures/fk_test_has_pk.yml +0 -2
  225. data/test/fixtures/funny_jokes.yml +0 -10
  226. data/test/fixtures/interests.yml +0 -33
  227. data/test/fixtures/items.yml +0 -4
  228. data/test/fixtures/jobs.yml +0 -7
  229. data/test/fixtures/legacy_things.yml +0 -3
  230. data/test/fixtures/mateys.yml +0 -4
  231. data/test/fixtures/member_types.yml +0 -6
  232. data/test/fixtures/members.yml +0 -6
  233. data/test/fixtures/memberships.yml +0 -20
  234. data/test/fixtures/men.yml +0 -5
  235. data/test/fixtures/minimalistics.yml +0 -2
  236. data/test/fixtures/mixed_case_monkeys.yml +0 -6
  237. data/test/fixtures/mixins.yml +0 -29
  238. data/test/fixtures/movies.yml +0 -7
  239. data/test/fixtures/naked/csv/accounts.csv +0 -1
  240. data/test/fixtures/naked/yml/accounts.yml +0 -1
  241. data/test/fixtures/naked/yml/companies.yml +0 -1
  242. data/test/fixtures/naked/yml/courses.yml +0 -1
  243. data/test/fixtures/organizations.yml +0 -5
  244. data/test/fixtures/owners.yml +0 -7
  245. data/test/fixtures/parrots.yml +0 -27
  246. data/test/fixtures/parrots_pirates.yml +0 -7
  247. data/test/fixtures/people.yml +0 -15
  248. data/test/fixtures/pets.yml +0 -14
  249. data/test/fixtures/pirates.yml +0 -9
  250. data/test/fixtures/polymorphic_designs.yml +0 -19
  251. data/test/fixtures/polymorphic_prices.yml +0 -19
  252. data/test/fixtures/posts.yml +0 -52
  253. data/test/fixtures/price_estimates.yml +0 -7
  254. data/test/fixtures/projects.yml +0 -7
  255. data/test/fixtures/readers.yml +0 -9
  256. data/test/fixtures/references.yml +0 -17
  257. data/test/fixtures/reserved_words/distinct.yml +0 -5
  258. data/test/fixtures/reserved_words/distincts_selects.yml +0 -11
  259. data/test/fixtures/reserved_words/group.yml +0 -14
  260. data/test/fixtures/reserved_words/select.yml +0 -8
  261. data/test/fixtures/reserved_words/values.yml +0 -7
  262. data/test/fixtures/ships.yml +0 -5
  263. data/test/fixtures/sponsors.yml +0 -9
  264. data/test/fixtures/subscribers.yml +0 -7
  265. data/test/fixtures/subscriptions.yml +0 -12
  266. data/test/fixtures/taggings.yml +0 -28
  267. data/test/fixtures/tags.yml +0 -7
  268. data/test/fixtures/tasks.yml +0 -7
  269. data/test/fixtures/tees.yml +0 -4
  270. data/test/fixtures/ties.yml +0 -4
  271. data/test/fixtures/topics.yml +0 -42
  272. data/test/fixtures/toys.yml +0 -4
  273. data/test/fixtures/treasures.yml +0 -10
  274. data/test/fixtures/vertices.yml +0 -4
  275. data/test/fixtures/warehouse-things.yml +0 -3
  276. data/test/fixtures/zines.yml +0 -5
  277. data/test/migrations/broken/100_migration_that_raises_exception.rb +0 -10
  278. data/test/migrations/decimal/1_give_me_big_numbers.rb +0 -15
  279. data/test/migrations/duplicate/1_people_have_last_names.rb +0 -9
  280. data/test/migrations/duplicate/2_we_need_reminders.rb +0 -12
  281. data/test/migrations/duplicate/3_foo.rb +0 -7
  282. data/test/migrations/duplicate/3_innocent_jointable.rb +0 -12
  283. data/test/migrations/duplicate_names/20080507052938_chunky.rb +0 -7
  284. data/test/migrations/duplicate_names/20080507053028_chunky.rb +0 -7
  285. data/test/migrations/interleaved/pass_1/3_innocent_jointable.rb +0 -12
  286. data/test/migrations/interleaved/pass_2/1_people_have_last_names.rb +0 -9
  287. data/test/migrations/interleaved/pass_2/3_innocent_jointable.rb +0 -12
  288. data/test/migrations/interleaved/pass_3/1_people_have_last_names.rb +0 -9
  289. data/test/migrations/interleaved/pass_3/2_i_raise_on_down.rb +0 -8
  290. data/test/migrations/interleaved/pass_3/3_innocent_jointable.rb +0 -12
  291. data/test/migrations/missing/1000_people_have_middle_names.rb +0 -9
  292. data/test/migrations/missing/1_people_have_last_names.rb +0 -9
  293. data/test/migrations/missing/3_we_need_reminders.rb +0 -12
  294. data/test/migrations/missing/4_innocent_jointable.rb +0 -12
  295. data/test/migrations/valid/1_people_have_last_names.rb +0 -9
  296. data/test/migrations/valid/2_we_need_reminders.rb +0 -12
  297. data/test/migrations/valid/3_innocent_jointable.rb +0 -12
  298. data/test/models/author.rb +0 -151
  299. data/test/models/auto_id.rb +0 -4
  300. data/test/models/binary.rb +0 -2
  301. data/test/models/bird.rb +0 -9
  302. data/test/models/book.rb +0 -4
  303. data/test/models/categorization.rb +0 -5
  304. data/test/models/category.rb +0 -34
  305. data/test/models/citation.rb +0 -6
  306. data/test/models/club.rb +0 -13
  307. data/test/models/column_name.rb +0 -3
  308. data/test/models/comment.rb +0 -29
  309. data/test/models/company.rb +0 -173
  310. data/test/models/company_in_module.rb +0 -78
  311. data/test/models/computer.rb +0 -3
  312. data/test/models/contact.rb +0 -16
  313. data/test/models/contract.rb +0 -5
  314. data/test/models/course.rb +0 -3
  315. data/test/models/customer.rb +0 -73
  316. data/test/models/default.rb +0 -2
  317. data/test/models/developer.rb +0 -101
  318. data/test/models/edge.rb +0 -5
  319. data/test/models/entrant.rb +0 -3
  320. data/test/models/essay.rb +0 -3
  321. data/test/models/event.rb +0 -3
  322. data/test/models/event_author.rb +0 -8
  323. data/test/models/face.rb +0 -7
  324. data/test/models/guid.rb +0 -2
  325. data/test/models/interest.rb +0 -5
  326. data/test/models/invoice.rb +0 -4
  327. data/test/models/item.rb +0 -7
  328. data/test/models/job.rb +0 -5
  329. data/test/models/joke.rb +0 -3
  330. data/test/models/keyboard.rb +0 -3
  331. data/test/models/legacy_thing.rb +0 -3
  332. data/test/models/line_item.rb +0 -3
  333. data/test/models/man.rb +0 -9
  334. data/test/models/matey.rb +0 -4
  335. data/test/models/member.rb +0 -12
  336. data/test/models/member_detail.rb +0 -5
  337. data/test/models/member_type.rb +0 -3
  338. data/test/models/membership.rb +0 -9
  339. data/test/models/minimalistic.rb +0 -2
  340. data/test/models/mixed_case_monkey.rb +0 -3
  341. data/test/models/movie.rb +0 -5
  342. data/test/models/order.rb +0 -4
  343. data/test/models/organization.rb +0 -6
  344. data/test/models/owner.rb +0 -5
  345. data/test/models/parrot.rb +0 -22
  346. data/test/models/person.rb +0 -16
  347. data/test/models/pet.rb +0 -5
  348. data/test/models/pirate.rb +0 -80
  349. data/test/models/polymorphic_design.rb +0 -3
  350. data/test/models/polymorphic_price.rb +0 -3
  351. data/test/models/post.rb +0 -102
  352. data/test/models/price_estimate.rb +0 -3
  353. data/test/models/project.rb +0 -30
  354. data/test/models/reader.rb +0 -4
  355. data/test/models/reference.rb +0 -4
  356. data/test/models/reply.rb +0 -46
  357. data/test/models/ship.rb +0 -19
  358. data/test/models/ship_part.rb +0 -7
  359. data/test/models/sponsor.rb +0 -4
  360. data/test/models/subject.rb +0 -4
  361. data/test/models/subscriber.rb +0 -8
  362. data/test/models/subscription.rb +0 -4
  363. data/test/models/tag.rb +0 -7
  364. data/test/models/tagging.rb +0 -10
  365. data/test/models/task.rb +0 -3
  366. data/test/models/tee.rb +0 -4
  367. data/test/models/tie.rb +0 -4
  368. data/test/models/topic.rb +0 -80
  369. data/test/models/toy.rb +0 -6
  370. data/test/models/treasure.rb +0 -8
  371. data/test/models/vertex.rb +0 -9
  372. data/test/models/warehouse_thing.rb +0 -5
  373. data/test/models/zine.rb +0 -3
  374. data/test/schema/mysql_specific_schema.rb +0 -31
  375. data/test/schema/postgresql_specific_schema.rb +0 -114
  376. data/test/schema/schema.rb +0 -550
  377. data/test/schema/schema2.rb +0 -6
  378. data/test/schema/sqlite_specific_schema.rb +0 -25
@@ -0,0 +1,27 @@
1
+ module ActiveRecord
2
+ module Railties
3
+ class Subscriber < Rails::Subscriber
4
+ def sql(event)
5
+ name = '%s (%.1fms)' % [event.payload[:name], event.duration]
6
+ sql = event.payload[:sql].squeeze(' ')
7
+
8
+ if odd?
9
+ name = color(name, :cyan, true)
10
+ sql = color(sql, nil, true)
11
+ else
12
+ name = color(name, :magenta, true)
13
+ end
14
+
15
+ debug " #{name} #{sql}"
16
+ end
17
+
18
+ def odd?
19
+ @odd_or_even = !@odd_or_even
20
+ end
21
+
22
+ def logger
23
+ ActiveRecord::Base.logger
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,8 +1,6 @@
1
1
  module ActiveRecord
2
2
  module Reflection # :nodoc:
3
- def self.included(base)
4
- base.extend(ClassMethods)
5
- end
3
+ extend ActiveSupport::Concern
6
4
 
7
5
  # Reflection allows you to interrogate Active Record classes and objects about their associations and aggregations.
8
6
  # This information can, for example, be used in a form builder that took an Active Record object and created input
@@ -18,7 +16,7 @@ module ActiveRecord
18
16
  when :composed_of
19
17
  reflection = AggregateReflection.new(macro, name, options, active_record)
20
18
  end
21
- write_inheritable_hiwa :reflections, name => reflection
19
+ write_inheritable_hash :reflections, name => reflection
22
20
  reflection
23
21
  end
24
22
 
@@ -277,17 +275,6 @@ module ActiveRecord
277
275
  !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
278
276
  end
279
277
 
280
- def dependent_conditions(record, base_class, extra_conditions)
281
- dependent_conditions = []
282
- dependent_conditions << "#{primary_key_name} = #{record.send(name).send(:owner_quoted_id)}"
283
- dependent_conditions << "#{options[:as]}_type = '#{base_class.name}'" if options[:as]
284
- dependent_conditions << klass.send(:sanitize_sql, options[:conditions]) if options[:conditions]
285
- dependent_conditions = dependent_conditions.collect {|where| "(#{where})" }.join(" AND ")
286
- dependent_conditions << extra_conditions if extra_conditions
287
- dependent_conditions = dependent_conditions.gsub('@', '\@')
288
- dependent_conditions
289
- end
290
-
291
278
  private
292
279
  def derive_class_name
293
280
  class_name = name.to_s.camelize
@@ -0,0 +1,339 @@
1
+ module ActiveRecord
2
+ class Relation
3
+ JoinOperation = Struct.new(:relation, :join_class, :on)
4
+ ASSOCIATION_METHODS = [:includes, :eager_load, :preload]
5
+ MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having]
6
+ SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from]
7
+
8
+ include FinderMethods, Calculations, SpawnMethods, QueryMethods
9
+
10
+ delegate :length, :collect, :map, :each, :all?, :include?, :to => :to_a
11
+ delegate :insert, :to => :arel
12
+
13
+ attr_reader :table, :klass
14
+
15
+ def initialize(klass, table)
16
+ @klass, @table = klass, table
17
+ (ASSOCIATION_METHODS + MULTI_VALUE_METHODS).each {|v| instance_variable_set(:"@#{v}_values", [])}
18
+ end
19
+
20
+ def new(*args, &block)
21
+ with_create_scope { @klass.new(*args, &block) }
22
+ end
23
+
24
+ alias build new
25
+
26
+ def create(*args, &block)
27
+ with_create_scope { @klass.create(*args, &block) }
28
+ end
29
+
30
+ def create!(*args, &block)
31
+ with_create_scope { @klass.create!(*args, &block) }
32
+ end
33
+
34
+ def respond_to?(method, include_private = false)
35
+ return true if arel.respond_to?(method, include_private) || Array.method_defined?(method) || @klass.respond_to?(method, include_private)
36
+
37
+ if match = DynamicFinderMatch.match(method)
38
+ return true if @klass.send(:all_attributes_exists?, match.attribute_names)
39
+ elsif match = DynamicScopeMatch.match(method)
40
+ return true if @klass.send(:all_attributes_exists?, match.attribute_names)
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ def to_a
47
+ return @records if loaded?
48
+
49
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql)
50
+
51
+ preload = @preload_values
52
+ preload += @includes_values unless eager_loading?
53
+ preload.each {|associations| @klass.send(:preload_associations, @records, associations) }
54
+
55
+ # @readonly_value is true only if set explicity. @implicit_readonly is true if there are JOINS and no explicit SELECT.
56
+ readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value
57
+ @records.each { |record| record.readonly! } if readonly
58
+
59
+ @loaded = true
60
+ @records
61
+ end
62
+
63
+ def size
64
+ loaded? ? @records.length : count
65
+ end
66
+
67
+ def empty?
68
+ loaded? ? @records.empty? : count.zero?
69
+ end
70
+
71
+ def any?
72
+ if block_given?
73
+ to_a.any? { |*block_args| yield(*block_args) }
74
+ else
75
+ !empty?
76
+ end
77
+ end
78
+
79
+ def many?
80
+ if block_given?
81
+ to_a.many? { |*block_args| yield(*block_args) }
82
+ else
83
+ @limit_value.present? ? to_a.many? : size > 1
84
+ end
85
+ end
86
+
87
+ # Updates all records with details given if they match a set of conditions supplied, limits and order can
88
+ # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
89
+ # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
90
+ # or validations.
91
+ #
92
+ # ==== Parameters
93
+ #
94
+ # * +updates+ - A string, array, or hash representing the SET part of an SQL statement.
95
+ # * +conditions+ - A string, array, or hash representing the WHERE part of an SQL statement. See conditions in the intro.
96
+ # * +options+ - Additional options are <tt>:limit</tt> and <tt>:order</tt>, see the examples for usage.
97
+ #
98
+ # ==== Examples
99
+ #
100
+ # # Update all customers with the given attributes
101
+ # Customer.update_all :wants_email => true
102
+ #
103
+ # # Update all books with 'Rails' in their title
104
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'"
105
+ #
106
+ # # Update all avatars migrated more than a week ago
107
+ # Avatar.update_all ['migrated_at = ?', Time.now.utc], ['migrated_at > ?', 1.week.ago]
108
+ #
109
+ # # Update all books that match our conditions, but limit it to 5 ordered by date
110
+ # Book.update_all "author = 'David'", "title LIKE '%Rails%'", :order => 'created_at', :limit => 5
111
+ def update_all(updates, conditions = nil, options = {})
112
+ if conditions || options.present?
113
+ where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates)
114
+ else
115
+ # Apply limit and order only if they're both present
116
+ if @limit_value.present? == @order_values.present?
117
+ arel.update(@klass.send(:sanitize_sql_for_assignment, updates))
118
+ else
119
+ except(:limit, :order).update_all(updates)
120
+ end
121
+ end
122
+ end
123
+
124
+ # Updates an object (or multiple objects) and saves it to the database, if validations pass.
125
+ # The resulting object is returned whether the object was saved successfully to the database or not.
126
+ #
127
+ # ==== Parameters
128
+ #
129
+ # * +id+ - This should be the id or an array of ids to be updated.
130
+ # * +attributes+ - This should be a hash of attributes to be set on the object, or an array of hashes.
131
+ #
132
+ # ==== Examples
133
+ #
134
+ # # Updating one record:
135
+ # Person.update(15, :user_name => 'Samuel', :group => 'expert')
136
+ #
137
+ # # Updating multiple records:
138
+ # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
139
+ # Person.update(people.keys, people.values)
140
+ def update(id, attributes)
141
+ if id.is_a?(Array)
142
+ idx = -1
143
+ id.collect { |one_id| idx += 1; update(one_id, attributes[idx]) }
144
+ else
145
+ object = find(id)
146
+ object.update_attributes(attributes)
147
+ object
148
+ end
149
+ end
150
+
151
+ # Destroys the records matching +conditions+ by instantiating each
152
+ # record and calling its +destroy+ method. Each object's callbacks are
153
+ # executed (including <tt>:dependent</tt> association options and
154
+ # +before_destroy+/+after_destroy+ Observer methods). Returns the
155
+ # collection of objects that were destroyed; each will be frozen, to
156
+ # reflect that no changes should be made (since they can't be
157
+ # persisted).
158
+ #
159
+ # Note: Instantiation, callback execution, and deletion of each
160
+ # record can be time consuming when you're removing many records at
161
+ # once. It generates at least one SQL +DELETE+ query per record (or
162
+ # possibly more, to enforce your callbacks). If you want to delete many
163
+ # rows quickly, without concern for their associations or callbacks, use
164
+ # +delete_all+ instead.
165
+ #
166
+ # ==== Parameters
167
+ #
168
+ # * +conditions+ - A string, array, or hash that specifies which records
169
+ # to destroy. If omitted, all records are destroyed. See the
170
+ # Conditions section in the introduction to ActiveRecord::Base for
171
+ # more information.
172
+ #
173
+ # ==== Examples
174
+ #
175
+ # Person.destroy_all("last_login < '2004-04-04'")
176
+ # Person.destroy_all(:status => "inactive")
177
+ def destroy_all(conditions = nil)
178
+ if conditions
179
+ where(conditions).destroy_all
180
+ else
181
+ to_a.each {|object| object.destroy}
182
+ reset
183
+ end
184
+ end
185
+
186
+ # Destroy an object (or multiple objects) that has the given id, the object is instantiated first,
187
+ # therefore all callbacks and filters are fired off before the object is deleted. This method is
188
+ # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
189
+ #
190
+ # This essentially finds the object (or multiple objects) with the given id, creates a new object
191
+ # from the attributes, and then calls destroy on it.
192
+ #
193
+ # ==== Parameters
194
+ #
195
+ # * +id+ - Can be either an Integer or an Array of Integers.
196
+ #
197
+ # ==== Examples
198
+ #
199
+ # # Destroy a single object
200
+ # Todo.destroy(1)
201
+ #
202
+ # # Destroy multiple objects
203
+ # todos = [1,2,3]
204
+ # Todo.destroy(todos)
205
+ def destroy(id)
206
+ if id.is_a?(Array)
207
+ id.map { |one_id| destroy(one_id) }
208
+ else
209
+ find(id).destroy
210
+ end
211
+ end
212
+
213
+ # Deletes the records matching +conditions+ without instantiating the records first, and hence not
214
+ # calling the +destroy+ method nor invoking callbacks. This is a single SQL DELETE statement that
215
+ # goes straight to the database, much more efficient than +destroy_all+. Be careful with relations
216
+ # though, in particular <tt>:dependent</tt> rules defined on associations are not honored. Returns
217
+ # the number of rows affected.
218
+ #
219
+ # ==== Parameters
220
+ #
221
+ # * +conditions+ - Conditions are specified the same way as with +find+ method.
222
+ #
223
+ # ==== Example
224
+ #
225
+ # Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
226
+ # Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
227
+ #
228
+ # Both calls delete the affected posts all at once with a single DELETE statement. If you need to destroy dependent
229
+ # associations or call your <tt>before_*</tt> or +after_destroy+ callbacks, use the +destroy_all+ method instead.
230
+ def delete_all(conditions = nil)
231
+ conditions ? where(conditions).delete_all : arel.delete.tap { reset }
232
+ end
233
+
234
+ # Deletes the row with a primary key matching the +id+ argument, using a
235
+ # SQL +DELETE+ statement, and returns the number of rows deleted. Active
236
+ # Record objects are not instantiated, so the object's callbacks are not
237
+ # executed, including any <tt>:dependent</tt> association options or
238
+ # Observer methods.
239
+ #
240
+ # You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
241
+ #
242
+ # Note: Although it is often much faster than the alternative,
243
+ # <tt>#destroy</tt>, skipping callbacks might bypass business logic in
244
+ # your application that ensures referential integrity or performs other
245
+ # essential jobs.
246
+ #
247
+ # ==== Examples
248
+ #
249
+ # # Delete a single row
250
+ # Todo.delete(1)
251
+ #
252
+ # # Delete multiple rows
253
+ # Todo.delete([2,3,4])
254
+ def delete(id_or_array)
255
+ where(@klass.primary_key => id_or_array).delete_all
256
+ end
257
+
258
+ def loaded?
259
+ @loaded
260
+ end
261
+
262
+ def reload
263
+ reset
264
+ to_a # force reload
265
+ self
266
+ end
267
+
268
+ def reset
269
+ @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
270
+ @should_eager_load = @join_dependency = nil
271
+ @records = []
272
+ self
273
+ end
274
+
275
+ def primary_key
276
+ @primary_key ||= table[@klass.primary_key]
277
+ end
278
+
279
+ def to_sql
280
+ @to_sql ||= arel.to_sql
281
+ end
282
+
283
+ def scope_for_create
284
+ @scope_for_create ||= begin
285
+ @create_with_value || @where_values.inject({}) do |hash, where|
286
+ if where.is_a?(Arel::Predicates::Equality)
287
+ hash[where.operand1.name] = where.operand2.respond_to?(:value) ? where.operand2.value : where.operand2
288
+ end
289
+
290
+ hash
291
+ end
292
+ end
293
+ end
294
+
295
+ def eager_loading?
296
+ @should_eager_load ||= (@eager_load_values.any? || (@includes_values.any? && references_eager_loaded_tables?))
297
+ end
298
+
299
+ protected
300
+
301
+ def method_missing(method, *args, &block)
302
+ if Array.method_defined?(method)
303
+ to_a.send(method, *args, &block)
304
+ elsif @klass.respond_to?(method)
305
+ @klass.send(:with_scope, self) { @klass.send(method, *args, &block) }
306
+ elsif arel.respond_to?(method)
307
+ arel.send(method, *args, &block)
308
+ elsif match = DynamicFinderMatch.match(method)
309
+ attributes = match.attribute_names
310
+ super unless @klass.send(:all_attributes_exists?, attributes)
311
+
312
+ if match.finder?
313
+ find_by_attributes(match, attributes, *args)
314
+ elsif match.instantiator?
315
+ find_or_instantiator_by_attributes(match, attributes, *args, &block)
316
+ end
317
+ else
318
+ super
319
+ end
320
+ end
321
+
322
+ private
323
+
324
+ def with_create_scope
325
+ @klass.send(:with_scope, :create => scope_for_create, :find => {}) { yield }
326
+ end
327
+
328
+ def references_eager_loaded_tables?
329
+ joined_tables = (tables_in_string(arel.joins(arel)) + [table.name, table.table_alias]).compact.uniq
330
+ (tables_in_string(to_sql) - joined_tables).any?
331
+ end
332
+
333
+ def tables_in_string(string)
334
+ return [] if string.blank?
335
+ string.scan(/([a-zA-Z_][\.\w]+).?\./).flatten.uniq
336
+ end
337
+
338
+ end
339
+ end
@@ -0,0 +1,259 @@
1
+ module ActiveRecord
2
+ module Calculations
3
+ # Count operates using three different approaches.
4
+ #
5
+ # * Count all: By not passing any parameters to count, it will return a count of all the rows for the model.
6
+ # * Count using column: By passing a column name to count, it will return a count of all the rows for the model with supplied column present
7
+ # * Count using options will find the row count matched by the options used.
8
+ #
9
+ # The third approach, count using options, accepts an option hash as the only parameter. The options are:
10
+ #
11
+ # * <tt>:conditions</tt>: An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
12
+ # * <tt>:joins</tt>: Either an SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id" (rarely needed)
13
+ # or named associations in the same form used for the <tt>:include</tt> option, which will perform an INNER JOIN on the associated table(s).
14
+ # If the value is a string, then the records will be returned read-only since they will have attributes that do not correspond to the table's columns.
15
+ # Pass <tt>:readonly => false</tt> to override.
16
+ # * <tt>:include</tt>: Named associations that should be loaded alongside using LEFT OUTER JOINs. The symbols named refer
17
+ # to already defined associations. When using named associations, count returns the number of DISTINCT items for the model you're counting.
18
+ # See eager loading under Associations.
19
+ # * <tt>:order</tt>: An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
20
+ # * <tt>:group</tt>: An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
21
+ # * <tt>:select</tt>: By default, this is * as in SELECT * FROM, but can be changed if you, for example, want to do a join but not
22
+ # include the joined columns.
23
+ # * <tt>:distinct</tt>: Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
24
+ # * <tt>:from</tt> - By default, this is the table name of the class, but can be changed to an alternate table name (or even the name
25
+ # of a database view).
26
+ #
27
+ # Examples for counting all:
28
+ # Person.count # returns the total count of all people
29
+ #
30
+ # Examples for counting by column:
31
+ # Person.count(:age) # returns the total count of all people whose age is present in database
32
+ #
33
+ # Examples for count with options:
34
+ # Person.count(:conditions => "age > 26")
35
+ # Person.count(:conditions => "age > 26 AND job.salary > 60000", :include => :job) # because of the named association, it finds the DISTINCT count using LEFT OUTER JOIN.
36
+ # Person.count(:conditions => "age > 26 AND job.salary > 60000", :joins => "LEFT JOIN jobs on jobs.person_id = person.id") # finds the number of rows matching the conditions and joins.
37
+ # Person.count('id', :conditions => "age > 26") # Performs a COUNT(id)
38
+ # Person.count(:all, :conditions => "age > 26") # Performs a COUNT(*) (:all is an alias for '*')
39
+ #
40
+ # Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead.
41
+ def count(column_name = nil, options = {})
42
+ column_name, options = nil, column_name if column_name.is_a?(Hash)
43
+ calculate(:count, column_name, options)
44
+ end
45
+
46
+ # Calculates the average value on a given column. The value is returned as
47
+ # a float, or +nil+ if there's no row. See +calculate+ for examples with
48
+ # options.
49
+ #
50
+ # Person.average('age') # => 35.8
51
+ def average(column_name, options = {})
52
+ calculate(:average, column_name, options)
53
+ end
54
+
55
+ # Calculates the minimum value on a given column. The value is returned
56
+ # with the same data type of the column, or +nil+ if there's no row. See
57
+ # +calculate+ for examples with options.
58
+ #
59
+ # Person.minimum('age') # => 7
60
+ def minimum(column_name, options = {})
61
+ calculate(:minimum, column_name, options)
62
+ end
63
+
64
+ # Calculates the maximum value on a given column. The value is returned
65
+ # with the same data type of the column, or +nil+ if there's no row. See
66
+ # +calculate+ for examples with options.
67
+ #
68
+ # Person.maximum('age') # => 93
69
+ def maximum(column_name, options = {})
70
+ calculate(:maximum, column_name, options)
71
+ end
72
+
73
+ # Calculates the sum of values on a given column. The value is returned
74
+ # with the same data type of the column, 0 if there's no row. See
75
+ # +calculate+ for examples with options.
76
+ #
77
+ # Person.sum('age') # => 4562
78
+ def sum(column_name, options = {})
79
+ calculate(:sum, column_name, options)
80
+ end
81
+
82
+ # This calculates aggregate values in the given column. Methods for count, sum, average, minimum, and maximum have been added as shortcuts.
83
+ # Options such as <tt>:conditions</tt>, <tt>:order</tt>, <tt>:group</tt>, <tt>:having</tt>, and <tt>:joins</tt> can be passed to customize the query.
84
+ #
85
+ # There are two basic forms of output:
86
+ # * Single aggregate value: The single value is type cast to Fixnum for COUNT, Float for AVG, and the given column's type for everything else.
87
+ # * Grouped values: This returns an ordered hash of the values and groups them by the <tt>:group</tt> option. It takes either a column name, or the name
88
+ # of a belongs_to association.
89
+ #
90
+ # values = Person.maximum(:age, :group => 'last_name')
91
+ # puts values["Drake"]
92
+ # => 43
93
+ #
94
+ # drake = Family.find_by_last_name('Drake')
95
+ # values = Person.maximum(:age, :group => :family) # Person belongs_to :family
96
+ # puts values[drake]
97
+ # => 43
98
+ #
99
+ # values.each do |family, max_age|
100
+ # ...
101
+ # end
102
+ #
103
+ # Options:
104
+ # * <tt>:conditions</tt> - An SQL fragment like "administrator = 1" or [ "user_name = ?", username ]. See conditions in the intro to ActiveRecord::Base.
105
+ # * <tt>:include</tt>: Eager loading, see Associations for details. Since calculations don't load anything, the purpose of this is to access fields on joined tables in your conditions, order, or group clauses.
106
+ # * <tt>:joins</tt> - An SQL fragment for additional joins like "LEFT JOIN comments ON comments.post_id = id". (Rarely needed).
107
+ # The records will be returned read-only since they will have attributes that do not correspond to the table's columns.
108
+ # * <tt>:order</tt> - An SQL fragment like "created_at DESC, name" (really only used with GROUP BY calculations).
109
+ # * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the GROUP BY SQL-clause.
110
+ # * <tt>:select</tt> - By default, this is * as in SELECT * FROM, but can be changed if you for example want to do a join, but not
111
+ # include the joined columns.
112
+ # * <tt>:distinct</tt> - Set this to true to make this a distinct calculation, such as SELECT COUNT(DISTINCT posts.id) ...
113
+ #
114
+ # Examples:
115
+ # Person.calculate(:count, :all) # The same as Person.count
116
+ # Person.average(:age) # SELECT AVG(age) FROM people...
117
+ # Person.minimum(:age, :conditions => ['last_name != ?', 'Drake']) # Selects the minimum age for everyone with a last name other than 'Drake'
118
+ # Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
119
+ # Person.sum("2 * age")
120
+ def calculate(operation, column_name, options = {})
121
+ if options.except(:distinct).present?
122
+ apply_finder_options(options.except(:distinct)).calculate(operation, column_name, :distinct => options[:distinct])
123
+ else
124
+ if eager_loading? || includes_values.present?
125
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
126
+ else
127
+ perform_calculation(operation, column_name, options)
128
+ end
129
+ end
130
+ rescue ThrowResult
131
+ 0
132
+ end
133
+
134
+ private
135
+
136
+ def perform_calculation(operation, column_name, options = {})
137
+ operation = operation.to_s.downcase
138
+
139
+ if operation == "count"
140
+ column_name ||= (select_for_count || :all)
141
+
142
+ joins = arel.joins(arel)
143
+ if joins.present? && joins =~ /LEFT OUTER/i
144
+ distinct = true
145
+ column_name = @klass.primary_key if column_name == :all
146
+ end
147
+
148
+ distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
149
+ distinct ||= options[:distinct]
150
+ else
151
+ distinct = nil
152
+ end
153
+
154
+ distinct = options[:distinct] || distinct
155
+ column_name = :all if column_name.blank? && operation == "count"
156
+
157
+ if @group_values.any?
158
+ return execute_grouped_calculation(operation, column_name)
159
+ else
160
+ return execute_simple_calculation(operation, column_name, distinct)
161
+ end
162
+ end
163
+
164
+ def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
165
+ column = if @klass.column_names.include?(column_name.to_s)
166
+ Arel::Attribute.new(@klass.unscoped, column_name)
167
+ else
168
+ Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
169
+ end
170
+
171
+ # Postgresql doesn't like ORDER BY when there are no GROUP BY
172
+ relation = except(:order).select(operation == 'count' ? column.count(distinct) : column.send(operation))
173
+ type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
174
+ end
175
+
176
+ def execute_grouped_calculation(operation, column_name) #:nodoc:
177
+ group_attr = @group_values.first
178
+ association = @klass.reflect_on_association(group_attr.to_sym)
179
+ associated = association && association.macro == :belongs_to # only count belongs_to associations
180
+ group_field = associated ? association.primary_key_name : group_attr
181
+ group_alias = column_alias_for(group_field)
182
+ group_column = column_for(group_field)
183
+
184
+ group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field
185
+
186
+ aggregate_alias = column_alias_for(operation, column_name)
187
+
188
+ select_statement = if operation == 'count' && column_name == :all
189
+ "COUNT(*) AS count_all"
190
+ else
191
+ Arel::Attribute.new(@klass.unscoped, column_name).send(operation).as(aggregate_alias).to_sql
192
+ end
193
+
194
+ select_statement << ", #{group_field} AS #{group_alias}"
195
+
196
+ relation = select(select_statement).group(group)
197
+
198
+ calculated_data = @klass.connection.select_all(relation.to_sql)
199
+
200
+ if association
201
+ key_ids = calculated_data.collect { |row| row[group_alias] }
202
+ key_records = association.klass.base_class.find(key_ids)
203
+ key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
204
+ end
205
+
206
+ calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
207
+ key = type_cast_calculated_value(row[group_alias], group_column)
208
+ key = key_records[key] if associated
209
+ value = row[aggregate_alias]
210
+ all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
211
+ all
212
+ end
213
+ end
214
+
215
+ # Converts the given keys to the value that the database adapter returns as
216
+ # a usable column name:
217
+ #
218
+ # column_alias_for("users.id") # => "users_id"
219
+ # column_alias_for("sum(id)") # => "sum_id"
220
+ # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
221
+ # column_alias_for("count(*)") # => "count_all"
222
+ # column_alias_for("count", "id") # => "count_id"
223
+ def column_alias_for(*keys)
224
+ table_name = keys.join(' ')
225
+ table_name.downcase!
226
+ table_name.gsub!(/\*/, 'all')
227
+ table_name.gsub!(/\W+/, ' ')
228
+ table_name.strip!
229
+ table_name.gsub!(/ +/, '_')
230
+
231
+ @klass.connection.table_alias_for(table_name)
232
+ end
233
+
234
+ def column_for(field)
235
+ field_name = field.to_s.split('.').last
236
+ @klass.columns.detect { |c| c.name.to_s == field_name }
237
+ end
238
+
239
+ def type_cast_calculated_value(value, column, operation = nil)
240
+ case operation
241
+ when 'count' then value.to_i
242
+ when 'sum' then type_cast_using_column(value || '0', column)
243
+ when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
244
+ else type_cast_using_column(value, column)
245
+ end
246
+ end
247
+
248
+ def type_cast_using_column(value, column)
249
+ column ? column.type_cast(value) : value
250
+ end
251
+
252
+ def select_for_count
253
+ if @select_values.present?
254
+ select = @select_values.join(", ")
255
+ select if select !~ /(,|\*)/
256
+ end
257
+ end
258
+ end
259
+ end