activerecord_authorails 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. data/CHANGELOG +3043 -0
  2. data/README +360 -0
  3. data/RUNNING_UNIT_TESTS +64 -0
  4. data/Rakefile +226 -0
  5. data/examples/associations.png +0 -0
  6. data/examples/associations.rb +87 -0
  7. data/examples/shared_setup.rb +15 -0
  8. data/examples/validation.rb +85 -0
  9. data/install.rb +30 -0
  10. data/lib/active_record.rb +85 -0
  11. data/lib/active_record/acts/list.rb +244 -0
  12. data/lib/active_record/acts/nested_set.rb +211 -0
  13. data/lib/active_record/acts/tree.rb +89 -0
  14. data/lib/active_record/aggregations.rb +191 -0
  15. data/lib/active_record/associations.rb +1637 -0
  16. data/lib/active_record/associations/association_collection.rb +190 -0
  17. data/lib/active_record/associations/association_proxy.rb +158 -0
  18. data/lib/active_record/associations/belongs_to_association.rb +56 -0
  19. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  20. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +169 -0
  21. data/lib/active_record/associations/has_many_association.rb +210 -0
  22. data/lib/active_record/associations/has_many_through_association.rb +247 -0
  23. data/lib/active_record/associations/has_one_association.rb +80 -0
  24. data/lib/active_record/attribute_methods.rb +75 -0
  25. data/lib/active_record/base.rb +2164 -0
  26. data/lib/active_record/calculations.rb +270 -0
  27. data/lib/active_record/callbacks.rb +367 -0
  28. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +279 -0
  29. data/lib/active_record/connection_adapters/abstract/database_statements.rb +130 -0
  30. data/lib/active_record/connection_adapters/abstract/quoting.rb +58 -0
  31. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +343 -0
  32. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +310 -0
  33. data/lib/active_record/connection_adapters/abstract_adapter.rb +161 -0
  34. data/lib/active_record/connection_adapters/db2_adapter.rb +228 -0
  35. data/lib/active_record/connection_adapters/firebird_adapter.rb +728 -0
  36. data/lib/active_record/connection_adapters/frontbase_adapter.rb +861 -0
  37. data/lib/active_record/connection_adapters/mysql_adapter.rb +414 -0
  38. data/lib/active_record/connection_adapters/openbase_adapter.rb +350 -0
  39. data/lib/active_record/connection_adapters/oracle_adapter.rb +689 -0
  40. data/lib/active_record/connection_adapters/postgresql_adapter.rb +584 -0
  41. data/lib/active_record/connection_adapters/sqlite_adapter.rb +407 -0
  42. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +591 -0
  43. data/lib/active_record/connection_adapters/sybase_adapter.rb +662 -0
  44. data/lib/active_record/deprecated_associations.rb +104 -0
  45. data/lib/active_record/deprecated_finders.rb +44 -0
  46. data/lib/active_record/fixtures.rb +628 -0
  47. data/lib/active_record/locking/optimistic.rb +106 -0
  48. data/lib/active_record/locking/pessimistic.rb +77 -0
  49. data/lib/active_record/migration.rb +394 -0
  50. data/lib/active_record/observer.rb +178 -0
  51. data/lib/active_record/query_cache.rb +64 -0
  52. data/lib/active_record/reflection.rb +222 -0
  53. data/lib/active_record/schema.rb +58 -0
  54. data/lib/active_record/schema_dumper.rb +149 -0
  55. data/lib/active_record/timestamp.rb +51 -0
  56. data/lib/active_record/transactions.rb +136 -0
  57. data/lib/active_record/validations.rb +843 -0
  58. data/lib/active_record/vendor/db2.rb +362 -0
  59. data/lib/active_record/vendor/mysql.rb +1214 -0
  60. data/lib/active_record/vendor/simple.rb +693 -0
  61. data/lib/active_record/version.rb +9 -0
  62. data/lib/active_record/wrappers/yaml_wrapper.rb +15 -0
  63. data/lib/active_record/wrappings.rb +58 -0
  64. data/lib/active_record/xml_serialization.rb +308 -0
  65. data/test/aaa_create_tables_test.rb +59 -0
  66. data/test/abstract_unit.rb +77 -0
  67. data/test/active_schema_test_mysql.rb +31 -0
  68. data/test/adapter_test.rb +87 -0
  69. data/test/adapter_test_sqlserver.rb +81 -0
  70. data/test/aggregations_test.rb +95 -0
  71. data/test/all.sh +8 -0
  72. data/test/ar_schema_test.rb +33 -0
  73. data/test/association_inheritance_reload.rb +14 -0
  74. data/test/associations/callbacks_test.rb +126 -0
  75. data/test/associations/cascaded_eager_loading_test.rb +138 -0
  76. data/test/associations/eager_test.rb +393 -0
  77. data/test/associations/extension_test.rb +42 -0
  78. data/test/associations/join_model_test.rb +497 -0
  79. data/test/associations_test.rb +1809 -0
  80. data/test/attribute_methods_test.rb +49 -0
  81. data/test/base_test.rb +1586 -0
  82. data/test/binary_test.rb +37 -0
  83. data/test/calculations_test.rb +219 -0
  84. data/test/callbacks_test.rb +377 -0
  85. data/test/class_inheritable_attributes_test.rb +32 -0
  86. data/test/column_alias_test.rb +17 -0
  87. data/test/connection_test_firebird.rb +8 -0
  88. data/test/connections/native_db2/connection.rb +25 -0
  89. data/test/connections/native_firebird/connection.rb +26 -0
  90. data/test/connections/native_frontbase/connection.rb +27 -0
  91. data/test/connections/native_mysql/connection.rb +24 -0
  92. data/test/connections/native_openbase/connection.rb +21 -0
  93. data/test/connections/native_oracle/connection.rb +27 -0
  94. data/test/connections/native_postgresql/connection.rb +23 -0
  95. data/test/connections/native_sqlite/connection.rb +34 -0
  96. data/test/connections/native_sqlite3/connection.rb +34 -0
  97. data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
  98. data/test/connections/native_sqlserver/connection.rb +23 -0
  99. data/test/connections/native_sqlserver_odbc/connection.rb +25 -0
  100. data/test/connections/native_sybase/connection.rb +23 -0
  101. data/test/copy_table_sqlite.rb +64 -0
  102. data/test/datatype_test_postgresql.rb +52 -0
  103. data/test/default_test_firebird.rb +16 -0
  104. data/test/defaults_test.rb +60 -0
  105. data/test/deprecated_associations_test.rb +396 -0
  106. data/test/deprecated_finder_test.rb +151 -0
  107. data/test/empty_date_time_test.rb +25 -0
  108. data/test/finder_test.rb +504 -0
  109. data/test/fixtures/accounts.yml +28 -0
  110. data/test/fixtures/author.rb +99 -0
  111. data/test/fixtures/author_favorites.yml +4 -0
  112. data/test/fixtures/authors.yml +7 -0
  113. data/test/fixtures/auto_id.rb +4 -0
  114. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
  115. data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
  116. data/test/fixtures/bad_fixtures/blank_line +3 -0
  117. data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
  118. data/test/fixtures/bad_fixtures/missing_value +1 -0
  119. data/test/fixtures/binary.rb +2 -0
  120. data/test/fixtures/categories.yml +14 -0
  121. data/test/fixtures/categories/special_categories.yml +9 -0
  122. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  123. data/test/fixtures/categories_ordered.yml +7 -0
  124. data/test/fixtures/categories_posts.yml +23 -0
  125. data/test/fixtures/categorization.rb +5 -0
  126. data/test/fixtures/categorizations.yml +17 -0
  127. data/test/fixtures/category.rb +20 -0
  128. data/test/fixtures/column_name.rb +3 -0
  129. data/test/fixtures/comment.rb +23 -0
  130. data/test/fixtures/comments.yml +59 -0
  131. data/test/fixtures/companies.yml +55 -0
  132. data/test/fixtures/company.rb +107 -0
  133. data/test/fixtures/company_in_module.rb +59 -0
  134. data/test/fixtures/computer.rb +3 -0
  135. data/test/fixtures/computers.yml +4 -0
  136. data/test/fixtures/course.rb +3 -0
  137. data/test/fixtures/courses.yml +7 -0
  138. data/test/fixtures/customer.rb +55 -0
  139. data/test/fixtures/customers.yml +17 -0
  140. data/test/fixtures/db_definitions/db2.drop.sql +32 -0
  141. data/test/fixtures/db_definitions/db2.sql +231 -0
  142. data/test/fixtures/db_definitions/db22.drop.sql +2 -0
  143. data/test/fixtures/db_definitions/db22.sql +5 -0
  144. data/test/fixtures/db_definitions/firebird.drop.sql +63 -0
  145. data/test/fixtures/db_definitions/firebird.sql +304 -0
  146. data/test/fixtures/db_definitions/firebird2.drop.sql +2 -0
  147. data/test/fixtures/db_definitions/firebird2.sql +6 -0
  148. data/test/fixtures/db_definitions/frontbase.drop.sql +32 -0
  149. data/test/fixtures/db_definitions/frontbase.sql +268 -0
  150. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  151. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  152. data/test/fixtures/db_definitions/mysql.drop.sql +32 -0
  153. data/test/fixtures/db_definitions/mysql.sql +234 -0
  154. data/test/fixtures/db_definitions/mysql2.drop.sql +2 -0
  155. data/test/fixtures/db_definitions/mysql2.sql +5 -0
  156. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  157. data/test/fixtures/db_definitions/openbase.sql +302 -0
  158. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  159. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  160. data/test/fixtures/db_definitions/oracle.drop.sql +65 -0
  161. data/test/fixtures/db_definitions/oracle.sql +325 -0
  162. data/test/fixtures/db_definitions/oracle2.drop.sql +2 -0
  163. data/test/fixtures/db_definitions/oracle2.sql +6 -0
  164. data/test/fixtures/db_definitions/postgresql.drop.sql +37 -0
  165. data/test/fixtures/db_definitions/postgresql.sql +263 -0
  166. data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
  167. data/test/fixtures/db_definitions/postgresql2.sql +5 -0
  168. data/test/fixtures/db_definitions/schema.rb +60 -0
  169. data/test/fixtures/db_definitions/sqlite.drop.sql +32 -0
  170. data/test/fixtures/db_definitions/sqlite.sql +215 -0
  171. data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
  172. data/test/fixtures/db_definitions/sqlite2.sql +5 -0
  173. data/test/fixtures/db_definitions/sqlserver.drop.sql +34 -0
  174. data/test/fixtures/db_definitions/sqlserver.sql +243 -0
  175. data/test/fixtures/db_definitions/sqlserver2.drop.sql +2 -0
  176. data/test/fixtures/db_definitions/sqlserver2.sql +5 -0
  177. data/test/fixtures/db_definitions/sybase.drop.sql +34 -0
  178. data/test/fixtures/db_definitions/sybase.sql +218 -0
  179. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  180. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  181. data/test/fixtures/default.rb +2 -0
  182. data/test/fixtures/developer.rb +52 -0
  183. data/test/fixtures/developers.yml +21 -0
  184. data/test/fixtures/developers_projects.yml +17 -0
  185. data/test/fixtures/developers_projects/david_action_controller +3 -0
  186. data/test/fixtures/developers_projects/david_active_record +3 -0
  187. data/test/fixtures/developers_projects/jamis_active_record +2 -0
  188. data/test/fixtures/edge.rb +5 -0
  189. data/test/fixtures/edges.yml +6 -0
  190. data/test/fixtures/entrant.rb +3 -0
  191. data/test/fixtures/entrants.yml +14 -0
  192. data/test/fixtures/fk_test_has_fk.yml +3 -0
  193. data/test/fixtures/fk_test_has_pk.yml +2 -0
  194. data/test/fixtures/flowers.jpg +0 -0
  195. data/test/fixtures/funny_jokes.yml +10 -0
  196. data/test/fixtures/joke.rb +6 -0
  197. data/test/fixtures/keyboard.rb +3 -0
  198. data/test/fixtures/legacy_thing.rb +3 -0
  199. data/test/fixtures/legacy_things.yml +3 -0
  200. data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
  201. data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
  202. data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
  203. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  204. data/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb +9 -0
  205. data/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb +12 -0
  206. data/test/fixtures/migrations_with_duplicate/3_foo.rb +7 -0
  207. data/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb +12 -0
  208. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  209. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  210. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  211. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  212. data/test/fixtures/mixed_case_monkey.rb +3 -0
  213. data/test/fixtures/mixed_case_monkeys.yml +6 -0
  214. data/test/fixtures/mixin.rb +63 -0
  215. data/test/fixtures/mixins.yml +127 -0
  216. data/test/fixtures/movie.rb +5 -0
  217. data/test/fixtures/movies.yml +7 -0
  218. data/test/fixtures/naked/csv/accounts.csv +1 -0
  219. data/test/fixtures/naked/yml/accounts.yml +1 -0
  220. data/test/fixtures/naked/yml/companies.yml +1 -0
  221. data/test/fixtures/naked/yml/courses.yml +1 -0
  222. data/test/fixtures/order.rb +4 -0
  223. data/test/fixtures/people.yml +3 -0
  224. data/test/fixtures/person.rb +4 -0
  225. data/test/fixtures/post.rb +58 -0
  226. data/test/fixtures/posts.yml +48 -0
  227. data/test/fixtures/project.rb +27 -0
  228. data/test/fixtures/projects.yml +7 -0
  229. data/test/fixtures/reader.rb +4 -0
  230. data/test/fixtures/readers.yml +4 -0
  231. data/test/fixtures/reply.rb +37 -0
  232. data/test/fixtures/subject.rb +4 -0
  233. data/test/fixtures/subscriber.rb +6 -0
  234. data/test/fixtures/subscribers/first +2 -0
  235. data/test/fixtures/subscribers/second +2 -0
  236. data/test/fixtures/tag.rb +7 -0
  237. data/test/fixtures/tagging.rb +6 -0
  238. data/test/fixtures/taggings.yml +18 -0
  239. data/test/fixtures/tags.yml +7 -0
  240. data/test/fixtures/task.rb +3 -0
  241. data/test/fixtures/tasks.yml +7 -0
  242. data/test/fixtures/topic.rb +25 -0
  243. data/test/fixtures/topics.yml +22 -0
  244. data/test/fixtures/vertex.rb +9 -0
  245. data/test/fixtures/vertices.yml +4 -0
  246. data/test/fixtures_test.rb +401 -0
  247. data/test/inheritance_test.rb +205 -0
  248. data/test/lifecycle_test.rb +137 -0
  249. data/test/locking_test.rb +190 -0
  250. data/test/method_scoping_test.rb +416 -0
  251. data/test/migration_test.rb +768 -0
  252. data/test/migration_test_firebird.rb +124 -0
  253. data/test/mixin_nested_set_test.rb +196 -0
  254. data/test/mixin_test.rb +550 -0
  255. data/test/modules_test.rb +34 -0
  256. data/test/multiple_db_test.rb +60 -0
  257. data/test/pk_test.rb +104 -0
  258. data/test/readonly_test.rb +107 -0
  259. data/test/reflection_test.rb +159 -0
  260. data/test/schema_authorization_test_postgresql.rb +75 -0
  261. data/test/schema_dumper_test.rb +96 -0
  262. data/test/schema_test_postgresql.rb +64 -0
  263. data/test/synonym_test_oracle.rb +17 -0
  264. data/test/table_name_test_sqlserver.rb +23 -0
  265. data/test/threaded_connections_test.rb +48 -0
  266. data/test/transactions_test.rb +230 -0
  267. data/test/unconnected_test.rb +32 -0
  268. data/test/validations_test.rb +1097 -0
  269. data/test/xml_serialization_test.rb +125 -0
  270. metadata +365 -0
