composite_primary_keys 0.3.3 → 0.6.0
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/CHANGELOG +4 -0
- data/README +16 -0
- data/lib/composite_primary_keys/associations.rb +328 -3
- data/lib/composite_primary_keys/base.rb +32 -32
- data/lib/composite_primary_keys/version.rb +2 -2
- data/test/associations_test.rb +44 -2
- data/test/fixtures/product.rb +1 -0
- data/test/fixtures/tariff.rb +2 -0
- data/test/miscellaneous_test.rb +5 -0
- metadata +2 -2
data/CHANGELOG
CHANGED
data/README
CHANGED
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
Composite Primary Keys for ActiveRecords
|
3
|
+
Summary:
|
4
|
+
ActiveRecords/Rails famously doesn't support composite primary keys.
|
5
|
+
For 2 years there has been no support for this aspect of legacy databases.
|
6
|
+
This project will be a fully-featured solution and I welcome use cases and
|
7
|
+
legacy DB schema samples to help make the project work with as many common
|
8
|
+
legacy environments as possible.
|
9
|
+
|
10
|
+
Url:
|
11
|
+
http://compositekeys.rubyforge.org
|
12
|
+
|
13
|
+
Questions and Discussion:
|
14
|
+
http://groups.google.com/compositekeys
|
15
|
+
|
16
|
+
Written by Dr Nic Williams, drnicwilliams@gmail
|
@@ -1,4 +1,329 @@
|
|
1
|
-
module
|
2
|
-
|
1
|
+
module CompositePrimaryKeys
|
2
|
+
module ActiveRecord
|
3
|
+
module Associations
|
4
|
+
def self.append_features(base)
|
5
|
+
super
|
6
|
+
base.send(:extend, ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
# Composite key versions of Association functions
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
def construct_counter_sql_with_included_associations(options, join_dependency)
|
13
|
+
scope = scope(:find)
|
14
|
+
sql = "SELECT COUNT(DISTINCT #{quoted_table_columns(primary_key)})"
|
15
|
+
|
16
|
+
# A (slower) workaround if we're using a backend, like sqlite, that doesn't support COUNT DISTINCT.
|
17
|
+
if !self.connection.supports_count_distinct?
|
18
|
+
sql = "SELECT COUNT(*) FROM (SELECT DISTINCT #{quoted_table_columns(primary_key)}"
|
19
|
+
end
|
20
|
+
|
21
|
+
sql << " FROM #{table_name} "
|
22
|
+
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
|
23
|
+
|
24
|
+
add_joins!(sql, options, scope)
|
25
|
+
add_conditions!(sql, options[:conditions], scope)
|
26
|
+
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
|
27
|
+
|
28
|
+
add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
|
29
|
+
|
30
|
+
if !self.connection.supports_count_distinct?
|
31
|
+
sql << ")"
|
32
|
+
end
|
33
|
+
|
34
|
+
return sanitize_sql(sql)
|
35
|
+
end
|
36
|
+
|
37
|
+
def construct_finder_sql_with_included_associations(options, join_dependency)
|
38
|
+
scope = scope(:find)
|
39
|
+
sql = "SELECT #{column_aliases(join_dependency)} FROM #{(scope && scope[:from]) || options[:from] || table_name} "
|
40
|
+
sql << join_dependency.join_associations.collect{|join| join.association_join }.join
|
41
|
+
|
42
|
+
add_joins!(sql, options, scope)
|
43
|
+
add_conditions!(sql, options[:conditions], scope)
|
44
|
+
add_limited_ids_condition!(sql, options, join_dependency) if !using_limitable_reflections?(join_dependency.reflections) && options[:limit]
|
45
|
+
|
46
|
+
sql << "ORDER BY #{options[:order]} " if options[:order]
|
47
|
+
|
48
|
+
add_limit!(sql, options, scope) if using_limitable_reflections?(join_dependency.reflections)
|
49
|
+
|
50
|
+
return sanitize_sql(sql)
|
51
|
+
end
|
52
|
+
|
53
|
+
def table_columns(columns)
|
54
|
+
columns.collect {|column| "#{self.table_name}.#{column}"}
|
55
|
+
end
|
56
|
+
|
57
|
+
def quoted_table_columns(columns)
|
58
|
+
table_columns(columns).join(ID_SEP)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
3
64
|
end
|
4
|
-
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 row[join.aliased_primary_key].nil?
|
84
|
+
association = join.instantiate(row)
|
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? ?
|
121
|
+
composite_association_join :
|
122
|
+
single_association_join
|
123
|
+
end
|
124
|
+
|
125
|
+
def composite_association_join
|
126
|
+
join = case reflection.macro
|
127
|
+
when :has_and_belongs_to_many
|
128
|
+
" LEFT OUTER JOIN %s ON (%s) = (%s) " % [
|
129
|
+
table_alias_for(options[:join_table], aliased_join_table_name),
|
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
|
+
" LEFT OUTER JOIN %s ON (%s) = (%s) " % [
|
133
|
+
table_name_and_alias,
|
134
|
+
full_keys(aliased_table_name, klass.primary_key),
|
135
|
+
full_keys(aliased_join_table_name, options[:association_foreign_key] || klass.table_name.classify.foreign_key)
|
136
|
+
]
|
137
|
+
when :has_many, :has_one
|
138
|
+
case
|
139
|
+
when reflection.macro == :has_many && reflection.options[:through]
|
140
|
+
through_conditions = through_reflection.options[:conditions] ? "AND #{interpolate_sql(sanitize_sql(through_reflection.options[:conditions]))}" : ''
|
141
|
+
if through_reflection.options[:as] # has_many :through against a polymorphic join
|
142
|
+
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
|
143
|
+
else
|
144
|
+
if source_reflection.macro == :has_many && source_reflection.options[:as]
|
145
|
+
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
|
146
|
+
else
|
147
|
+
case source_reflection.macro
|
148
|
+
when :belongs_to
|
149
|
+
first_key = primary_key
|
150
|
+
second_key = options[:foreign_key] || klass.to_s.classify.foreign_key
|
151
|
+
when :has_many
|
152
|
+
first_key = through_reflection.klass.to_s.classify.foreign_key
|
153
|
+
second_key = options[:foreign_key] || primary_key
|
154
|
+
end
|
155
|
+
|
156
|
+
" LEFT OUTER JOIN %s ON (%s) = (%s) " % [
|
157
|
+
table_alias_for(through_reflection.klass.table_name, aliased_join_table_name),
|
158
|
+
full_keys(aliased_join_table_name, through_reflection.primary_key_name),
|
159
|
+
full_keys(parent.aliased_table_name, parent.primary_key)] +
|
160
|
+
" LEFT OUTER JOIN %s ON (%s) = (%s) " % [
|
161
|
+
table_name_and_alias,
|
162
|
+
full_keys(aliased_table_name, first_key),
|
163
|
+
full_keys(aliased_join_table_name, second_key)
|
164
|
+
]
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
when reflection.macro == :has_many && reflection.options[:as]
|
169
|
+
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
|
170
|
+
when reflection.macro == :has_one && reflection.options[:as]
|
171
|
+
raise AssociationNotSupported, "Polymorphic joins not supported for composite keys"
|
172
|
+
else
|
173
|
+
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
|
174
|
+
" LEFT OUTER JOIN %s ON (%s) = (%s) " % [
|
175
|
+
table_name_and_alias,
|
176
|
+
full_keys(aliased_table_name, foreign_key),
|
177
|
+
full_keys(parent.aliased_table_name, parent.primary_key)
|
178
|
+
]
|
179
|
+
end
|
180
|
+
when :belongs_to
|
181
|
+
" LEFT OUTER JOIN %s ON (%s) = (%s) " % [
|
182
|
+
table_name_and_alias,
|
183
|
+
full_keys(aliased_table_name, reflection.klass.primary_key),
|
184
|
+
full_keys(parent.aliased_table_name, options[:foreign_key] || klass.to_s.foreign_key)
|
185
|
+
]
|
186
|
+
else
|
187
|
+
""
|
188
|
+
end || ''
|
189
|
+
join << %(AND %s.%s = %s ) % [
|
190
|
+
aliased_table_name,
|
191
|
+
reflection.active_record.connection.quote_column_name(reflection.active_record.inheritance_column),
|
192
|
+
klass.quote(klass.name)] unless klass.descends_from_active_record?
|
193
|
+
join << "AND #{interpolate_sql(sanitize_sql(reflection.options[:conditions]))} " if reflection.options[:conditions]
|
194
|
+
join
|
195
|
+
end
|
196
|
+
|
197
|
+
def full_keys(table_name, keys)
|
198
|
+
keys.collect {|key| "#{table_name}.#{key}"}.join(CompositePrimaryKeys::ID_SEP)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
module ActiveRecord::Associations
|
205
|
+
class AssociationProxy #:nodoc:
|
206
|
+
def full_keys(table_name, keys)
|
207
|
+
keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
|
208
|
+
keys.is_a?(Array) ?
|
209
|
+
keys.collect {|key| "#{table_name}.#{key}"}.join(CompositePrimaryKeys::ID_SEP) :
|
210
|
+
"#{table_name}.#{keys}"
|
211
|
+
end
|
212
|
+
|
213
|
+
def full_columns_equals(table_name, keys, quoted_ids)
|
214
|
+
keys = keys.split(CompositePrimaryKeys::ID_SEP) if keys.is_a?(String)
|
215
|
+
keys_ids = [keys, quoted_ids]
|
216
|
+
keys_ids = keys.is_a?(Symbol) ? [keys_ids] : keys_ids.transpose
|
217
|
+
keys_ids.collect {|key, id| "(#{table_name}.#{key} = #{id})"}.join(' AND ')
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
class HasManyAssociation < AssociationCollection #:nodoc:
|
222
|
+
def construct_sql
|
223
|
+
case
|
224
|
+
when @reflection.options[:finder_sql]
|
225
|
+
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
226
|
+
|
227
|
+
when @reflection.options[:as]
|
228
|
+
@finder_sql =
|
229
|
+
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
|
230
|
+
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}"
|
231
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
232
|
+
|
233
|
+
else
|
234
|
+
@finder_sql = full_columns_equals(@reflection.klass.table_name,
|
235
|
+
@reflection.primary_key_name, @owner.quoted_id)
|
236
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
237
|
+
end
|
238
|
+
|
239
|
+
if @reflection.options[:counter_sql]
|
240
|
+
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
241
|
+
elsif @reflection.options[:finder_sql]
|
242
|
+
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
|
243
|
+
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
|
244
|
+
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
245
|
+
else
|
246
|
+
@counter_sql = @finder_sql
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
class HasOneAssociation < BelongsToAssociation #:nodoc:
|
252
|
+
def construct_sql
|
253
|
+
case
|
254
|
+
when @reflection.options[:as]
|
255
|
+
@finder_sql =
|
256
|
+
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_id = #{@owner.quoted_id} AND " +
|
257
|
+
"#{@reflection.klass.table_name}.#{@reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}"
|
258
|
+
else
|
259
|
+
@finder_sql = "(%s) = (%s)" % [
|
260
|
+
full_keys(@reflection.table_name, @reflection.primary_key_name),
|
261
|
+
@owner.quoted_id
|
262
|
+
]
|
263
|
+
end
|
264
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
class HasManyThroughAssociation < AssociationProxy #:nodoc:
|
269
|
+
def construct_conditions
|
270
|
+
conditions = if @reflection.through_reflection.options[:as]
|
271
|
+
"#{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_id = #{@owner.quoted_id} " +
|
272
|
+
"AND #{@reflection.through_reflection.table_name}.#{@reflection.through_reflection.options[:as]}_type = #{@owner.class.quote @owner.class.base_class.name.to_s}"
|
273
|
+
else
|
274
|
+
@finder_sql = full_columns_equals(@reflection.through_reflection.table_name,
|
275
|
+
@reflection.through_reflection.primary_key_name, @owner.quoted_id)
|
276
|
+
end
|
277
|
+
conditions << " AND (#{sql_conditions})" if sql_conditions
|
278
|
+
|
279
|
+
return conditions
|
280
|
+
end
|
281
|
+
|
282
|
+
def construct_joins(custom_joins = nil)
|
283
|
+
polymorphic_join = nil
|
284
|
+
if @reflection.through_reflection.options[:as] || @reflection.source_reflection.macro == :belongs_to
|
285
|
+
reflection_primary_key = @reflection.klass.primary_key
|
286
|
+
source_primary_key = @reflection.source_reflection.primary_key_name
|
287
|
+
else
|
288
|
+
reflection_primary_key = @reflection.source_reflection.primary_key_name
|
289
|
+
source_primary_key = @reflection.klass.primary_key
|
290
|
+
if @reflection.source_reflection.options[:as]
|
291
|
+
polymorphic_join = "AND %s.%s = %s" % [
|
292
|
+
@reflection.table_name, "#{@reflection.source_reflection.options[:as]}_type",
|
293
|
+
@owner.class.quote(@reflection.through_reflection.klass.name)
|
294
|
+
]
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
"INNER JOIN %s ON (%s) = (%s) %s #{@reflection.options[:joins]} #{custom_joins}" % [
|
299
|
+
@reflection.through_reflection.table_name,
|
300
|
+
full_keys(@reflection.table_name, reflection_primary_key),
|
301
|
+
full_keys(@reflection.through_reflection.table_name, source_primary_key),
|
302
|
+
polymorphic_join
|
303
|
+
]
|
304
|
+
end
|
305
|
+
|
306
|
+
def construct_sql
|
307
|
+
case
|
308
|
+
when @reflection.options[:finder_sql]
|
309
|
+
@finder_sql = interpolate_sql(@reflection.options[:finder_sql])
|
310
|
+
|
311
|
+
@finder_sql = "(%s) = (%s)" % [
|
312
|
+
full_keys(@reflection.klass.table_name, @reflection.primary_key_name),
|
313
|
+
@owner.quoted_id
|
314
|
+
]
|
315
|
+
@finder_sql << " AND (#{conditions})" if conditions
|
316
|
+
end
|
317
|
+
|
318
|
+
if @reflection.options[:counter_sql]
|
319
|
+
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
320
|
+
elsif @reflection.options[:finder_sql]
|
321
|
+
# replace the SELECT clause with COUNT(*), preserving any hints within /* ... */
|
322
|
+
@reflection.options[:counter_sql] = @reflection.options[:finder_sql].sub(/SELECT (\/\*.*?\*\/ )?(.*)\bFROM\b/im) { "SELECT #{$1}COUNT(*) FROM" }
|
323
|
+
@counter_sql = interpolate_sql(@reflection.options[:counter_sql])
|
324
|
+
else
|
325
|
+
@counter_sql = @finder_sql
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
@@ -16,16 +16,15 @@ module CompositePrimaryKeys
|
|
16
16
|
|
17
17
|
module ClassMethods
|
18
18
|
def set_primary_keys(*keys)
|
19
|
-
keys = keys.first if keys.first.is_a?(Array)
|
19
|
+
keys = keys.first if keys.first.is_a?(Array)
|
20
20
|
cattr_accessor :primary_keys
|
21
21
|
self.primary_keys = keys.to_composite_keys
|
22
22
|
|
23
23
|
class_eval <<-EOV
|
24
|
-
include CompositePrimaryKeys::ActiveRecord::Base::CompositeInstanceMethods
|
25
24
|
extend CompositePrimaryKeys::ActiveRecord::Base::CompositeClassMethods
|
25
|
+
include CompositePrimaryKeys::ActiveRecord::Base::CompositeInstanceMethods
|
26
|
+
include CompositePrimaryKeys::ActiveRecord::Associations
|
26
27
|
EOV
|
27
|
-
|
28
|
-
#puts "#{self.class}-#{self}.composite = #{self.composite?}"
|
29
28
|
end
|
30
29
|
|
31
30
|
def composite?
|
@@ -75,19 +74,6 @@ module CompositePrimaryKeys
|
|
75
74
|
id
|
76
75
|
end
|
77
76
|
|
78
|
-
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
79
|
-
# be made (since they can't be persisted).
|
80
|
-
def destroy
|
81
|
-
unless new_record?
|
82
|
-
connection.delete <<-end_sql, "#{self.class.name} Destroy"
|
83
|
-
DELETE FROM #{self.class.table_name}
|
84
|
-
WHERE (#{self.class.primary_key}) = (#{quoted_id})
|
85
|
-
end_sql
|
86
|
-
end
|
87
|
-
|
88
|
-
freeze
|
89
|
-
end
|
90
|
-
|
91
77
|
# Returns a clone of the record that hasn't been assigned an id yet and
|
92
78
|
# is treated as a new record. Note that this is a "shallow" clone:
|
93
79
|
# it copies the object's attributes only, not its associations.
|
@@ -171,20 +157,11 @@ module CompositePrimaryKeys
|
|
171
157
|
end
|
172
158
|
|
173
159
|
private
|
174
|
-
#
|
175
|
-
|
176
|
-
connection.update(
|
177
|
-
"UPDATE #{self.class.table_name} " +
|
178
|
-
"SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
|
179
|
-
"WHERE (#{self.class.primary_key}) = (#{id})",
|
180
|
-
"#{self.class.name} Update"
|
181
|
-
)
|
182
|
-
return true
|
183
|
-
end
|
184
|
-
|
160
|
+
# The xx_without_callbacks methods are overwritten as that is the end of the alias chain
|
161
|
+
|
185
162
|
# Creates a new record with values matching those of the instance attributes.
|
186
|
-
def
|
187
|
-
|
163
|
+
def create_without_callbacks
|
164
|
+
unless self.id
|
188
165
|
raise CompositeKeyError, "Composite keys do not generated ids from sequences, you must provide id values"
|
189
166
|
end
|
190
167
|
|
@@ -195,11 +172,34 @@ module CompositePrimaryKeys
|
|
195
172
|
"#{self.class.name} Create",
|
196
173
|
self.class.primary_key, self.id
|
197
174
|
)
|
198
|
-
|
199
175
|
@new_record = false
|
200
|
-
|
201
176
|
return true
|
202
177
|
end
|
178
|
+
|
179
|
+
# Updates the associated record with values matching those of the instance attributes.
|
180
|
+
def update_without_callbacks
|
181
|
+
connection.update(
|
182
|
+
"UPDATE #{self.class.table_name} " +
|
183
|
+
"SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
|
184
|
+
"WHERE (#{self.class.primary_key}) = (#{id})",
|
185
|
+
"#{self.class.name} Update"
|
186
|
+
)
|
187
|
+
return true
|
188
|
+
end
|
189
|
+
|
190
|
+
# Deletes the record in the database and freezes this instance to reflect that no changes should
|
191
|
+
# be made (since they can't be persisted).
|
192
|
+
def destroy_without_callbacks
|
193
|
+
unless new_record?
|
194
|
+
connection.delete <<-end_sql, "#{self.class.name} Destroy"
|
195
|
+
DELETE FROM #{self.class.table_name}
|
196
|
+
WHERE (#{self.class.primary_key}) = (#{quoted_id})
|
197
|
+
end_sql
|
198
|
+
end
|
199
|
+
|
200
|
+
freeze
|
201
|
+
end
|
202
|
+
|
203
203
|
end
|
204
204
|
|
205
205
|
module CompositeClassMethods
|
data/test/associations_test.rb
CHANGED
@@ -21,11 +21,22 @@ class AssociationTest < Test::Unit::TestCase
|
|
21
21
|
assert_not_nil @first_flat
|
22
22
|
end
|
23
23
|
|
24
|
+
def test_quoted_table_columns
|
25
|
+
assert_equal "product_tariffs.product_id,product_tariffs.tariff_id,product_tariffs.tariff_start_date",
|
26
|
+
ProductTariff.send(:quoted_table_columns, ProductTariff.primary_key)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_count
|
30
|
+
assert_equal 2, Product.count(:include => :product_tariffs)
|
31
|
+
assert_equal 3, Tariff.count(:include => :product_tariffs)
|
32
|
+
end
|
33
|
+
|
24
34
|
def test_products
|
25
35
|
assert_not_nil @first_product.product_tariffs
|
26
36
|
assert_equal 2, @first_product.product_tariffs.length
|
27
37
|
assert_not_nil @first_product.tariffs
|
28
38
|
assert_equal 2, @first_product.tariffs.length
|
39
|
+
assert_not_nil @first_product.product_tariff
|
29
40
|
end
|
30
41
|
|
31
42
|
def test_product_tariffs
|
@@ -37,8 +48,39 @@ class AssociationTest < Test::Unit::TestCase
|
|
37
48
|
|
38
49
|
def test_tariffs
|
39
50
|
assert_not_nil @flat.product_tariffs
|
40
|
-
assert_equal
|
51
|
+
assert_equal 1, @flat.product_tariffs.length
|
41
52
|
assert_not_nil @flat.products
|
42
|
-
assert_equal
|
53
|
+
assert_equal 1, @flat.products.length
|
54
|
+
assert_not_nil @flat.product_tariff
|
55
|
+
end
|
56
|
+
|
57
|
+
# Its not generating the instances of associated classes from the rows
|
58
|
+
def test_find_includes_products
|
59
|
+
assert @products = Product.find(:all, :include => :product_tariffs)
|
60
|
+
assert_equal 2, @products.length
|
61
|
+
assert_not_nil @products.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array'
|
62
|
+
assert_equal 3, @products.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, "Incorrect number of product_tariffs returned"
|
43
63
|
end
|
64
|
+
|
65
|
+
def test_find_includes_tariffs
|
66
|
+
assert @tariffs = Tariff.find(:all, :include => :product_tariffs)
|
67
|
+
assert_equal 3, @tariffs.length
|
68
|
+
assert_not_nil @tariffs.first.instance_variable_get('@product_tariffs'), '@product_tariffs not set; should be array'
|
69
|
+
assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, "Incorrect number of product_tariffs returned"
|
70
|
+
end
|
71
|
+
|
72
|
+
def XXX_test_find_includes_extended
|
73
|
+
# TODO - what's the correct syntax?
|
74
|
+
assert @products = Product.find(:all, :include => {:product_tariffs => :tariffs})
|
75
|
+
assert_equal 3, @products.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, "Incorrect number of product_tariffs returned"
|
76
|
+
|
77
|
+
assert @tariffs = Tariff.find(:all, :include => {:product_tariffs => :products})
|
78
|
+
assert_equal 3, @tariffs.inject(0) {|sum, tariff| sum + tariff.instance_variable_get('@product_tariffs').length}, "Incorrect number of product_tariffs returned"
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
#I'm also having problems when I use composite primary keys together with eager loading of associations. Here I'm doing
|
83
|
+
#ArtistName.find(:all, :include => :artist, ...)
|
84
|
+
# => ActiveRecord::StatementInvalid: Mysql::Error: Unknown column 'artists_names.artist_id,name' in 'field list': SELECT artists_names.`artist_id,name` AS t0_r0, ....
|
85
|
+
# Had a brief look into it but couldn't spot the code causing this...
|
44
86
|
end
|
data/test/fixtures/product.rb
CHANGED
data/test/fixtures/tariff.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
class Tariff < ActiveRecord::Base
|
2
2
|
set_primary_keys [:tariff_id, :start_date]
|
3
3
|
has_many :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]
|
4
|
+
has_one :product_tariff, :foreign_key => [:tariff_id, :tariff_start_date]
|
5
|
+
has_many :products, :through => :product_tariffs, :foreign_key => [:tariff_id, :tariff_start_date]
|
4
6
|
end
|
data/test/miscellaneous_test.rb
CHANGED
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11
|
|
3
3
|
specification_version: 1
|
4
4
|
name: composite_primary_keys
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2006-
|
6
|
+
version: 0.6.0
|
7
|
+
date: 2006-08-02 00:00:00 +02:00
|
8
8
|
summary: Support for composite primary keys in ActiveRecords
|
9
9
|
require_paths:
|
10
10
|
- lib
|