activerecord 1.14.4 → 1.15.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 (159) hide show
  1. data/CHANGELOG +400 -1
  2. data/README +2 -2
  3. data/RUNNING_UNIT_TESTS +21 -3
  4. data/Rakefile +55 -10
  5. data/lib/active_record.rb +10 -4
  6. data/lib/active_record/acts/list.rb +15 -4
  7. data/lib/active_record/acts/nested_set.rb +11 -12
  8. data/lib/active_record/acts/tree.rb +13 -14
  9. data/lib/active_record/aggregations.rb +46 -22
  10. data/lib/active_record/associations.rb +213 -162
  11. data/lib/active_record/associations/association_collection.rb +45 -15
  12. data/lib/active_record/associations/association_proxy.rb +32 -13
  13. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +18 -18
  14. data/lib/active_record/associations/has_many_association.rb +37 -17
  15. data/lib/active_record/associations/has_many_through_association.rb +120 -30
  16. data/lib/active_record/associations/has_one_association.rb +1 -1
  17. data/lib/active_record/attribute_methods.rb +75 -0
  18. data/lib/active_record/base.rb +282 -203
  19. data/lib/active_record/calculations.rb +95 -54
  20. data/lib/active_record/callbacks.rb +13 -24
  21. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +12 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb.rej +21 -0
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -4
  24. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -9
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +121 -37
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +55 -23
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -0
  28. data/lib/active_record/connection_adapters/db2_adapter.rb +1 -11
  29. data/lib/active_record/connection_adapters/firebird_adapter.rb +364 -50
  30. data/lib/active_record/connection_adapters/frontbase_adapter.rb +861 -0
  31. data/lib/active_record/connection_adapters/mysql_adapter.rb +86 -33
  32. data/lib/active_record/connection_adapters/openbase_adapter.rb +4 -3
  33. data/lib/active_record/connection_adapters/oracle_adapter.rb +151 -127
  34. data/lib/active_record/connection_adapters/postgresql_adapter.rb +125 -48
  35. data/lib/active_record/connection_adapters/sqlite_adapter.rb +38 -10
  36. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +183 -155
  37. data/lib/active_record/connection_adapters/sybase_adapter.rb +190 -212
  38. data/lib/active_record/deprecated_associations.rb +24 -10
  39. data/lib/active_record/deprecated_finders.rb +4 -1
  40. data/lib/active_record/fixtures.rb +37 -23
  41. data/lib/active_record/locking/optimistic.rb +106 -0
  42. data/lib/active_record/locking/pessimistic.rb +77 -0
  43. data/lib/active_record/migration.rb +8 -5
  44. data/lib/active_record/observer.rb +73 -34
  45. data/lib/active_record/reflection.rb +21 -7
  46. data/lib/active_record/schema_dumper.rb +33 -5
  47. data/lib/active_record/timestamp.rb +23 -34
  48. data/lib/active_record/transactions.rb +37 -30
  49. data/lib/active_record/validations.rb +46 -30
  50. data/lib/active_record/vendor/mysql.rb +20 -5
  51. data/lib/active_record/version.rb +2 -2
  52. data/lib/active_record/wrappings.rb +1 -2
  53. data/lib/active_record/xml_serialization.rb +308 -0
  54. data/test/aaa_create_tables_test.rb +5 -1
  55. data/test/abstract_unit.rb +18 -8
  56. data/test/{active_schema_mysql.rb → active_schema_test_mysql.rb} +2 -2
  57. data/test/adapter_test.rb +9 -7
  58. data/test/adapter_test_sqlserver.rb +81 -0
  59. data/test/aggregations_test.rb +29 -0
  60. data/test/{association_callbacks_test.rb → associations/callbacks_test.rb} +10 -8
  61. data/test/{associations_cascaded_eager_loading_test.rb → associations/cascaded_eager_loading_test.rb} +35 -3
  62. data/test/{associations_go_eager_test.rb → associations/eager_test.rb} +36 -2
  63. data/test/{associations_extensions_test.rb → associations/extension_test.rb} +5 -0
  64. data/test/{associations_join_model_test.rb → associations/join_model_test.rb} +118 -8
  65. data/test/associations_test.rb +339 -45
  66. data/test/attribute_methods_test.rb +49 -0
  67. data/test/base_test.rb +321 -67
  68. data/test/calculations_test.rb +48 -10
  69. data/test/callbacks_test.rb +13 -0
  70. data/test/connection_test_firebird.rb +8 -0
  71. data/test/connections/native_db2/connection.rb +18 -17
  72. data/test/connections/native_firebird/connection.rb +19 -17
  73. data/test/connections/native_frontbase/connection.rb +27 -0
  74. data/test/connections/native_mysql/connection.rb +18 -15
  75. data/test/connections/native_openbase/connection.rb +14 -15
  76. data/test/connections/native_oracle/connection.rb +16 -12
  77. data/test/connections/native_postgresql/connection.rb +16 -17
  78. data/test/connections/native_sqlite/connection.rb +3 -6
  79. data/test/connections/native_sqlite3/connection.rb +3 -6
  80. data/test/connections/native_sqlserver/connection.rb +16 -17
  81. data/test/connections/native_sqlserver_odbc/connection.rb +18 -19
  82. data/test/connections/native_sybase/connection.rb +16 -17
  83. data/test/datatype_test_postgresql.rb +52 -0
  84. data/test/defaults_test.rb +52 -10
  85. data/test/deprecated_associations_test.rb +151 -107
  86. data/test/deprecated_finder_test.rb +83 -66
  87. data/test/empty_date_time_test.rb +25 -0
  88. data/test/finder_test.rb +118 -11
  89. data/test/fixtures/accounts.yml +6 -1
  90. data/test/fixtures/author.rb +27 -4
  91. data/test/fixtures/categorizations.yml +8 -2
  92. data/test/fixtures/category.rb +1 -2
  93. data/test/fixtures/comments.yml +0 -6
  94. data/test/fixtures/companies.yml +6 -1
  95. data/test/fixtures/company.rb +23 -1
  96. data/test/fixtures/company_in_module.rb +8 -10
  97. data/test/fixtures/customer.rb +2 -2
  98. data/test/fixtures/customers.yml +9 -0
  99. data/test/fixtures/db_definitions/db2.drop.sql +1 -0
  100. data/test/fixtures/db_definitions/db2.sql +9 -0
  101. data/test/fixtures/db_definitions/firebird.drop.sql +3 -0
  102. data/test/fixtures/db_definitions/firebird.sql +13 -1
  103. data/test/fixtures/db_definitions/frontbase.drop.sql +31 -0
  104. data/test/fixtures/db_definitions/frontbase.sql +262 -0
  105. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  106. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  107. data/test/fixtures/db_definitions/mysql.drop.sql +1 -0
  108. data/test/fixtures/db_definitions/mysql.sql +23 -14
  109. data/test/fixtures/db_definitions/openbase.sql +13 -1
  110. data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
  111. data/test/fixtures/db_definitions/oracle.sql +29 -2
  112. data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
  113. data/test/fixtures/db_definitions/postgresql.sql +13 -3
  114. data/test/fixtures/db_definitions/schema.rb +29 -1
  115. data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
  116. data/test/fixtures/db_definitions/sqlite.sql +12 -3
  117. data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
  118. data/test/fixtures/db_definitions/sqlserver.sql +35 -0
  119. data/test/fixtures/db_definitions/sybase.drop.sql +2 -0
  120. data/test/fixtures/db_definitions/sybase.sql +13 -4
  121. data/test/fixtures/developer.rb +12 -0
  122. data/test/fixtures/edge.rb +5 -0
  123. data/test/fixtures/edges.yml +6 -0
  124. data/test/fixtures/funny_jokes.yml +3 -7
  125. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  126. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  127. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  128. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  129. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  130. data/test/fixtures/mixin.rb +15 -0
  131. data/test/fixtures/mixins.yml +38 -0
  132. data/test/fixtures/post.rb +3 -2
  133. data/test/fixtures/project.rb +3 -1
  134. data/test/fixtures/topic.rb +6 -1
  135. data/test/fixtures/topics.yml +4 -4
  136. data/test/fixtures/vertex.rb +9 -0
  137. data/test/fixtures/vertices.yml +4 -0
  138. data/test/fixtures_test.rb +45 -0
  139. data/test/inheritance_test.rb +67 -6
  140. data/test/lifecycle_test.rb +40 -19
  141. data/test/locking_test.rb +170 -26
  142. data/test/method_scoping_test.rb +2 -2
  143. data/test/migration_test.rb +387 -110
  144. data/test/migration_test_firebird.rb +124 -0
  145. data/test/mixin_nested_set_test.rb +14 -2
  146. data/test/mixin_test.rb +56 -18
  147. data/test/modules_test.rb +8 -2
  148. data/test/multiple_db_test.rb +2 -2
  149. data/test/pk_test.rb +1 -0
  150. data/test/reflection_test.rb +8 -2
  151. data/test/schema_authorization_test_postgresql.rb +75 -0
  152. data/test/schema_dumper_test.rb +40 -4
  153. data/test/table_name_test_sqlserver.rb +23 -0
  154. data/test/threaded_connections_test.rb +19 -16
  155. data/test/transactions_test.rb +86 -72
  156. data/test/validations_test.rb +126 -56
  157. data/test/xml_serialization_test.rb +125 -0
  158. metadata +45 -11
  159. data/lib/active_record/locking.rb +0 -79