@@ -0,0 +1,169 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
4
+ def initialize(owner, reflection)
5
+ super
6
+ construct_sql
7
+ end
8
+
9
+ def build(attributes = {})
10
+ load_target
11
+ record = @reflection.klass.new(attributes)
12
+ @target << record
13
+ record
14
+ end
15
+
16
+ def create(attributes = {})
17
+ # Can't use Base.create since the foreign key may be a protected attribute.
18
+ if attributes.is_a?(Array)
19
+ attributes.collect { |attr| create(attr) }
20
+ else
21
+ record = build(attributes)
22
+ insert_record(record) unless @owner.new_record?
23
+ record
24
+ end
25
+ end
26
+
27
+ def find_first
28
+ load_target.first
29
+ end
30
+
31
+ def find(*args)
32
+ options = Base.send(:extract_options_from_args!, args)
33
+
34
+ # If using a custom finder_sql, scan the entire collection.
35
+ if @reflection.options[:finder_sql]
36
+ expects_array = args.first.kind_of?(Array)
37
+ ids = args.flatten.compact.uniq
38
+
39
+ if ids.size == 1
40
+ id = ids.first.to_i
41
+ record = load_target.detect { |record| id == record.id }
42
+ expects_array ? [record] : record
43
+ else
44
+ load_target.select { |record| ids.include?(record.id) }
45
+ end
46
+ else
47
+ conditions = "#{@finder_sql}"
48
+
49
+ if sanitized_conditions = sanitize_sql(options[:conditions])
50
+ conditions << " AND (#{sanitized_conditions})"
51
+ end
52
+
53
+ options[:conditions] = conditions
54
+ options[:joins] = @join_sql
55
+ options[:readonly] = finding_with_ambigious_select?(options[:select])
56
+
57
+ if options[:order] && @reflection.options[:order]
58
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
59
+ elsif @reflection.options[:order]
60
+ options[:order] = @reflection.options[:order]
61
+ end
62
+
63
+ merge_options_from_reflection!(options)
64
+
65
+ # Pass through args exactly as we received them.
66
+ args << options
67
+ @reflection.klass.find(*args)
68
+ end
69
+ end
70
+
71
+ # Deprecated as of Rails 1.2. If your associations require attributes
72
+ # you should be using has_many :through
73
+ def push_with_attributes(record, join_attributes = {})
74
+ raise_on_type_mismatch(record)
75
+ join_attributes.each { |key, value| record[key.to_s] = value }
76
+
77
+ callback(:before_add, record)
78
+ insert_record(record) unless @owner.new_record?
79
+ @target << record
80
+ callback(:after_add, record)
81
+
82
+ self
83
+ end
84
+ deprecate :push_with_attributes => "consider using has_many :through instead"
85
+
86
+ alias :concat_with_attributes :push_with_attributes
87
+
88
+ protected
89
+ def method_missing(method, *args, &block)
90
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
91
+ super
92
+ else
93
+ @reflection.klass.with_scope(:find => { :conditions => @finder_sql, :joins => @join_sql, :readonly => false }) do
94
+ @reflection.klass.send(method, *args, &block)
95
+ end
96
+ end
97
+ end
98
+
99
+ def count_records
100
+ load_target.size
101
+ end
102
+
103
+ def insert_record(record)
104
+ if record.new_record?
105
+ return false unless record.save
106
+ end
107
+
108
+ if @reflection.options[:insert_sql]
109
+ @owner.connection.execute(interpolate_sql(@reflection.options[:insert_sql], record))
110
+ else
111
+ columns = @owner.connection.columns(@reflection.options[:join_table], "#{@reflection.options[:join_table]} Columns")
112
+
113
+ attributes = columns.inject({}) do |attributes, column|
114
+ case column.name
115
+ when @reflection.primary_key_name
116
+ attributes[column.name] = @owner.quoted_id
117
+ when @reflection.association_foreign_key
118
+ attributes[column.name] = record.quoted_id
119
+ else
120
+ if record.attributes.has_key?(column.name)
121
+ value = @owner.send(:quote_value, record[column.name], column)
122
+ attributes[column.name] = value unless value.nil?
123
+ end
124
+ end
125
+ attributes
126
+ end
127
+
128
+ sql =
129
+ "INSERT INTO #{@reflection.options[:join_table]} (#{@owner.send(:quoted_column_names, attributes).join(', ')}) " +
130
+ "VALUES (#{attributes.values.join(', ')})"
131
+
132
+ @owner.connection.execute(sql)
133
+ end
134
+
135
+ return true
136
+ end
137
+
138
+ def delete_records(records)
139
+ if sql = @reflection.options[:delete_sql]
140
+ records.each { |record| @owner.connection.execute(interpolate_sql(sql, record)) }
141
+ else
142
+ ids = quoted_record_ids(records)
143
+ sql = "DELETE FROM #{@reflection.options[:join_table]} WHERE #{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.association_foreign_key} IN (#{ids})"
144
+ @owner.connection.execute(sql)
145
+ end
146
+ end
147
+
148
+ def construct_sql
149
+ interpolate_sql_options!(@reflection.options, :finder_sql)
150
+
151
+ if @reflection.options[:finder_sql]
152
+ @finder_sql = @reflection.options[:finder_sql]
153
+ else
154
+ @finder_sql = "#{@reflection.options[:join_table]}.#{@reflection.primary_key_name} = #{@owner.quoted_id} "
155
+ @finder_sql << " AND (#{conditions})" if conditions
156
+ end
157
+
158
+ @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}"
159
+ end
160
+
161
+ # Join tables with additional columns on top of the two foreign keys must be considered ambigious unless a select
162
+ # clause has been explicitly defined. Otherwise you can get broken records back, if, say, the join column also has
163
+ # and id column, which will then overwrite the id column of the records coming back.
164
+ def finding_with_ambigious_select?(select_clause)
165
+ !select_clause && @owner.connection.columns(@reflection.options[:join_table], "Join Table Columns").size != 2
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,210 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class HasManyAssociation < AssociationCollection #:nodoc:
4
+ def initialize(owner, reflection)
5
+ super
6
+ construct_sql
7
+ end
8
+
9
+ def build(attributes = {})
10
+ if attributes.is_a?(Array)
11
+ attributes.collect { |attr| build(attr) }
12
+ else
13
+ record = @reflection.klass.new(attributes)
14
+ set_belongs_to_association_for(record)
15
+
16
+ @target ||= [] unless loaded?
17
+ @target << record
18
+
19
+ record
20
+ end
21
+ end
22
+
23
+ # DEPRECATED.
24
+ def find_all(runtime_conditions = nil, orderings = nil, limit = nil, joins = nil)
25
+ if @reflection.options[:finder_sql]
26
+ @reflection.klass.find_by_sql(@finder_sql)
27
+ else
28
+ conditions = @finder_sql
29
+ conditions += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions
30
+ orderings ||= @reflection.options[:order]
31
+ @reflection.klass.find_all(conditions, orderings, limit, joins)
32
+ end
33
+ end
34
+ deprecate :find_all => "use find(:all, ...) instead"
35
+
36
+ # DEPRECATED. Find the first associated record. All arguments are optional.
37
+ def find_first(conditions = nil, orderings = nil)
38
+ find_all(conditions, orderings, 1).first
39
+ end
40
+ deprecate :find_first => "use find(:first, ...) instead"
41
+
42
+ # Count the number of associated records. All arguments are optional.
43
+ def count(*args)
44
+ if @reflection.options[:counter_sql]
45
+ @reflection.klass.count_by_sql(@counter_sql)
46
+ elsif @reflection.options[:finder_sql]
47
+ @reflection.klass.count_by_sql(@finder_sql)
48
+ else
49
+ column_name, options = @reflection.klass.send(:construct_count_options_from_legacy_args, *args)
50
+ options[:conditions] = options[:conditions].nil? ?
51
+ @finder_sql :
52
+ @finder_sql + " AND (#{sanitize_sql(options[:conditions])})"
53
+ options[:include] = @reflection.options[:include]
54
+
55
+ @reflection.klass.count(column_name, options)
56
+ end
57
+ end
58
+
59
+ def find(*args)
60
+ options = Base.send(:extract_options_from_args!, args)
61
+
62
+ # If using a custom finder_sql, scan the entire collection.
63
+ if @reflection.options[:finder_sql]
64
+ expects_array = args.first.kind_of?(Array)
65
+ ids = args.flatten.compact.uniq
66
+
67
+ if ids.size == 1
68
+ id = ids.first
69
+ record = load_target.detect { |record| id == record.id }
70
+ expects_array ? [ record ] : record
71
+ else
72
+ load_target.select { |record| ids.include?(record.id) }
73
+ end
74
+ else
75
+ conditions = "#{@finder_sql}"
76
+ if sanitized_conditions = sanitize_sql(options[:conditions])
77
+ conditions << " AND (#{sanitized_conditions})"
78
+ end
79
+ options[:conditions] = conditions
80
+
81
+ if options[:order] && @reflection.options[:order]
82
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
83
+ elsif @reflection.options[:order]
84
+ options[:order] = @reflection.options[:order]
85
+ end
86
+
87
+ merge_options_from_reflection!(options)
88
+
89
+ # Pass through args exactly as we received them.
90
+ args << options
91
+ @reflection.klass.find(*args)
92
+ end
93
+ end
94
+
95
+ protected
96
+ def method_missing(method, *args, &block)
97
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
98
+ super
99
+ else
100
+ create_scoping = {}
101
+ set_belongs_to_association_for(create_scoping)
102
+
103
+ @reflection.klass.with_scope(
104
+ :create => create_scoping,
105
+ :find => {
106
+ :conditions => @finder_sql,
107
+ :joins => @join_sql,
108
+ :readonly => false
109
+ }
110
+ ) do
111
+ @reflection.klass.send(method, *args, &block)
112
+ end
113
+ end
114
+ end
115
+
116
+ def load_target
117
+ if !@owner.new_record? || foreign_key_present
118
+ begin
119
+ if !loaded?
120
+ if @target.is_a?(Array) && @target.any?
121
+ @target = (find_target + @target).uniq
122
+ else
123
+ @target = find_target
124
+ end
125
+ end
126
+ rescue ActiveRecord::RecordNotFound
127
+ reset
128
+ end
129
+ end
130
+
131
+ loaded if target
132
+ target
133
+ end
134
+
135
+ def count_records
136
+ count = if has_cached_counter?
137
+ @owner.send(:read_attribute, cached_counter_attribute_name)
138
+ elsif @reflection.options[:counter_sql]
139
+ @reflection.klass.count_by_sql(@counter_sql)
140
+ else
141
+ @reflection.klass.count(:conditions => @counter_sql)
142
+ end
143
+
144
+ @target = [] and loaded if count == 0
145
+
146
+ if @reflection.options[:limit]
147
+ count = [ @reflection.options[:limit], count ].min
148
+ end
149
+
150
+ return count
151
+ end
152
+
153
+ def has_cached_counter?
154
+ @owner.attribute_present?(cached_counter_attribute_name)
155
+ end
156
+
157
+ def cached_counter_attribute_name
158
+ "#{@reflection.name}_count"
159
+ end
160
+
161
+ def insert_record(record)
162
+ set_belongs_to_association_for(record)
163
+ record.save
164
+ end
165
+
166
+ def delete_records(records)
167
+ if @reflection.options[:dependent]
168
+ records.each { |r| r.destroy }
169
+ else
170
+ ids = quoted_record_ids(records)
171
+ @reflection.klass.update_all(
172
+ "#{@reflection.primary_key_name} = NULL",
173
+ "#{@reflection.primary_key_name} = #{@owner.quoted_id} AND #{@reflection.klass.primary_key} IN (#{ids})"
174
+ )
175
+ end
176
+ end
177
+
178
+ def target_obsolete?
179
+ false
180
+ end
181
+
182
+ def construct_sql
183
+ case
184
+ when @reflection.options[:finder_sql]
185
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
186
+
187
+ when @reflection.options[:as]
188
+ @finder_sql =
189
+ "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
190
+ "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
191
+ @finder_sql << " AND (#{conditions})" if conditions
192
+
193
+ else
194
+ @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
195
+ @finder_sql << " AND (#{conditions})" if conditions
196
+ end
197
+
198
+ if @reflection.options[:counter_sql]
199
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
200
+ elsif @reflection.options[:finder_sql]
201
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
202
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
203
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
204
+ else
205
+ @counter_sql = @finder_sql
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,247 @@
1
+ module ActiveRecord
2
+ module Associations
3
+ class HasManyThroughAssociation < AssociationProxy #:nodoc:
4
+ def initialize(owner, reflection)
5
+ super
6
+ reflection.check_validity!
7
+ @finder_sql = construct_conditions
8
+ construct_sql
9
+ end
10
+
11
+ def find(*args)
12
+ options = Base.send(:extract_options_from_args!, args)
13
+
14
+ conditions = "#{@finder_sql}"
15
+ if sanitized_conditions = sanitize_sql(options[:conditions])
16
+ conditions << " AND (#{sanitized_conditions})"
17
+ end
18
+ options[:conditions] = conditions
19
+
20
+ if options[:order] && @reflection.options[:order]
21
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
22
+ elsif @reflection.options[:order]
23
+ options[:order] = @reflection.options[:order]
24
+ end
25
+
26
+ options[:select] = construct_select(options[:select])
27
+ options[:from] ||= construct_from
28
+ options[:joins] = construct_joins(options[:joins])
29
+ options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
30
+
31
+ merge_options_from_reflection!(options)
32
+
33
+ # Pass through args exactly as we received them.
34
+ args << options
35
+ @reflection.klass.find(*args)
36
+ end
37
+
38
+ def reset
39
+ @target = []
40
+ @loaded = false
41
+ end
42
+
43
+ # Adds records to the association. The source record and its associates
44
+ # must have ids in order to create records associating them, so this
45
+ # will raise ActiveRecord::HasManyThroughCantAssociateNewRecords if
46
+ # either is a new record. Calls create! so you can rescue errors.
47
+ #
48
+ # The :before_add and :after_add callbacks are not yet supported.
49
+ def <<(*records)
50
+ return if records.empty?
51
+ through = @reflection.through_reflection
52
+ raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) if @owner.new_record?
53
+
54
+ load_target
55
+
56
+ klass = through.klass
57
+ klass.transaction do
58
+ flatten_deeper(records).each do |associate|
59
+ raise_on_type_mismatch(associate)
60
+ raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, through) unless associate.respond_to?(:new_record?) && !associate.new_record?
61
+
62
+ @owner.send(@reflection.through_reflection.name).proxy_target << klass.with_scope(:create => construct_join_attributes(associate)) { klass.create! }
63
+ @target << associate
64
+ end
65
+ end
66
+
67
+ self
68
+ end
69
+
70
+ [:push, :concat].each { |method| alias_method method, :<< }
71
+
72
+ # Remove +records+ from this association. Does not destroy +records+.
73
+ def delete(*records)
74
+ records = flatten_deeper(records)
75
+ records.each { |associate| raise_on_type_mismatch(associate) }
76
+ records.reject! { |associate| @target.delete(associate) if associate.new_record? }
77
+ return if records.empty?
78
+
79
+ @delete_join_finder ||= "find_all_by_#{@reflection.source_reflection.association_foreign_key}"
80
+ through = @reflection.through_reflection
81
+ through.klass.transaction do
82
+ records.each do |associate|
83
+ joins = @owner.send(through.name).send(@delete_join_finder, associate.id)
84
+ @owner.send(through.name).delete(joins)
85
+ @target.delete(associate)
86
+ end
87
+ end
88
+ end
89
+
90
+ def build(attrs = nil)
91
+ raise ActiveRecord::HasManyThroughCantAssociateNewRecords.new(@owner, @reflection.through_reflection)
92
+ end
93
+
94
+ def create!(attrs = nil)
95
+ @reflection.klass.transaction do
96
+ self << @reflection.klass.with_scope(:create => attrs) { @reflection.klass.create! }
97
+ end
98
+ end
99
+
100
+ # Calculate sum using SQL, not Enumerable
101
+ def sum(*args, &block)
102
+ calculate(:sum, *args, &block)
103
+ end
104
+
105
+ protected
106
+ def method_missing(method, *args, &block)
107
+ if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
108
+ super
109
+ else
110
+ @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) }
111
+ end
112
+ end
113
+
114
+ def find_target
115
+ records = @reflection.klass.find(:all,
116
+ :select => construct_select,
117
+ :conditions => construct_conditions,
118
+ :from => construct_from,
119
+ :joins => construct_joins,
120
+ :order => @reflection.options[:order],
121
+ :limit => @reflection.options[:limit],
122
+ :group => @reflection.options[:group],
123
+ :include => @reflection.options[:include] || @reflection.source_reflection.options[:include]
124
+ )
125
+
126
+ @reflection.options[:uniq] ? records.to_set.to_a : records
127
+ end
128
+
129
+ # Construct attributes for associate pointing to owner.
130
+ def construct_owner_attributes(reflection)
131
+ if as = reflection.options[:as]
132
+ { "#{as}_id" => @owner.id,
133
+ "#{as}_type" => @owner.class.base_class.name.to_s }
134
+ else
135
+ { reflection.primary_key_name => @owner.id }
136
+ end
137
+ end
138
+
139
+ # Construct attributes for :through pointing to owner and associate.
140
+ def construct_join_attributes(associate)
141
+ returning construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.primary_key_name => associate.id) do |join_attributes|
142
+ if @reflection.options[:source_type]
143
+ join_attributes.merge!(@reflection.source_reflection.options[:foreign_type] => associate.class.base_class.name.to_s)
144
+ end
145
+ end
146
+ end
147
+
148
+ # Associate attributes pointing to owner, quoted.
149
+ def construct_quoted_owner_attributes(reflection)
150
+ if as = reflection.options[:as]
151
+ { "#{as}_id" => @owner.quoted_id,
152
+ "#{as}_type" => reflection.klass.quote_value(
153
+ @owner.class.base_class.name.to_s,
154
+ reflection.klass.columns_hash["#{as}_type"]) }
155
+ else
156
+ { reflection.primary_key_name => @owner.quoted_id }
157
+ end
158
+ end
159
+
160
+ # Build SQL conditions from attributes, qualified by table name.
161
+ def construct_conditions
162
+ table_name = @reflection.through_reflection.table_name
163
+ conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
164
+ "#{table_name}.#{attr} = #{value}"
165
+ end
166
+ conditions << sql_conditions if sql_conditions
167
+ "(" + conditions.join(') AND (') + ")"
168
+ end
169
+
170
+ def construct_from
171
+ @reflection.table_name
172
+ end
173
+
174
+ def construct_select(custom_select = nil)
175
+ selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*"
176
+ end
177
+
178
+ def construct_joins(custom_joins = nil)
179
+ polymorphic_join = nil
180
+ if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to
181
+ reflection_primary_key = @reflection.klass.primary_key
182
+ source_primary_key = @reflection.source_reflection.primary_key_name
183
+ if @reflection.options[:source_type]
184
+ polymorphic_join = "AND %s.%s = %s" % [
185
+ @reflection.through_reflection.table_name, "#{@reflection.source_reflection.options[:foreign_type]}",
186
+ @owner.class.quote_value(@reflection.options[:source_type])
187
+ ]
188
+ end
189
+ else
190
+ reflection_primary_key = @reflection.source_reflection.primary_key_name
191
+ source_primary_key = @reflection.klass.primary_key
192
+ if @reflection.source_reflection.options[:as]
193
+ polymorphic_join = "AND %s.%s = %s" % [
194
+ @reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type",
195
+ @owner.class.quote_value(@reflection.through_reflection.klass.name)
196
+ ]
197
+ end
198
+ end
199
+
200
+ "INNER JOIN %s ON %s.%s = %s.%s %s #{@reflection.options[:joins]} #{custom_joins}" % [
201
+ @reflection.through_reflection.table_name,
202
+ @reflection.table_name, reflection_primary_key,
203
+ @reflection.through_reflection.table_name, source_primary_key,
204
+ polymorphic_join
205
+ ]
206
+ end
207
+
208
+ def construct_scope
209
+ { :create => construct_owner_attributes(@reflection),
210
+ :find => { :from => construct_from,
211
+ :conditions => construct_conditions,
212
+ :joins => construct_joins,
213
+ :select => construct_select } }
214
+ end
215
+
216
+ def construct_sql
217
+ case
218
+ when @reflection.options[:finder_sql]
219
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
220
+
221
+ @finder_sql = "#{@reflection.klass.table_name}.#{@reflection.primary_key_name} = #{@owner.quoted_id}"
222
+ @finder_sql << " AND (#{conditions})" if conditions
223
+ end
224
+
225
+ if @reflection.options[:counter_sql]
226
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
227
+ elsif @reflection.options[:finder_sql]
228
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
229
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
230
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
231
+ else
232
+ @counter_sql = @finder_sql
233
+ end
234
+ end
235
+
236
+ def conditions
237
+ @conditions ||= [
238
+ (interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]),
239
+ (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]),
240
+ ("#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.klass.inheritance_column} = #{@reflection.klass.quote_value(@reflection.through_reflection.klass.name.demodulize)}" unless @reflection.through_reflection.klass.descends_from_active_record?)
241
+ ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?)
242
+ end
243
+
244
+ alias_method :sql_conditions, :conditions
245
+ end
246
+ end
247
+ end