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.
- data/VERSION.yml +4 -0
- data/lib/adapter_helper/base.rb +63 -0
- data/lib/adapter_helper/mysql.rb +13 -0
- data/lib/adapter_helper/oracle.rb +12 -0
- data/lib/adapter_helper/postgresql.rb +13 -0
- data/lib/adapter_helper/sqlite3.rb +13 -0
- data/lib/composite_primary_keys.rb +55 -0
- data/lib/composite_primary_keys/association_preload.rb +236 -0
- data/lib/composite_primary_keys/associations.rb +428 -0
- data/lib/composite_primary_keys/attribute_methods.rb +84 -0
- data/lib/composite_primary_keys/base.rb +320 -0
- data/lib/composite_primary_keys/calculations.rb +68 -0
- data/lib/composite_primary_keys/composite_arrays.rb +30 -0
- data/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb +21 -0
- data/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb +15 -0
- data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +53 -0
- data/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb +15 -0
- data/lib/composite_primary_keys/fixtures.rb +8 -0
- data/lib/composite_primary_keys/migration.rb +20 -0
- data/lib/composite_primary_keys/reflection.rb +19 -0
- data/lib/composite_primary_keys/version.rb +8 -0
- metadata +77 -0
data/VERSION.yml
ADDED
@@ -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,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,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
|