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
@@ -0,0 +1,50 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class BelongsToPolymorphicAssociation < AssociationProxy #:nodoc:
4
+ def replace(record)
5
+ if record.nil?
6
+ @target = @owner[@reflection.primary_key_name] = @owner[@reflection.options[:foreign_type]] = nil
7
+ else
8
+ @target = (AssociationProxy === record ? record.target : record)
9
+
10
+ unless record.new_record?
11
+ @owner[@reflection.primary_key_name] = record.id
12
+ @owner[@reflection.options[:foreign_type]] = record.class.base_class.name.to_s
13
+ end
14
+
15
+ @updated = true
16
+ end
17
+
18
+ loaded
19
+ record
20
+ end
21
+
22
+ def updated?
23
+ @updated
24
+ end
25
+
26
+ private
27
+ def find_target
28
+ return nil if association_class.nil?
29
+
30
+ if @reflection.options[:conditions]
31
+ association_class.find(
32
+ @owner[@reflection.primary_key_name],
33
+ :conditions => conditions,
34
+ :include => @reflection.options[:include]
35
+ )
36
+ else
37
+ association_class.find(@owner[@reflection.primary_key_name], :include => @reflection.options[:include])
38
+ end
39
+ end
40
+
41
+ def foreign_key_present
42
+ !@owner[@reflection.primary_key_name].nil?
43
+ end
44
+
45
+ def association_class
46
+ @owner[@reflection.options[:foreign_type]] ? @owner[@reflection.options[:foreign_type]].constantize : nil
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,46 +1,164 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
- class HasAndBelongsToManyCollection < AssociationCollection #:nodoc:
4
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, join_table, options)
5
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
-
7
- @association_foreign_key = options[:association_foreign_key] || association_class_name.downcase + "_id"
8
- association_table_name = options[:table_name] || @association_class.table_name(association_class_name)
9
- @join_table = join_table
10
- @order = options[:order] || "t.#{@owner.class.primary_key}"
11
-
12
- @finder_sql = options[:finder_sql] ||
13
- "SELECT t.* FROM #{association_table_name} t, #{@join_table} j " +
14
- "WHERE t.#{@owner.class.primary_key} = j.#{@association_foreign_key} AND " +
15
- "j.#{association_class_primary_key_name} = '#{@owner.id}' ORDER BY #{@order}"
3
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4
+ def initialize(owner, reflection)
5
+ super
6
+ construct_sql
16
7
  end
17
-
18
- def <<(record)
19
- raise ActiveRecord::AssociationTypeMismatch unless @association_class === record
20
- sql = @options[:insert_sql] ||
21
- "INSERT INTO #{@join_table} (#{@association_class_primary_key_name}, #{@association_foreign_key}) " +
22
- "VALUES ('#{@owner.id}', '#{record.id}')"
23
- @owner.connection.execute(sql)
24
- @collection_array << record unless @collection_array.nil?
8
+
9
+ def build(attributes = {})
10
+ load_target
11
+ build_record(attributes)
25
12
  end
26
-
27
- def delete(records)
28
- records = duplicated_records_array(records)
29
- sql = @options[:delete_sql] || "DELETE FROM #{@join_table} WHERE #{@association_class_primary_key_name} = '#{@owner.id}'"
30
- ids = records.map { |record| "'" + record.id.to_s + "'" }.join(',')
31
- @owner.connection.delete "#{sql} AND #{@association_foreign_key} in (#{ids})"
32
- records.each {|record| @collection_array.delete(record) } unless @collection_array.nil?
13
+
14
+ def create(attributes = {})
15
+ create_record(attributes) { |record| insert_record(record) }
33
16
  end
