diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 7f9d3766c3..ce33de1966 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,12 @@
+* Allow delegated_type to be specified primary_key and foreign_key.
+
+ Since delegated_type assumes that the foreign_key ends with `_id`,
+ `singular_id` defined by it does not work when the foreign_key does
+ not end with `id`. This change fixes it by taking into account
+ `primary_key` and `foreign_key` in the options.
+
+ *Ryota Egusa*
+
* Restore possibility of passing `false` to :polymorphic option of `belongs_to`.
Previously, passing `false` would trigger the option validation logic
diff --git a/activerecord/lib/active_record/delegated_type.rb b/activerecord/lib/active_record/delegated_type.rb
index 4a4d27fb29..209115673c 100644
--- a/activerecord/lib/active_record/delegated_type.rb
+++ b/activerecord/lib/active_record/delegated_type.rb
@@ -156,8 +156,6 @@ module ActiveRecord
# Entry#comment # => returns the comment record, when entryable_type == "Comment", otherwise nil
# Entry#comment_id # => returns entryable_id, when entryable_type == "Comment", otherwise nil
#
- # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
- #
# You can also declare namespaced types:
#
# class Entry < ApplicationRecord
@@ -167,15 +165,38 @@ module ActiveRecord
# Entry.access_notice_messages
# entry.access_notice_message
# entry.access_notice_message?
+ #
+ # === Options
+ #
+ # The +options+ are passed directly to the +belongs_to+ call, so this is where you declare +dependent+ etc.
+ # The following options can be included to specialize the behavior of the delegated type convenience methods.
+ #
+ # [:foreign_key]
+ # Specify the foreign key used for the convenience methods. By default this is guessed to be the passed
+ # +role+ with an "_id" suffix. So a class that defines a
+ # delegated_type :entryable, types: %w[ Message Comment ] association will use "entryable_id" as
+ # the default :foreign_key.
+ # [:primary_key]
+ # Specify the method that returns the primary key of associated object used for the convenience methods.
+ # By default this is +id+.
+ #
+ # Option examples:
+ # class Entry < ApplicationRecord
+ # delegated_type :entryable, types: %w[ Message Comment ], primary_key: :uuid, foreign_key: :entryable_uuid
+ # end
+ #
+ # Entry#message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
+ # Entry#comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
def delegated_type(role, types:, **options)
belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
- define_delegated_type_methods role, types: types
+ define_delegated_type_methods role, types: types, options: options
end
private
- def define_delegated_type_methods(role, types:)
+ def define_delegated_type_methods(role, types:, options:)
+ primary_key = options[:primary_key] || "id"
role_type = "#{role}_type"
- role_id = "#{role}_id"
+ role_id = options[:foreign_key] || "#{role}_id"
define_method "#{role}_class" do
public_send("#{role}_type").constantize
@@ -200,7 +221,7 @@ module ActiveRecord
public_send(role) if public_send(query)
end
- define_method "#{singular}_id" do
+ define_method "#{singular}_#{primary_key}" do
public_send(role_id) if public_send(query)
end
end
diff --git a/activerecord/test/cases/delegated_type_test.rb b/activerecord/test/cases/delegated_type_test.rb
index b6c0680649..2228b22703 100644
--- a/activerecord/test/cases/delegated_type_test.rb
+++ b/activerecord/test/cases/delegated_type_test.rb
@@ -4,6 +4,9 @@ require "cases/helper"
require "models/entry"
require "models/message"
require "models/comment"
+require "models/uuid_entry"
+require "models/uuid_message"
+require "models/uuid_comment"
class DelegatedTypeTest < ActiveRecord::TestCase
fixtures :comments
@@ -11,6 +14,11 @@ class DelegatedTypeTest < ActiveRecord::TestCase
setup do
@entry_with_message = Entry.create! entryable: Message.new(subject: "Hello world!")
@entry_with_comment = Entry.create! entryable: comments(:greetings)
+
+ if current_adapter?(:PostgreSQLAdapter)
+ @uuid_entry_with_message = UuidEntry.create! uuid: SecureRandom.uuid, entryable: UuidMessage.new(uuid: SecureRandom.uuid, subject: "Hello world!")
+ @uuid_entry_with_comment = UuidEntry.create! uuid: SecureRandom.uuid, entryable: UuidComment.new(uuid: SecureRandom.uuid, content: "comment")
+ end
end
test "delegated class" do
@@ -54,4 +62,14 @@ class DelegatedTypeTest < ActiveRecord::TestCase
assert_equal @entry_with_comment.entryable_id, @entry_with_comment.comment_id
assert_nil @entry_with_comment.message_id
end
+
+ test "association uuid" do
+ skip unless current_adapter?(:PostgreSQLAdapter)
+
+ assert_equal @uuid_entry_with_message.entryable_uuid, @uuid_entry_with_message.uuid_message_uuid
+ assert_nil @uuid_entry_with_message.uuid_comment_uuid
+
+ assert_equal @uuid_entry_with_comment.entryable_uuid, @uuid_entry_with_comment.uuid_comment_uuid
+ assert_nil @uuid_entry_with_comment.uuid_message_uuid
+ end
end
diff --git a/activerecord/test/models/uuid_comment.rb b/activerecord/test/models/uuid_comment.rb
new file mode 100644
index 0000000000..62736af32f
--- /dev/null
+++ b/activerecord/test/models/uuid_comment.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class UuidComment < ActiveRecord::Base
+ has_one :uuid_entry, as: :entryable
+end
diff --git a/activerecord/test/models/uuid_entry.rb b/activerecord/test/models/uuid_entry.rb
new file mode 100644
index 0000000000..9bb79a0e7c
--- /dev/null
+++ b/activerecord/test/models/uuid_entry.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class UuidEntry < ActiveRecord::Base
+ delegated_type :entryable, types: %w[ UuidMessage UuidComment ], primary_key: :uuid, foreign_key: :entryable_uuid
+end
diff --git a/activerecord/test/models/uuid_message.rb b/activerecord/test/models/uuid_message.rb
new file mode 100644
index 0000000000..904414f8de
--- /dev/null
+++ b/activerecord/test/models/uuid_message.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class UuidMessage < ActiveRecord::Base
+ has_one :uuid_entry, as: :entryable
+end
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index a24c190a93..573273040f 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -104,11 +104,27 @@ _SQL
t.decimal :decimal_array_default, array: true, default: [1.23, 3.45]
end
+ create_table :uuid_comments, force: true, id: false do |t|
+ t.uuid :uuid, primary_key: true, **uuid_default
+ t.string :content
+ end
+
+ create_table :uuid_entries, force: true, id: false do |t|
+ t.uuid :uuid, primary_key: true, **uuid_default
+ t.string :entryable_type, null: false
+ t.uuid :entryable_uuid, null: false
+ end
+
create_table :uuid_items, force: true, id: false do |t|
t.uuid :uuid, primary_key: true, **uuid_default
t.string :title
end
+ create_table :uuid_messages, force: true, id: false do |t|
+ t.uuid :uuid, primary_key: true, **uuid_default
+ t.string :subject
+ end
+
if supports_partitioned_indexes?
create_table(:measurements, id: false, force: true, options: "PARTITION BY LIST (city_id)") do |t|
t.string :city_id, null: false