jwulff-composite_primary_keys 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :patch: 9
3
+ :major: 1
4
+ :minor: 0
@@ -0,0 +1,63 @@
1
+ module AdapterHelper
2
+ class Base
3
+ class << self
4
+ attr_accessor :adapter
5
+
6
+ def load_connection_from_env(adapter)
7
+ self.adapter = adapter
8
+ unless ENV['cpk_adapters']
9
+ puts error_msg_setup_helper
10
+ exit
11
+ end
12
+
13
+ ActiveRecord::Base.configurations = YAML.load(ENV['cpk_adapters'])
14
+ unless spec = ActiveRecord::Base.configurations[adapter]
15
+ puts error_msg_adapter_helper
16
+ exit
17
+ end
18
+ spec[:adapter] = adapter
19
+ spec
20
+ end
21
+
22
+ def error_msg_setup_helper
23
+ <<-EOS
24
+ Setup Helper:
25
+ CPK now has a place for your individual testing configuration.
26
+ That is, instead of hardcoding it in the Rakefile and test/connections files,
27
+ there is now a local/database_connections.rb file that is NOT in the
28
+ repository. Your personal DB information (username, password etc) can
29
+ be stored here without making it difficult to submit patches etc.
30
+
31
+ Installation:
32
+ i) cp locals/database_connections.rb.sample locals/database_connections.rb
33
+ ii) For #{adapter} connection details see "Adapter Setup Helper" below.
34
+ iii) Rerun this task
35
+
36
+ #{error_msg_adapter_helper}
37
+
38
+ Current ENV:
39
+ #{ENV.inspect}
40
+ EOS
41
+ end
42
+
43
+ def error_msg_adapter_helper
44
+ <<-EOS
45
+ Adapter Setup Helper:
46
+ To run #{adapter} tests, you need to setup your #{adapter} connections.
47
+ In your local/database_connections.rb file, within the ENV['cpk_adapter'] hash, add:
48
+ "#{adapter}" => { adapter settings }
49
+
50
+ That is, it will look like:
51
+ ENV['cpk_adapters'] = {
52
+ "#{adapter}" => {
53
+ :adapter => "#{adapter}",
54
+ :username => "root",
55
+ :password => "root",
56
+ # ...
57
+ }
58
+ }.to_yaml
59
+ EOS
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,13 @@
1
+ require File.join(File.dirname(__FILE__), 'base')
2
+
3
+ module AdapterHelper
4
+ class MySQL < Base
5
+ class << self
6
+ def load_connection_from_env
7
+ spec = super('mysql')
8
+ spec[:database] ||= 'composite_primary_keys_unittest'
9
+ spec
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,12 @@
1
+ require File.join(File.dirname(__FILE__), 'base')
2
+
3
+ module AdapterHelper
4
+ class Oracle < Base
5
+ class << self
6
+ def load_connection_from_env
7
+ spec = super('oracle')
8
+ spec
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ require File.join(File.dirname(__FILE__), 'base')
2
+
3
+ module AdapterHelper
4
+ class Postgresql < Base
5
+ class << self
6
+ def load_connection_from_env
7
+ spec = super('postgresql')
8
+ spec[:database] ||= 'composite_primary_keys_unittest'
9
+ spec
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require File.join(File.dirname(__FILE__), 'base')
2
+
3
+ module AdapterHelper
4
+ class Sqlite3 < Base
5
+ class << self
6
+ def load_connection_from_env
7
+ spec = super('sqlite3')
8
+ spec[:dbfile] ||= "tmp/test.db"
9
+ spec
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,55 @@
1
+ #--
2
+ # Copyright (c) 2006 Nic Williams
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ $:.unshift(File.dirname(__FILE__)) unless
25
+ $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
26
+
27
+ unless defined?(ActiveRecord)
28
+ begin
29
+ require 'active_record'
30
+ rescue LoadError
31
+ require 'rubygems'
32
+ require_gem 'activerecord'
33
+ end
34
+ end
35
+
36
+ require 'composite_primary_keys/fixtures'
37
+ require 'composite_primary_keys/composite_arrays'
38
+ require 'composite_primary_keys/associations'
39
+ require 'composite_primary_keys/association_preload'
40
+ require 'composite_primary_keys/reflection'
41
+ require 'composite_primary_keys/base'
42
+ require 'composite_primary_keys/calculations'
43
+ require 'composite_primary_keys/migration'
44
+ require 'composite_primary_keys/attribute_methods'
45
+
46
+ ActiveRecord::Base.class_eval do
47
+ include CompositePrimaryKeys::ActiveRecord::Base
48
+ end
49
+
50
+ Dir[File.dirname(__FILE__) + '/composite_primary_keys/connection_adapters/*.rb'].each do |adapter|
51
+ begin
52
+ require adapter.gsub('.rb','')
53
+ rescue MissingSourceFile
54
+ end
55
+ end
@@ -0,0 +1,236 @@
1
+ module CompositePrimaryKeys
2
+ module ActiveRecord
3
+ module AssociationPreload
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
+ def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
12
+ table_name = reflection.klass.quoted_table_name
13
+ id_to_record_map, ids = construct_id_map(records)
14
+ records.each {|record| record.send(reflection.name).loaded}
15
+ options = reflection.options
16
+
17
+ if composite?
18
+ primary_key = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
19
+ where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
20
+ "(" + keys.map{|key| "t0.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
21
+ end.join(" OR ")
22
+
23
+ conditions = [where, ids].flatten
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
+ else
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
+ parent_record_id = reflection.primary_key_name
31
+ end
32
+
33
+ conditions.first << append_conditions(options, preload_options)
34
+
35
+ associated_records = reflection.klass.find(:all,
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
+
42
+ set_association_collection_records(id_to_record_map, reflection.name, associated_records, 'parent_record_id_')
43
+ end
44
+
45
+ def preload_has_many_association(records, reflection, preload_options={})
46
+ id_to_record_map, ids = construct_id_map(records)
47
+ records.each {|record| record.send(reflection.name).loaded}
48
+ options = reflection.options
49
+
50
+ if options[:through]
51
+ through_records = preload_through_records(records, reflection, options[:through])
52
+ through_reflection = reflections[options[:through]]
53
+ through_primary_key = through_reflection.primary_key_name
54
+
55
+ unless through_records.empty?
56
+ source = reflection.source_reflection.name
57
+ #add conditions from reflection!
58
+ through_records.first.class.preload_associations(through_records, source, reflection.options)
59
+ through_records.each do |through_record|
60
+ key = through_primary_key.to_s.split(CompositePrimaryKeys::ID_SEP).map{|k| through_record.send(k)}.join(CompositePrimaryKeys::ID_SEP)
61
+ add_preloaded_records_to_collection(id_to_record_map[key], reflection.name, through_record.send(source))
62
+ end
63
+ end
64
+ else
65
+ associated_records = find_associated_records(ids, reflection, preload_options)
66
+ set_association_collection_records(id_to_record_map, reflection.name, associated_records, reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP))
67
+ end
68
+ end
69
+
70
+ def preload_through_records(records, reflection, through_association)
71
+ through_reflection = reflections[through_association]
72
+ through_primary_key = through_reflection.primary_key_name
73
+
74
+ if reflection.options[:source_type]
75
+ interface = reflection.source_reflection.options[:foreign_type]
76
+ preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
77
+
78
+ records.compact!
79
+ records.first.class.preload_associations(records, through_association, preload_options)
80
+
81
+ # Dont cache the association - we would only be caching a subset
82
+ through_records = []
83
+ records.each do |record|
84
+ proxy = record.send(through_association)
85
+
86
+ if proxy.respond_to?(:target)
87
+ through_records << proxy.target
88
+ proxy.reset
89
+ else # this is a has_one :through reflection
90
+ through_records << proxy if proxy
91
+ end
92
+ end
93
+ through_records.flatten!
94
+ else
95
+ records.first.class.preload_associations(records, through_association)
96
+ through_records = records.map {|record| record.send(through_association)}.flatten
97
+ end
98
+
99
+ through_records.compact!
100
+ through_records
101
+ end
102
+
103
+ def preload_belongs_to_association(records, reflection, preload_options={})
104
+ options = reflection.options
105
+ primary_key_name = reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP)
106
+
107
+ if options[:polymorphic]
108
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
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...]}}
114
+ id_map = {}
115
+
116
+ records.each do |record|
117
+ key = primary_key_name.map{|k| record.send(k)}
118
+ key_as_string = key.join(CompositePrimaryKeys::ID_SEP)
119
+
120
+ if key_as_string
121
+ mapped_records = (id_map[key_as_string] ||= {:id => key, :records => []})
122
+ mapped_records[:records] << record
123
+ end
124
+ end
125
+
126
+
127
+ klasses_and_ids = [[reflection.klass.name, id_map]]
128
+ end
129
+
130
+ klasses_and_ids.each do |klass_and_id|
131
+ klass_name, id_map = *klass_and_id
132
+ klass = klass_name.constantize
133
+ table_name = klass.quoted_table_name
134
+ connection = reflection.active_record.connection
135
+
136
+ if composite?
137
+ primary_key = klass.primary_key.to_s.split(CompositePrimaryKeys::ID_SEP)
138
+ ids = id_map.keys.uniq.map {|id| id_map[id][:id]}
139
+
140
+ where = (primary_key * ids.size).in_groups_of(primary_key.size).map do |keys|
141
+ "(" + keys.map{|key| "#{table_name}.#{connection.quote_column_name(key)} = ?"}.join(" AND ") + ")"
142
+ end.join(" OR ")
143
+
144
+ conditions = [where, ids].flatten
145
+ else
146
+ conditions = ["#{table_name}.#{connection.quote_column_name(primary_key)} IN (?)", id_map.keys.uniq]
147
+ end
148
+
149
+ conditions.first << append_conditions(options, preload_options)
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
+
158
+ set_association_single_records(id_map, reflection.name, associated_records, primary_key)
159
+ end
160
+ end
161
+
162
+ def set_association_collection_records(id_to_record_map, reflection_name, associated_records, key)
163
+ associated_records.each do |associated_record|
164
+ associated_record_key = associated_record[key]
165
+ associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
166
+ mapped_records = id_to_record_map[associated_record_key]
167
+ add_preloaded_records_to_collection(mapped_records, reflection_name, associated_record)
168
+ end
169
+ end
170
+
171
+ def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
172
+ seen_keys = {}
173
+ associated_records.each do |associated_record|
174
+ associated_record_key = associated_record[key]
175
+ associated_record_key = associated_record_key.is_a?(Array) ? associated_record_key.join(CompositePrimaryKeys::ID_SEP) : associated_record_key.to_s
176
+
177
+ #this is a has_one or belongs_to: there should only be one record.
178
+ #Unfortunately we can't (in portable way) ask the database for 'all records where foo_id in (x,y,z), but please
179
+ # only one row per distinct foo_id' so this where we enforce that
180
+ next if seen_keys[associated_record_key]
181
+ seen_keys[associated_record_key] = true
182
+ mapped_records = id_to_record_map[associated_record_key][:records]
183
+ mapped_records.each do |mapped_record|
184
+ mapped_record.send("set_#{reflection_name}_target", associated_record)
185
+ end
186
+ end
187
+ end
188
+
189
+ def find_associated_records(ids, reflection, preload_options)
190
+ options = reflection.options
191
+ table_name = reflection.klass.quoted_table_name
192
+
193
+ if interface = reflection.options[:as]
194
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
195
+ else
196
+ connection = reflection.active_record.connection
197
+ foreign_key = reflection.primary_key_name
198
+ conditions = ["#{table_name}.#{connection.quote_column_name(foreign_key)} IN (?)", ids]
199
+
200
+ if composite?
201
+ foreign_keys = foreign_key.to_s.split(CompositePrimaryKeys::ID_SEP)
202
+
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 ") + ")"
205
+ end.join(" OR ")
206
+
207
+ conditions = [where, ids].flatten
208
+ end
209
+ end
210
+
211
+ conditions.first << append_conditions(options, preload_options)
212
+
213
+ reflection.klass.find(:all,
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])
220
+ end
221
+
222
+ def full_composite_join_clause(reflection, table1, full_keys1, table2, full_keys2)
223
+ connection = reflection.active_record.connection
224
+ full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
225
+ full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
226
+ where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
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)}"
230
+ end.join(" AND ")
231
+ "(#{where_clause})"
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +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 #{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