composite_primary_keys 1.0.8 → 1.0.10

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.
@@ -1,3 +1,13 @@
1
+ == 1.0.10 2008-10-22
2
+
3
+ * add composite key where clause creator method [timurv]
4
+
5
+ == 1.0.9 2008-09-08
6
+
7
+ * fix postgres tests
8
+ * fix for delete_records when has_many association has composite keys [darxriggs]
9
+ * more consistent table/column name quoting [pbrant]
10
+
1
11
  == 1.0.8 2008-08-27
2
12
 
3
13
  * fix has_many :through for non composite models [thx rcarver]
@@ -5,43 +5,43 @@ module CompositePrimaryKeys
5
5
  super
6
6
  base.send(:extend, ClassMethods)
7
7
  end
8
-
8
+
9
9
  # Composite key versions of Association functions
10
- module ClassMethods
10
+ module ClassMethods
11
11
  def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
12
12
  table_name = reflection.klass.quoted_table_name
13
13
  id_to_record_map, ids = construct_id_map(records)
14
14
  records.each {|record| record.send(reflection.name).loaded}
15
15
  options = reflection.options
16
16
 
17
- if(composite?)
17
+ if composite?
18
18
  primary_key = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
19
19
  where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
20
- "(" + keys.map{|key| "t0.#{key} = ?"}.join(" AND ") + ")"
20
+ "(" + keys.map{|key| "t0.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
21
21
  end.join(" OR ")
22
-
22
+
23
23
  conditions = [where, ids].flatten
24
- joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{full_composite_join_clause(reflection.klass.quoted_table_name, reflection.klass.primary_key, 't0', reflection.association_foreign_key)}"
25
- parent_primary_keys = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| "t0.#{k}"}
26
- parent_record_id = connection.concat(parent_primary_keys.zip(["','"] * (parent_primary_keys.size - 1)).flatten.compact)
24
+ joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{full_composite_join_clause(reflection, reflection.klass.table_name, reflection.klass.primary_key, 't0', reflection.association_foreign_key)}"
25
+ parent_primary_keys = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| "t0.#{connection.quote_column_name(k)}"}
26
+ parent_record_id = connection.concat(*parent_primary_keys.zip(["','"] * (parent_primary_keys.size - 1)).flatten.compact)
27
27
  else
28
- conditions = ["t0.#{reflection.primary_key_name} IN (?)", ids]
29
- joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{reflection.klass.primary_key} = t0.#{reflection.association_foreign_key}"
28
+ conditions = ["t0.#{connection.quote_column_name(reflection.primary_key_name)} IN (?)", ids]
29
+ joins = "INNER JOIN #{connection.quote_table_name options[:join_table]} t0 ON #{reflection.klass.quoted_table_name}.#{connection.quote_column_name(reflection.klass.primary_key)} = t0.#{connection.quote_column_name(reflection.association_foreign_key)})"
30
30
  parent_record_id = reflection.primary_key_name
31
31
  end
32
-
32
+
33
33
  conditions.first << append_conditions(options, preload_options)
34
34
 
35
- associated_records = reflection.klass.find(:all,
35
+ associated_records = reflection.klass.find(:all,
36
36
  :conditions => conditions,
37
- :include => options[:include],
38
- :joins => joins,
39
- :select => "#{options[:select] || table_name+'.*'}, #{parent_record_id} as parent_record_id_",
40
- :order => options[:order])
41
-
37
+ :include => options[:include],
38
+ :joins => joins,
39
+ :select => "#{options[:select] || table_name+'.*'}, #{parent_record_id} as parent_record_id_",
40
+ :order => options[:order])
41
+
42
42
  set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'parent_record_id_')
43
43
  end
44
-
44
+
45
45
  def preload_has_many_association(records, reflection, preload_options={})
46
46
  id_to_record_map, ids = construct_id_map(records)
47
47
  records.each {|record| record.send(reflection.name).loaded}
@@ -51,7 +51,7 @@ module CompositePrimaryKeys
51
51
  through_records = preload_through_records(records, reflection, options[:through])
52
52
  through_reflection = reflections[options[:through]]
53
53
  through_primary_key = through_reflection.primary_key_name
54
-
54
+
55
55
  unless through_records.empty?
56
56
  source = reflection.source_reflection.name
57
57
  #add conditions from reflection!
@@ -61,7 +61,7 @@ module CompositePrimaryKeys
61
61
  add_preloaded_records_to_collection(id_to_record_map[key], reflection.name, through_record.send(source))
62
62
  end
63
63
  end
64
- else
64
+ else
65
65
  associated_records = find_associated_records(ids, reflection, preload_options)
66
66
  set_association_collection_records(id_to_record_map, reflection.name, associated_records, reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP))
67
67
  end
@@ -95,7 +95,7 @@ module CompositePrimaryKeys
95
95
  records.first.class.preload_associations(records, through_association)
96
96
  through_records = records.map {|record| record.send(through_association)}.flatten
97
97
  end
98
-
98
+
99
99
  through_records.compact!
100
100
  through_records
101
101
  end
@@ -107,23 +107,23 @@ module CompositePrimaryKeys
107
107
  if options[:polymorphic]
108
108
  raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
109
109
  else
110
- # I need to keep the original ids for each record (as opposed to the stringified) so
111
- # that they get properly converted for each db so the id_map ends up looking like:
112
- #
113
- # { '1,2' => {:id => [1,2], :records => [...records...]}}
110
+ # I need to keep the original ids for each record (as opposed to the stringified) so
111
+ # that they get properly converted for each db so the id_map ends up looking like:
112
+ #
113
+ # { '1,2' => {:id => [1,2], :records => [...records...]}}
114
114
  id_map = {}
115
-
115
+
116
116
  records.each do |record|
117
117
  key = primary_key_name.map{|k| record.send(k)}
118
118
  key_as_string = key.join(CompositePrimaryKeys::ID_SEP)
119
-
119
+
120
120
  if key_as_string
121
121
  mapped_records = (id_map[key_as_string] ||= {:id => key, :records => []})
122
122
  mapped_records[:records] << record
123
123
  end
124
124
  end
125
-
126
-
125
+
126
+
127
127
  klasses_and_ids = [[reflection.klass.name, id_map]]
128
128
  end
129
129
 
@@ -131,28 +131,30 @@ module CompositePrimaryKeys
131
131
  klass_name, id_map = *klass_and_id
132
132
  klass = klass_name.constantize
133
133
  table_name = klass.quoted_table_name
134
-
135
- if(composite?)
134
+ connection = reflection.active_record.connection
135
+
136
+ if composite?
136
137
  primary_key = klass.primary_key.to_s.split(CompositePrimaryKeys::ID_SEP)
137
138
  ids = id_map.keys.uniq.map {|id| id_map[id][:id]}
138
139
 
139
140
  where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
140
- "(" + keys.map{|key| "#{table_name}.#{key} = ?"}.join(" AND ") + ")"
141
+ "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
141
142
  end.join(" OR ")
142
-
143
+
143
144
  conditions = [where, ids].flatten
144
145
  else
145
- conditions = ["#{table_name}.#{primary_key} IN (?)", id_map.keys.uniq]
146
+ conditions = ["#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)", id_map.keys.uniq]
146
147
  end
147
-
148
+
148
149
  conditions.first << append_conditions(options, preload_options)
149
-
150
- associated_records = klass.find(:all, :conditions => conditions,
151
- :include => options[:include],
152
- :select => options[:select],
153
- :joins => options[:joins],
154
- :order => options[:order])
155
-
150
+
151
+ associated_records = klass.find(:all,
152
+ :conditions => conditions,
153
+ :include => options[:include],
154
+ :select => options[:select],
155
+ :joins => options[:joins],
156
+ :order => options[:order])
157
+
156
158
  set_association_single_records(id_map, reflection.name, associated_records, primary_key)
157
159
  end
158
160
  end
@@ -165,13 +167,13 @@ module CompositePrimaryKeys
165
167
  add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
166
168
  end
167
169
  end
168
-
170
+
169
171
  def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
170
172
  seen_keys = {}
171
173
  associated_records.each do |associated_record|
172
174
  associated_record_key = associated_record[key]
173
175
  associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
174
-
176
+
175
177
  #this is a has_one or belongs_to: there should only be one record.
176
178
  #Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
177
179
  # only one row per distinct foo_id' so this where we enforce that
@@ -183,7 +185,7 @@ module CompositePrimaryKeys
183
185
  end
184
186
  end
185
187
  end
186
-
188
+
187
189
  def find_associated_records(ids, reflection, preload_options)
188
190
  options = reflection.options
189
191
  table_name = reflection.klass.quoted_table_name
@@ -191,40 +193,44 @@ module CompositePrimaryKeys
191
193
  if interface = reflection.options[:as]
192
194
  raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
193
195
  else
196
+ connection = reflection.active_record.connection
194
197
  foreign_key = reflection.primary_key_name
195
- conditions = ["#{reflection.klass.quoted_table_name}.#{foreign_key} IN (?)", ids]
198
+ conditions = ["#{table_name}.#{connection.quote_column_name(foreign_key)} IN (?)", ids]
196
199
 
197
- if(composite?)
200
+ if composite?
198
201
  foreign_keys = foreign_key.to_s.split(CompositePrimaryKeys::ID_SEP)
199
202
 
200
- where = (foreign_keys * ids.size).in_groups_of(foreign_keys.size).map do |keys|
201
- "(" + keys.map{|key| "#{reflection.klass.quoted_table_name}.#{key} = ?"}.join(" AND ") + ")"
203
+ where = (foreign_keys * ids.size).in_groups_of(foreign_keys.size).map do |keys|
204
+ "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
202
205
  end.join(" OR ")
203
-
206
+
204
207
  conditions = [where, ids].flatten
208
+ end
205
209
  end
206
- end
207
-
210
+
208
211
  conditions.first << append_conditions(options, preload_options)
209
212
 
