activerecord 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (311) hide show
  1. data/CHANGELOG +4928 -3
  2. data/README +45 -46
  3. data/RUNNING_UNIT_TESTS +8 -11
  4. data/Rakefile +247 -0
  5. data/install.rb +8 -38
  6. data/lib/active_record/aggregations.rb +64 -49
  7. data/lib/active_record/associations/association_collection.rb +217 -47
  8. data/lib/active_record/associations/association_proxy.rb +159 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +56 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +155 -37
  12. data/lib/active_record/associations/has_many_association.rb +145 -75
  13. data/lib/active_record/associations/has_many_through_association.rb +283 -0
  14. data/lib/active_record/associations/has_one_association.rb +96 -0
  15. data/lib/active_record/associations.rb +1537 -304
  16. data/lib/active_record/attribute_methods.rb +328 -0
  17. data/lib/active_record/base.rb +2001 -588
  18. data/lib/active_record/calculations.rb +269 -0
  19. data/lib/active_record/callbacks.rb +169 -165
  20. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +308 -0
  21. data/lib/active_record/connection_adapters/abstract/database_statements.rb +171 -0
  22. data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
  23. data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
  24. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +472 -0
  25. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +306 -0
  26. data/lib/active_record/connection_adapters/abstract_adapter.rb +125 -279
  27. data/lib/active_record/connection_adapters/mysql_adapter.rb +442 -77
  28. data/lib/active_record/connection_adapters/postgresql_adapter.rb +805 -135
  29. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  30. data/lib/active_record/connection_adapters/sqlite_adapter.rb +353 -69
  31. data/lib/active_record/fixtures.rb +946 -100
  32. data/lib/active_record/locking/optimistic.rb +144 -0
  33. data/lib/active_record/locking/pessimistic.rb +77 -0
  34. data/lib/active_record/migration.rb +417 -0
  35. data/lib/active_record/observer.rb +142 -32
  36. data/lib/active_record/query_cache.rb +23 -0
  37. data/lib/active_record/reflection.rb +163 -70
  38. data/lib/active_record/schema.rb +58 -0
  39. data/lib/active_record/schema_dumper.rb +171 -0
  40. data/lib/active_record/serialization.rb +98 -0
  41. data/lib/active_record/serializers/json_serializer.rb +71 -0
  42. data/lib/active_record/serializers/xml_serializer.rb +315 -0
  43. data/lib/active_record/timestamp.rb +41 -0
  44. data/lib/active_record/transactions.rb +87 -57
  45. data/lib/active_record/validations.rb +909 -122
  46. data/lib/active_record/vendor/db2.rb +362 -0
  47. data/lib/active_record/vendor/mysql.rb +126 -29
  48. data/lib/active_record/version.rb +9 -0
  49. data/lib/active_record.rb +35 -7
  50. data/lib/activerecord.rb +1 -0
  51. data/test/aaa_create_tables_test.rb +72 -0
  52. data/test/abstract_unit.rb +73 -5
  53. data/test/active_schema_test_mysql.rb +43 -0
  54. data/test/adapter_test.rb +105 -0
  55. data/test/adapter_test_sqlserver.rb +95 -0
  56. data/test/aggregations_test.rb +110 -16
  57. data/test/all.sh +2 -2
  58. data/test/ar_schema_test.rb +33 -0
  59. data/test/association_inheritance_reload.rb +14 -0
  60. data/test/associations/ar_joins_test.rb +0 -0
  61. data/test/associations/callbacks_test.rb +147 -0
  62. data/test/associations/cascaded_eager_loading_test.rb +110 -0
  63. data/test/associations/eager_singularization_test.rb +145 -0
  64. data/test/associations/eager_test.rb +442 -0
  65. data/test/associations/extension_test.rb +47 -0
  66. data/test/associations/inner_join_association_test.rb +88 -0
  67. data/test/associations/join_model_test.rb +553 -0
  68. data/test/associations_test.rb +1930 -267
  69. data/test/attribute_methods_test.rb +146 -0
  70. data/test/base_test.rb +1316 -84
  71. data/test/binary_test.rb +32 -0
  72. data/test/calculations_test.rb +251 -0
  73. data/test/callbacks_test.rb +400 -0
  74. data/test/class_inheritable_attributes_test.rb +3 -4
  75. data/test/column_alias_test.rb +17 -0
  76. data/test/connection_test_firebird.rb +8 -0
  77. data/test/connection_test_mysql.rb +30 -0
  78. data/test/connections/native_db2/connection.rb +25 -0
  79. data/test/connections/native_firebird/connection.rb +26 -0
  80. data/test/connections/native_frontbase/connection.rb +27 -0
  81. data/test/connections/native_mysql/connection.rb +21 -18
  82. data/test/connections/native_openbase/connection.rb +21 -0
  83. data/test/connections/native_oracle/connection.rb +27 -0
  84. data/test/connections/native_postgresql/connection.rb +17 -18
  85. data/test/connections/native_sqlite/connection.rb +17 -16
  86. data/test/connections/native_sqlite3/connection.rb +25 -0
  87. data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
  88. data/test/connections/native_sybase/connection.rb +23 -0
  89. data/test/copy_table_test_sqlite.rb +69 -0
  90. data/test/datatype_test_postgresql.rb +203 -0
  91. data/test/date_time_test.rb +37 -0
  92. data/test/default_test_firebird.rb +16 -0
  93. data/test/defaults_test.rb +67 -0
  94. data/test/deprecated_finder_test.rb +30 -0
  95. data/test/finder_test.rb +607 -32
  96. data/test/fixtures/accounts.yml +28 -0
  97. data/test/fixtures/all/developers.yml +0 -0
  98. data/test/fixtures/all/people.csv +0 -0
  99. data/test/fixtures/all/tasks.yml +0 -0
  100. data/test/fixtures/author.rb +107 -0
  101. data/test/fixtures/author_favorites.yml +4 -0
  102. data/test/fixtures/authors.yml +7 -0
  103. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
  104. data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
  105. data/test/fixtures/bad_fixtures/blank_line +3 -0
  106. data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
  107. data/test/fixtures/bad_fixtures/missing_value +1 -0
  108. data/test/fixtures/binaries.yml +132 -0
  109. data/test/fixtures/binary.rb +2 -0
  110. data/test/fixtures/book.rb +4 -0
  111. data/test/fixtures/books.yml +7 -0
  112. data/test/fixtures/categories/special_categories.yml +9 -0
  113. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  114. data/test/fixtures/categories.yml +14 -0
  115. data/test/fixtures/categories_ordered.yml +7 -0
  116. data/test/fixtures/categories_posts.yml +23 -0
  117. data/test/fixtures/categorization.rb +5 -0
  118. data/test/fixtures/categorizations.yml +17 -0
  119. data/test/fixtures/category.rb +26 -0
  120. data/test/fixtures/citation.rb +6 -0
  121. data/test/fixtures/comment.rb +23 -0
  122. data/test/fixtures/comments.yml +59 -0
  123. data/test/fixtures/companies.yml +55 -0
  124. data/test/fixtures/company.rb +81 -4
  125. data/test/fixtures/company_in_module.rb +32 -6
  126. data/test/fixtures/computer.rb +4 -0
  127. data/test/fixtures/computers.yml +4 -0
  128. data/test/fixtures/contact.rb +16 -0
  129. data/test/fixtures/courses.yml +7 -0
  130. data/test/fixtures/customer.rb +28 -3
  131. data/test/fixtures/customers.yml +17 -0
  132. data/test/fixtures/db_definitions/db2.drop.sql +33 -0
  133. data/test/fixtures/db_definitions/db2.sql +235 -0
  134. data/test/fixtures/db_definitions/db22.drop.sql +2 -0
  135. data/test/fixtures/db_definitions/db22.sql +5 -0
  136. data/test/fixtures/db_definitions/firebird.drop.sql +65 -0
  137. data/test/fixtures/db_definitions/firebird.sql +310 -0
  138. data/test/fixtures/db_definitions/firebird2.drop.sql +2 -0
  139. data/test/fixtures/db_definitions/firebird2.sql +6 -0
  140. data/test/fixtures/db_definitions/frontbase.drop.sql +33 -0
  141. data/test/fixtures/db_definitions/frontbase.sql +273 -0
  142. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  143. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  144. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  145. data/test/fixtures/db_definitions/openbase.sql +318 -0
  146. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  147. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  148. data/test/fixtures/db_definitions/oracle.drop.sql +67 -0
  149. data/test/fixtures/db_definitions/oracle.sql +330 -0
  150. data/test/fixtures/db_definitions/oracle2.drop.sql +2 -0
  151. data/test/fixtures/db_definitions/oracle2.sql +6 -0
  152. data/test/fixtures/db_definitions/postgresql.drop.sql +44 -0
  153. data/test/fixtures/db_definitions/postgresql.sql +217 -38
  154. data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
  155. data/test/fixtures/db_definitions/postgresql2.sql +2 -2
  156. data/test/fixtures/db_definitions/schema.rb +354 -0
  157. data/test/fixtures/db_definitions/schema2.rb +11 -0
  158. data/test/fixtures/db_definitions/sqlite.drop.sql +33 -0
  159. data/test/fixtures/db_definitions/sqlite.sql +139 -5
  160. data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
  161. data/test/fixtures/db_definitions/sqlite2.sql +1 -0
  162. data/test/fixtures/db_definitions/sybase.drop.sql +35 -0
  163. data/test/fixtures/db_definitions/sybase.sql +222 -0
  164. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  165. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  166. data/test/fixtures/developer.rb +70 -6
  167. data/test/fixtures/developers.yml +21 -0
  168. data/test/fixtures/developers_projects/david_action_controller +2 -1
  169. data/test/fixtures/developers_projects/david_active_record +2 -1
  170. data/test/fixtures/developers_projects.yml +17 -0
  171. data/test/fixtures/edge.rb +5 -0
  172. data/test/fixtures/edges.yml +6 -0
  173. data/test/fixtures/entrants.yml +14 -0
  174. data/test/fixtures/example.log +1 -0
  175. data/test/fixtures/fk_test_has_fk.yml +3 -0
  176. data/test/fixtures/fk_test_has_pk.yml +2 -0
  177. data/test/fixtures/flowers.jpg +0 -0
  178. data/test/fixtures/funny_jokes.yml +10 -0
  179. data/test/fixtures/item.rb +7 -0
  180. data/test/fixtures/items.yml +4 -0
  181. data/test/fixtures/joke.rb +3 -0
  182. data/test/fixtures/keyboard.rb +3 -0
  183. data/test/fixtures/legacy_thing.rb +3 -0
  184. data/test/fixtures/legacy_things.yml +3 -0
  185. data/test/fixtures/matey.rb +4 -0
  186. data/test/fixtures/mateys.yml +4 -0
  187. data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
  188. data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
  189. data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
  190. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  191. data/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb +9 -0
  192. data/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb +12 -0
  193. data/test/fixtures/migrations_with_duplicate/3_foo.rb +7 -0
  194. data/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb +12 -0
  195. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  196. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  197. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  198. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  199. data/test/fixtures/minimalistic.rb +2 -0
  200. data/test/fixtures/minimalistics.yml +2 -0
  201. data/test/fixtures/mixed_case_monkey.rb +3 -0
  202. data/test/fixtures/mixed_case_monkeys.yml +6 -0
  203. data/test/fixtures/mixins.yml +29 -0
  204. data/test/fixtures/movies.yml +7 -0
  205. data/test/fixtures/naked/csv/accounts.csv +1 -0
  206. data/test/fixtures/naked/yml/accounts.yml +1 -0
  207. data/test/fixtures/naked/yml/companies.yml +1 -0
  208. data/test/fixtures/naked/yml/courses.yml +1 -0
  209. data/test/fixtures/order.rb +4 -0
  210. data/test/fixtures/parrot.rb +13 -0
  211. data/test/fixtures/parrots.yml +27 -0
  212. data/test/fixtures/parrots_pirates.yml +7 -0
  213. data/test/fixtures/people.yml +3 -0
  214. data/test/fixtures/person.rb +4 -0
  215. data/test/fixtures/pirate.rb +5 -0
  216. data/test/fixtures/pirates.yml +9 -0
  217. data/test/fixtures/post.rb +59 -0
  218. data/test/fixtures/posts.yml +48 -0
  219. data/test/fixtures/project.rb +27 -2
  220. data/test/fixtures/projects.yml +7 -0
  221. data/test/fixtures/reader.rb +4 -0
  222. data/test/fixtures/readers.yml +4 -0
  223. data/test/fixtures/reply.rb +18 -2
  224. data/test/fixtures/reserved_words/distinct.yml +5 -0
  225. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  226. data/test/fixtures/reserved_words/group.yml +14 -0
  227. data/test/fixtures/reserved_words/select.yml +8 -0
  228. data/test/fixtures/reserved_words/values.yml +7 -0
  229. data/test/fixtures/ship.rb +3 -0
  230. data/test/fixtures/ships.yml +5 -0
  231. data/test/fixtures/subject.rb +4 -0
  232. data/test/fixtures/subscriber.rb +4 -3
  233. data/test/fixtures/tag.rb +7 -0
  234. data/test/fixtures/tagging.rb +10 -0
  235. data/test/fixtures/taggings.yml +25 -0
  236. data/test/fixtures/tags.yml +7 -0
  237. data/test/fixtures/task.rb +3 -0
  238. data/test/fixtures/tasks.yml +7 -0
  239. data/test/fixtures/topic.rb +20 -3
  240. data/test/fixtures/topics.yml +22 -0
  241. data/test/fixtures/treasure.rb +4 -0
  242. data/test/fixtures/treasures.yml +10 -0
  243. data/test/fixtures/vertex.rb +9 -0
  244. data/test/fixtures/vertices.yml +4 -0
  245. data/test/fixtures_test.rb +574 -8
  246. data/test/inheritance_test.rb +113 -27
  247. data/test/json_serialization_test.rb +180 -0
  248. data/test/lifecycle_test.rb +56 -29
  249. data/test/locking_test.rb +273 -0
  250. data/test/method_scoping_test.rb +416 -0
  251. data/test/migration_test.rb +933 -0
  252. data/test/migration_test_firebird.rb +124 -0
  253. data/test/mixin_test.rb +95 -0
  254. data/test/modules_test.rb +23 -10
  255. data/test/multiple_db_test.rb +17 -3
  256. data/test/pk_test.rb +59 -15
  257. data/test/query_cache_test.rb +104 -0
  258. data/test/readonly_test.rb +107 -0
  259. data/test/reflection_test.rb +124 -27
  260. data/test/reserved_word_test_mysql.rb +177 -0
  261. data/test/schema_authorization_test_postgresql.rb +75 -0
  262. data/test/schema_dumper_test.rb +131 -0
  263. data/test/schema_test_postgresql.rb +64 -0
  264. data/test/serialization_test.rb +47 -0
  265. data/test/synonym_test_oracle.rb +17 -0
  266. data/test/table_name_test_sqlserver.rb +23 -0
  267. data/test/threaded_connections_test.rb +48 -0
  268. data/test/transactions_test.rb +227 -29
  269. data/test/unconnected_test.rb +14 -6
  270. data/test/validations_test.rb +1293 -32
  271. data/test/xml_serialization_test.rb +202 -0
  272. metadata +347 -143
  273. data/dev-utils/eval_debugger.rb +0 -9
  274. data/examples/associations.rb +0 -87
  275. data/examples/shared_setup.rb +0 -15
  276. data/examples/validation.rb +0 -88
  277. data/lib/active_record/deprecated_associations.rb +0 -70
  278. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  279. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  280. data/lib/active_record/support/clean_logger.rb +0 -10
  281. data/lib/active_record/support/inflector.rb +0 -70
  282. data/lib/active_record/vendor/simple.rb +0 -702
  283. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  284. data/lib/active_record/wrappings.rb +0 -59
  285. data/rakefile +0 -122
  286. data/test/deprecated_associations_test.rb +0 -336
  287. data/test/fixtures/accounts/signals37 +0 -3
  288. data/test/fixtures/accounts/unknown +0 -2
  289. data/test/fixtures/companies/first_client +0 -6
  290. data/test/fixtures/companies/first_firm +0 -4
  291. data/test/fixtures/companies/second_client +0 -6
  292. data/test/fixtures/courses/java +0 -2
  293. data/test/fixtures/courses/ruby +0 -2
  294. data/test/fixtures/customers/david +0 -6
  295. data/test/fixtures/db_definitions/mysql.sql +0 -96
  296. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  297. data/test/fixtures/developers/david +0 -2
  298. data/test/fixtures/developers/jamis +0 -2
  299. data/test/fixtures/entrants/first +0 -3
  300. data/test/fixtures/entrants/second +0 -3
  301. data/test/fixtures/entrants/third +0 -3
  302. data/test/fixtures/fixture_database.sqlite +0 -0
  303. data/test/fixtures/fixture_database_2.sqlite +0 -0
  304. data/test/fixtures/movies/first +0 -2
  305. data/test/fixtures/movies/second +0 -2
  306. data/test/fixtures/projects/action_controller +0 -2
  307. data/test/fixtures/projects/active_record +0 -2
  308. data/test/fixtures/topics/first +0 -9
  309. data/test/fixtures/topics/second +0 -8
  310. data/test/inflector_test.rb +0 -104
  311. data/test/thread_safety_test.rb +0 -33