@@ -9,7 +9,7 @@ module ActiveRecord
9
9
  end
10
10
 
11
11
  def reset
12
- @target = []
12
+ reset_target!
13
13
  @loaded = false
14
14
  end
15
15
 
@@ -28,7 +28,7 @@ module ActiveRecord
28
28
  callback(:after_add, record)
29
29
  end
30
30
  end
31
-
31
+
32
32
  result && self
33
33
  end
34
34
 
@@ -39,7 +39,12 @@ module ActiveRecord
39
39
  def delete_all
40
40
  load_target
41
41
  delete(@target)
42
- @target = []
42
+ reset_target!
43
+ end
44
+
45
+ # Calculate sum using SQL, not Enumerable
46
+ def sum(*args, &block)
47
+ calculate(:sum, *args, &block)
43
48
  end
44
49
 
45
50
  # Remove +records+ from this association. Does not destroy +records+.
@@ -77,9 +82,9 @@ module ActiveRecord
77
82
  each { |record| record.destroy }
78
83
  end
79
84
 
80
- @target = []
85
+ reset_target!
81
86
  end
82
-
87
+
83
88
  def create(attributes = {})
84
89
  # Can't use Base.create since the foreign key may be a protected attribute.
85
90
  if attributes.is_a?(Array)
@@ -95,21 +100,35 @@ module ActiveRecord
95
100
  # calling collection.size if it has. If it's more likely than not that the collection does have a size larger than zero