210
213
  reflection.klass.find(:all,
211
- :select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
212
- :include => preload_options[:include] || options[:include],
213
- :conditions => conditions,
214
- :joins => options[:joins],
215
- :group => preload_options[:group] || options[:group],
216
- :order => preload_options[:order] || options[:order])
214
+ :select => (preload_options[:select] || options[:select] || "#{table_name}.*"),
215
+ :include => preload_options[:include] || options[:include],
216
+ :conditions => conditions,
217
+ :joins => options[:joins],
218
+ :group => preload_options[:group] || options[:group],
219
+ :order => preload_options[:order] || options[:order])
217
220
  end
218
221
 
219
- def full_composite_join_clause(table1, full_keys1, table2, full_keys2)
222
+ def full_composite_join_clause(reflection, table1, full_keys1, table2, full_keys2)
223
+ connection = reflection.active_record.connection
220
224
  full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
221
225
  full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
222
226
  where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
223
- "#{table1}.#{key_pair.first}=#{table2}.#{key_pair.last}"
227
+ quoted1 = connection.quote_table_name(table1)
228
+ quoted2 = connection.quote_table_name(table2)
229
+ "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
224
230
  end.join(" AND ")
225
231
  "(#{where_clause})"
226
232
  end
227
233
  end
228
234
  end
229
235
  end
