Skip to content

Commit 8fad423

Browse files
joshuay03byroot
authored andcommitted
[Fix rails#54591] Raise an error in order dependent finder methods when the model has no order columns
1 parent 7b3ebaa commit 8fad423

File tree

14 files changed

+198
-11
lines changed

14 files changed

+198
-11
lines changed

activerecord/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,19 @@
7070
7171
*Eileen M. Uchitelle*
7272
73+
* Raise `ActiveRecord::MissingRequiredOrderError` when order dependent finder methods (e.g. `#first`, `#last`) are
74+
called without `order` values on the relation, and the model does not have any order columns (`implicit_order_column`,
75+
`query_constraints`, or `primary_key`) to fall back on.
76+
77+
This change will be introduced with a new framework default for Rails 8.1, and the current behavior of not raising
78+
an error has been deprecated with the aim of removing the configuration option in Rails 8.2.
79+
80+
```ruby
81+
config.active_record.raise_on_missing_required_finder_order_columns = true
82+
```
83+
84+
*Joshua Young*
85+
7386
* `:class_name` is now invalid in polymorphic `belongs_to` associations.
7487
7588
Reason is `:class_name` does not make sense in those associations because

activerecord/lib/active_record.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,9 @@ def self.permanent_connection_checkout=(value)
355355
singleton_class.attr_accessor :run_after_transaction_callbacks_in_order_defined
356356
self.run_after_transaction_callbacks_in_order_defined = false
357357

358+
singleton_class.attr_accessor :raise_on_missing_required_finder_order_columns
359+
self.run_after_transaction_callbacks_in_order_defined = false
360+
358361
singleton_class.attr_accessor :application_record_class
359362
self.application_record_class = nil
360363

activerecord/lib/active_record/errors.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,11 @@ class SerializationFailure < TransactionRollbackError
552552
class Deadlocked < TransactionRollbackError
553553
end
554554

555+
# MissingRequiredOrderError is raised when a relation requires ordering but
556+
# lacks any +order+ values in scope or any model order columns to use.
557+
class MissingRequiredOrderError < ActiveRecordError
558+
end
559+
555560
# IrreversibleOrderError is raised when a relation's order is too complex for
556561
# +reverse_order+ to automatically reverse.
557562
class IrreversibleOrderError < ActiveRecordError

activerecord/lib/active_record/relation/finder_methods.rb

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,27 @@ def find_last(limit)
638638
end
639639

640640
def ordered_relation
641-
if order_values.empty? && !_order_columns.empty?
642-
order(_order_columns.map { |column| table[column].asc })
641+
if order_values.empty?
642+
if !_order_columns.empty?
643+
return order(_order_columns.map { |column| table[column].asc })
644+
end
645+
646+
if ActiveRecord.raise_on_missing_required_finder_order_columns
647+
raise MissingRequiredOrderError, <<~MSG.squish
648+
Relation has no order values, and #{model} has no order columns to use as a default.
649+
Set at least one of `implicit_order_column`, `query_constraints` or `primary_key` on
650+
the model when no `order `is specified on the relation.
651+
MSG
652+
else
653+
ActiveRecord.deprecator.warn(<<~MSG)
654+
Calling order dependent finder methods (e.g. `#first`, `#second`) without `order` values on the relation,
655+
and on a model (#{model}) that does not have any order columns (`implicit_order_column`, `query_constraints`,
656+
or `primary_key`) to fall back on is deprecated and will raise `ActiveRecord::MissingRequiredOrderError`
657+
in Rails 8.2.
658+
MSG
659+
660+
self
661+
end
643662
else
644663
self
645664
end

activerecord/lib/active_record/relation/query_methods.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2018,8 +2018,11 @@ def reverse_sql_order(order_query)
20182018
return _reverse_order_columns.map { |column| table[column].desc }
20192019
end
20202020

2021-
raise IrreversibleOrderError,
2022-
"Relation has no current order and table has no order columns to be used as default order"
2021+
raise IrreversibleOrderError, <<~MSG.squish
2022+
Relation has no order values, and #{model} has no order columns to use as a default.
2023+
Set at least one of `implicit_order_column`, or `primary_key` on the model when no
2024+
`order `is specified on the relation.
2025+
MSG
20232026
end
20242027

20252028
order_query.flat_map do |o|

activerecord/test/cases/connection_adapters/connection_handlers_sharding_db_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -369,8 +369,8 @@ def test_same_shards_across_clusters
369369
ShardConnectionTestModelB.lease_connection.execute("CREATE TABLE `shard_connection_test_model_bs` (shard_key VARCHAR (255))")
370370
ShardConnectionTestModelB.create!(shard_key: "test_model_b_default")
371371

372-
assert_equal "test_model_default", ShardConnectionTestModel.where(shard_key: "test_model_default").first.shard_key
373-
assert_equal "test_model_b_default", ShardConnectionTestModelB.where(shard_key: "test_model_b_default").first.shard_key
372+
assert_equal "test_model_default", ShardConnectionTestModel.where(shard_key: "test_model_default").take.shard_key
373+
assert_equal "test_model_b_default", ShardConnectionTestModelB.where(shard_key: "test_model_b_default").take.shard_key
374374
end
375375
end
376376

activerecord/test/cases/finder_test.rb

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
require "models/non_primary_key"
2727
require "models/clothing_item"
2828
require "models/cpk"
29+
require "models/edge"
2930
require "support/stubs/strong_parameters"
3031
require "support/async_helper"
3132

@@ -1055,10 +1056,120 @@ def test_last_on_loaded_relation_should_not_use_sql
10551056
end
10561057
end
10571058

1058-
def test_last_with_irreversible_order
1059-
assert_raises(ActiveRecord::IrreversibleOrderError) do
1059+
def test_first_without_order_columns
1060+
assert_nil Edge.primary_key
1061+
assert_nil Edge.implicit_order_column
1062+
assert_nil Edge.query_constraints_list
1063+
error = assert_raises(ActiveRecord::MissingRequiredOrderError) do
1064+
Edge.all.first
1065+
end
1066+
assert_match(/Relation has no order values/, error.message)
1067+
end
1068+
1069+
# TODO: Remove this test when we remove the deprecated `raise_on_missing_required_finder_order_columns`
1070+
def test_first_without_order_columns_and_raise_on_missing_required_finder_order_columns_disabled
1071+
raise_on_missing_required_finder_order_columns_before = ActiveRecord.raise_on_missing_required_finder_order_columns
1072+
ActiveRecord.raise_on_missing_required_finder_order_columns = false
1073+
1074+
assert_nil Edge.primary_key
1075+
assert_nil Edge.implicit_order_column
1076+
assert_nil Edge.query_constraints_list
1077+
assert_nothing_raised do
1078+
assert_deprecated(/Calling order dependent finder methods/, ActiveRecord.deprecator) do
1079+
Edge.all.first
1080+
end
1081+
end
1082+
ensure
1083+
ActiveRecord.raise_on_missing_required_finder_order_columns = raise_on_missing_required_finder_order_columns_before
1084+
end
1085+
1086+
def test_first_with_at_least_primary_key
1087+
ordered_edge = Class.new(Edge) do
1088+
self.primary_key = "source_id"
1089+
end
1090+
assert_nothing_raised do
1091+
ordered_edge.all.first
1092+
end
1093+
end
1094+
1095+
def test_first_with_at_least_implict_order_column
1096+
ordered_edge = Class.new(Edge) do
1097+
self.implicit_order_column = "source_id"
1098+
end
1099+
assert_nothing_raised do
1100+
ordered_edge.all.first
1101+
end
1102+
end
1103+
1104+
def first_with_at_least_query_constraints
1105+
ordered_edge = Class.new(Edge) do
1106+
query_constraints "source_id"
1107+
end
1108+
assert_nothing_raised do
1109+
ordered_edge.all.first
1110+
end
1111+
end
1112+
1113+
def test_last_without_order_columns
1114+
assert_nil Edge.primary_key
1115+
assert_nil Edge.implicit_order_column
1116+
assert_nil Edge.query_constraints_list
1117+
error = assert_raises(ActiveRecord::MissingRequiredOrderError) do
1118+
Edge.all.last
1119+
end
1120+
assert_match(/Relation has no order values/, error.message)
1121+
end
1122+
1123+
# TODO: Remove this test when we remove `raise_on_missing_required_finder_order_columns`
1124+
def test_last_without_order_columns_and_raise_on_missing_required_finder_order_columns_disabled
1125+
raise_on_missing_required_finder_order_columns_before = ActiveRecord.raise_on_missing_required_finder_order_columns
1126+
ActiveRecord.raise_on_missing_required_finder_order_columns = false
1127+
1128+
assert_nil Edge.primary_key
1129+
assert_nil Edge.implicit_order_column
1130+
assert_nil Edge.query_constraints_list
1131+
error = assert_raises(ActiveRecord::IrreversibleOrderError) do
1132+
assert_deprecated(/Calling order dependent finder methods/, ActiveRecord.deprecator) do
1133+
Edge.all.last
1134+
end
1135+
end
1136+
assert_match(/Relation has no order values/, error.message)
1137+
ensure
1138+
ActiveRecord.raise_on_missing_required_finder_order_columns = raise_on_missing_required_finder_order_columns_before
1139+
end
1140+
1141+
def test_last_with_at_least_primary_key
1142+
ordered_edge = Class.new(Edge) do
1143+
self.primary_key = "source_id"
1144+
end
1145+
assert_nothing_raised do
1146+
ordered_edge.all.last
1147+
end
1148+
end
1149+
1150+
def test_last_with_at_least_implict_order_column
1151+
ordered_edge = Class.new(Edge) do
1152+
self.implicit_order_column = "source_id"
1153+
end
1154+
assert_nothing_raised do
1155+
ordered_edge.all.last
1156+
end
1157+
end
1158+
1159+
def last_with_at_least_query_constraints
1160+
ordered_edge = Class.new(Edge) do
1161+
query_constraints "source_id"
1162+
end
1163+
assert_nothing_raised do
1164+
ordered_edge.all.last
1165+
end
1166+
end
1167+
1168+
def test_last_with_irreversible_order_value
1169+
error = assert_raises(ActiveRecord::IrreversibleOrderError) do
10601170
Topic.order(Arel.sql("coalesce(author_name, title)")).last
10611171
end
1172+
assert_match(/Order .* cannot be reversed automatically/, error.message)
10621173
end
10631174

10641175
def test_last_on_relation_with_limit_and_offset

activerecord/test/cases/fixtures_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1467,7 +1467,7 @@ def test_supports_polymorphic_belongs_to
14671467

14681468
def test_only_generates_a_pk_if_necessary
14691469
assert_nothing_raised do
1470-
m = Matey.first
1470+
m = Matey.take
14711471
m.pirate = pirates(:blackbeard)
14721472
m.target = pirates(:redbeard)
14731473
end

activerecord/test/cases/helper.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
ActiveRecord.raise_on_assign_to_attr_readonly = true
4343
ActiveRecord.belongs_to_required_validates_foreign_key = false
4444

45+
# Ensure that required finder order columns are present in the test suite until
46+
# this deprecated configuration is removed.
47+
ActiveRecord.raise_on_missing_required_finder_order_columns = true
48+
4549
ActiveRecord::ConnectionAdapters.register("abstract", "ActiveRecord::ConnectionAdapters::AbstractAdapter", "active_record/connection_adapters/abstract_adapter")
4650
ActiveRecord::ConnectionAdapters.register("fake", "FakeActiveRecordAdapter", File.expand_path("../support/fake_adapter.rb", __dir__))
4751

activerecord/test/cases/relations_test.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,9 +368,10 @@ def test_reverse_order_with_nulls_first_or_last
368368
end if current_adapter?(:PostgreSQLAdapter)
369369

370370
def test_default_reverse_order_on_table_without_primary_key
371-
assert_raises(ActiveRecord::IrreversibleOrderError) do
371+
error = assert_raises(ActiveRecord::IrreversibleOrderError) do
372372
Edge.all.reverse_order
373373
end
374+
assert_match(/Relation has no order values/, error.message)
374375
end
375376

376377
def test_default_reverse_order_on_table_without_primary_key_with_implicit_order_column

0 commit comments

Comments
 (0)