34
-
35
- protected
36
- def find_all_records
37
- @association_class.find_by_sql(@finder_sql)
17
+
18
+ def create!(attributes = {})
19
+ create_record(attributes) { |record| insert_record(record, true) }
20
+ end
21
+
22
+ def find_first
23
+ load_target.first
24
+ end
25
+
26
+ def find(*args)
27
+ options = args.extract_options!
28
+
29
+ # If using a custom finder_sql, scan the entire collection.
30
+ if @reflection.options[:finder_sql]
31
+ expects_array = args.first.kind_of?(Array)
32
+ ids = args.flatten.compact.uniq
33
+
34
+ if ids.size == 1
35
+ id = ids.first.to_i
36
+ record = load_target.detect { |record| id == record.id }
37
+ expects_array ? [record] : record
38
+ else
39
+ load_target.select { |record| ids.include?(record.id) }
40
+ end
41
+ else
42
+ conditions = "#{@finder_sql}"
43
+
44
+ if sanitized_conditions = sanitize_sql(options[:conditions])
45
+ conditions << " AND (#{sanitized_conditions})"
46
+ end
47
+
48
+ options[:conditions] = conditions
49
+ options[:joins] = @join_sql
50
+ options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
51
+
52
+ if options[:order] && @reflection.options[:order]
53
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
54
+ elsif @reflection.options[:order]
55
+ options[:order] = @reflection.options[:order]
56
+ end
57
+
58
+ merge_options_from_reflection!(options)
59
+
60
+ options[:select] ||= (@reflection.options[:select] || '*')
61
+
62
+ # Pass through args exactly as we received them.
63
+ args << options
64
+ @reflection.klass.find(*args)
38
65
  end
39
-
66
+ end
67
+
68
+ protected
40
69
  def count_records
41
- load_collection_to_array
42
- @collection_array.size
70
+ load_target.size
43
71
  end
44
- end
72
+
73
+ def insert_record(record, force=true)
74
+ if record.new_record?
75
+ if force
76
+ record.save!
77
+ else
78
+ return false unless record.save
79
+ end
80
+ end
81
+
82
+ if @reflection.options[:insert_sql]
83
+ @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
84
+ else
85
+ columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
86
+
87
+ attributes = columns.inject({}) do |attributes, column|
88
+ case column.name
89
+ when @reflection.primary_key_name
90
+ attributes[column.name] = @owner.quoted_id
91
+ when @reflection.association_foreign_key
92
+ attributes[column.name] = record.quoted_id
93
+ else
94
+ if record.attributes.has_key?(column.name)
95
+ value = @owner.send(:quote_value, record[column.name], column)
96
+ attributes[column.name] = value unless value.nil?
97
+ end
98
+ end
99
+ attributes
100
+ end
101
+
102
+ sql =
103
+ "INSERT INTO #{@reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
104
+ "VALUES (#{attributes.values.join(', ')})"
105
+
106
+ @owner.connection.execute(sql)
107
+ end
108
+
109
+ return true
110
+ end
111
+
112
+ def delete_records(records)
113
+ if sql = @reflection.options[:delete_sql]
114
+ records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
115
+ else
116
+ ids = quoted_record_ids(records)
117
+ sql = "DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
118
+ @owner.connection.execute(sql)
119
+ end
120
+ end
121
+
122
+ def construct_sql
123
+ interpolate_sql_options!(@reflection.options, :finder_sql)
124
+
125
+ if @reflection.options[:finder_sql]
126
+ @finder_sql = @reflection.options[:finder_sql]
127
+ else
128
+ @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
129
+ @finder_sql << " AND (#{conditions})" if conditions
130
+ end
131
+
132
+ @join_sql = "INNER JOIN #{@reflection.options[:join_table]} ON #{@reflection.klass.table_name}.#{@reflection.klass.primary_key} = #{@reflection.options[:join_table]}.#{@reflection.association_foreign_key}"
133
+ end
134
+
135
+ def construct_scope
136
+ { :find => { :conditions => @finder_sql,
137
+ :joins => @join_sql,
138
+ :readonly => false,
139
+ :order => @reflection.options[:order],
140
+ :limit => @reflection.options[:limit] } }
141
+ end
142
+
143
+ # Join tables with additional columns on top of the two foreign keys must be considered ambiguous unless a select
144
+ # clause has been explicitly defined. Otherwise you can get broken records back, if, for example, the join column also has
145
+ # an id column. This will then overwrite the id column of the records coming back.
146
+ def finding_with_ambiguous_select?(select_clause)
147
+ !select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
148
+ end
149
+
150
+ private
151
+ def create_record(attributes)
152
+ # Can't use Base.create because the foreign key may be a protected attribute.
153
+ ensure_owner_is_not_new
154
+ if attributes.is_a?(Array)
155
+ attributes.collect { |attr| create(attr) }
156
+ else
157
+ record = build(attributes)
158
+ yield(record)
159
+ record
160
+ end
161
+ end
162
+ end
45
163
  end
46
- end
164
+ end
@@ -1,104 +1,174 @@
1
1
  module ActiveRecord