96
101
  # and you need to fetch that collection afterwards, it'll take one less SELECT query if you use length.
97
102
  def size
98
- if loaded? then @target.size else count_records end
103
+ if loaded? && !@reflection.options[:uniq]
104
+ @target.size
105
+ elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
106
+ unsaved_records = Array(@target.detect { |r| r.new_record? })
107
+ unsaved_records.size + count_records
108
+ else
109
+ count_records
110
+ end
99
111
  end
100
-
112
+
101
113
  # Returns the size of the collection by loading it and calling size on the array. If you want to use this method to check
102
114
  # whether the collection is empty, use collection.length.zero? instead of collection.empty?
103
115
  def length
104
116
  load_target.size
105
117
  end
106
-
118
+
107
119
  def empty?
108
120
  size.zero?
109
121
  end
110
-
122
+
111
123
  def uniq(collection = self)
112
- collection.inject([]) { |uniq_records, record| uniq_records << record unless uniq_records.include?(record); uniq_records }
124
+ seen = Set.new
125
+ collection.inject([]) do |kept, record|
126
+ unless seen.include?(record.id)
127
+ kept << record
128
+ seen << record.id
129
+ end
130
+ kept
131
+ end
113
132
  end
114
133
 
115
134
  # Replace this collection with +other_array+