@@ -1,14 +1,19 @@
1
1
  module ActiveRecord
2
2
  module Aggregations # :nodoc:
3
- def self.append_features(base)
4
- super
3
+ def self.included(base)
5
4
  base.extend(ClassMethods)
6
5
  end
7
6
 
7
+ def clear_aggregation_cache #:nodoc:
8
+ self.class.reflect_on_all_aggregations.to_a.each do |assoc|
9
+ instance_variable_set "@#{assoc.name}", nil
10
+ end unless self.new_record?
11
+ end
12
+
8
13
  # Active Record implements aggregation through a macro-like class method called +composed_of+ for representing attributes
9
14
  # as value objects. It expresses relationships like "Account [is] composed of Money [among other things]" or "Person [is]
10
- # composed of [an] address". Each call to the macro adds a description on how the value objects are created from the
11
- # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing)
15
+ # composed of [an] address". Each call to the macro adds a description of how the value objects are created from the
16
+ # attributes of the entity object (when the entity is initialized either as a new object or from finding an existing object)
12
17
  # and how it can be turned back into attributes (when the entity is saved to the database). Example:
13
18
  #
14
19
  # class Customer < ActiveRecord::Base
@@ -42,7 +47,7 @@ module ActiveRecord
42
47
  #
43
48
  # def <=>(other_money)
