From 8f055c4f2377c6312813cbabf4cbfd36fd3af4d7 Mon Sep 17 00:00:00 2001 From: Violet Graves Date: Fri, 18 Sep 2026 15:30:00 -0400 Subject: [PATCH 1/2] add composite primary key failing spec --- spec/support/models/active_record.rb | 1 + spec/support/models/active_record/order.rb | 26 +++++++++++ spec/support/shared_examples/active_record.rb | 44 +++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 spec/support/models/active_record/order.rb diff --git a/spec/support/models/active_record.rb b/spec/support/models/active_record.rb index c48f8cee..62d8d68e 100644 --- a/spec/support/models/active_record.rb +++ b/spec/support/models/active_record.rb @@ -8,6 +8,7 @@ def self.define_tables SuperDiff::Test::Models::ActiveRecord::Person.define_table SuperDiff::Test::Models::ActiveRecord::ShippingAddress.define_table SuperDiff::Test::Models::ActiveRecord::TimeSeriesData.define_table + SuperDiff::Test::Models::ActiveRecord::Order.define_table end end end diff --git a/spec/support/models/active_record/order.rb b/spec/support/models/active_record/order.rb new file mode 100644 index 00000000..88c63d2f --- /dev/null +++ b/spec/support/models/active_record/order.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module SuperDiff + module Test + module Models + module ActiveRecord + class Order < ::ActiveRecord::Base + def self.define_table + ::ActiveRecord::Base + .connection + .create_table( + :orders, + force: true, + primary_key: %i[store_id sku] + ) do |t| + t.integer :latest_outcome, null: false + t.string :details + t.integer :store_id, null: false + t.string :sku, null: false + end + end + end + end + end + end +end diff --git a/spec/support/shared_examples/active_record.rb b/spec/support/shared_examples/active_record.rb index 855067e7..5404f233 100644 --- a/spec/support/shared_examples/active_record.rb +++ b/spec/support/shared_examples/active_record.rb @@ -103,6 +103,50 @@ end end + context 'when comparing two instances of an ActiveRecord model that has a composite primary key' do + it 'produces the correct output' do + as_both_colored_and_uncolored do |color_enabled| + snippet = <<~TEST.strip + actual = SuperDiff::Test::Models::ActiveRecord::Order.new( + store_id: 456, + sku: 'hi', + latest_outcome: 1, + ) + expected = SuperDiff::Test::Models::ActiveRecord::Order.new( + store_id: 123, + sku: 'hello', + latest_outcome: 2, + ) + expect(actual).to eq(expected) + TEST + program = make_program(snippet, color_enabled: color_enabled) + + expected_output = + build_expected_output( + color_enabled: color_enabled, + snippet: 'expect(actual).to eq(expected)', + newline_before_expectation: true, + expectation: + proc do + line do + plain 'Expected ' + actual %(#) + end + + line do + plain ' to eq ' + expected %(#) + end + end + ) + + expect(program).to produce_output_when_run(expected_output).in_color( + color_enabled + ) + end + end + end + context 'when comparing instances of two different ActiveRecord models' do it 'produces the correct output' do as_both_colored_and_uncolored do |color_enabled| From d2bdd95e67430b0211afa97116c95748ebca8255 Mon Sep 17 00:00:00 2001 From: Violet Graves Date: Fri, 18 Sep 2026 15:30:16 -0400 Subject: [PATCH 2/2] tolerate composite primary keys --- lib/super_diff/active_record/monkey_patches.rb | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/super_diff/active_record/monkey_patches.rb b/lib/super_diff/active_record/monkey_patches.rb index 00c2c796..8d83f5a4 100644 --- a/lib/super_diff/active_record/monkey_patches.rb +++ b/lib/super_diff/active_record/monkey_patches.rb @@ -4,11 +4,12 @@ class ActiveRecord::Base # TODO: Remove this monkey patch if possible def attributes_for_super_diff - id_attr = self.class.primary_key + id_attrs = Array(self.class.primary_key).compact + other_attrs = attributes.keys.sort - id_attrs + attributes_to_hash = ->(keys, hash) { keys.reduce(hash) { |memo, key| memo.merge(key.to_sym => attributes[key]) } } - (attributes.keys.sort - [id_attr]).reduce( - id_attr.nil? ? {} : { id_attr.to_sym => id } - ) { |hash, key| hash.merge(key.to_sym => attributes[key]) } + attributes_to_hash.call(id_attrs, {}) + .then { |result| attributes_to_hash.call(other_attrs, result) } end end # rubocop:enable Style/ClassAndModuleChildren