@@ -127,12 +146,23 @@ module ActiveRecord
127
146
  end
128
147
  end
129
148
 
130
- private
131
- # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
132
- def flatten_deeper(array)
133
- array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
149
+ protected
150
+ def reset_target!
151
+ @target = Array.new
134
152
  end
135
-
153
+
154
+ def find_target
155
+ records =
156
+ if @reflection.options[:finder_sql]
157
+ @reflection.klass.find_by_sql(@finder_sql)
158
+ else
159
+ find(:all)
160
+ end
161
+
162
+ @reflection.options[:uniq] ? uniq(records) : records
163
+ end
164
+
165
+ private
136
166
  def callback(method, record)
137
167
  callbacks_for(method).each do |callback|
138
168
  case callback
@@ -4,14 +4,27 @@ module ActiveRecord
4
4
  attr_reader :reflection
5
5
  alias_method :proxy_respond_to?, :respond_to?
6
6
  alias_method :proxy_extend, :extend
7
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?|^proxy_respond_to\?|^proxy_extend|^send)/ }
7
+ delegate :to_param, :to => :proxy_target
8
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_)/ }
8
9
 
9
10
  def initialize(owner, reflection)
10
11
  @owner, @reflection = owner, reflection
11
- proxy_extend(reflection.options[:extend]) if reflection.options[:extend]
12
+ Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
12
13
  reset
13
14
  end
14
15
 
16
+ def proxy_owner
17
+ @owner
18
+ end
19
+
20
+ def proxy_reflection
21
+ @reflection
22
+ end
23
+
24
+ def proxy_target
25
+ @target
26
+ end
27
+
15
28
  def respond_to?(symbol, include_priv = false)
16
29
  proxy_respond_to?(symbol, include_priv) || (load_target && @target.respond_to?(symbol, include_priv))
17
30
  end
@@ -28,7 +41,7 @@ module ActiveRecord
28
41
  end
29
42
 
30
43
  def conditions
31
- @conditions ||= eval("%(#{@reflection.active_record.send :sanitize_sql, @reflection.options[:conditions]})") if @reflection.options[:conditions]
44
+ @conditions ||= interpolate_sql(sanitize_sql(@reflection.options[:conditions])) if @reflection.options[:conditions]
32
45
  end
33
46
  alias :sql_conditions :conditions
34
47
 
@@ -106,21 +119,22 @@ module ActiveRecord
106
119
 
107
120
  private
108
121
  def method_missing(method, *args, &block)
109
- load_target
110
- @target.send(method, *args, &block)
122
+ if load_target
123
+ @target.send(method, *args, &block)
124
+ end
111
125
  end
112
126
 
113
127
  def load_target
114
- if !@owner.new_record? || foreign_key_present
115
- begin
116
- @target = find_target if !loaded?
117
- rescue ActiveRecord::RecordNotFound
118
- reset
119
- end
128
+ return nil unless defined?(@loaded)
129
+
130
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
131
+ @target = find_target
120
132
  end
121
133
 
122
- loaded if target
123
- target
134
+ @loaded = true
135
+ @target
136
+ rescue ActiveRecord::RecordNotFound
137
+ reset
124
138
  end
125
139
 
126
140
  # Can be overwritten by associations that might have the foreign key available for an association without
@@ -134,6 +148,11 @@ module ActiveRecord
134
148
  raise ActiveRecord::AssociationTypeMismatch, "#{@reflection.class_name} expected, got #{record.class}"
135
149
  end
136
150
  end