44
49
  # if currency == other_money.currency
45
- # among <=> amount
50
+ # amount <=> amount
46
51
  # else
47
52
  # amount <=> other_money.exchange_to(currency).amount
48
53
  # end
@@ -65,7 +70,7 @@ module ActiveRecord
65
70
  # end
66
71
  #
67
72
  # Now it's possible to access attributes from the database through the value objects instead. If you choose to name the
68
- # composition the same as the attributes name, it will be the only way to access that attribute. That's the case with our
73
+ # composition the same as the attribute's name, it will be the only way to access that attribute. That's the case with our
69
74
  # +balance+ attribute. You interact with the value objects just like you would any other attribute, though:
70
75
  #
71
76
  # customer.balance = Money.new(20) # sets the Money value object and the attribute
@@ -87,78 +92,88 @@ module ActiveRecord
87
92
  #
88
93
  # == Writing value objects
89
94
  #
90
- # Value objects are immutable and interchangeable objects that represent a given value, such as a Money object representing
91
- # $5. Two Money objects both representing $5 should be equal (through methods such == and <=> from Comparable if ranking makes
92
- # sense). This is unlike a entity objects where equality is determined by identity. An entity class such as Customer can
95
+ # Value objects are immutable and interchangeable objects that represent a given value, such as a +Money+ object representing
96
+ # $5. Two +Money+ objects both representing $5 should be equal (through methods such as == and <=> from +Comparable+ if ranking
97
+ # makes sense). This is unlike entity objects where equality is determined by identity. An entity class such as +Customer+ can
93
98
  # easily have two different objects that both have an address on Hyancintvej. Entity identity is determined by object or
