kovyrin-composite_primary_keys 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. data/History.txt +164 -0
  2. data/Manifest.txt +121 -0
  3. data/README.txt +41 -0
  4. data/README_DB2.txt +33 -0
  5. data/Rakefile +65 -0
  6. data/init.rb +2 -0
  7. data/install.rb +30 -0
  8. data/lib/adapter_helper/base.rb +63 -0
  9. data/lib/adapter_helper/mysql.rb +13 -0
  10. data/lib/adapter_helper/oracle.rb +12 -0
  11. data/lib/adapter_helper/postgresql.rb +13 -0
  12. data/lib/adapter_helper/sqlite3.rb +13 -0
  13. data/lib/composite_primary_keys/association_preload.rb +236 -0
  14. data/lib/composite_primary_keys/associations.rb +427 -0
  15. data/lib/composite_primary_keys/attribute_methods.rb +84 -0
  16. data/lib/composite_primary_keys/base.rb +341 -0
  17. data/lib/composite_primary_keys/calculations.rb +68 -0
  18. data/lib/composite_primary_keys/composite_arrays.rb +30 -0
  19. data/lib/composite_primary_keys/connection_adapters/ibm_db_adapter.rb +21 -0
  20. data/lib/composite_primary_keys/connection_adapters/oracle_adapter.rb +15 -0
  21. data/lib/composite_primary_keys/connection_adapters/postgresql_adapter.rb +53 -0
  22. data/lib/composite_primary_keys/connection_adapters/sqlite3_adapter.rb +15 -0
  23. data/lib/composite_primary_keys/fixtures.rb +8 -0
  24. data/lib/composite_primary_keys/migration.rb +20 -0
  25. data/lib/composite_primary_keys/reflection.rb +19 -0
  26. data/lib/composite_primary_keys/version.rb +8 -0
  27. data/lib/composite_primary_keys.rb +57 -0
  28. data/loader.rb +24 -0
  29. data/local/database_connections.rb.sample +10 -0
  30. data/local/paths.rb.sample +2 -0
  31. data/local/tasks.rb.sample +2 -0
  32. data/scripts/console.rb +48 -0
  33. data/scripts/txt2html +67 -0
  34. data/scripts/txt2js +59 -0
  35. data/tasks/activerecord_selection.rake +43 -0
  36. data/tasks/databases/mysql.rake +30 -0
  37. data/tasks/databases/oracle.rake +25 -0
  38. data/tasks/databases/postgresql.rake +26 -0
  39. data/tasks/databases/sqlite3.rake +28 -0
  40. data/tasks/databases.rake +12 -0
  41. data/tasks/deployment.rake +22 -0
  42. data/tasks/local_setup.rake +13 -0
  43. data/tasks/website.rake +18 -0
  44. data/test/README_tests.txt +67 -0
  45. data/test/abstract_unit.rb +96 -0
  46. data/test/connections/native_ibm_db/connection.rb +23 -0
  47. data/test/connections/native_mysql/connection.rb +13 -0
  48. data/test/connections/native_oracle/connection.rb +14 -0
  49. data/test/connections/native_postgresql/connection.rb +9 -0
  50. data/test/connections/native_sqlite/connection.rb +9 -0
  51. data/test/fixtures/article.rb +5 -0
  52. data/test/fixtures/articles.yml +6 -0
  53. data/test/fixtures/comment.rb +6 -0
  54. data/test/fixtures/comments.yml +16 -0
  55. data/test/fixtures/db_definitions/db2-create-tables.sql +113 -0
  56. data/test/fixtures/db_definitions/db2-drop-tables.sql +16 -0
  57. data/test/fixtures/db_definitions/mysql.sql +174 -0
  58. data/test/fixtures/db_definitions/oracle.drop.sql +39 -0
  59. data/test/fixtures/db_definitions/oracle.sql +188 -0
  60. data/test/fixtures/db_definitions/postgresql.sql +199 -0
  61. data/test/fixtures/db_definitions/sqlite.sql +160 -0
  62. data/test/fixtures/department.rb +5 -0
  63. data/test/fixtures/departments.yml +3 -0
  64. data/test/fixtures/employee.rb +4 -0
  65. data/test/fixtures/employees.yml +9 -0
  66. data/test/fixtures/group.rb +3 -0
  67. data/test/fixtures/groups.yml +3 -0
  68. data/test/fixtures/hack.rb +6 -0
  69. data/test/fixtures/hacks.yml +2 -0
  70. data/test/fixtures/membership.rb +7 -0
  71. data/test/fixtures/membership_status.rb +3 -0
  72. data/test/fixtures/membership_statuses.yml +10 -0
  73. data/test/fixtures/memberships.yml +6 -0
  74. data/test/fixtures/product.rb +7 -0
  75. data/test/fixtures/product_tariff.rb +5 -0
  76. data/test/fixtures/product_tariffs.yml +12 -0
  77. data/test/fixtures/products.yml +6 -0
  78. data/test/fixtures/reading.rb +4 -0
  79. data/test/fixtures/readings.yml +10 -0
  80. data/test/fixtures/reference_code.rb +7 -0
  81. data/test/fixtures/reference_codes.yml +28 -0
  82. data/test/fixtures/reference_type.rb +7 -0
  83. data/test/fixtures/reference_types.yml +9 -0
  84. data/test/fixtures/street.rb +3 -0
  85. data/test/fixtures/streets.yml +15 -0
  86. data/test/fixtures/suburb.rb +6 -0
  87. data/test/fixtures/suburbs.yml +9 -0
  88. data/test/fixtures/tariff.rb +6 -0
  89. data/test/fixtures/tariffs.yml +13 -0
  90. data/test/fixtures/user.rb +10 -0
  91. data/test/fixtures/users.yml +6 -0
  92. data/test/hash_tricks.rb +34 -0
  93. data/test/plugins/pagination.rb +405 -0
  94. data/test/plugins/pagination_helper.rb +135 -0
  95. data/test/test_associations.rb +160 -0
  96. data/test/test_attribute_methods.rb +22 -0
  97. data/test/test_attributes.rb +84 -0
  98. data/test/test_clone.rb +34 -0
  99. data/test/test_composite_arrays.rb +51 -0
  100. data/test/test_create.rb +68 -0
  101. data/test/test_delete.rb +96 -0
  102. data/test/test_dummy.rb +28 -0
  103. data/test/test_find.rb +73 -0
  104. data/test/test_ids.rb +97 -0
  105. data/test/test_miscellaneous.rb +39 -0
  106. data/test/test_pagination.rb +38 -0
  107. data/test/test_polymorphic.rb +31 -0
  108. data/test/test_santiago.rb +27 -0
  109. data/test/test_tutorial_examle.rb +26 -0
  110. data/test/test_update.rb +40 -0
  111. data/tmp/test.db +0 -0
  112. data/website/index.html +195 -0
  113. data/website/index.txt +159 -0
  114. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  115. data/website/stylesheets/screen.css +126 -0
  116. data/website/template.js +3 -0
  117. data/website/template.rhtml +53 -0
  118. data/website/version-raw.js +4 -0
  119. data/website/version-raw.txt +2 -0
  120. data/website/version.js +4 -0
  121. data/website/version.txt +3 -0
  122. metadata +197 -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,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(reflection, 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(reflection, 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(reflection, 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,427 @@
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
84
+ association = join.instantiate(row) unless [*join.aliased_primary_key].any? { |key| row[key].nil? }
85
+ record.send("set_#{join.reflection.name}_target", association)
86
+ else
87
+ raise ConfigurationError, "unknown macro: #{join.reflection.macro}"
88
+ end
89
+ return association
90
+ end
91
+
92
+ class JoinBase
93
+ def aliased_primary_key
94
+ active_record.composite? ?
95
+ primary_key.inject([]) {|aliased_keys, key| aliased_keys << "#{ aliased_prefix }_r#{aliased_keys.length}"} :
96
+ "#{ aliased_prefix }_r0"
97
+ end
98
+
99
+ def record_id(row)
100
+ active_record.composite? ?
101
+ aliased_primary_key.map {|key| row[key]}.to_composite_ids :
102
+ row[aliased_primary_key]
103
+ end
104
+
105
+ def column_names_with_alias
106
+ unless @column_names_with_alias
107
+ @column_names_with_alias = []
108
+ keys = active_record.composite? ? primary_key.map(&:to_s) : [primary_key]
109
+ (keys + (column_names - keys)).each_with_index do |column_name, i|
110
+ @column_names_with_alias << [column_name, "#{ aliased_prefix }_r#{ i }"]
111
+ end
112
+ end
113
+ return @column_names_with_alias
114
+ end
115
+ end
116
+
117
+ class JoinAssociation < JoinBase
118
+ alias single_association_join association_join
119
+ def association_join
120
+ reflection.active_record.composite? ? composite_association_join : single_association_join
121
+ end
122
+
123
+ def composite_association_join
124
+ join = case reflection.macro
125
+ when :has_and_belongs_to_many
126
+ " LEFT OUTER JOIN %s ON %s " % [
127
+ table_alias_for(options[:join_table], aliased_join_table_name),
128
+ composite_join_clause(
129
+ full_keys(aliased_join_table_name, options[:foreign_key] || reflection.active_record.to_s.classify.foreign_key),
130
+ full_keys(reflection.active_record.table_name, reflection.active_record.primary_key)
131
+ )
132
+ ] +
133
+ " LEFT OUTER JOIN %s ON %s " % [
134
+ table_name_and_alias,
135
+ composite_join_clause(
136
+ full_keys(aliased_table_name, klass.primary_key),
137
+ full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key)
138
+ )
139
+ ]
140
+ when :has_many, :has_one
141
+ case
142
+ when reflection.macro == :has_many && reflection.options[:through]
143
+ through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
144
+ if through_reflection.options[:as] # has_many :through against a polymorphic join
145
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
146
+ else
147
+ if source_reflection.macro == :has_many && source_reflection.options[:as]
148
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
149
+ else
150
+ case source_reflection.macro
151
+ when :belongs_to
152
+ first_key = primary_key
153
+ second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
154
+ when :has_many
155
+ first_key = through_reflection.klass.to_s.classify.foreign_key
156
+ second_key = options[:foreign_key] || primary_key
157
+ end
158
+
159
+ " LEFT OUTER JOIN %s ON %s " % [
160
+ table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
161
+ composite_join_clause(
162
+ full_keys(aliased_join_table_name, through_reflection.primary_key_name),
163
+ full_keys(parent.aliased_table_name, parent.primary_key)
164
+ )
165
+ ] +
166
+ " LEFT OUTER JOIN %s ON %s " % [
167
+ table_name_and_alias,
168
+ composite_join_clause(
169
+ full_keys(aliased_table_name, first_key),
170
+ full_keys(aliased_join_table_name, second_key)
171
+ )
172
+ ]
173
+ end
174
+ end
175
+
176
+ when reflection.macro == :has_many && reflection.options[:as]
177
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
178
+ when reflection.macro == :has_one && reflection.options[:as]
179
+ raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
180
+ else
181
+ foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
182
+ " LEFT OUTER JOIN %s ON %s " % [
183
+ table_name_and_alias,
184
+ composite_join_clause(
185
+ full_keys(aliased_table_name, foreign_key),
186
+ full_keys(parent.aliased_table_name, parent.primary_key)),
187
+ ]
188
+ end
189
+ when :belongs_to
190
+ " LEFT OUTER JOIN %s ON %s " % [
191
+ table_name_and_alias,
192
+ composite_join_clause(
193
+ full_keys(aliased_table_name, reflection.klass.primary_key),
194
+ full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)),
195
+ ]
196
+ else
197
+ ""
198
+ end || ''
199
+ join << %(AND %s.%s = %s ) % [
200
+ aliased_table_name,
201
+ reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
202
+ klass.connection.quote(klass.name)] unless klass.descends_from_active_record?
203
+ join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
204
+ join
205
+ end
206
+
207
+ def full_keys(table_name, keys)
208
+ connection = reflection.active_record.connection
209
+ quoted_table_name = connection.quote_table_name(table_name)
210
+ if keys.is_a?(Array)
211
+ keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP)
212
+ else
213
+ "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
214
+ end
215
+ end
216
+
217
+ def composite_join_clause(full_keys1, full_keys2)
218
+ full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
219
+ full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
220
+ where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
221
+ "#{key1}=#{key2}"
222
+ end.join(" AND ")
223
+ "(#{where_clause})"
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ module ActiveRecord::Associations
230
+ class AssociationProxy #:nodoc:
231
+
232
+ def composite_where_clause(full_keys, ids)
233
+ full_keys = full_keys.split(CompositePrimaryKeys::ID_SEP) if full_keys.is_a?(String)
234
+
235
+ if ids.is_a?(String)
236
+ ids = [[ids]]
237
+ elsif not ids.first.is_a?(Array) # if single comp key passed, turn into an array of 1
238
+ ids = [ids.to_composite_ids]
239
+ end
240
+
241
+ where_clause = ids.map do |id_set|
242
+ transposed = id_set.size == 1 ? [[full_keys, id_set.first]] : [full_keys, id_set].transpose
243
+ transposed.map do |full_key, id|
244
+ "#{full_key.to_s}=#{@reflection.klass.sanitize(id)}"
245
+ end.join(" AND ")
246
+ end.join(") OR (")
247
+
248
+ "(#{where_clause})"
249
+ end
250
+
251
+ def composite_join_clause(full_keys1, full_keys2)
252
+ full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
253
+ full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
254
+
255
+ where_clause = [full_keys1, full_keys2].transpose.map do |key1, key2|
256
+ "#{key1}=#{key2}"
257
+ end.join(" AND ")
258
+
259
+ "(#{where_clause})"
260
+ end
261
+
262
+ def full_composite_join_clause(table1, full_keys1, table2, full_keys2)
263
+ connection = @reflection.active_record.connection
264
+ full_keys1 = full_keys1.split(CompositePrimaryKeys::ID_SEP) if full_keys1.is_a?(String)
265
+ full_keys2 = full_keys2.split(CompositePrimaryKeys::ID_SEP) if full_keys2.is_a?(String)
266
+
267
+ quoted1 = connection.quote_table_name(table1)
268
+ quoted2 = connection.quote_table_name(table2)
269
+
270
+ where_clause = [full_keys1, full_keys2].transpose.map do |key_pair|
271
+ "#{quoted1}.#{connection.quote_column_name(key_pair.first)}=#{quoted2}.#{connection.quote_column_name(key_pair.last)}"
272
+ end.join(" AND ")
273
+
274
+ "(#{where_clause})"
275
+ end
276
+
277
+ def full_keys(table_name, keys)
278
+ connection = @reflection.active_record.connection
279
+ quoted_table_name = connection.quote_table_name(table_name)
280
+ keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
281
+ if keys.is_a?(Array)
282
+ keys.collect {|key| "#{quoted_table_name}.#{connection.quote_column_name(key)}"}.join(CompositePrimaryKeys::ID_SEP)
283
+ else
284
+ "#{quoted_table_name}.#{connection.quote_column_name(keys)}"
285
+ end
286
+ end
287
+
288
+ def full_columns_equals(table_name, keys, quoted_ids)
289
+ connection = @reflection.active_record.connection
290
+ quoted_table_name = connection.quote_table_name(table_name)
291
+ if keys.is_a?(Symbol) or (keys.is_a?(String) and keys == keys.to_s.split(CompositePrimaryKeys::ID_SEP))
292
+ return "#{quoted_table_name}.#{connection.quote_column_name(keys)} = #{quoted_ids}"
293
+ end
294
+ keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
295
+ quoted_ids = quoted_ids.split(CompositePrimaryKeys::ID_SEP) if quoted_ids.is_a?(String)
296
+ keys_ids = [keys, quoted_ids].transpose
297
+ keys_ids.collect {|key, id| "(#{quoted_table_name}.#{connection.quote_column_name(key)} = #{id})"}.join(' AND ')
298
+ end
299
+
300
+ def set_belongs_to_association_for(record)
301
+ if @reflection.options[:as]
302
+ record["#{@reflection.options[:as]}_id"] = @owner.id unless @owner.new_record?
303
+ record["#{@reflection.options[:as]}_type"] = @owner.class.base_class.name.to_s
304
+ else
305
+ key_values = @reflection.primary_key_name.to_s.split(CompositePrimaryKeys::ID_SEP).zip([@owner.id].flatten)
306
+ key_values.each{|key, value| record[key] = value} unless @owner.new_record?
307
+ end
308
+ end
309
+ end
310
+
311
+ class HasAndBelongsToManyAssociation < AssociationCollection #:nodoc:
312
+ def construct_sql
313
+ @reflection.options[:finder_sql] &&= interpolate_sql(@reflection.options[:finder_sql])
314
+
315
+ if @reflection.options[:finder_sql]
316
+ @finder_sql = @reflection.options[:finder_sql]
317
+ else
318
+ @finder_sql = full_columns_equals(@reflection.options[:join_table], @reflection.primary_key_name, @owner.quoted_id)
319
+ @finder_sql << " AND (#{conditions})" if conditions
320
+ end
321
+
322
+ @join_sql = "INNER JOIN #{@reflection.active_record.connection.quote_table_name(@reflection.options[:join_table])} ON " +
323
+ full_composite_join_clause(@reflection.klass.table_name, @reflection.klass.primary_key, @reflection.options[:join_table], @reflection.association_foreign_key)
324
+ end
325
+ end
326
+
327
+ class HasManyAssociation < AssociationCollection #:nodoc:
328
+ def construct_sql
329
+ case
330
+ when @reflection.options[:finder_sql]
331
+ @finder_sql = interpolate_sql(@reflection.options[:finder_sql])
332
+
333
+ when @reflection.options[:as]
334
+ @finder_sql =
335
+ "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
336
+ "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
337
+ @finder_sql << " AND (#{conditions})" if conditions
338
+
339
+ else
340
+ @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
341
+ @finder_sql << " AND (#{conditions})" if conditions
342
+ end
343
+
344
+ if @reflection.options[:counter_sql]
345
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
346
+ elsif @reflection.options[:finder_sql]
347
+ # replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
348
+ @reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
349
+ @counter_sql = interpolate_sql(@reflection.options[:counter_sql])
350
+ else
351
+ @counter_sql = @finder_sql
352
+ end
353
+ end
354
+
355
+ def delete_records(records)
356
+ if @reflection.options[:dependent]
357
+ records.each { |r| r.destroy }
358
+ else
359
+ connection = @reflection.active_record.connection
360
+ field_names = @reflection.primary_key_name.split(',')
361
+ field_names.collect! {|n| connection.quote_column_name(n) + " = NULL"}
362
+ records.each do |r|
363
+ where_clause = nil
364
+
365
+ if r.quoted_id.to_s.include?(CompositePrimaryKeys::ID_SEP)
366
+ where_clause_terms = [@reflection.klass.primary_key, r.quoted_id].transpose.map do |pair|
367
+ "(#{connection.quote_column_name(pair[0])} = #{pair[1]})"
368
+ end
369
+ where_clause = where_clause_terms.join(" AND ")
370
+ else
371
+ where_clause = connection.quote_column_name(@reflection.klass.primary_key) + ' = ' + r.quoted_id
372
+ end
373
+
374
+ @reflection.klass.update_all( field_names.join(',') , where_clause)
375
+ end
376
+ end
377
+ end
378
+ end
379
+
380
+ class HasOneAssociation < BelongsToAssociation #:nodoc:
381
+ def construct_sql
382
+ case
383
+ when @reflection.options[:as]
384
+ @finder_sql =
385
+ "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
386
+ "#{@reflection.klass.quoted_table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote_value(@owner.class.base_class.name.to_s)}"
387
+ else
388
+ @finder_sql = full_columns_equals(@reflection.klass.table_name, @reflection.primary_key_name, @owner.quoted_id)
389
+ end
390
+
391
+ @finder_sql << " AND (#{conditions})" if conditions
392
+ end
393
+ end
394
+
395
+ class HasManyThroughAssociation < HasManyAssociation #:nodoc:
396
+ def construct_conditions_with_composite_keys
397
+ if @reflection.through_reflection.options[:as]
398
+ construct_conditions_without_composite_keys
399
+ else
400
+ conditions = full_columns_equals(@reflection.through_reflection.table_name, @reflection.through_reflection.primary_key_name, @owner.quoted_id)
401
+ conditions << " AND (#{sql_conditions})" if sql_conditions
402
+ conditions
403
+ end
404
+ end
405
+ alias_method_chain :construct_conditions, :composite_keys
406
+
407
+ def construct_joins_with_composite_keys(custom_joins = nil)
408
+ if @reflection.through_reflection.options[:as] || @reflection.source_reflection.options[:as]
409
+ construct_joins_without_composite_keys(custom_joins)
410
+ else
411
+ if @reflection.source_reflection.macro == :belongs_to
412
+ reflection_primary_key = @reflection.klass.primary_key
413
+ source_primary_key = @reflection.source_reflection.primary_key_name
414
+ else
415
+ reflection_primary_key = @reflection.source_reflection.primary_key_name
416
+ source_primary_key = @reflection.klass.primary_key
417
+ end
418
+
419
+ "INNER JOIN %s ON %s #{@reflection.options[:joins]} #{custom_joins}" % [
420
+ @reflection.through_reflection.quoted_table_name,
421
+ composite_join_clause(full_keys(@reflection.table_name, reflection_primary_key), full_keys(@reflection.through_reflection.table_name, source_primary_key))
422
+ ]
423
+ end
424
+ end
425
+ alias_method_chain :construct_joins, :composite_keys
426
+ end
427
+ end