2
2
  module Associations
3
3
  class HasManyAssociation < AssociationCollection #:nodoc:
4
- def initialize(owner, association_name, association_class_name, association_class_primary_key_name, options)
5
- super(owner, association_name, association_class_name, association_class_primary_key_name, options)
6
- @conditions = options[:conditions]
7
-
8
- if options[:finder_sql]
9
- @counter_sql = options[:finder_sql].gsub(/SELECT (.*) FROM/, "SELECT COUNT(*) FROM")
10
- @finder_sql = options[:finder_sql]
11
- else
12
- @counter_sql = "#{@association_class_primary_key_name} = '#{@owner.id}'#{@conditions ? " AND " + @conditions : ""}"
13
- @finder_sql = "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
14
- end
15
- end
16
-
17
- def <<(record)
18
- raise ActiveRecord::AssociationTypeMismatch unless @association_class === record
19
- record.send(@association_class_primary_key_name + "=", @owner.id)
20
- record.save(false)
21
- @collection_array << record unless @collection_array.nil?
22
- end
23
-
24
- def delete(records)
25
- duplicated_records_array(records).each do |record|
26
- next if record.send(@association_class_primary_key_name) != @owner.id
27
- record.send(@association_class_primary_key_name + "=", nil)
28
- record.save(false)
29
- @collection_array.delete(record) unless @collection_array.nil?
30
- end
31
- end
32
-
33
- def create(attributes = {})
34
- # We can't use the regular Base.create method as the foreign key might be a protected attribute, hence the repetion
35
- record = @association_class.new(attributes || {})
36
- record.send(@association_class_primary_key_name + "=", @owner.id)
37
- record.save
38
-
39
- @collection_array << record unless @collection_array.nil?
40
-
41
- return record
4
+ def initialize(owner, reflection)
5
+ super
6
+ construct_sql
42
7
  end
43
8
 
44
9
  def build(attributes = {})
45
- association = @association_class.new
46
- association.attributes = attributes.merge({ "#{@association_class_primary_key_name}" => @owner.id})
47
- association
10
+ if attributes.is_a?(Array)
11
+ attributes.collect { |attr| build(attr) }
12
+ else
13
+ build_record(attributes) { |record| set_belongs_to_association_for(record) }
14
+ end
48
15
  end
49
-
50
- def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil, &block)
51
- if block_given? || @options[:finder_sql]
52
- load_collection_to_array
53
- @collection_array.send(:find_all, &block)
16
+
17
+ # Count the number of associated records. All arguments are optional.
18
+ def count(*args)
19
+ if @reflection.options[:counter_sql]
20
+ @reflection.klass.count_by_sql(@counter_sql)
21
+ elsif @reflection.options[:finder_sql]
22
+ @reflection.klass.count_by_sql(@finder_sql)
54
23
  else
55
- @association_class.find_all(
56
- "#{@association_class_primary_key_name} = '#{@owner.id}' " +
57
- "#{@conditions ? " AND " + @conditions : ""} #{runtime_conditions ? " AND " + runtime_conditions : ""}",
58
- orderings,
59
- limit,
60
- joins
61
- )
24
+ column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
25
+ options[:conditions] = options[:conditions].nil? ?
26
+ @finder_sql :
27
+ @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
28
+ options[:include] ||= @reflection.options[:include]
29
+
30
+ @reflection.klass.count(column_name, options)
62
31
  end
63
32
  end
64
33
 
65
- def find(association_id = nil, &block)
66
- if block_given? || @options[:finder_sql]
67
- load_collection_to_array
68
- return @collection_array.send(:find, &block)
34
+ def find(*args)
35
+ options = args.extract_options!
36
+
37
+ # If using a custom finder_sql, scan the entire collection.
38
+ if @reflection.options[:finder_sql]
39
+ expects_array = args.first.kind_of?(Array)
40
+ ids = args.flatten.compact.uniq.map(&:to_i)
41
+
42
+ if ids.size == 1
43
+ id = ids.first
44
+ record = load_target.detect { |record| id == record.id }
45
+ expects_array ? [ record ] : record
46
+ else
47
+ load_target.select { |record| ids.include?(record.id) }
48
+ end
69
49
  else