94
- # relational unique identifiers (such as primary keys). Normal ActiveRecord::Base classes are entity objects.
99
+ # relational unique identifiers (such as primary keys). Normal <tt>ActiveRecord::Base</tt> classes are entity objects.
95
100
  #
96
- # It's also important to treat the value objects as immutable. Don't allow the Money object to have its amount changed after
97
- # creation. Create a new money object with the new value instead. This is examplified by the Money#exchanged_to method that
101
+ # It's also important to treat the value objects as immutable. Don't allow the +Money+ object to have its amount changed after
102
+ # creation. Create a new +Money+ object with the new value instead. This is exemplified by the <tt>Money#exchanged_to</tt> method that
98
103
  # returns a new value object instead of changing its own values. Active Record won't persist value objects that have been
99
- # changed through other means than the writer method.
104
+ # changed through means other than the writer method.
100
105
  #
101
106
  # The immutable requirement is enforced by Active Record by freezing any object assigned as a value object. Attempting to
102
- # change it afterwards will result in a TypeError.
107
+ # change it afterwards will result in a <tt>TypeError</tt>.
103
108
  #
104
109
  # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not keeping value objects
105
110
  # immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
106
111
  module ClassMethods
107
- # Adds the a reader and writer method for manipulating a value object, so
108
- # <tt>composed_of :address</tt> would add <tt>address</tt> and <tt>address=(new_address)</tt>.
112
+ # Adds reader and writer methods for manipulating a value object:
113
+ # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods.
109
114
  #
