jwulff-composite_primary_keys 1.0.9

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.
@@ -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