diff --git a/CHANGELOG.md b/CHANGELOG.md index 517b404..8071e54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixed + +- On Rails 7.1+, reloading a `has_many` while holding records already built into it wrote the mapped class name into the integer type column (cast to `0`), so those in-memory records pointed at the wrong type. `_write_attribute` now maps the class name back to its integer ([#69](https://github.com/clio/polymorphic_integer_type/issues/69)). + ## v3.5.0 (2026-04-30) ### Added diff --git a/lib/polymorphic_integer_type/extensions.rb b/lib/polymorphic_integer_type/extensions.rb index 8e2144f..ef32a87 100644 --- a/lib/polymorphic_integer_type/extensions.rb +++ b/lib/polymorphic_integer_type/extensions.rb @@ -153,6 +153,19 @@ def []=(attr_name, value) end end + # Rails 7.1+ copies reloaded has_many rows onto records the caller already + # holds with `_write_attribute(name, record[name])` + # (CollectionAssociation#merge_target_lists). `[]` above returns the mapped + # class name for these columns, so map it back to the integer here instead + # of letting the integer column cast "Drink" to 0. + def _write_attribute(attr_name, value) + name = attr_name.to_s + if value.is_a?(String) && _polymorphic_foreign_types.include?(name) + value = self.class.send("#{name}_mapping").key(value) || value + end + super(attr_name, value) + end + end end diff --git a/spec/polymorphic_integer_type_spec.rb b/spec/polymorphic_integer_type_spec.rb index 73a4cdd..7458e8a 100644 --- a/spec/polymorphic_integer_type_spec.rb +++ b/spec/polymorphic_integer_type_spec.rb @@ -184,6 +184,30 @@ end end end + context "When a has_many reload merges into records the caller still holds" do + # Rails 7.1+ copies the reloaded values onto the in-memory records with + # _write_attribute (CollectionAssociation#merge_target_lists), reading + # them with [] first, which returns the mapped class name (issue #69). + let(:source) { cat } + + it "keeps the integer type on the in-memory records" do + first = source.source_links.new(target: kibble) + first.save! + second = source.source_links.new(target: milk) + second.save! + + expect(source.source_links).not_to be_loaded + source.source_links.length + + expect(source.source_links).to include(second) + expect(second._read_attribute("target_type")).to eql 2 + expect(second.target_type).to eql "Drink" + expect(second.target).to eql milk + expect(first.target_type).to eql "Food" + expect(first._read_attribute("source_type")).to eql 2 + end + end + context "When a link is given polymorphic record" do let(:link) { Link.create(source: source) } let(:source) { cat }