composite_primary_keys 0.3.3 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ * 0.3.3 * - bug fixes
2
+ - id=
3
+ - create now work
4
+
1
5
  * 0.1.4 *
2
6
  - it was important that #{primary_key} for composites --> 'key1,key2' and not 'key1key2'
3
7
  so created PrimaryKeys class
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 ActiveRecord
2
- class Base
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) or keys.first.is_a?(CompositeKeys)
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
- # Updates the associated record with values matching those of the instance attributes.
175
- def update
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 create
187
- if self.id.nil?
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
@@ -1,8 +1,8 @@
1
1
  module CompositePrimaryKeys
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 3
5
- TINY = 3
4
+ MINOR = 6
5
+ TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -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 2, @flat.product_tariffs.length
51
+ assert_equal 1, @flat.product_tariffs.length
41
52
  assert_not_nil @flat.products
42
- assert_equal 2, @flat.products.length
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
@@ -1,5 +1,6 @@
1
1
  class Product < ActiveRecord::Base
2
2
  set_primary_key :id # redundant
3
3
  has_many :product_tariffs, :foreign_key => :product_id
4
+ has_one :product_tariff, :foreign_key => :product_id
4
5
  has_many :tariffs, :through => :product_tariffs, :foreign_key => :product_id
5
6
  end
@@ -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
@@ -32,4 +32,9 @@ class MiscellaneousTest < Test::Unit::TestCase
32
32
  assert_equal composite?, @first.composite?
33
33
  end
34
34
  end
35
+
36
+ def test_count
37
+ assert_equal 2, Product.count
38
+ end
39
+
35
40
  end
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.3.3
7
- date: 2006-07-30 00:00:00 +02:00
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