110
115
  # Options are:
111
- # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be infered
116
+ # * <tt>:class_name</tt> - specify the class name of the association. Use it only if that name can't be inferred
112
117
  # from the part id. So <tt>composed_of :address</tt> will by default be linked to the +Address+ class, but
113
118
  # if the real class name is +CompanyAddress+, you'll have to specify it with this option.
114
119
  # * <tt>:mapping</tt> - specifies a number of mapping arrays (attribute, parameter) that bind an attribute name
115
120
  # to a constructor parameter on the value class.
121
+ # * <tt>:allow_nil</tt> - specifies that the aggregate object will not be instantiated when all mapped
122
+ # attributes are +nil+. Setting the aggregate class to +nil+ has the effect of writing +nil+ to all mapped attributes.
123
+ # This defaults to +false+.
124
+ #
125
+ # An optional block can be passed to convert the argument that is passed to the writer method into an instance of
126
+ # <tt>:class_name</tt>. The block will only be called if the argument is not already an instance of <tt>:class_name</tt>.
116
127
  #
117
128
  # Option examples:
118
129
  # composed_of :temperature, :mapping => %w(reading celsius)
119
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
130
+ # composed_of(:balance, :class_name => "Money", :mapping => %w(balance amount)) {|balance| balance.to_money }
120
131
  # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
121
- def composed_of(part_id, options = {})
122
- validate_options([ :class_name, :mapping ], options.keys)
132
+ # composed_of :gps_location
133
+ # composed_of :gps_location, :allow_nil => true
134
+ #
135
+ def composed_of(part_id, options = {}, &block)
136
+ options.assert_valid_keys(:class_name, :mapping, :allow_nil)
123
137
 
124
138
  name = part_id.id2name
125
- class_name = options[:class_name] || name_to_class_name(name)
126
- mapping = options[:mapping]
139
+ class_name = options[:class_name] || name.camelize
140
+ mapping = options[:mapping] || [ name, name ]
141
+ mapping = [ mapping ] unless mapping.first.is_a?(Array)
142
+ allow_nil = options[:allow_nil] || false
127
143
 
128
- reader_method(name, class_name, mapping)
129
- writer_method(name, class_name, mapping)
144
+ reader_method(name, class_name, mapping, allow_nil)
145
+ writer_method(name, class_name, mapping, allow_nil, block)
146
+
147
+ create_reflection(:composed_of, part_id, options, self)
130
148
  end
131
149
 
132
150
  private