151
+
152
+ # Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
153
+ def flatten_deeper(array)
154
+ array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
155
+ end
137
156
  end
138
157
  end
139
158
  end
@@ -13,6 +13,17 @@ module ActiveRecord
13
13
  record
14
14
  end
15
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
+
16
27
  def find_first
17
28
  load_target.first
18
29
  end
@@ -56,7 +67,9 @@ module ActiveRecord
56
67
  @reflection.klass.find(*args)
57
68
  end
58
69
  end
59
-
70
+
71
+ # Deprecated as of Rails 1.2. If your associations require attributes
72
+ # you should be using has_many :through
60
73
  def push_with_attributes(record, join_attributes = {})
61
74
  raise_on_type_mismatch(record)
62
75
  join_attributes.each { |key, value| record[key.to_s] = value }
@@ -68,13 +81,10 @@ module ActiveRecord
68
81
 
69
82
  self
70
83
  end
71
-
84
+ deprecate :push_with_attributes => "consider using has_many :through instead"
85
+
72
86
  alias :concat_with_attributes :push_with_attributes
73
87
 
74
- def size
75
- @reflection.options[:uniq] ? count_records : super
76
- end
77
-
78
88
  protected
79
89
  def method_missing(method, *args, &block)
80
90
  if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
@@ -85,17 +95,7 @@ module ActiveRecord
85
95
  end
86
96
  end
87
97
  end
88
-
89
- def find_target
90
- if @reflection.options[:finder_sql]
91
- records = @reflection.klass.find_by_sql(@finder_sql)
92
- else
93
- records = find(:all)
94
- end
95
-
96
- @reflection.options[:uniq] ? uniq(records) : records
97
- end
98
-
98
+
99
99
  def count_records
100
100
  load_target.size
101
101
  end
@@ -118,7 +118,7 @@ module ActiveRecord
118
118
  attributes[column.name] = record.quoted_id
119
119
  else
120
120
  if record.attributes.has_key?(column.name)
121
- value = @owner.send(:quote, record[column.name], column)
121
+ value = @owner.send(:quote_value, record[column.name], column)
122
122
  attributes[column.name] = value unless value.nil?
123
123
  end
124
124
  end
@@ -10,10 +10,12 @@ module ActiveRecord
10
10
  if attributes.is_a?(Array)
11
11
  attributes.collect { |attr| build(attr) }
12
12
  else
13
- load_target
14
13
  record = @reflection.klass.new(attributes)
15
14
  set_belongs_to_association_for(record)
15
+
16
+ @target ||= [] unless loaded?
16
17
  @target << record
18
+
17
19
  record
18
20
  end
19
21
  end
@@ -29,22 +31,28 @@ module ActiveRecord
29
31
  @reflection.klass.find_all(conditions, orderings, limit, joins)
30
32
  end
31
33
  end
34
+ deprecate :find_all => "use find(:all, ...) instead"
32
35
 
33
36
  # DEPRECATED. Find the first associated record. All arguments are optional.
34
37
  def find_first(conditions = nil, orderings = nil)
35
38
  find_all(conditions, orderings, 1).first
36
39
  end
40
+ deprecate :find_first => "use find(:first, ...) instead"
37
41
 
38
42
  # Count the number of associated records. All arguments are optional.
39
- def count(runtime_conditions = nil)
43
+ def count(*args)
40
44
  if @reflection.options[:counter_sql]
41
45
  @reflection.klass.count_by_sql(@counter_sql)
42
46
  elsif @reflection.options[:finder_sql]
43
47
  @reflection.klass.count_by_sql(@finder_sql)
44
48
  else
45
- sql = @finder_sql
46
- sql += " AND (#{sanitize_sql(runtime_conditions)})" if runtime_conditions
47
- @reflection.klass.count(sql)
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)
48
56
  end
49
57
  end
50
58
 
@@ -83,33 +91,45 @@ module ActiveRecord
83
91
  @reflection.klass.find(*args)
84
92
  end
85
93
  end