70
- @association_class.find_on_conditions(
71
- association_id, "#{@association_class_primary_key_name} = '#{@owner.id}' #{@conditions ? " AND " + @conditions : ""}"
72
- )
50
+ conditions = "#{@finder_sql}"
51
+ if sanitized_conditions = sanitize_sql(options[:conditions])
52
+ conditions << " AND (#{sanitized_conditions})"
53
+ end
54
+ options[:conditions] = conditions
55
+
56
+ if options[:order] && @reflection.options[:order]
57
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
58
+ elsif @reflection.options[:order]
59
+ options[:order] = @reflection.options[:order]
60
+ end
61
+
62
+ merge_options_from_reflection!(options)
63
+
64
+ # Pass through args exactly as we received them.
65
+ args << options
66
+ @reflection.klass.find(*args)
73
67
  end
74
68
  end
75
-
69
+
76
70
  protected
77
- def find_all_records
78
- if @options[:finder_sql]
79
- @association_class.find_by_sql(@finder_sql)
80
- else
81
- @association_class.find_all(@finder_sql, @options[:order] ? @options[:order] : nil)
71
+ def load_target
72
+ if !@owner.new_record? || foreign_key_present
73
+ begin
74
+ if !loaded?
75
+ if @target.is_a?(Array) && @target.any?
76
+ @target = (find_target + @target).uniq
77
+ else
78
+ @target = find_target
79
+ end
80
+ end
81
+ rescue ActiveRecord::RecordNotFound
82
+ reset
83
+ end
82
84
  end
85
+
86
+ loaded if target
87
+ target
83
88
  end
84
-
89
+
85
90
  def count_records
86
- if has_cached_counter?
91
+ count = if has_cached_counter?
87
92
  @owner.send(:read_attribute, cached_counter_attribute_name)
88
- elsif @options[:finder_sql]
89
- @association_class.count_by_sql(@counter_sql)
93
+ elsif @reflection.options[:counter_sql]
94
+ @reflection.klass.count_by_sql(@counter_sql)
90
95
  else
91
- @association_class.count(@counter_sql)
96
+ @reflection.klass.count(:conditions => @counter_sql, :include => @reflection.options[:include])
97
+ end
98
+
99
+ @target = [] and loaded if count == 0
100
+
101
+ if @reflection.options[:limit]
102
+ count = [ @reflection.options[:limit], count ].min
92
103
  end
104
+
105
+ return count
93
106
  end
94
-
107
+
95
108
  def has_cached_counter?
96
109
  @owner.attribute_present?(cached_counter_attribute_name)
97
110
  end
98
-
111
+
99
112
  def cached_counter_attribute_name
100
- @association_name + "_count"
113
+ "#{@reflection.name}_count"
114
+ end
115
+
116
+ def insert_record(record)
117
+ set_belongs_to_association_for(record)
118
+ record.save
119
+ end
120
+
121
+ def delete_records(records)
122
+ case @reflection.options[:dependent]
123
+ when :destroy
124
+ records.each(&:destroy)
125
+ when :delete_all
126
+ @reflection.klass.delete(records.map(&:id))
127
+ else
128
+ ids = quoted_record_ids(records)
129
+ @reflection.klass.update_all(
130
+ "#{@reflection.primary_key_name} = NULL",
131
+ "#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
132
+ )
133
+ end
134
+ end
135
+
136
+ def target_obsolete?
137
+ false
138
+ end
139
+
140
+ def construct_sql
141
+ case
142
+ when @reflection.options[:finder_sql]
143
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
144
+
145
+ when @reflection.options[:as]
146
+ @finder_sql =
147
+ "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
148
+ "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
149
+ @finder_sql << " AND (#{conditions})" if conditions
150
+
151
+ else
152
+ @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
153
+ @finder_sql << " AND (#{conditions})" if conditions
154
+ end
155
+
156
+ if @reflection.options[:counter_sql]
157
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
158
+ elsif @reflection.options[:finder_sql]
159
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
160
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
161
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
162
+ else
163
+ @counter_sql = @finder_sql
164
+ end
165
+ end
166
+
167
+ def construct_scope
168
+ create_scoping = {}
169
+ set_belongs_to_association_for(create_scoping)
170
+ { :find => { :conditions => @finder_sql, :readonly => false, :order => @reflection.options[:order], :limit => @reflection.options[:limit] }, :create => create_scoping }
101
171
  end
102
172
  end
103
173
  end
104
- end
174
+ end