133
- # Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
134
- def validate_options(valid_option_keys, supplied_option_keys)
135
- unknown_option_keys = supplied_option_keys - valid_option_keys
136
- raise(ActiveRecordError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
137
- end
151
+ def reader_method(name, class_name, mapping, allow_nil)
152
+ module_eval do
153
+ define_method(name) do |*args|
154
+ force_reload = args.first || false
155
+ if (instance_variable_get("@#{name}").nil? || force_reload) && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
156
+ instance_variable_set("@#{name}", class_name.constantize.new(*mapping.collect {|pair| read_attribute(pair.first)}))
157
+ end
158
+ return instance_variable_get("@#{name}")
159
+ end
160
+ end
138
161
 
139
- def name_to_class_name(name)
140
- name.capitalize.gsub(/_(.)/) { |s| $1.capitalize }
141
162
  end
142
-
143
- def reader_method(name, class_name, mapping)
144
- module_eval <<-end_eval
145
- def #{name}(force_reload = false)
146
- if @#{name}.nil? || force_reload
147
- @#{name} = #{class_name}.new(#{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "read_attribute(\"#{pair.first}\")"}.join(", ")})
163
+
164
+ def writer_method(name, class_name, mapping, allow_nil, conversion)
165
+ module_eval do
166
+ define_method("#{name}=") do |part|
167
+ if part.nil? && allow_nil
168
+ mapping.each { |pair| @attributes[pair.first] = nil }
169
+ instance_variable_set("@#{name}", nil)
170
+ else
171
+ part = conversion.call(part) unless part.is_a?(class_name.constantize) || conversion.nil?
172
+ mapping.each { |pair| @attributes[pair.first] = part.send(pair.last) }
173
+ instance_variable_set("@#{name}", part.freeze)
148
174
  end
149
-
150
- return @#{name}
151
- end
152
- end_eval
153
- end
154
-
155
- def writer_method(name, class_name, mapping)
156
- module_eval <<-end_eval
157
- def #{name}=(part)
158
- @#{name} = part.freeze
159
- #{(Array === mapping.first ? mapping : [ mapping ]).collect{ |pair| "@attributes[\"#{pair.first}\"] = part.#{pair.last}" }.join("\n")}
160
175
  end
161
- end_eval
176
+ end
162
177
  end
163
178
  end
164
179
  end
@@ -1,70 +1,240 @@
1
+ require 'set'
2
+
1
3
  module ActiveRecord
2
4
  module Associations
3
- class AssociationCollection #:nodoc:
4
- alias_method :proxy_respond_to?, :respond_to?
5
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?)/ }
6
-
7
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
8
- @owner = owner
9
- @options = options
10
- @association_name = association_name
11
- @association_class = eval(association_class_name)
12
- @association_class_primary_key_name = association_class_primary_key_name
5
+ class AssociationCollection < AssociationProxy #:nodoc:
6
+ def to_ary
7
+ load_target
8
+ @target.to_ary
13
9
  end
14
-
15
- def method_missing(symbol, *args, &block)
16
- load_collection_to_array
17
- @collection_array.send(symbol, *args, &block)
10
+
11
+ def reset
12
+ reset_target!
13
+ @loaded = false
18
14
  end
19
-
20
- def to_ary
21
- load_collection_to_array
22
- @collection_array.to_ary
15
+
16
+ # Add +records+ to this association. Returns +self+ so method calls may be chained.
17
+ # Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
18
+ def <<(*records)
19
+ result = true
20
+ load_target if @owner.new_record?
21
+
22
+ @owner.transaction do
23
+ flatten_deeper(records).each do |record|
24
+ raise_on_type_mismatch(record)
25
+ callback(:before_add, record)
26
+ result &&= insert_record(record) unless @owner.new_record?
27
+ @target << record
28
+ callback(:after_add, record)
29
+ end
30
+ end
31
+
32
+ result && self
23
33
  end
24
-
25
- def respond_to?(symbol)
26
- proxy_respond_to?(symbol) || [].respond_to?(symbol)
34
+
35
+ alias_method :push, :<<
36
+ alias_method :concat, :<<
37
+
38
+ # Remove all records from this association
39
+ def delete_all
40
+ load_target
41
+ delete(@target)
42
+ reset_target!
27
43
  end
28
-
29
- def reload
30
- @collection_array = nil
44
+
45
+ # Calculate sum using SQL, not Enumerable
46
+ def sum(*args, &block)
47
+ calculate(:sum, *args, &block)
31
48
  end
32
-
33
- def concat(*records)
34
- records.flatten!
35
- records.each {|record| self << record; }
49
+
50
+ # Remove +records+ from this association. Does not destroy +records+.
51
+ def delete(*records)
52
+ records = flatten_deeper(records)
53
+ records.each { |record| raise_on_type_mismatch(record) }
54
+ records.reject! { |record| @target.delete(record) if record.new_record? }
55
+ return if records.empty?
56
+
57
+ @owner.transaction do
58
+ records.each { |record| callback(:before_remove, record) }
59
+ delete_records(records)
60
+ records.each do |record|
61
+ @target.delete(record)
62
+ callback(:after_remove, record)
63
+ end
64
+ end
65
+ end
66
+
67
+ # Removes all records from this association. Returns +self+ so method calls may be chained.
68
+ def clear
69
+ return self if length.zero? # forces load_target if it hasn't happened already
70
+
71
+ if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
72
+ destroy_all
73
+ else
74
+ delete_all
75
+ end
76
+
77
+ self
36
78
  end
37
79
 
38
80
  def destroy_all
39
- load_collection_to_array
40
- @collection_array.each { |object| object.destroy }
41
- @collection_array = []
81
+ @owner.transaction do
82
+ each { |record| record.destroy }
83
+ end
84
+
85
+ reset_target!
42
86
  end
43
87
 
88
+ def create(attrs = {})
89
+ if attrs.is_a?(Array)
90
+ attrs.collect { |attr| create(attr) }
91
+ else
92
+ create_record(attrs) { |record| record.save }
93
+ end
94
+ end
95
+
96
+ def create!(attrs = {})
97
+ create_record(attrs) { |record| record.save! }
98
+ end
99
+
100
+ # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been loaded and
101
+ # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
102
+ # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
44
103
  def size
