From 3b9982a3b778b63488975eb03592d33aa9fb04dd Mon Sep 17 00:00:00 2001
From: Tekin Suleyman <tekin@tekin.co.uk>
Date: Sat, 17 Nov 2018 13:50:01 -0800
Subject: Make implicit order column configurable

When calling ordered finder methods such as +first+ or +last+ without an
explicit order clause, ActiveRecord sorts records by primary key. This
can result in unpredictable and surprising behaviour when the primary
key is not an auto-incrementing integer, for example when it's a UUID.
This change makes it possible to override the column used for implicit
ordering such that +first+ and +last+ will return more predictable
results. For Example:

  class Project < ActiveRecord::Base
    self.implicit_order_column = "created_at"
  end
---
 activerecord/CHANGELOG.md                               | 17 +++++++++++++++++
 activerecord/lib/active_record/model_schema.rb          | 16 ++++++++++++++++
 .../lib/active_record/relation/finder_methods.rb        |  4 ++--
 activerecord/test/cases/finder_test.rb                  | 10 ++++++++++
 4 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 6186595a56..13cc486e7f 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,20 @@
+*   Make the implicit order column configurable.
+
+    When calling ordered finder methods such as +first+ or +last+ without an
+    explicit order clause, ActiveRecord sorts records by primary key. This can
+    result in unpredictable and surprising behaviour when the primary key is
+    not an auto-incrementing integer, for example when it's a UUID. This change
+    makes it possible to override the column used for implicit ordering such
+    that +first+ and +last+ will return more predictable results.
+
+    Example:
+
+        class Project < ActiveRecord::Base
+          self.implicit_order_column = "created_at"
+        end
+
+    *Tekin Suleyman*
+
 *   Bump minimum PostgreSQL version to 9.3.
 
     *Yasuo Honda*
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index c50a420432..55fc58e339 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -102,6 +102,21 @@ module ActiveRecord
     # If true, the default table name for a Product class will be "products". If false, it would just be "product".
     # See table_name for the full rules on table/class naming. This is true, by default.
 
+    ##
+    # :singleton-method: implicit_order_column
+    # :call-seq: implicit_order_column
+    #
+    # The name of the column records are ordered by if no explicit order clause
+    # is used during an ordered finder call. If not set the primary key is used.
+
+    ##
+    # :singleton-method: implicit_order_column=
+    # :call-seq: implicit_order_column=(column_name)
+    #
+    # Sets the column to sort records by when no explicit order clause is used
+    # during an ordered finder call. Useful when the primary key is not an
+    # auto-incrementing integer, for example when it's a UUID. Note that using
+    # a non-unique column can result in non-deterministic results.
     included do
       mattr_accessor :primary_key_prefix_type, instance_writer: false
 
@@ -110,6 +125,7 @@ module ActiveRecord
       class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations"
       class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata"
       class_attribute :pluralize_table_names, instance_writer: false, default: true
+      class_attribute :implicit_order_column, instance_accessor: false
 
       self.protected_environments = ["production"]
       self.inheritance_column = "type"
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index afaa900442..dc03b196f4 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -550,8 +550,8 @@ module ActiveRecord
       end
 
       def ordered_relation
-        if order_values.empty? && primary_key
-          order(arel_attribute(primary_key).asc)
+        if order_values.empty? && (implicit_order_column || primary_key)
+          order(arel_attribute(implicit_order_column || primary_key).asc)
         else
           self
         end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 52fd9291b2..21e84d850b 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -741,6 +741,16 @@ class FinderTest < ActiveRecord::TestCase
     assert_equal expected, clients.limit(5).first(2)
   end
 
+  def test_implicit_order_column_is_configurable
+    old_implicit_order_column = Topic.implicit_order_column
+    Topic.implicit_order_column = "title"
+
+    assert_equal topics(:fifth), Topic.first
+    assert_equal topics(:third), Topic.last
+  ensure
+    Topic.implicit_order_column = old_implicit_order_column
+  end
+
   def test_take_and_first_and_last_with_integer_should_return_an_array
     assert_kind_of Array, Topic.take(5)
     assert_kind_of Array, Topic.first(5)
-- 
cgit v1.2.3