86
-
94
+
87
95
  protected
88
96
  def method_missing(method, *args, &block)
89
97
  if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
90
98
  super
91
99
  else
100
+ create_scoping = {}
101
+ set_belongs_to_association_for(create_scoping)
102
+
92
103
  @reflection.klass.with_scope(
104
+ :create => create_scoping,
93
105
  :find => {
94
106
  :conditions => @finder_sql,
95
107
  :joins => @join_sql,
96
108
  :readonly => false
97
- },
98
- :create => {
99
- @reflection.primary_key_name => @owner.id
100
109
  }
101
110
  ) do
102
111
  @reflection.klass.send(method, *args, &block)
103
112
  end
104
113
  end
105
114
  end
106
-
107
- def find_target
108
- if @reflection.options[:finder_sql]
109
- @reflection.klass.find_by_sql(@finder_sql)
110
- else
111
- find(:all)
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
112
129
  end
130
+
131
+ loaded if target
132
+ target
113
133
  end
114
134
 
115
135
  def count_records
@@ -118,7 +138,7 @@ module ActiveRecord
118
138
  elsif @reflection.options[:counter_sql]
119
139
  @reflection.klass.count_by_sql(@counter_sql)
120
140
  else
121
- @reflection.klass.count(@counter_sql)
141
+ @reflection.klass.count(:conditions => @counter_sql)
122
142
  end
123
143
 
124
144
  @target = [] and loaded if count == 0
@@ -167,7 +187,7 @@ module ActiveRecord
167
187
  when @reflection.options[:as]
168
188
  @finder_sql =
169
189
  "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
170
- "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}"
190
+ "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
171
191
  @finder_sql << " AND (#{conditions})" if conditions
172
192
 
173
193
  else
@@ -8,7 +8,6 @@ module ActiveRecord
8
8
  construct_sql
9
9
  end
10
10
 
11
-
12
11
  def find(*args)
13
12
  options = Base.send(:extract_options_from_args!, args)
14
13
 
@@ -23,12 +22,12 @@ module ActiveRecord
23
22
  elsif @reflection.options[:order]
24
23
  options[:order] = @reflection.options[:order]
25
24
  end
26
-
25
+
27
26
  options[:select] = construct_select(options[:select])
28
27
  options[:from] ||= construct_from
29
28
  options[:joins] = construct_joins(options[:joins])
30
29
  options[:include] = @reflection.source_reflection.options[:include] if options[:include].nil?
31
-
30
+
32
31
  merge_options_from_reflection!(options)
33
32
 
34
33
  # Pass through args exactly as we received them.
@@ -41,6 +40,68 @@ module ActiveRecord
41
40
  @loaded = false
42
41
  end
43
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
+
44
105
  protected
45
106
  def method_missing(method, *args, &block)
46
107
  if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
@@ -49,41 +110,68 @@ module ActiveRecord
49
110
  @reflection.klass.with_scope(construct_scope) { @reflection.klass.send(method, *args, &block) }
50
111
  end
51
112
  end
52
-
113
+
53
114
  def find_target
54
- @reflection.klass.find(:all,
115
+ records = @reflection.klass.find(:all,
55
116
  :select => construct_select,
56
117
  :conditions => construct_conditions,
57
118
  :from => construct_from,
58
119
  :joins => construct_joins,
59
- :order => @reflection.options[:order],
120
+ :order => @reflection.options[:order],
60
121
  :limit => @reflection.options[:limit],
61
122
  :group => @reflection.options[:group],
62
123
  :include => @reflection.options[:include] || @reflection.source_reflection.options[:include]
63
124
  )
125
+
126
+ @reflection.options[:uniq] ? records.to_set.to_a : records
64
127
  end
65
128
 
66
- def construct_conditions
67
- conditions = if @reflection.through_reflection.options[:as]
68
- "#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_id = #{@owner.quoted_id} " +
69
- "AND #{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}"
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 }
70
134
  else