45
- (@collection_array.nil?) ? count_records : @collection_array.size
104
+ if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
105
+ @target.size
106
+ elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
107
+ unsaved_records = Array(@target.detect { |r| r.new_record? })
108
+ unsaved_records.size + count_records
109
+ else
110
+ count_records
111
+ end
46
112
  end
47
-
113
+
114
+ # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
115
+ # whether the collection is empty, use collection.length.zero? instead of collection.empty?
116
+ def length
117
+ load_target.size
118
+ end
119
+
48
120
  def empty?
49
- size == 0
121
+ size.zero?
50
122
  end
51
-
52
- alias_method :length, :size
53
-
123
+
124
+ def any?(&block)
125
+ if block_given?
126
+ method_missing(:any?, &block)
127
+ else
128
+ !empty?
129
+ end
130
+ end
131
+
132
+ def uniq(collection = self)
133
+ seen = Set.new
134
+ collection.inject([]) do |kept, record|
135
+ unless seen.include?(record.id)
136
+ kept << record
137
+ seen << record.id
138
+ end
139
+ kept
140
+ end
141
+ end
142
+
143
+ # Replace this collection with +other_array+
144
+ # This will perform a diff and delete/add only records that have changed.
145
+ def replace(other_array)
146
+ other_array.each { |val| raise_on_type_mismatch(val) }
147
+
148
+ load_target
149
+ other = other_array.size < 100 ? other_array : other_array.to_set
150
+ current = @target.size < 100 ? @target : @target.to_set
151
+
152
+ @owner.transaction do
153
+ delete(@target.select { |v| !other.include?(v) })
154
+ concat(other_array.select { |v| !current.include?(v) })
155
+ end
156
+ end
157
+
158
+
159
+ protected
160
+ def method_missing(method, *args, &block)
161
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
162
+ super
163
+ else
164
+ @reflection.klass.send(:with_scope, construct_scope) { @reflection.klass.send(method, *args, &block) }
165
+ end
166
+ end
167
+
168
+ # overloaded in derived Association classes to provide useful scoping depending on association type.
169
+ def construct_scope
170
+ {}
171
+ end
172
+
173
+ def reset_target!
174
+ @target = Array.new
175
+ end
176
+
177
+ def find_target
178
+ records =
179
+ if @reflection.options[:finder_sql]
180
+ @reflection.klass.find_by_sql(@finder_sql)
181
+ else
182
+ find(:all)
183
+ end
184
+
185
+ @reflection.options[:uniq] ? uniq(records) : records
186
+ end
187
+
54
188
  private
55
- def load_collection_to_array
56
- return unless @collection_array.nil?
57
- begin
58
- @collection_array = find_all_records
59
- rescue ActiveRecord::StatementInvalid, ActiveRecord::RecordNotFound
60
- @collection_array = []
61
- end
189
+
190
+ def create_record(attrs, &block)
191
+ ensure_owner_is_not_new
192
+ record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
193
+ add_record_to_target_with_callbacks(record, &block)
62
194
  end
195
+
196
+ def build_record(attrs, &block)
197
+ record = @reflection.klass.new(attrs)
198
+ add_record_to_target_with_callbacks(record, &block)
199
+ end
200
+
201
+ def add_record_to_target_with_callbacks(record)
202
+ callback(:before_add, record)
203
+ yield(record) if block_given?
204
+ @target ||= [] unless loaded?
205
+ @target << record
206
+ callback(:after_add, record)
207
+ record
208
+ end
209
+
210
+ def callback(method, record)
211
+ callbacks_for(method).each do |callback|
212
+ case callback
213
+ when Symbol
214
+ @owner.send(callback, record)
215
+ when Proc, Method
216
+ callback.call(@owner, record)
217
+ else
218
+ if callback.respond_to?(method)
219
+ callback.send(method, @owner, record)
220
+ else
221
+ raise ActiveRecordError, "Callbacks must be a symbol denoting the method to call, a string to be evaluated, a block to be invoked, or an object responding to the callback method."
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ def callbacks_for(callback_name)
228
+ full_callback_name = "#{callback_name}_for_#{@reflection.name}"
229
+ @owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
230
+ end
63
231
 
64
- def duplicated_records_array(records)
65
- records = [records] unless records.is_a?(Array) || records.is_a?(ActiveRecord::Associations::AssociationCollection)
66
- records.dup
232
+ def ensure_owner_is_not_new
233
+ if @owner.new_record?
234
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
235
+ end
67
236
  end
237
+
68
238
  end
69
239
  end
70
240
  end
