Skip to content

Commit 3d82019

Browse files
committed
Support fixture associations for composite models
Most of the support here is in implementing how to correctly substitute multiple values in place of one, for composite caes. In composite cases, it's not sufficient to hash a label into a single integer value. Instead, we build an API that accepts a single label, and a list of columns that we'd like to map to. The algorithm used internally is very similar to #identify, with an additional bit shift and modulo to cycle the hash and ensure it doesn't exceed a max.
1 parent 444df0e commit 3d82019

File tree

9 files changed

+134
-16
lines changed

9 files changed

+134
-16
lines changed

activerecord/lib/active_record/fixture_set/table_row.rb

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,11 @@ def generate_primary_key
125125
end
126126

127127
def generate_composite_primary_key
128-
id = ActiveRecord::FixtureSet.identify(@label)
129-
model_metadata.primary_key_name.each_with_index do |column, index|
128+
composite_key = ActiveRecord::FixtureSet.composite_identify(@label, model_metadata.primary_key_name)
129+
composite_key.each do |column, value|
130130
next if column_defined?(column)
131-
raise "Automatic key generation assumes columns of type Integer." unless model_metadata.column_type(column) == :integer
132131

133-
# Shift label identifier index-#-of-times to differentiate sub-components in deterministic manner.
134-
@row[column] = (id << index) % ActiveRecord::FixtureSet::MAX_ID
132+
@row[column] = value
135133
end
136134
end
137135

@@ -165,8 +163,17 @@ def resolve_sti_reflections
165163
raise PrimaryKeyError.new(@label, association, value)
166164
end
167165

168-
fk_type = reflection_class.type_for_attribute(fk_name).type
169-
@row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
166+
if fk_name.is_a?(Array)
167+
composite_key = ActiveRecord::FixtureSet.composite_identify(value, fk_name)
168+
composite_key.each do |column, value|
169+
next if column_defined?(column)
170+
171+
@row[column] = value
172+
end
173+
else
174+
fk_type = reflection_class.type_for_attribute(fk_name).type
175+
@row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
176+
end
170177
end
171178
when :has_many
172179
if association.options[:through]

activerecord/lib/active_record/fixtures.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,18 @@ def identify(label, column_type = :integer)
585585
end
586586
end
587587

588+
# Returns a consistent, platform-independent hash representing a mapping
589+
# between the label and the subcomponents of the provided composite key.
590+
#
591+
# Example:
592+
# composite_identify("label", [:a, :b, :c]) => { a: hash_1, b: hash_2, c: hash_3 }
593+
def composite_identify(label, key)
594+
key
595+
.index_with
596+
.with_index { |sub_key, index| (identify(label) << index) % MAX_ID }
597+
.with_indifferent_access
598+
end
599+
588600
# Superclass for the evaluation contexts used by ERB fixtures.
589601
def context_class
590602
@context_class ||= Class.new

activerecord/test/cases/fixtures_test.rb

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,15 +1643,15 @@ def readonly_config
16431643
end
16441644

16451645
class CompositePkFixturesTest < ActiveRecord::TestCase
1646-
fixtures :cpk_orders, :cpk_books, :authors
1646+
fixtures :cpk_orders, :cpk_books, :cpk_authors, :cpk_reviews, :cpk_order_agreements
16471647

16481648
def test_generates_composite_primary_key_for_partially_filled_fixtures
1649-
david = authors(:david)
1650-
david_cpk_book = cpk_books(:cpk_known_author_david_book)
1649+
alice = cpk_authors(:cpk_great_author)
1650+
alice_cpk_book = cpk_books(:cpk_great_author_first_book)
16511651

1652-
assert_not_empty(david_cpk_book.id.compact)
1653-
assert_equal david.id, david_cpk_book.author_id
1654-
assert_not_nil david_cpk_book.number
1652+
assert_not_empty(alice_cpk_book.id.compact)
1653+
assert_equal alice.id, alice_cpk_book.author_id
1654+
assert_not_nil alice_cpk_book.number
16551655
end
16561656

