Add "attribute_for_pretty_print" method to ActiveRecord

I’d like to to improve output of records in a Rails console when some of the fields have very long values.

As of IRB version 1.3.1 (release in Jan 2021) records are pretty_printed in the console instead of inspected, so records with large values (e.g. the content of a document) look like this:

This makes it hard to navigate output in a console. Especially for arrays of items like this.

Another option is to override inspect (or attribute_for_inspect), but the output of inspect is not nearly as nice as the pretty-print output:

It would be great to:

  1. Provide a better default for long strings
  2. Provide a method that can be overridden in applications for customizing the display of fields

Would adding something like the following be OK? I can make a PR if so.

diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 4dc57d6eea..f0f9b55d36 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -288,6 +288,15 @@ def attribute_for_inspect(attr_name)
     def attribute_for_inspect(attr_name)
       attr_name = attr_name.to_s
       attr_name = self.class.attribute_aliases[attr_name] || attr_name
       value = _read_attribute(attr_name)
       format_for_inspect(attr_name, value)
     end
 
+    def attribute_for_pretty_print(attr_name)
+      value = _read_attribute(attr_name)
+      if value.is_a?(String) && value.length > 50
+        "#{value[0, 50]}..."
+      else
+        value
+      end
+    end
+
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index d2fc5fa711..ab7a2a08ee 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -698,7 +698,7 @@ def pretty_print(pp)
               pp.text attr_name
               pp.text ":"
               pp.breakable
-              value = _read_attribute(attr_name)
+              value = attribute_for_pretty_print(attr_name)
               value = inspection_filter.filter_param(attr_name, value) unless value.nil?
               pp.pp value
             end
2 Likes

The intention definitely makes sense but I personally don’t think having this in Rails core is the way to go. Have you considered using something like Pry-rails?

IMO it would make sense for Rails to provide a good experience for this out of the box. Storing long values in a field is a perfectly reasonable thing to do, and having it behave well in the console is a reasonable expectation. In fact, Rails already does this for inspect, but IRB has changed so that “inspect” isn’t used by default, as of IRB version 1.3.1, which was released in January of last year.

Even if Rails itself doesn’t customize the actual output of (i.e. if it doesn’t do any truncating, etc) it would be helpful if Rails would at least provide an extension point where applications or libraries could customize this. E.g. even if just def attribute_for_pretty_print(attr_name); _read_attribute(attr_name) end was provided, then applications or libraries could override that. The only option right now is to either override inspect and lose the nice pretty printing entirely, or to reimplement all of ActiveRecord::Core#pretty_print, which doesn’t seem very maintainable.

That makes a lot of sense! I would love to see something like this land.

Using that until one of the two changes lands in a release:

class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class

  # Copied from Rails Core, until maybe https://github.com/rails/rails/pull/45122 lands
  # Solution inspired by https://discuss.rubyonrails.org/t/add-attribute-for-pretty-print-method-to-activerecord/81741
  unless Object.const_defined?("ActiveSupport::Inspect")
    def attribute_for_pretty_print(attr_name)
      return "<BINARY VALUE>" if self.class.type_for_attribute(attr_name).type == :binary

      _read_attribute(attr_name)
    end

    def pretty_print(pp)
      return super if custom_inspect_method_defined?

      pp.object_address_group(self) do
        if defined?(@attributes) && @attributes
          attr_names = self.class.attribute_names.select { |name| _has_attribute?(name) }
          pp.seplist(attr_names, proc { pp.text "," }) do |attr_name|
            pp.breakable " "
            pp.group(1) do
              pp.text attr_name
              pp.text ":"
              pp.breakable
              value = attribute_for_pretty_print(attr_name) # this has been changed to create a hook
              value = inspection_filter.filter_param(attr_name, value) unless value.nil?
              pp.pp value
            end
          end
        else
          pp.breakable " "
          pp.text "not initialized"
        end
      end
    end
  end
end

Please send a pull request.