activerecord 1.0.0 → 4.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 (255) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +2102 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.rdoc +213 -0
  5. data/examples/performance.rb +172 -0
  6. data/examples/simple.rb +14 -0
  7. data/lib/active_record/aggregations.rb +180 -84
  8. data/lib/active_record/associations/alias_tracker.rb +76 -0
  9. data/lib/active_record/associations/association.rb +248 -0
  10. data/lib/active_record/associations/association_scope.rb +135 -0
  11. data/lib/active_record/associations/belongs_to_association.rb +92 -0
  12. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +35 -0
  13. data/lib/active_record/associations/builder/association.rb +108 -0
  14. data/lib/active_record/associations/builder/belongs_to.rb +98 -0
  15. data/lib/active_record/associations/builder/collection_association.rb +89 -0
  16. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +39 -0
  17. data/lib/active_record/associations/builder/has_many.rb +15 -0
  18. data/lib/active_record/associations/builder/has_one.rb +25 -0
  19. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  20. data/lib/active_record/associations/collection_association.rb +608 -0
  21. data/lib/active_record/associations/collection_proxy.rb +986 -0
  22. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +58 -39
  23. data/lib/active_record/associations/has_many_association.rb +116 -85
  24. data/lib/active_record/associations/has_many_through_association.rb +197 -0
  25. data/lib/active_record/associations/has_one_association.rb +102 -0
  26. data/lib/active_record/associations/has_one_through_association.rb +36 -0
  27. data/lib/active_record/associations/join_dependency/join_association.rb +174 -0
  28. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  29. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  30. data/lib/active_record/associations/join_dependency.rb +235 -0
  31. data/lib/active_record/associations/join_helper.rb +45 -0
  32. data/lib/active_record/associations/preloader/association.rb +121 -0
  33. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  34. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  35. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  36. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  37. data/lib/active_record/associations/preloader/has_many_through.rb +19 -0
  38. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  39. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  40. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  41. data/lib/active_record/associations/preloader/through_association.rb +63 -0
  42. data/lib/active_record/associations/preloader.rb +178 -0
  43. data/lib/active_record/associations/singular_association.rb +64 -0
  44. data/lib/active_record/associations/through_association.rb +87 -0
  45. data/lib/active_record/associations.rb +1437 -431
  46. data/lib/active_record/attribute_assignment.rb +201 -0
  47. data/lib/active_record/attribute_methods/before_type_cast.rb +70 -0
  48. data/lib/active_record/attribute_methods/dirty.rb +118 -0
  49. data/lib/active_record/attribute_methods/primary_key.rb +122 -0
  50. data/lib/active_record/attribute_methods/query.rb +40 -0
  51. data/lib/active_record/attribute_methods/read.rb +107 -0
  52. data/lib/active_record/attribute_methods/serialization.rb +162 -0
  53. data/lib/active_record/attribute_methods/time_zone_conversion.rb +59 -0
  54. data/lib/active_record/attribute_methods/write.rb +63 -0
  55. data/lib/active_record/attribute_methods.rb +393 -0
  56. data/lib/active_record/autosave_association.rb +426 -0
  57. data/lib/active_record/base.rb +268 -930
  58. data/lib/active_record/callbacks.rb +203 -230
  59. data/lib/active_record/coders/yaml_column.rb +38 -0
  60. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +638 -0
  61. data/lib/active_record/connection_adapters/abstract/database_limits.rb +67 -0
  62. data/lib/active_record/connection_adapters/abstract/database_statements.rb +390 -0
  63. data/lib/active_record/connection_adapters/abstract/query_cache.rb +95 -0
  64. data/lib/active_record/connection_adapters/abstract/quoting.rb +129 -0
  65. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +501 -0
  66. data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +70 -0
  67. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +873 -0
  68. data/lib/active_record/connection_adapters/abstract/transaction.rb +203 -0
  69. data/lib/active_record/connection_adapters/abstract_adapter.rb +389 -275
  70. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +782 -0
  71. data/lib/active_record/connection_adapters/column.rb +318 -0
  72. data/lib/active_record/connection_adapters/connection_specification.rb +96 -0
  73. data/lib/active_record/connection_adapters/mysql2_adapter.rb +273 -0
  74. data/lib/active_record/connection_adapters/mysql_adapter.rb +517 -90
  75. data/lib/active_record/connection_adapters/postgresql/array_parser.rb +97 -0
  76. data/lib/active_record/connection_adapters/postgresql/cast.rb +152 -0
  77. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +242 -0
  78. data/lib/active_record/connection_adapters/postgresql/oid.rb +366 -0
  79. data/lib/active_record/connection_adapters/postgresql/quoting.rb +171 -0
  80. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +30 -0
  81. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +489 -0
  82. data/lib/active_record/connection_adapters/postgresql_adapter.rb +911 -138
  83. data/lib/active_record/connection_adapters/schema_cache.rb +129 -0
  84. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +624 -0
  85. data/lib/active_record/connection_adapters/statement_pool.rb +40 -0
  86. data/lib/active_record/connection_handling.rb +98 -0
  87. data/lib/active_record/core.rb +463 -0
  88. data/lib/active_record/counter_cache.rb +122 -0
  89. data/lib/active_record/dynamic_matchers.rb +131 -0
  90. data/lib/active_record/errors.rb +213 -0
  91. data/lib/active_record/explain.rb +38 -0
  92. data/lib/active_record/explain_registry.rb +30 -0
  93. data/lib/active_record/explain_subscriber.rb +29 -0
  94. data/lib/active_record/fixture_set/file.rb +55 -0
  95. data/lib/active_record/fixtures.rb +892 -138
  96. data/lib/active_record/inheritance.rb +200 -0
  97. data/lib/active_record/integration.rb +60 -0
  98. data/lib/active_record/locale/en.yml +47 -0
  99. data/lib/active_record/locking/optimistic.rb +181 -0
  100. data/lib/active_record/locking/pessimistic.rb +77 -0
  101. data/lib/active_record/log_subscriber.rb +82 -0
  102. data/lib/active_record/migration/command_recorder.rb +164 -0
  103. data/lib/active_record/migration/join_table.rb +15 -0
  104. data/lib/active_record/migration.rb +1015 -0
  105. data/lib/active_record/model_schema.rb +345 -0
  106. data/lib/active_record/nested_attributes.rb +546 -0
  107. data/lib/active_record/null_relation.rb +65 -0
  108. data/lib/active_record/persistence.rb +509 -0
  109. data/lib/active_record/query_cache.rb +56 -0
  110. data/lib/active_record/querying.rb +62 -0
  111. data/lib/active_record/railtie.rb +205 -0
  112. data/lib/active_record/railties/console_sandbox.rb +5 -0
  113. data/lib/active_record/railties/controller_runtime.rb +50 -0
  114. data/lib/active_record/railties/databases.rake +402 -0
  115. data/lib/active_record/railties/jdbcmysql_error.rb +16 -0
  116. data/lib/active_record/readonly_attributes.rb +30 -0
  117. data/lib/active_record/reflection.rb +544 -87
  118. data/lib/active_record/relation/batches.rb +93 -0
  119. data/lib/active_record/relation/calculations.rb +399 -0
  120. data/lib/active_record/relation/delegation.rb +125 -0
  121. data/lib/active_record/relation/finder_methods.rb +349 -0
  122. data/lib/active_record/relation/merger.rb +161 -0
  123. data/lib/active_record/relation/predicate_builder.rb +106 -0
  124. data/lib/active_record/relation/query_methods.rb +1044 -0
  125. data/lib/active_record/relation/spawn_methods.rb +73 -0
  126. data/lib/active_record/relation.rb +655 -0
  127. data/lib/active_record/result.rb +67 -0
  128. data/lib/active_record/runtime_registry.rb +17 -0
  129. data/lib/active_record/sanitization.rb +168 -0
  130. data/lib/active_record/schema.rb +65 -0
  131. data/lib/active_record/schema_dumper.rb +204 -0
  132. data/lib/active_record/schema_migration.rb +39 -0
  133. data/lib/active_record/scoping/default.rb +146 -0
  134. data/lib/active_record/scoping/named.rb +175 -0
  135. data/lib/active_record/scoping.rb +82 -0
  136. data/lib/active_record/serialization.rb +22 -0
  137. data/lib/active_record/serializers/xml_serializer.rb +197 -0
  138. data/lib/active_record/statement_cache.rb +26 -0
  139. data/lib/active_record/store.rb +156 -0
  140. data/lib/active_record/tasks/database_tasks.rb +203 -0
  141. data/lib/active_record/tasks/firebird_database_tasks.rb +56 -0
  142. data/lib/active_record/tasks/mysql_database_tasks.rb +143 -0
  143. data/lib/active_record/tasks/oracle_database_tasks.rb +45 -0
  144. data/lib/active_record/tasks/postgresql_database_tasks.rb +90 -0
  145. data/lib/active_record/tasks/sqlite_database_tasks.rb +51 -0
  146. data/lib/active_record/tasks/sqlserver_database_tasks.rb +48 -0
  147. data/lib/active_record/test_case.rb +96 -0
  148. data/lib/active_record/timestamp.rb +119 -0
  149. data/lib/active_record/transactions.rb +366 -69
  150. data/lib/active_record/translation.rb +22 -0
  151. data/lib/active_record/validations/associated.rb +49 -0
  152. data/lib/active_record/validations/presence.rb +65 -0
  153. data/lib/active_record/validations/uniqueness.rb +225 -0
  154. data/lib/active_record/validations.rb +64 -185
  155. data/lib/active_record/version.rb +11 -0
  156. data/lib/active_record.rb +149 -24
  157. data/lib/rails/generators/active_record/migration/migration_generator.rb +62 -0
  158. data/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +19 -0
  159. data/lib/rails/generators/active_record/migration/templates/migration.rb +39 -0
  160. data/lib/rails/generators/active_record/model/model_generator.rb +48 -0
  161. data/lib/rails/generators/active_record/model/templates/model.rb +10 -0
  162. data/lib/rails/generators/active_record/model/templates/module.rb +7 -0
  163. data/lib/rails/generators/active_record.rb +23 -0
  164. metadata +261 -161
  165. data/CHANGELOG +0 -581
  166. data/README +0 -361
  167. data/RUNNING_UNIT_TESTS +0 -36
  168. data/dev-utils/eval_debugger.rb +0 -9
  169. data/examples/associations.png +0 -0
  170. data/examples/associations.rb +0 -87
  171. data/examples/shared_setup.rb +0 -15
  172. data/examples/validation.rb +0 -88
  173. data/install.rb +0 -60
  174. data/lib/active_record/associations/association_collection.rb +0 -70
  175. data/lib/active_record/connection_adapters/sqlite_adapter.rb +0 -107
  176. data/lib/active_record/deprecated_associations.rb +0 -70
  177. data/lib/active_record/observer.rb +0 -71
  178. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  179. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  180. data/lib/active_record/support/clean_logger.rb +0 -10
  181. data/lib/active_record/support/inflector.rb +0 -70
  182. data/lib/active_record/vendor/mysql.rb +0 -1117
  183. data/lib/active_record/vendor/simple.rb +0 -702
  184. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  185. data/lib/active_record/wrappings.rb +0 -59
  186. data/rakefile +0 -122
  187. data/test/abstract_unit.rb +0 -16
  188. data/test/aggregations_test.rb +0 -34
  189. data/test/all.sh +0 -8
  190. data/test/associations_test.rb +0 -477
  191. data/test/base_test.rb +0 -513
  192. data/test/class_inheritable_attributes_test.rb +0 -33
  193. data/test/connections/native_mysql/connection.rb +0 -24
  194. data/test/connections/native_postgresql/connection.rb +0 -24
  195. data/test/connections/native_sqlite/connection.rb +0 -24
  196. data/test/deprecated_associations_test.rb +0 -336
  197. data/test/finder_test.rb +0 -67
  198. data/test/fixtures/accounts/signals37 +0 -3
  199. data/test/fixtures/accounts/unknown +0 -2
  200. data/test/fixtures/auto_id.rb +0 -4
  201. data/test/fixtures/column_name.rb +0 -3
  202. data/test/fixtures/companies/first_client +0 -6
  203. data/test/fixtures/companies/first_firm +0 -4
  204. data/test/fixtures/companies/second_client +0 -6
  205. data/test/fixtures/company.rb +0 -37
  206. data/test/fixtures/company_in_module.rb +0 -33
  207. data/test/fixtures/course.rb +0 -3
  208. data/test/fixtures/courses/java +0 -2
  209. data/test/fixtures/courses/ruby +0 -2
  210. data/test/fixtures/customer.rb +0 -30
  211. data/test/fixtures/customers/david +0 -6
  212. data/test/fixtures/db_definitions/mysql.sql +0 -96
  213. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  214. data/test/fixtures/db_definitions/postgresql.sql +0 -113
  215. data/test/fixtures/db_definitions/postgresql2.sql +0 -4
  216. data/test/fixtures/db_definitions/sqlite.sql +0 -85
  217. data/test/fixtures/db_definitions/sqlite2.sql +0 -4
  218. data/test/fixtures/default.rb +0 -2
  219. data/test/fixtures/developer.rb +0 -8
  220. data/test/fixtures/developers/david +0 -2
  221. data/test/fixtures/developers/jamis +0 -2
  222. data/test/fixtures/developers_projects/david_action_controller +0 -2
  223. data/test/fixtures/developers_projects/david_active_record +0 -2
  224. data/test/fixtures/developers_projects/jamis_active_record +0 -2
  225. data/test/fixtures/entrant.rb +0 -3
  226. data/test/fixtures/entrants/first +0 -3
  227. data/test/fixtures/entrants/second +0 -3
  228. data/test/fixtures/entrants/third +0 -3
  229. data/test/fixtures/fixture_database.sqlite +0 -0
  230. data/test/fixtures/fixture_database_2.sqlite +0 -0
  231. data/test/fixtures/movie.rb +0 -5
  232. data/test/fixtures/movies/first +0 -2
  233. data/test/fixtures/movies/second +0 -2
  234. data/test/fixtures/project.rb +0 -3
  235. data/test/fixtures/projects/action_controller +0 -2
  236. data/test/fixtures/projects/active_record +0 -2
  237. data/test/fixtures/reply.rb +0 -21
  238. data/test/fixtures/subscriber.rb +0 -5
  239. data/test/fixtures/subscribers/first +0 -2
  240. data/test/fixtures/subscribers/second +0 -2
  241. data/test/fixtures/topic.rb +0 -20
  242. data/test/fixtures/topics/first +0 -9
  243. data/test/fixtures/topics/second +0 -8
  244. data/test/fixtures_test.rb +0 -20
  245. data/test/inflector_test.rb +0 -104
  246. data/test/inheritance_test.rb +0 -125
  247. data/test/lifecycle_test.rb +0 -110
  248. data/test/modules_test.rb +0 -21
  249. data/test/multiple_db_test.rb +0 -46
  250. data/test/pk_test.rb +0 -57
  251. data/test/reflection_test.rb +0 -78
  252. data/test/thread_safety_test.rb +0 -33
  253. data/test/transactions_test.rb +0 -83
  254. data/test/unconnected_test.rb +0 -24
  255. data/test/validations_test.rb +0 -126
@@ -0,0 +1,608 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ # = Active Record Association Collection
4
+ #
5
+ # CollectionAssociation is an abstract class that provides common stuff to
6
+ # ease the implementation of association proxies that represent
7
+ # collections. See the class hierarchy in AssociationProxy.
8
+ #
9
+ # CollectionAssociation:
10
+ # HasAndBelongsToManyAssociation => has_and_belongs_to_many
11
+ # HasManyAssociation => has_many
12
+ # HasManyThroughAssociation + ThroughAssociation => has_many :through
13
+ #
14
+ # CollectionAssociation class provides common methods to the collections
15
+ # defined by +has_and_belongs_to_many+, +has_many+ or +has_many+ with
16
+ # +:through association+ option.
17
+ #
18
+ # You need to be careful with assumptions regarding the target: The proxy
19
+ # does not fetch records from the database until it needs them, but new
20
+ # ones created with +build+ are added to the target. So, the target may be
21
+ # non-empty and still lack children waiting to be read from the database.
22
+ # If you look directly to the database you cannot assume that's the entire
23
+ # collection because new records may have been added to the target, etc.
24
+ #
25
+ # If you need to work on all current children, new and existing records,
26
+ # +load_target+ and the +loaded+ flag are your friends.
27
+ class CollectionAssociation < Association #:nodoc:
28
+
29
+ # Implements the reader method, e.g. foo.items for Foo.has_many :items
30
+ def reader(force_reload = false)
31
+ if force_reload
32
+ klass.uncached { reload }
33
+ elsif stale_target?
34
+ reload
35
+ end
36
+
37
+ @proxy ||= CollectionProxy.new(klass, self)
38
+ end
39
+
40
+ # Implements the writer method, e.g. foo.items= for Foo.has_many :items
41
+ def writer(records)
42
+ replace(records)
43
+ end
44
+
45
+ # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
46
+ def ids_reader
47
+ if loaded? || options[:finder_sql]
48
+ load_target.map do |record|
49
+ record.send(reflection.association_primary_key)
50
+ end
51
+ else
52
+ column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}"
53
+ scope.pluck(column)
54
+ end
55
+ end
56
+
57
+ # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
58
+ def ids_writer(ids)
59
+ pk_column = reflection.primary_key_column
60
+ ids = Array(ids).reject { |id| id.blank? }
61
+ ids.map! { |i| pk_column.type_cast(i) }
62
+ replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
63
+ end
64
+
65
+ def reset
66
+ super
67
+ @target = []
68
+ end
69
+
70
+ def select(select = nil)
71
+ if block_given?
72
+ load_target.select.each { |e| yield e }
73
+ else
74
+ scope.select(select)
75
+ end
76
+ end
77
+
78
+ def find(*args)
79
+ if block_given?
80
+ load_target.find(*args) { |*block_args| yield(*block_args) }
81
+ else
82
+ if options[:finder_sql]
83
+ find_by_scan(*args)
84
+ elsif options[:inverse_of]
85
+ args = args.flatten
86
+ raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args.blank?
87
+
88
+ result = find_by_scan(*args)
89
+
90
+ result_size = Array(result).size
91
+ if !result || result_size != args.size
92
+ scope.raise_record_not_found_exception!(args, result_size, args.size)
93
+ else
94
+ result
95
+ end
96
+ else
97
+ scope.find(*args)
98
+ end
99
+ end
100
+ end
101
+
102
+ def first(*args)
103
+ first_or_last(:first, *args)
104
+ end
105
+
106
+ def last(*args)
107
+ first_or_last(:last, *args)
108
+ end
109
+
110
+ def build(attributes = {}, &block)
111
+ if attributes.is_a?(Array)
112
+ attributes.collect { |attr| build(attr, &block) }
113
+ else
114
+ add_to_target(build_record(attributes)) do |record|
115
+ yield(record) if block_given?
116
+ end
117
+ end
118
+ end
119
+
120
+ def create(attributes = {}, &block)
121
+ create_record(attributes, &block)
122
+ end
123
+
124
+ def create!(attributes = {}, &block)
125
+ create_record(attributes, true, &block)
126
+ end
127
+
128
+ # Add +records+ to this association. Returns +self+ so method calls may
129
+ # be chained. Since << flattens its argument list and inserts each record,
130
+ # +push+ and +concat+ behave identically.
131
+ def concat(*records)
132
+ load_target if owner.new_record?
133
+
134
+ if owner.new_record?
135
+ concat_records(records)
136
+ else
137
+ transaction { concat_records(records) }
138
+ end
139
+ end
140
+
141
+ # Starts a transaction in the association class's database connection.
142
+ #
143
+ # class Author < ActiveRecord::Base
144
+ # has_many :books
145
+ # end
146
+ #
147
+ # Author.first.books.transaction do
148
+ # # same effect as calling Book.transaction
149
+ # end
150
+ def transaction(*args)
151
+ reflection.klass.transaction(*args) do
152
+ yield
153
+ end
154
+ end
155
+
156
+ # Remove all records from this association.
157
+ #
158
+ # See delete for more info.
159
+ def delete_all
160
+ delete(:all).tap do
161
+ reset
162
+ loaded!
163
+ end
164
+ end
165
+
166
+ # Destroy all the records from this association.
167
+ #
168
+ # See destroy for more info.
169
+ def destroy_all
170
+ destroy(load_target).tap do
171
+ reset
172
+ loaded!
173
+ end
174
+ end
175
+
176
+ # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
177
+ # association, it will be used for the query. Otherwise, construct options and pass them with
178
+ # scope to the target class's +count+.
179
+ def count(column_name = nil, count_options = {})
180
+ column_name, count_options = nil, column_name if column_name.is_a?(Hash)
181
+
182
+ if options[:counter_sql] || options[:finder_sql]
183
+ unless count_options.blank?
184
+ raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
185
+ end
186
+
187
+ reflection.klass.count_by_sql(custom_counter_sql)
188
+ else
189
+ relation = scope
190
+ if association_scope.distinct_value
191
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
192
+ column_name ||= reflection.klass.primary_key
193
+ relation = relation.distinct
194
+ end
195
+
196
+ value = relation.count(column_name)
197
+
198
+ limit = options[:limit]
199
+ offset = options[:offset]
200
+
201
+ if limit || offset
202
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
203
+ else
204
+ value
205
+ end
206
+ end
207
+ end
208
+
209
+ # Removes +records+ from this association calling +before_remove+ and
210
+ # +after_remove+ callbacks.
211
+ #
212
+ # This method is abstract in the sense that +delete_records+ has to be
213
+ # provided by descendants. Note this method does not imply the records
214
+ # are actually removed from the database, that depends precisely on
215
+ # +delete_records+. They are in any case removed from the collection.
216
+ def delete(*records)
217
+ dependent = options[:dependent]
218
+
219
+ if records.first == :all
220
+
221
+ if dependent && dependent == :destroy
222
+ message = 'In Rails 4.1 delete_all on associations would not fire callbacks. ' \
223
+ 'It means if the :dependent option is :destroy then the associated ' \
224
+ 'records would be deleted without loading and invoking callbacks.'
225
+
226
+ ActiveRecord::Base.logger ? ActiveRecord::Base.logger.warn(message) : $stderr.puts(message)
227
+ end
228
+
229
+ if loaded? || dependent == :destroy
230
+ delete_or_destroy(load_target, dependent)
231
+ else
232
+ delete_records(:all, dependent)
233
+ end
234
+ else
235
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
236
+ delete_or_destroy(records, dependent)
237
+ end
238
+ end
239
+
240
+ # Destroy +records+ and remove them from this association calling
241
+ # +before_remove+ and +after_remove+ callbacks.
242
+ #
243
+ # Note that this method will _always_ remove records from the database
244
+ # ignoring the +:dependent+ option.
245
+ def destroy(*records)
246
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
247
+ delete_or_destroy(records, :destroy)
248
+ end
249
+
250
+ # Returns the size of the collection by executing a SELECT COUNT(*)
251
+ # query if the collection hasn't been loaded, and calling
252
+ # <tt>collection.size</tt> if it has.
253
+ #
254
+ # If the collection has been already loaded +size+ and +length+ are
255
+ # equivalent. If not and you are going to need the records anyway
256
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
257
+ #
258
+ # This method is abstract in the sense that it relies on
259
+ # +count_records+, which is a method descendants have to provide.
260
+ def size
261
+ if !find_target? || loaded?
262
+ if association_scope.distinct_value
263
+ target.uniq.size
264
+ else
265
+ target.size
266
+ end
267
+ elsif !loaded? && !association_scope.group_values.empty?
268
+ load_target.size
269
+ elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
270
+ unsaved_records = target.select { |r| r.new_record? }
271
+ unsaved_records.size + count_records
272
+ else
273
+ count_records
274
+ end
275
+ end
276
+
277
+ # Returns the size of the collection calling +size+ on the target.
278
+ #
279
+ # If the collection has been already loaded +length+ and +size+ are
280
+ # equivalent. If not and you are going to need the records anyway this
281
+ # method will take one less query. Otherwise +size+ is more efficient.
282
+ def length
283
+ load_target.size
284
+ end
285
+
286
+ # Returns true if the collection is empty.
287
+ #
288
+ # If the collection has been loaded or the <tt>:counter_sql</tt> option
289
+ # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the
290
+ # collection has not been loaded, it is equivalent to
291
+ # <tt>collection.exists?</tt>. If the collection has not already been
292
+ # loaded and you are going to fetch the records anyway it is better to
293
+ # check <tt>collection.length.zero?</tt>.
294
+ def empty?
295
+ if loaded? || options[:counter_sql]
296
+ size.zero?
297
+ else
298
+ @target.blank? && !scope.exists?
299
+ end
300
+ end
301
+
302
+ # Returns true if the collections is not empty.
303
+ # Equivalent to +!collection.empty?+.
304
+ def any?
305
+ if block_given?
306
+ load_target.any? { |*block_args| yield(*block_args) }
307
+ else
308
+ !empty?
309
+ end
310
+ end
311
+
312
+ # Returns true if the collection has more than 1 record.
313
+ # Equivalent to +collection.size > 1+.
314
+ def many?
315
+ if block_given?
316
+ load_target.many? { |*block_args| yield(*block_args) }
317
+ else
318
+ size > 1
319
+ end
320
+ end
321
+
322
+ def distinct
323
+ seen = {}
324
+ load_target.find_all do |record|
325
+ seen[record.id] = true unless seen.key?(record.id)
326
+ end
327
+ end
328
+ alias uniq distinct
329
+
330
+ # Replace this collection with +other_array+. This will perform a diff
331
+ # and delete/add only records that have changed.
332
+ def replace(other_array)
333
+ other_array.each { |val| raise_on_type_mismatch!(val) }
334
+ original_target = load_target.dup
335
+
336
+ if owner.new_record?
337
+ replace_records(other_array, original_target)
338
+ else
339
+ transaction { replace_records(other_array, original_target) }
340
+ end
341
+ end
342
+
343
+ def include?(record)
344
+ if record.is_a?(reflection.klass)
345
+ if record.new_record?
346
+ include_in_memory?(record)
347
+ else
348
+ load_target if options[:finder_sql]
349
+ loaded? ? target.include?(record) : scope.exists?(record)
350
+ end
351
+ else
352
+ false
353
+ end
354
+ end
355
+
356
+ def load_target
357
+ if find_target?
358
+ @target = merge_target_lists(find_target, target)
359
+ end
360
+
361
+ loaded!
362
+ target
363
+ end
364
+
365
+ def add_to_target(record)
366
+ callback(:before_add, record)
367
+ yield(record) if block_given?
368
+
369
+ if association_scope.distinct_value && index = @target.index(record)
370
+ @target[index] = record
371
+ else
372
+ @target << record
373
+ end
374
+
375
+ callback(:after_add, record)
376
+ set_inverse_instance(record)
377
+
378
+ record
379
+ end
380
+
381
+ def scope(opts = {})
382
+ scope = super()
383
+ scope.none! if opts.fetch(:nullify, true) && null_scope?
384
+ scope
385
+ end
386
+
387
+ def null_scope?
388
+ owner.new_record? && !foreign_key_present?
389
+ end
390
+
391
+ private
392
+
393
+ def custom_counter_sql
394
+ if options[:counter_sql]
395
+ interpolate(options[:counter_sql])
396
+ else
397
+ # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
398
+ interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
399
+ count_with = $2.to_s
400
+ count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
401
+ "SELECT #{$1}COUNT(#{count_with}) FROM"
402
+ end
403
+ end
404
+ end
405
+
406
+ def custom_finder_sql
407
+ interpolate(options[:finder_sql])
408
+ end
409
+
410
+ def find_target
411
+ records =
412
+ if options[:finder_sql]
413
+ reflection.klass.find_by_sql(custom_finder_sql)
414
+ else
415
+ scope.to_a
416
+ end
417
+
418
+ records.each { |record| set_inverse_instance(record) }
419
+ records
420
+ end
421
+
422
+ # We have some records loaded from the database (persisted) and some that are
423
+ # in-memory (memory). The same record may be represented in the persisted array
424
+ # and in the memory array.
425
+ #
426
+ # So the task of this method is to merge them according to the following rules:
427
+ #
428
+ # * The final array must not have duplicates
429
+ # * The order of the persisted array is to be preserved
430
+ # * Any changes made to attributes on objects in the memory array are to be preserved
431
+ # * Otherwise, attributes should have the value found in the database
432
+ def merge_target_lists(persisted, memory)
433
+ return persisted if memory.empty?
434
+ return memory if persisted.empty?
435
+
436
+ persisted.map! do |record|
437
+ if mem_record = memory.delete(record)
438
+
439
+ ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name|
440
+ mem_record[name] = record[name]
441
+ end
442
+
443
+ mem_record
444
+ else
445
+ record
446
+ end
447
+ end
448
+
449
+ persisted + memory
450
+ end
451
+
452
+ def create_record(attributes, raise = false, &block)
453
+ unless owner.persisted?
454
+ raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
455
+ end
456
+
457
+ if attributes.is_a?(Array)
458
+ attributes.collect { |attr| create_record(attr, raise, &block) }
459
+ else
460
+ transaction do
461
+ add_to_target(build_record(attributes)) do |record|
462
+ yield(record) if block_given?
463
+ insert_record(record, true, raise)
464
+ end
465
+ end
466
+ end
467
+ end
468
+
469
+ # Do the relevant stuff to insert the given record into the association collection.
470
+ def insert_record(record, validate = true, raise = false)
471
+ raise NotImplementedError
472
+ end
473
+
474
+ def create_scope
475
+ scope.scope_for_create.stringify_keys
476
+ end
477
+
478
+ def delete_or_destroy(records, method)
479
+ records = records.flatten
480
+ records.each { |record| raise_on_type_mismatch!(record) }
481
+ existing_records = records.reject { |r| r.new_record? }
482
+
483
+ if existing_records.empty?
484
+ remove_records(existing_records, records, method)
485
+ else
486
+ transaction { remove_records(existing_records, records, method) }
487
+ end
488
+ end
489
+
490
+ def remove_records(existing_records, records, method)
491
+ records.each { |record| callback(:before_remove, record) }
492
+
493
+ delete_records(existing_records, method) if existing_records.any?
494
+ records.each { |record| target.delete(record) }
495
+
496
+ records.each { |record| callback(:after_remove, record) }
497
+ end
498
+
499
+ # Delete the given records from the association, using one of the methods :destroy,
500
+ # :delete_all or :nullify (or nil, in which case a default is used).
501
+ def delete_records(records, method)
502
+ raise NotImplementedError
503
+ end
504
+
505
+ def replace_records(new_target, original_target)
506
+ delete(target - new_target)
507
+
508
+ unless concat(new_target - target)
509
+ @target = original_target
510
+ raise RecordNotSaved, "Failed to replace #{reflection.name} because one or more of the " \
511
+ "new records could not be saved."
512
+ end
513
+
514
+ target
515
+ end
516
+
517
+ def concat_records(records)
518
+ result = true
519
+
520
+ records.flatten.each do |record|
521
+ raise_on_type_mismatch!(record)
522
+ add_to_target(record) do |rec|
523
+ result &&= insert_record(rec) unless owner.new_record?
524
+ end
525
+ end
526
+
527
+ result && records
528
+ end
529
+
530
+ def callback(method, record)
531
+ callbacks_for(method).each do |callback|
532
+ case callback
533
+ when Symbol
534
+ owner.send(callback, record)
535
+ when Proc
536
+ callback.call(owner, record)
537
+ else
538
+ callback.send(method, owner, record)
539
+ end
540
+ end
541
+ end
542
+
543
+ def callbacks_for(callback_name)
544
+ full_callback_name = "#{callback_name}_for_#{reflection.name}"
545
+ owner.class.send(full_callback_name.to_sym) || []
546
+ end
547
+
548
+ # Should we deal with assoc.first or assoc.last by issuing an independent query to
549
+ # the database, or by getting the target, and then taking the first/last item from that?
550
+ #
551
+ # If the args is just a non-empty options hash, go to the database.
552
+ #
553
+ # Otherwise, go to the database only if none of the following are true:
554
+ # * target already loaded
555
+ # * owner is new record
556
+ # * custom :finder_sql exists
557
+ # * target contains new or changed record(s)
558
+ # * the first arg is an integer (which indicates the number of records to be returned)
559
+ def fetch_first_or_last_using_find?(args)
560
+ if args.first.is_a?(Hash)
561
+ true
562
+ else
563
+ !(loaded? ||
564
+ owner.new_record? ||
565
+ options[:finder_sql] ||
566
+ target.any? { |record| record.new_record? || record.changed? } ||
567
+ args.first.kind_of?(Integer))
568
+ end
569
+ end
570
+
571
+ def include_in_memory?(record)
572
+ if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
573
+ owner.send(reflection.through_reflection.name).any? { |source|
574
+ target = source.send(reflection.source_reflection.name)
575
+ target.respond_to?(:include?) ? target.include?(record) : target == record
576
+ } || target.include?(record)
577
+ else
578
+ target.include?(record)
579
+ end
580
+ end
581
+
582
+ # If using a custom finder_sql or if the :inverse_of option has been
583
+ # specified, then #find scans the entire collection.
584
+ def find_by_scan(*args)
585
+ expects_array = args.first.kind_of?(Array)
586
+ ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq
587
+
588
+ if ids.size == 1
589
+ id = ids.first
590
+ record = load_target.detect { |r| id == r.id }
591
+ expects_array ? [ record ] : record
592
+ else
593
+ load_target.select { |r| ids.include?(r.id) }
594
+ end
595
+ end
596
+
597
+ # Fetches the first/last using SQL if possible, otherwise from the target array.
598
+ def first_or_last(type, *args)
599
+ args.shift if args.first.is_a?(Hash) && args.first.empty?
600
+
601
+ collection = fetch_first_or_last_using_find?(args) ? scope : load_target
602
+ collection.send(type, *args).tap do |record|
603
+ set_inverse_instance record if record.is_a? ActiveRecord::Base
604
+ end
605
+ end
606
+ end
607
+ end
608
+ end