Skip to content

Commit 881ea9d

Browse files
authored
Merge pull request rails#47947 from Shopify/pm/fixtures-with-composite-key
Support building fixtures with associations that use CPK
2 parents bb6eb87 + 3d82019 commit 881ea9d

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
@@ -589,6 +589,18 @@ def identify(label, column_type = :integer)
589589
end
590590
end
591591

592+
# Returns a consistent, platform-independent hash representing a mapping
593+
# between the label and the subcomponents of the provided composite key.
594+
#
595+
# Example:
596+
# composite_identify("label", [:a, :b, :c]) => { a: hash_1, b: hash_2, c: hash_3 }
597+
def composite_identify(label, key)
598+
key
599+
.index_with
600+
.with_index { |sub_key, index| (identify(label) << index) % MAX_ID }
601+
.with_indifferent_access
602+
end
603+
592604
# Superclass for the evaluation contexts used by ERB fixtures.
593605
def context_class
594606
@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)