@@ -0,0 +1,159 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class AssociationProxy #:nodoc:
4
+ attr_reader :reflection
5
+ alias_method :proxy_respond_to?, :respond_to?
6
+ alias_method :proxy_extend, :extend
7
+ delegate :to_param, :to => :proxy_target
8
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
9
+
10
+ def initialize(owner, reflection)
11
+ @owner, @reflection = owner, reflection
12
+ Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
13
+ reset
14
+ end
15
+
16
+ def proxy_owner
17
+ @owner
18
+ end
19
+
20
+ def proxy_reflection
21
+ @reflection
22
+ end
23
+
24
+ def proxy_target
25
+ @target
26
+ end
27
+
28
+ def respond_to?(symbol, include_priv = false)
29
+ proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
30
+ end
31
+
32
+ # Explicitly proxy === because the instance method removal above
33
+ # doesn't catch it.
34
+ def ===(other)
35
+ load_target
36
+ other === @target
37
+ end
38
+
39
+ def aliased_table_name
40
+ @reflection.klass.table_name
41
+ end
42
+
43
+ def conditions
44
+ @conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
45
+ end
46
+ alias :sql_conditions :conditions
47
+
48
+ def reset
49
+ @loaded = false
50
+ @target = nil
51
+ end
52
+
53
+ def reload
54
+ reset
55
+ load_target
56
+ end
57
+
58
+ def loaded?
59
+ @loaded
60
+ end
61
+
62
+ def loaded
63
+ @loaded = true
64
+ end
65
+
66
+ def target
67
+ @target
68
+ end
69
+
70
+ def target=(target)
71
+ @target = target
72
+ loaded
73
+ end
74
+
75
+ def inspect
76
+ reload unless loaded?
77
+ @target.inspect
78
+ end
79
+
80
+ protected
81
+ def dependent?
82
+ @reflection.options[:dependent]
83
+ end
84
+
85
+ def quoted_record_ids(records)
86
+ records.map { |record| record.quoted_id }.join(',')
87
+ end
88
+
89
+ def interpolate_sql_options!(options, *keys)
90
+ keys.each { |key| options[key] &&= interpolate_sql(options[key]) }
91
+ end
92
+
93
+ def interpolate_sql(sql, record = nil)
94
+ @owner.send(:interpolate_sql, sql, record)
95
+ end
96
+
97
+ def sanitize_sql(sql)
98
+ @reflection.klass.send(:sanitize_sql, sql)
99
+ end
100
+
101
+ def set_belongs_to_association_for(record)
102
+ if @reflection.options[:as]
103
+ record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
104
+ record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
105
+ else
106
+ record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
107
+ end
108
+ end
109
+
110
+ def merge_options_from_reflection!(options)
111
+ options.reverse_merge!(
112
+ :group => @reflection.options[:group],
113
+ :limit => @reflection.options[:limit],
114
+ :offset => @reflection.options[:offset],
115
+ :joins => @reflection.options[:joins],
116
+ :include => @reflection.options[:include],
117
+ :select => @reflection.options[:select]
118
+ )
119
+ end
120
+
121
+ private
122
+ def method_missing(method, *args, &block)
123
+ if load_target
124
+ @target.send(method, *args, &block)
125
+ end
126
+ end
127
+
128
+ def load_target
129
+ return nil unless defined?(@loaded)
130
+
131
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
132
+ @target = find_target
133
+ end
134
+
135
+ @loaded = true
136
+ @target
137
+ rescue ActiveRecord::RecordNotFound
138
+ reset
139
+ end
140
+
141
+ # Can be overwritten by associations that might have the foreign key available for an association without
142
+ # having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
143
+ def foreign_key_present
144
+ false
145
+ end
146
+
147
+ def raise_on_type_mismatch(record)
148
+ unless record.is_a?(@reflection.klass)
149
+ raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.klass} expected, got #{record.class}"
150
+ end
151
+ end
152
+
153
+ # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
154
+ def flatten_deeper(array)
155
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class BelongsToAssociation < AssociationProxy #:nodoc:
4
+ def create(attributes = {})
5
+ replace(@reflection.klass.create(attributes))
6
+ end
7
+
8
+ def build(attributes = {})
9
+ replace(@reflection.klass.new(attributes))
10
+ end
11
+
12
+ def replace(record)
13
+ counter_cache_name = @reflection.counter_cache_column
14
+
15
+ if record.nil?
16
+ if counter_cache_name && @owner[counter_cache_name] && !@owner.new_record?
17
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
18
+ end
19
+
20
+ @target = @owner[@reflection.primary_key_name] = nil
21
+ else
22
+ raise_on_type_mismatch(record)
23
+
24
+ if counter_cache_name && !@owner.new_record?
25
+ @reflection.klass.increment_counter(counter_cache_name, record.id)
26
+ @reflection.klass.decrement_counter(counter_cache_name, @owner[@reflection.primary_key_name]) if @owner[@reflection.primary_key_name]
27
+ end
28
+
29
+ @target = (AssociationProxy === record ? record.target : record)
30
+ @owner[@reflection.primary_key_name] = record.id unless record.new_record?
31
+ @updated = true
32
+ end
33
+
34
+ loaded
35
+ record
36
+ end
37
+
38
+ def updated?
39
+ @updated
40
+ end
41
+
42
+ private
43
+ def find_target
44
+ @reflection.klass.find(
45
+ @owner[@reflection.primary_key_name],
46
+ :conditions => conditions,
47
+ :include => @reflection.options[:include]
48
+ )
49
+ end
50
+
51
+ def foreign_key_present
52
+ !@owner[@reflection.primary_key_name].nil?
53
+ end
54
+ end
55
+ end
56
+ end