16571657
def test_generates_composite_primary_key_ids
@@ -1664,5 +1664,70 @@ def test_generates_composite_primary_key_ids
16641664
def test_generates_composite_primary_key_with_unique_components
16651665
assert_equal 2, cpk_orders(:cpk_groceries_order_1).id.uniq.size
16661666
end
1667+
1668+
def test_resolves_associations_using_composite_primary_keys
1669+
review = cpk_reviews(:first_book_review)
1670+
generated_book = cpk_books(:cpk_book_with_generated_pk)
1671+
1672+
assert_equal generated_book.id, [review.author_id, review.number]
1673+
assert_equal generated_book, review.book
1674+
end
1675+
1676+
def test_resolves_associations_using_composite_primary_keys_with_partially_filled_values
1677+
review = cpk_reviews(:second_book_review_for_book_with_partial_pk_defined)
1678+
book_with_partially_filled_cpk = cpk_books(:cpk_great_author_first_book)
1679+
1680+
assert_equal book_with_partially_filled_cpk.id, [review.author_id, review.number]
1681+
assert_equal book_with_partially_filled_cpk, review.book
1682+
end
1683+
1684+
def test_association_with_custom_primary_key
1685+
order = cpk_orders(:cpk_groceries_order_2)
1686+
order_agreement = cpk_order_agreements(:order_agreement_three)
1687+
1688+
_, order_id = order.id
1689+
1690+
assert_equal order_id, order_agreement.order_id
1691+
assert_equal order, order_agreement.order
1692+
end
1693+
1694+
def test_composite_identify_resolves_to_same_values
1695+
identify_one = ActiveRecord::FixtureSet.composite_identify("label", [:a, :b, :c])
1696+
identify_two = ActiveRecord::FixtureSet.composite_identify("label", [:a, :b, :c])
1697+
1698+
assert_equal identify_one, identify_two
1699+
end
1700+
1701+
def test_composite_identify_returns_hash_with_key_names
1702+
id = ActiveRecord::FixtureSet.composite_identify("order", Cpk::Order.primary_key)
1703+
1704+
assert_equal ["shop_id", "id"], id.keys
1705+
end
1706+
1707+
def test_composite_identify_uses_same_hashing_algorithm_as_identify_for_first_attribute
1708+
id_hash = ActiveRecord::FixtureSet.composite_identify("order", [:first_attribute, :second_attribute])
1709+
id = ActiveRecord::FixtureSet.identify("order")
1710+
1711+
assert_equal id, id_hash[:first_attribute]
1712+
assert_not_equal id, id_hash[:second_attribute]
1713+
end
1714+
1715+
def test_composite_identify_hashes_one_label_to_same_values_irrespective_of_column_names
1716+
id_hash_one = ActiveRecord::FixtureSet.composite_identify("order", [:first_attribute, :second_attribute])
1717+
id_hash_two = ActiveRecord::FixtureSet.composite_identify("order", [:shop_id, :id])
1718+
1719+
assert_equal id_hash_one.values, id_hash_two.values
1720+
assert_not_equal id_hash_one.keys, id_hash_two.keys
1721+
end
1722+
1723+
def test_composite_identify_hashes_to_same_values_based_on_position_in_key
1724+
id = ActiveRecord::FixtureSet.identify("order")
1725+
id_hash_two = ActiveRecord::FixtureSet.composite_identify("order", [:one, :two])
1726+
id_hash_three = ActiveRecord::FixtureSet.composite_identify("order", [:one, :two, :three])
1727+
1728+
assert_equal id, id_hash_two.values.first
1729+
assert_equal id, id_hash_three.values.first
1730+
assert_equal id_has_two.values, id_hash_three.values.slice(0, 2)
1731+
end
16671732
end
16681733
end

activerecord/test/fixtures/cpk_books.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ cpk_famous_author_first_book:
1616
title: "Ruby on Rails"
1717
revision: 1
1818

19-
cpk_known_author_david_book:
20-
author_id: 1
21-
title: "David's CPK Book"
19+
cpk_book_with_generated_pk:
20+
title: "Generated author's book"
2221
revision: 1

activerecord/test/fixtures/cpk_order_agreements.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ order_agreement_one:
66

77
order_agreement_two:
88
signature: "xyz789"
9+
10+
order_agreement_three:
11+
order_id: <%= ActiveRecord::FixtureSet.composite_identify(:cpk_groceries_order_2, Cpk::Order.primary_key)[:id] %>
12+
signature: "def321"
13+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
_fixture:
2+
model_class: Cpk::Review
3+
4+
first_book_review:
5+
book: cpk_book_with_generated_pk
6+
rating: 5
7+
comment: "The first book was alright."
8+
9+
second_book_review_for_book_with_partial_pk_defined:
10+
book: cpk_great_author_first_book
11+
author_id: <%= ActiveRecord::FixtureSet.identify(:cpk_great_author) %>
12+
rating: 5
13+
comment: "The first book was alright."

activerecord/test/models/cpk.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
require_relative "cpk/author"
44
require_relative "cpk/book"
55
require_relative "cpk/order"
6+
require_relative "cpk/review"
67
require_relative "cpk/order_agreement"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
module Cpk
4+
class Review < ActiveRecord::Base
5+
self.table_name = :cpk_reviews
6+
7+
belongs_to :book, class_name: "Cpk::Book", query_constraints: [:author_id, :number]
8+
end
9+
end

activerecord/test/schema/schema.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,13 @@
251251
t.string :name
252252
end
253253

254+
create_table :cpk_reviews, force: true do |t|
255+
t.integer :author_id
256+
t.integer :number
257+
t.integer :rating
258+
t.string :comment
259+
end
260+
254261
create_table :cpk_orders, primary_key: [:shop_id, :id], force: true do |t|
255262
t.integer :shop_id
256263
t.integer :id

0 commit comments

Comments
 (0)