diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index d6f081ff18..b38e2b8c35 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,18 @@
+* Allow passing raw SQL as `returning` statement to `#upsert_all`:
+
+ ```ruby
+ Article.insert_all(
+ [
+ {title: "Article 1", slug: "article-1", published: false},
+ {title: "Article 2", slug: "article-2", published: false}
+ ],
+ # Some PostgreSQL magic here to detect which rows have been actually inserted
+ returning: "id, (xmax = '0') as inserted, name as new_name"
+ )
+ ```
+
+ *Vladimir Dementyev*
+
* Deprecate `legacy_connection_handling`.
*Eileen M. Uchitelle*
diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb
index 97c440cd9c..61c13ab4ab 100644
--- a/activerecord/lib/active_record/insert_all.rb
+++ b/activerecord/lib/active_record/insert_all.rb
@@ -151,7 +151,13 @@ module ActiveRecord
end
def returning
- format_columns(insert_all.returning) if insert_all.returning
+ return unless insert_all.returning
+
+ if insert_all.returning.is_a?(String)
+ insert_all.returning
+ else
+ format_columns(insert_all.returning)
+ end
end
def conflict_target
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 9406b84274..7004f447a9 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -91,6 +91,9 @@ module ActiveRecord
# or returning: false to omit the underlying RETURNING SQL
# clause entirely.
#
+ # You can also pass an SQL string if you need more control on the return values
+ # (for example, returning: "id, name as new_name").
+ #
# [:unique_by]
# (PostgreSQL and SQLite only) By default rows are considered to be unique
# by every unique index on the table. Any duplicate rows are skipped.
@@ -168,6 +171,9 @@ module ActiveRecord
# or returning: false to omit the underlying RETURNING SQL
# clause entirely.
#
+ # You can also pass an SQL string if you need more control on the return values
+ # (for example, returning: "id, name as new_name").
+ #
# ==== Examples
#
# # Insert multiple records
@@ -216,6 +222,9 @@ module ActiveRecord
# or returning: false to omit the underlying RETURNING SQL
# clause entirely.
#
+ # You can also pass an SQL string if you need more control on the return values
+ # (for example, returning: "id, name as new_name").
+ #
# [:unique_by]
# (PostgreSQL and SQLite only) By default rows are considered to be unique
# by every unique index on the table. Any duplicate rows are skipped.
diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb
index b35c4724fe..87851518c2 100644
--- a/activerecord/test/cases/insert_all_test.rb
+++ b/activerecord/test/cases/insert_all_test.rb
@@ -109,6 +109,13 @@ class InsertAllTest < ActiveRecord::TestCase
assert_equal %w[ Rework ], result.pluck("name")
end
+ def test_insert_all_returns_requested_sql_fields
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: "UPPER(name) as name"
+ assert_equal %w[ REWORK ], result.pluck("name")
+ end
+
def test_insert_all_can_skip_duplicate_records
skip unless supports_insert_on_duplicate_skip?