230
- end
236
+ end
@@ -1,409 +1,428 @@
1
- module CompositePrimaryKeys
2
- module ActiveRecord
3
- module Associations
4
- def self.append_features(base)
5
- super
6
- base.send(:extend, ClassMethods)
7
- end
8
-
9
- # Composite key versions of Association functions
10
- module ClassMethods
11
-
12
- def construct_counter_sql_with_included_associations(options, join_dependency)
13
- scope = scope(:find)
14
- sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})"
15
-
16
- # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
17
- if !self.connection.supports_count_distinct?
18
- sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{quoted_table_columns(primary_key)}"
19
- end
20
-
21
- sql << " FROM #{table_name} "
22
- sql << join_dependency.join_associations.collect{|join| join.association_join }.join
23
-
24
- add_joins!(sql, options, scope)
25
- add_conditions!(sql, options[:conditions], scope)
26
- add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
27
-
28
- add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
29
-
30
- if !self.connection.supports_count_distinct?
31
- sql << ")"
32
- end
33
-
34
- return sanitize_sql(sql)
35
- end
36
-
37
- def construct_finder_sql_with_included_associations(options, join_dependency)
38
- scope = scope(:find)
39
- sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || table_name} "
40
- sql << join_dependency.join_associations.collect{|join| join.association_join }.join
41
-
42
- add_joins!(sql, options, scope)
43
- add_conditions!(sql, options[:conditions], scope)
44
- add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
45
-
46
- sql << "ORDER BY #{options[:order]} " if options[:order]
47
-
48
- add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
49
-
50
- return sanitize_sql(sql)
51
- end
52
-
53
- def table_columns(columns)
54
- columns.collect {|column| "#{self.table_name}.#{column}"}
55
- end
56
-
57
- def quoted_table_columns(columns)
58
- table_columns(columns).join(ID_SEP)
59
- end
60
-
61
- end
62
-
63
- end
64
- end
65
- end
66
-
67
- module ActiveRecord::Associations::ClassMethods
68
- class JoinDependency
69
- def construct_association(record, join, row)
70
- case join.reflection.macro
71
- when :has_many, :has_and_belongs_to_many
72
- collection = record.send(join.reflection.name)
73
- collection.loaded
74
-
75
- join_aliased_primary_keys = join.active_record.composite? ?
76
- join.aliased_primary_key : [join.aliased_primary_key]
77
- return nil if
78
- record.id.to_s != join.parent.record_id(row).to_s or not
79
- join_aliased_primary_keys.select {|key| row[key].nil?}.blank?
80
- association = join.instantiate(row)
81
- collection.target.push(association) unless collection.target.include?(association)
82
- when :has_one, :belongs_to
83
- return if record.id.to_s != join.parent.record_id(row).to_s or
84
- join.aliased_primary_key.to_a.any? { |key| row[key].nil? }
85
- association = join.instantiate(row)
86
- record.send("set_#{join.reflection.name}_target", association)
87
- else
88
- raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
89
- end
90
- return association
91
- end
92
-
93
- class JoinBase
94
- def aliased_primary_key
95
- active_record.composite? ?
96
- primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} :
97
- "#{ aliased_prefix }_r0"
98
- end
99
-
100
- def record_id(row)
101
- active_record.composite? ?
102
- aliased_primary_key.map {|key| row[key]}.to_composite_ids :
103
- row[aliased_primary_key]
104
- end
105
-
106
- def column_names_with_alias
107
- unless @column_names_with_alias
108
- @column_names_with_alias = []
109
- keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key]
110
- (keys + (column_names - keys)).each_with_index do |column_name, i|
111
- @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
112
- end
113
- end
114
- return @column_names_with_alias
115
- end
116
- end
117
-
118
- class JoinAssociation < JoinBase
119
- alias single_association_join association_join
120
- def association_join
121
- reflection.active_record.composite? ?
122
- composite_association_join :
123
- single_association_join
124
- end
125
-
126
- def composite_association_join
127
- join = case reflection.macro
128
- when :has_and_belongs_to_many
129
- " LEFT OUTER JOIN %s ON %s " % [
130
- table_alias_for(options[:join_table], aliased_join_table_name),
131
- composite_join_clause(
132
- full_keys(aliased_join_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key),
133
- full_keys(reflection.active_record.table_name, reflection.active_record.primary_key)
134
- )
135
- ] +
136
- " LEFT OUTER JOIN %s ON %s " % [
137
- table_name_and_alias,
138
- composite_join_clause(
139
- full_keys(aliased_table_name, klass.primary_key),
140
- full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key)
141
- )
142
- ]
143
- when :has_many, :has_one
144
- case
145
- when reflection.macro == :has_many && reflection.options[:through]
146
- through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
147
- if through_reflection.options[:as] # has_many :through against a polymorphic join
148
- raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
149
- else
150
- if source_reflection.macro == :has_many && source_reflection.options[:as]
151
- raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
152
- else
153
- case source_reflection.macro
154
- when :belongs_to
155
- first_key = primary_key
156
- second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
157
- when :has_many
158
- first_key = through_reflection.klass.to_s.classify.foreign_key
159
- second_key = options[:foreign_key] || primary_key
160
- end
161
-
162
- " LEFT OUTER JOIN %s ON %s " % [
163
- table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
164
- composite_join_clause(
165
- full_keys(aliased_join_table_name, through_reflection.primary_key_name),
166
- full_keys(parent.aliased_table_name, parent.primary_key)
167
- )
168
- ] +
169
- " LEFT OUTER JOIN %s ON %s " % [
170
- table_name_and_alias,
171
- composite_join_clause(
172
- full_keys(aliased_table_name, first_key),
173
- full_keys(aliased_join_table_name, second_key)
174
- )
175
- ]
176
- end
177
- end
178
-
179
- when reflection.macro == :has_many && reflection.options[:as]
180
- raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
181
- when reflection.macro == :has_one && reflection.options[:as]
182
- raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
183
- else
184
- foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
185
- " LEFT OUTER JOIN %s ON %s " % [
186
- table_name_and_alias,
187
- composite_join_clause(
188
- full_keys(aliased_table_name, foreign_key),
189
- full_keys(parent.aliased_table_name, parent.primary_key)),
190
- ]
191
- end
192
- when :belongs_to
193
- " LEFT OUTER JOIN %s ON %s " % [
194
- table_name_and_alias,
195
- composite_join_clause(
196
- full_keys(aliased_table_name, reflection.klass.primary_key),
197
- full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)),
198
- ]
199
- else
200
- ""
201
- end || ''
202
- join << %(AND %s.%s = %s ) % [
203
- aliased_table_name,
204
- reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
205
- klass.connection.quote(klass.name)] unless klass.descends_from_active_record?
206
- join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
207
- join
208
- end
209
-
210
- def full_keys(table_name, keys)
211
- keys.is_a?(Array) ? keys.collect {|key| "#{table_name}.#{key}"}.join(CompositePrimaryKeys::ID_SEP) : "#{table_name}.#{keys}"
212
- end
213
-
214
- def composite_join_clause(full_keys1, full_keys2)
215
- full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
216
- full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
217
- where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
218
- "#{key_pair.first}=#{key_pair.last}"
219
- end.join(" AND ")
220
- "(#{where_clause})"
221
- end
222
- end
223
- end
224
- end
225
-
226
- module ActiveRecord::Associations
227
- class AssociationProxy #:nodoc:
228
-
229
- def composite_where_clause(full_keys, ids)
230
- full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String)
231
-
232
- if ids.is_a?(String)
233
- ids = [[ids]]
234
- elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
235
- ids = [ids.to_composite_ids]
236
- end
237
-
238
- where_clause = ids.map do |id_set|
239
- transposed = id_set.size == 1 ? [[full_keys, id_set.first]] : [full_keys, id_set].transpose
240
- transposed.map do |full_key, id|
241
- "#{full_key.to_s}=#{@reflection.klass.sanitize(id)}"
242
- end.join(" AND ")
243
- end.join(") OR (")
244
-
245
- "(#{where_clause})"
246
- end
247
-
248
- def composite_join_clause(full_keys1, full_keys2)
249
- full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
250
- full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
251
-
252
- where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
253
- "#{key_pair.first}=#{key_pair.last}"
254
- end.join(" AND ")
255
-
256
- "(#{where_clause})"
257
- end
258
-
259
- def full_composite_join_clause(table1, full_keys1, table2, full_keys2)
260
- full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
261
- full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
262
-
263
- where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
264
- "#{table1}.#{key_pair.first}=#{table2}.#{key_pair.last}"
265
- end.join(" AND ")
266
-
267
- "(#{where_clause})"
268
- end
269
-
270
- def full_keys(table_name, keys)
271
- keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
272
- keys.is_a?(Array) ? keys.collect {|key| "#{table_name}.#{key}"}.join(CompositePrimaryKeys::ID_SEP) : "#{table_name}.#{keys}"
273
- end
274
-
275
- def full_columns_equals(table_name, keys, quoted_ids)
276
- if keys.is_a?(Symbol) or (keys.is_a?(String) and keys == keys.to_s.split(CompositePrimaryKeys::ID_SEP))
277
- return "#{table_name}.#{keys} = #{quoted_ids}"
278
- end
279
- keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
280
- quoted_ids = quoted_ids.split(CompositePrimaryKeys::ID_SEP) if quoted_ids.is_a?(String)
281
- keys_ids = [keys, quoted_ids].transpose
282
- keys_ids.collect {|key, id| "(#{table_name}.#{key} = #{id})"}.join(' AND ')
283
- end
284
-
285
- def set_belongs_to_association_for(record)
286
- if @reflection.options[:as]
287
- record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
288
- record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
289
- else
290
- key_values = @reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).zip([@owner.id].flatten)
291
- key_values.each{|kv| record[kv.first] = kv.last} unless @owner.new_record?
292
- end
293
- end
294
- end
295
-
296
- class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
297
- def construct_sql
298
- interpolate_sql_options!(@reflection.options, :finder_sql)
299
-
300
- if @reflection.options[:finder_sql]
301
- @finder_sql = @reflection.options[:finder_sql]
302
- else
303
- @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id)
304
- @finder_sql << " AND (#{conditions})" if conditions
305
- end
306
-
307
- @join_sql = "INNER JOIN #{@reflection.options[:join_table]} ON " +
308
- full_composite_join_clause(@reflection.klass.table_name, @reflection.klass.primary_key, @reflection.options[:join_table], @reflection.association_foreign_key)
309
- end
310
- end
311
-
312
- class HasManyAssociation < AssociationCollection #:nodoc:
313
- def construct_sql
314
- case
315
- when @reflection.options[:finder_sql]
316
- @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
317
-
318
- when @reflection.options[:as]
319
- @finder_sql =
320
- "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
321
- "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
322
- @finder_sql << " AND (#{conditions})" if conditions
323
-
324
- else
325
- @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
326
- @finder_sql << " AND (#{conditions})" if conditions
327
- end
328
-
329
- if @reflection.options[:counter_sql]
330
- @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
331
- elsif @reflection.options[:finder_sql]
332
- # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
333
- @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
334
- @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
335
- else
336
- @counter_sql = @finder_sql
337
- end
338
- end
339
-
340
- def delete_records(records)
341
- if @reflection.options[:dependent]
342
- records.each { |r| r.destroy }
343
- else
344
- field_names = @reflection.primary_key_name.split(',')
345
- field_names.collect! {|n| n + " = NULL"}
346
- records.each do |r|
347
- where_class = nil
348
-
349
- if r.quoted_id.include?(',')
350
- where_class = [@reflection.klass.primary_key, r.quoted_id].transpose.map {|pair| "(#{pair[0]} = #{pair[1]})"}.join(" AND ")
351
- else
352
- where_class = @reflection.klass.primary_key + ' = ' + r.quoted_id
353
- end
354
-
355
- @reflection.klass.update_all( field_names.join(',') , where_class)
356
- end
357
- end
358
- end
359
- end
360
-
361
- class HasOneAssociation < BelongsToAssociation #:nodoc:
362
- def construct_sql
363
- case
364
- when @reflection.options[:as]
365
- @finder_sql =
366
- "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
367
- "#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
368
- else
369
- @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
370
- end
371
-
372
- @finder_sql << " AND (#{conditions})" if conditions
373
- end
374
- end
375
-
376
- class HasManyThroughAssociation < HasManyAssociation #:nodoc:
377
- def construct_conditions_with_composite_keys
378
- if @reflection.through_reflection.options[:as]
379
- construct_conditions_without_composite_keys;
380
- else
381
- conditions = full_columns_equals(@reflection.through_reflection.table_name, @reflection.through_reflection.primary_key_name, @owner.quoted_id)
382
- conditions << " AND (#{sql_conditions})" if sql_conditions
383
- conditions
384
- end
385
- end
386
- alias_method_chain :construct_conditions, :composite_keys
387
-
388
- def construct_joins_with_composite_keys(custom_joins = nil)
389
- if @reflection.through_reflection.options[:as] || @reflection.source_reflection.options[:as]
390
- construct_joins_without_composite_keys(custom_joins)
391
- else
392
- if @reflection.source_reflection.macro == :belongs_to
393
- reflection_primary_key = @reflection.klass.primary_key
394
- source_primary_key = @reflection.source_reflection.primary_key_name
395
- else
396
- reflection_primary_key = @reflection.source_reflection.primary_key_name
397
- source_primary_key = @reflection.klass.primary_key
398
- end
399
-
400
- "INNER JOIN %s ON %s #{@reflection.options[:joins]} #{custom_joins}" % [
401
- @reflection.through_reflection.table_name,
402
- composite_join_clause(full_keys(@reflection.table_name, reflection_primary_key), full_keys(@reflection.through_reflection.table_name, source_primary_key))
403
- ]
404
- end
405
- end
406
- alias_method_chain :construct_joins, :composite_keys
407
- end
408
-
409
- end
1
+ module CompositePrimaryKeys
2
+ module ActiveRecord
3
+ module Associations
4
+ def self.append_features(base)
5
+ super
6
+ base.send(:extend, ClassMethods)
7
+ end
8
+
9
+ # Composite key versions of Association functions
10
+ module ClassMethods
11
+
12
+ def construct_counter_sql_with_included_associations(options, join_dependency)
13
+ scope = scope(:find)
14
+ sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})"
15
+
16
+ # A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
17
+ if !self.connection.supports_count_distinct?
18
+ sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{quoted_table_columns(primary_key)}"
19
+ end
20
+
21
+ sql << " FROM #{quoted_table_name} "
22
+ sql << join_dependency.join_associations.collect{|join| join.association_join }.join
23
+
24
+ add_joins!(sql, options, scope)
25
+ add_conditions!(sql, options[:conditions], scope)
26
+ add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
27
+
28
+ add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
29
+
30
+ if !self.connection.supports_count_distinct?
31
+ sql << ")"
32
+ end
33
+
34
+ return sanitize_sql(sql)
35
+ end
36
+
37
+ def construct_finder_sql_with_included_associations(options, join_dependency)
38
+ scope = scope(:find)
39
+ sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
40
+ sql << join_dependency.join_associations.collect{|join| join.association_join }.join
41
+
42
+ add_joins!(sql, options, scope)
43
+ add_conditions!(sql, options[:conditions], scope)
44
+ add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
45
+
46
+ sql << "ORDER BY #{options[:order]} " if options[:order]
47
+
48
+ add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
49
+
50
+ return sanitize_sql(sql)
51
+ end
52
+
53
+ def table_columns(columns)
54
+ columns.collect {|column| "#{self.quoted_table_name}.#{connection.quote_column_name(column)}"}
55
+ end
56
+
57
+ def quoted_table_columns(columns)
58
+ table_columns(columns).join(ID_SEP)
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
65
+ end
66
+
67
+ module ActiveRecord::Associations::ClassMethods
68
+ class JoinDependency
69
+ def construct_association(record, join, row)
70
+ case join.reflection.macro
71
+ when :has_many, :has_and_belongs_to_many
72
+ collection = record.send(join.reflection.name)
73
+ collection.loaded
74
+
75
+ join_aliased_primary_keys = join.active_record.composite? ?
76
+ join.aliased_primary_key : [join.aliased_primary_key]
77
+ return nil if
78
+ record.id.to_s != join.parent.record_id(row).to_s or not
79
+ join_aliased_primary_keys.select {|key| row[key].nil?}.blank?
80
+ association = join.instantiate(row)
81
+ collection.target.push(association) unless collection.target.include?(association)
82
+ when :has_one, :belongs_to
83
+ return if record.id.to_s != join.parent.record_id(row).to_s or
84
+ [*join.aliased_primary_key].any? { |key| row[key].nil? }
85
+ association = join.instantiate(row)
86
+ record.send("set_#{join.reflection.name}_target", association)
87
+ else
88
+ raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
89
+ end
90
+ return association
91
+ end
92
+
93
+ class JoinBase
94
+ def aliased_primary_key
95
+ active_record.composite? ?
96
+ primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} :
97
+ "#{ aliased_prefix }_r0"
98
+ end
99
+
100
+ def record_id(row)
101
+ active_record.composite? ?
102
+ aliased_primary_key.map {|key| row[key]}.to_composite_ids :
103
+ row[aliased_primary_key]
104
+ end
105
+
106
+ def column_names_with_alias
107
+ unless @column_names_with_alias
108
+ @column_names_with_alias = []
109
+ keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key]
110
+ (keys + (column_names - keys)).each_with_index do |column_name, i|
111
+ @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
112
+ end
113
+ end
114
+ return @column_names_with_alias
115
+ end
116
+ end
117
+
118
+ class JoinAssociation < JoinBase
119
+ alias single_association_join association_join
120
+ def association_join
121
+ reflection.active_record.composite? ? composite_association_join : single_association_join
122
+ end
123
+
124
+ def composite_association_join
125
+ join = case reflection.macro
126
+ when :has_and_belongs_to_many
127
+ " LEFT OUTER JOIN %s ON %s " % [
128
+ table_alias_for(options[:join_table], aliased_join_table_name),
129
+ composite_join_clause(
130
+ full_keys(aliased_join_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key),
131
+ full_keys(reflection.active_record.table_name, reflection.active_record.primary_key)
132
+ )
133
+ ] +
134
+ " LEFT OUTER JOIN %s ON %s " % [
135
+ table_name_and_alias,
136
+ composite_join_clause(
137
+ full_keys(aliased_table_name, klass.primary_key),
138
+ full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key)
139
+ )
140
+ ]
141
+ when :has_many, :has_one
142
+ case
143
+ when reflection.macro == :has_many && reflection.options[:through]
144
+ through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
145
+ if through_reflection.options[:as] # has_many :through against a polymorphic join
146
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
147
+ else
148
+ if source_reflection.macro == :has_many && source_reflection.options[:as]
149
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
150
+ else
151
+ case source_reflection.macro
152
+ when :belongs_to
153
+ first_key = primary_key
154
+ second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
155
+ when :has_many
156
+ first_key = through_reflection.klass.to_s.classify.foreign_key
157
+ second_key = options[:foreign_key] || primary_key
158
+ end
159
+
160
+ " LEFT OUTER JOIN %s ON %s " % [
161
+ table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
162
+ composite_join_clause(
163
+ full_keys(aliased_join_table_name, through_reflection.primary_key_name),
164
+ full_keys(parent.aliased_table_name, parent.primary_key)
165
+ )
166
+ ] +
167
+ " LEFT OUTER JOIN %s ON %s " % [
168
+ table_name_and_alias,
169
+ composite_join_clause(
170
+ full_keys(aliased_table_name, first_key),
171
+ full_keys(aliased_join_table_name, second_key)
172
+ )
173
+ ]
174
+ end
175
+ end
176
+
177
+ when reflection.macro == :has_many && reflection.options[:as]
178
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
179
+ when reflection.macro == :has_one && reflection.options[:as]
180
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
181
+ else
182
+ foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
183
+ " LEFT OUTER JOIN %s ON %s " % [
184
+ table_name_and_alias,
185
+ composite_join_clause(
186
+ full_keys(aliased_table_name, foreign_key),
187
+ full_keys(parent.aliased_table_name, parent.primary_key)),
188
+ ]
189
+ end
190
+ when :belongs_to
191
+ " LEFT OUTER JOIN %s ON %s " % [
192
+ table_name_and_alias,
193
+ composite_join_clause(
194
+ full_keys(aliased_table_name, reflection.klass.primary_key),
195
+ full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)),
196
+ ]
197
+ else
198
+ ""
199
+ end || ''
200
+ join << %(AND %s.%s = %s ) % [
201
+ aliased_table_name,
202
+ reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
203
+ klass.connection.quote(klass.name)] unless klass.descends_from_active_record?
204
+ join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
205
+ join
206
+ end
207
+
208
+ def full_keys(table_name, keys)
209
+ connection = reflection.active_record.connection
210
+ quoted_table_name = connection.quote_table_name(table_name)
211
+ if keys.is_a?(Array)
212
+ keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP)
213
+ else
214
+ "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
215
+ end
216
+ end
217
+
218
+ def composite_join_clause(full_keys1, full_keys2)
219
+ full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
220
+ full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
221
+ where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
222
+ "#{key1}=#{key2}"
223
+ end.join(" AND ")
224
+ "(#{where_clause})"
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ module ActiveRecord::Associations
231
+ class AssociationProxy #:nodoc:
232
+
233
+ def composite_where_clause(full_keys, ids)
234
+ full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String)
235
+
236
+ if ids.is_a?(String)
237
+ ids = [[ids]]
238
+ elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
239
+ ids = [ids.to_composite_ids]
240
+ end
241
+
242
+ where_clause = ids.map do |id_set|
243
+ transposed = id_set.size == 1 ? [[full_keys, id_set.first]] : [full_keys, id_set].transpose
244
+ transposed.map do |full_key, id|
245
+ "#{full_key.to_s}=#{@reflection.klass.sanitize(id)}"
246
+ end.join(" AND ")
247
+ end.join(") OR (")
248
+
249
+ "(#{where_clause})"
250
+ end
251
+
252
+ def composite_join_clause(full_keys1, full_keys2)
253
+ full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
254
+ full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
255
+
256
+ where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
257
+ "#{key1}=#{key2}"
258
+ end.join(" AND ")
259
+
260
+ "(#{where_clause})"
261
+ end
262
+
263
+ def full_composite_join_clause(table1, full_keys1, table2, full_keys2)
264
+ connection = @reflection.active_record.connection
265
+ full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
266
+ full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
267
+
268
+ quoted1 = connection.quote_table_name(table1)
269
+ quoted2 = connection.quote_table_name(table2)
270
+
271
+ where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
272
+ "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
273
+ end.join(" AND ")
274
+
275
+ "(#{where_clause})"
276
+ end
277
+
278
+ def full_keys(table_name, keys)
279
+ connection = @reflection.active_record.connection
280
+ quoted_table_name = connection.quote_table_name(table_name)
281
+ keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
282
+ if keys.is_a?(Array)
283
+ keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP)
284
+ else
285
+ "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
286
+ end
287
+ end
288
+
289
+ def full_columns_equals(table_name, keys, quoted_ids)
290
+ connection = @reflection.active_record.connection
291
+ quoted_table_name = connection.quote_table_name(table_name)
292
+ if keys.is_a?(Symbol) or (keys.is_a?(String) and keys == keys.to_s.split(CompositePrimaryKeys::ID_SEP))
293
+ return "#{quoted_table_name}.#{connection.quote_column_name(keys)} = #{quoted_ids}"
294
+ end
295
+ keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
296
+ quoted_ids = quoted_ids.split(CompositePrimaryKeys::ID_SEP) if quoted_ids.is_a?(String)
297
+ keys_ids = [keys, quoted_ids].transpose
298
+ keys_ids.collect {|key, id| "(#{quoted_table_name}.#{connection.quote_column_name(key)} = #{id})"}.join(' AND ')
299
+ end
300
+
301
+ def set_belongs_to_association_for(record)
302
+ if @reflection.options[:as]
303
+ record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
304
+ record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
305
+ else
306
+ key_values = @reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).zip([@owner.id].flatten)
307
+ key_values.each{|key, value| record[key] = value} unless @owner.new_record?
308
+ end
309
+ end
310
+ end
311
+
312
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
313
+ def construct_sql
314
+ @reflection.options[:finder_sql] &&= interpolate_sql(@reflection.options[:finder_sql])
315
+
316
+ if @reflection.options[:finder_sql]
317
+ @finder_sql = @reflection.options[:finder_sql]
318
+ else
319
+ @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id)
320
+ @finder_sql << " AND (#{conditions})" if conditions
321
+ end
322
+
323
+ @join_sql = "INNER JOIN #{@reflection.active_record.connection.quote_table_name(@reflection.options[:join_table])} ON " +
324
+ full_composite_join_clause(@reflection.klass.table_name, @reflection.klass.primary_key, @reflection.options[:join_table], @reflection.association_foreign_key)
325
+ end
326
+ end
327
+
328
+ class HasManyAssociation < AssociationCollection #:nodoc:
329
+ def construct_sql
330
+ case
331
+ when @reflection.options[:finder_sql]
332
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
333
+
334
+ when @reflection.options[:as]
335
+ @finder_sql =
336
+ "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
337
+ "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
338
+ @finder_sql << " AND (#{conditions})" if conditions
339
+
340
+ else
341
+ @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
342
+ @finder_sql << " AND (#{conditions})" if conditions
343
+ end
344
+
345
+ if @reflection.options[:counter_sql]
346
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
347
+ elsif @reflection.options[:finder_sql]
348
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
349
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
350
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
351
+ else
352
+ @counter_sql = @finder_sql
353
+ end
354
+ end
355
+
356
+ def delete_records(records)
357
+ if @reflection.options[:dependent]
358
+ records.each { |r| r.destroy }
359
+ else
360
+ connection = @reflection.active_record.connection
361
+ field_names = @reflection.primary_key_name.split(',')
362
+ field_names.collect! {|n| connection.quote_column_name(n) + " = NULL"}
363
+ records.each do |r|
364
+ where_clause = nil
365
+
366
+ if r.quoted_id.to_s.include?(CompositePrimaryKeys::ID_SEP)
367
+ where_clause_terms = [@reflection.klass.primary_key, r.quoted_id].transpose.map do |pair|
368
+ "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
369
+ end
370
+ where_clause = where_clause_terms.join(" AND ")
371
+ else
372
+ where_clause = connection.quote_column_name(@reflection.klass.primary_key) + ' = ' + r.quoted_id
373
+ end
374
+
375
+ @reflection.klass.update_all( field_names.join(',') , where_clause)
376
+ end
377
+ end
378
+ end
379
+ end
380
+
381
+ class HasOneAssociation < BelongsToAssociation #:nodoc:
382
+ def construct_sql
383
+ case
384
+ when @reflection.options[:as]
385
+ @finder_sql =
386
+ "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
387
+ "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
388
+ else
389
+ @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
390
+ end
391
+
392
+ @finder_sql << " AND (#{conditions})" if conditions
393
+ end
394
+ end
395
+
396
+ class HasManyThroughAssociation < HasManyAssociation #:nodoc:
397
+ def construct_conditions_with_composite_keys
398
+ if @reflection.through_reflection.options[:as]
399
+ construct_conditions_without_composite_keys
400
+ else
401
+ conditions = full_columns_equals(@reflection.through_reflection.table_name, @reflection.through_reflection.primary_key_name, @owner.quoted_id)
402
+ conditions << " AND (#{sql_conditions})" if sql_conditions
403
+ conditions
404
+ end
405
+ end
406
+ alias_method_chain :construct_conditions, :composite_keys
407
+
408
+ def construct_joins_with_composite_keys(custom_joins = nil)
409
+ if @reflection.through_reflection.options[:as] || @reflection.source_reflection.options[:as]
410
+ construct_joins_without_composite_keys(custom_joins)
411
+ else
412
+ if @reflection.source_reflection.macro == :belongs_to
413
+ reflection_primary_key = @reflection.klass.primary_key
414
+ source_primary_key = @reflection.source_reflection.primary_key_name
415
+ else
416
+ reflection_primary_key = @reflection.source_reflection.primary_key_name
417
+ source_primary_key = @reflection.klass.primary_key
418
+ end
419
+
420
+ "INNER JOIN %s ON %s #{@reflection.options[:joins]} #{custom_joins}" % [
421
+ @reflection.through_reflection.quoted_table_name,
422
+ composite_join_clause(full_keys(@reflection.table_name, reflection_primary_key), full_keys(@reflection.through_reflection.table_name, source_primary_key))
423
+ ]
424
+ end
425
+ end
426
+ alias_method_chain :construct_joins, :composite_keys
427
+ end
428
+ end