71
- "#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.primary_key_name} = #{@owner.quoted_id}"
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
+ construct_owner_attributes(@reflection.through_reflection).merge(@reflection.source_reflection.association_foreign_key => associate.id)
142
+ end
143
+
144
+ # Associate attributes pointing to owner, quoted.
145
+ def construct_quoted_owner_attributes(reflection)
146
+ if as = reflection.options[:as]
147
+ { "#{as}_id" => @owner.quoted_id,
148
+ "#{as}_type" => reflection.klass.quote_value(
149
+ @owner.class.base_class.name.to_s,
150
+ reflection.klass.columns_hash["#{as}_type"]) }
151
+ else
152
+ { reflection.primary_key_name => @owner.quoted_id }
153
+ end
154
+ end
155
+
156
+ # Build SQL conditions from attributes, qualified by table name.
157
+ def construct_conditions
158
+ table_name = @reflection.through_reflection.table_name
159
+ conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
160
+ "#{table_name}.#{attr} = #{value}"
72
161
  end
73
- conditions << " AND (#{sql_conditions})" if sql_conditions
74
-
75
- return conditions
162
+ conditions << sql_conditions if sql_conditions
163
+ "(" + conditions.join(') AND (') + ")"
76
164
  end
77
165
 
78
166
  def construct_from
79
167
  @reflection.table_name
80
168
  end
81
-
169
+
82
170
  def construct_select(custom_select = nil)
83
- selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*"
171
+ selected = custom_select || @reflection.options[:select] || "#{@reflection.table_name}.*"
84
172
  end
85
-
86
- def construct_joins(custom_joins = nil)
173
+
174
+ def construct_joins(custom_joins = nil)
87
175
  polymorphic_join = nil
88
176
  if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to
89
177
  reflection_primary_key = @reflection.klass.primary_key
@@ -94,7 +182,7 @@ module ActiveRecord
94
182
  if @reflection.source_reflection.options[:as]
95
183
  polymorphic_join = "AND %s.%s = %s" % [
96
184
  @reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type",
97
- @owner.class.quote(@reflection.through_reflection.klass.name)
185
+ @owner.class.quote_value(@reflection.through_reflection.klass.name)
98
186
  ]
99
187
  end
100
188
  end
@@ -106,14 +194,15 @@ module ActiveRecord
106
194
  polymorphic_join
107
195
  ]
108
196
  end
109
-
197
+
110
198
  def construct_scope
111
- {
112
- :find => { :from => construct_from, :conditions => construct_conditions, :joins => construct_joins, :select => construct_select },
113
- :create => { @reflection.primary_key_name => @owner.id }
114
- }
199
+ { :create => construct_owner_attributes(@reflection),
200
+ :find => { :from => construct_from,
201
+ :conditions => construct_conditions,
202
+ :joins => construct_joins,
203
+ :select => construct_select } }
115
204
  end
116
-
205
+
117
206
  def construct_sql
118
207
  case
119
208
  when @reflection.options[:finder_sql]
@@ -133,14 +222,15 @@ module ActiveRecord
133
222
  @counter_sql = @finder_sql
134
223
  end
135
224
  end
136
-
225
+
137
226
  def conditions
138
227
  @conditions ||= [
139
- (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]),
140
- (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions])
141
- ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions])
228
+ (interpolate_sql(@reflection.klass.send(:sanitize_sql, @reflection.options[:conditions])) if @reflection.options[:conditions]),
229
+ (interpolate_sql(@reflection.active_record.send(:sanitize_sql, @reflection.through_reflection.options[:conditions])) if @reflection.through_reflection.options[:conditions]),
230
+ ("#{@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?)
231
+ ].compact.collect { |condition| "(#{condition})" }.join(' AND ') unless (!@reflection.options[:conditions] && !@reflection.through_reflection.options[:conditions] && @reflection.through_reflection.klass.descends_from_active_record?)
142
232
  end
143
-
233
+
144
234
  alias_method :sql_conditions, :conditions
145
235
  end
146
236
  end