How Ransack Processes Sort Parameters Internally: From URL Params to SQL ORDER BY
Ransack converts raw sort parameters into AST nodes via Ransack::Nodes::Sort, validates them against ransortable_attributes, and translates valid nodes into Arel ordering clauses through the visitor pattern, falling back to custom scopes when attributes are not explicitly sortable.
When building search interfaces with the activerecord-hackery/ransack gem, understanding how sort parameters are processed internally helps you debug ordering issues and implement custom sort logic. This article traces the complete lifecycle of a sort parameter—from the initial s or sorts key in your params hash through to the final SQL ORDER BY clause—using the actual source code implementation.
Phase 1: Parameter Ingestion in Ransack::Search
The entry point for sort processing begins in lib/ransack/search.rb. When you initialize a Ransack search with parameters, the build method scans the incoming hash and routes any key named 's' or 'sorts' to the sorts= setter:
# lib/ransack/search.rb
def build(params)
...
if ['s'.freeze, 'sorts'.freeze].freeze.include?(key)
send("#{key}=", value) # <-- routes to #sorts=
...
end
The sorts= method handles multiple input formats through a case statement, normalizing everything into Ransack::Nodes::Sort objects:
# lib/ransack/search.rb
def sorts=(args)
case args
when Array
args.each do |sort|
sort = sort.kind_of?(Hash) ?
Nodes::Sort.new(@context).build(sort) :
Nodes::Sort.extract(@context, sort)
self.sorts << sort if sort
end
when Hash
args.each { |_, attrs| self.sorts << Nodes::Sort.new(@context).build(attrs) }
when String
self.sorts = [args]
else
raise InvalidSearchError, "Invalid argument (#{args.class}) supplied to sorts="
end
end
Array inputs iterate through each element, converting strings via Sort.extract and hashes via Sort.new.build. Hash inputs treat keys as attribute names and values as direction options. String inputs recursively trigger the array path.
Phase 2: Node Construction and Validation
Once the raw parameter reaches lib/ransack/nodes/sort.rb, Ransack constructs a formal AST node that encapsulates the attribute name, sort direction, and binding context.
Extracting Sort Components from Strings
For string inputs like "name desc", the extract class method splits the string and instantiates a new node:
# lib/ransack/nodes/sort.rb
def self.extract(context, str)
return if str.blank?
attr, direction = str.split(/\s+/, 2)
new(context).build(name: attr, dir: direction)
end
The split operation separates the attribute name from the direction using whitespace as the delimiter.
Building and Binding the Sort Node
The build method assigns parameters and triggers binding to the search context:
# lib/ransack/nodes/sort.rb
def build(params)
params.with_indifferent_access.each do |key, value|
send("#{key}=", value) if key.match(/^(name|dir|ransacker_args)$/)
end
self
end
The name= setter resolves aliases and binds the node to the context:
# lib/ransack/nodes/sort.rb
def name=(name)
@name = context.ransackable_alias(name) || name
context.bind(self, @name) # <-- binds to the parent/attribute
end
Binding occurs in lib/ransack/context.rb:
# lib/ransack/context.rb
def bind(object, str)
return nil unless str
object.parent, object.attr_name = bind_pair_for(str)
end
The dir= setter normalizes direction values, defaulting to 'asc' when the supplied value is not explicitly 'asc' or 'desc':
# lib/ransack/nodes/sort.rb
def dir=(dir)
@dir = dir.to_s.downcase
@dir = 'asc' unless ['asc', 'desc'].include?(@dir)
end
Validating Sortable Attributes
Before translation to SQL, Ransack validates that the attribute is permitted for sorting:
# lib/ransack/nodes/sort.rb
def valid?
bound? && attr &&
context.klassify(parent).ransortable_attributes(context.auth_object)
.include?(attr_name)
end
A sort is valid only when:
- The node has been bound to a parent object (
bound?) - The attribute exists (
attr) - The attribute appears in the model's
ransortable_attributeswhitelist
Phase 3: Arel Translation and SQL Generation
The final phase converts validated Sort nodes into Arel ordering clauses that ActiveRecord converts to SQL. This happens in lib/ransack/visitor.rb:
# lib/ransack/visitor.rb
def visit_Ransack_Nodes_Sort(object)
if object.valid?
if object.attr.is_a?(Arel::Attributes::Attribute)
object.attr.send(object.dir) # => Arel ordering (ASC/DESC)
else
ordered(object) # fallback for custom attributes
end
else
scope_name = :"sort_by_#{object.name}_#{object.dir}"
scope_name if object.context.object.respond_to?(scope_name)
end
end
The visitor handles three distinct paths:
-
Standard Arel Attributes: When
object.attris anArel::Attributes::Attribute, the direction method (:ascor:desc) is invoked directly, producing anArel::Nodes::AscendingorArel::Nodes::Descendingnode. -
Custom Ransackers: For custom attributes that aren't standard Arel columns, the
orderedhelper method constructs the appropriate Arel node. -
Custom Scope Fallback: If the sort is invalid (not in
ransortable_attributes), Ransack looks for a custom scope namedsort_by_<attribute>_<direction>on the model, allowing developers to define arbitrary ordering logic.
The resulting Arel nodes are combined into the final ActiveRecord relation by Context#evaluate, which is called from Search#result.
Practical Implementation Examples
Simple Hash-Based Sorting
# controller
@q = User.ransack(name_desc: 'desc', s: 'created_at asc')
@users = @q.result
The s: 'created_at asc' parameter triggers the string extraction path in Sort.extract, while name_desc: 'desc' is interpreted as a hash with key :name_desc mapping to Sort.build(name: :name_desc, dir: 'desc').
Array of Sort Strings
@q = Post.ransack(s: ['title desc', 'published_at'])
@posts = @q.result
Each array element is processed by Sort.extract. The second element defaults to ascending order since no direction is specified.
Using the View Helper
<%# app/views/articles/index.html.erb %>
<%= sort_link @q, :title, "Title" %>
<%= sort_link @q, :created_at, "Created" %>
The sort_link helper in lib/ransack/helpers/form_helper.rb internally constructs a Sort node for the specified attribute and generates a URL with the appropriate query parameters.
Custom Scope Fallback
class Product < ApplicationRecord
# Called when the sort attribute is not in ransortable_attributes
scope :sort_by_price_desc, -> { order(price: :desc) }
end
@q = Product.ransack(s: 'price desc')
@products = @q.result # uses the custom scope above
If price is not declared in ransortable_attributes, the visitor falls back to the custom scope discovered in visit_Ransack_Nodes_Sort.
Summary
- Parameter Ingestion: The
Ransack::Search#sorts=method inlib/ransack/search.rbaccepts strings, hashes, or arrays and normalizes them intoSortnodes. - Node Construction:
Ransack::Nodes::Sortinlib/ransack/nodes/sort.rbextracts attribute names and directions, binds nodes to the query context viaContext#bind, and validates againstransortable_attributes. - Arel Translation: The visitor in
lib/ransack/visitor.rbconverts validSortnodes intoArel::Nodes::AscendingorDescendingobjects, with fallback support for custom scopes namedsort_by_<attribute>_<direction>.
Frequently Asked Questions
What happens if I try to sort by an attribute that isn't in ransortable_attributes?
Ransack first checks if the attribute is valid via Nodes::Sort#valid?. If the attribute is not included in ransortable_attributes, the visitor falls back to looking for a custom scope named sort_by_<attribute>_<direction> on your model. If no such scope exists, the sort is silently ignored and no ordering is applied to the query.
How does Ransack handle multiple sort parameters in a single request?
When you pass an array to the s or sorts parameter, such as s: ['title desc', 'created_at'], the Search#sorts= method iterates through each element. Each string is processed by Nodes::Sort.extract to create individual Sort nodes. The visitor then processes each node in sequence, building a compound ORDER BY clause that respects the specified priority.
Can I use custom SQL expressions or calculated fields for sorting?
Yes, through custom ransackers defined with ransacker blocks in your model. When a sort node references a custom ransacker, the visitor detects that object.attr is not a standard Arel::Attributes::Attribute and routes it through the ordered helper method. This allows you to define complex SQL expressions, database functions, or joins-based sorting logic while still using Ransack's parameter handling.
What is the difference between ransackable_attributes and ransortable_attributes?
ransackable_attributes controls which columns can be used in search predicates (where clauses), while ransortable_attributes specifically controls which columns can be used in order clauses. A column can be searchable without being sortable, and vice versa. Both methods can be overridden in your model to whitelist or blacklist specific attributes for their respective operations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →