# How Ransack Processes Sort Parameters Internally: From URL Params to SQL ORDER BY

> Discover how Ransack processes sort parameters internally. Learn the journey from URL params to SQL ORDER BY clauses, ensuring efficient and secure sorting for your Rails app.

- Repository: [ActiveRecord Hackery/ransack](https://github.com/activerecord-hackery/ransack)
- Tags: internals
- Published: 2026-02-23

---

**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](https://github.com/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`](https://github.com/activerecord-hackery/ransack/blob/main/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:

```ruby

# 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:

```ruby

# 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`](https://github.com/activerecord-hackery/ransack/blob/main/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:

```ruby

# 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:

```ruby

# 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:

```ruby

# 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`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/context.rb):

```ruby

# 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'`:

```ruby

# 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:

```ruby

# 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_attributes` whitelist

## 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`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/visitor.rb):

```ruby

# 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:

1. **Standard Arel Attributes**: When `object.attr` is an `Arel::Attributes::Attribute`, the direction method (`:asc` or `:desc`) is invoked directly, producing an `Arel::Nodes::Ascending` or `Arel::Nodes::Descending` node.

2. **Custom Ransackers**: For custom attributes that aren't standard Arel columns, the `ordered` helper method constructs the appropriate Arel node.

3. **Custom Scope Fallback**: If the sort is invalid (not in `ransortable_attributes`), Ransack looks for a custom scope named `sort_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

```ruby

# 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

```ruby
@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

```erb
<%# 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`](https://github.com/activerecord-hackery/ransack/blob/main/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

```ruby
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 in [`lib/ransack/search.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/search.rb) accepts strings, hashes, or arrays and normalizes them into `Sort` nodes.
- **Node Construction**: `Ransack::Nodes::Sort` in [`lib/ransack/nodes/sort.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/nodes/sort.rb) extracts attribute names and directions, binds nodes to the query context via `Context#bind`, and validates against `ransortable_attributes`.
- **Arel Translation**: The visitor in [`lib/ransack/visitor.rb`](https://github.com/activerecord-hackery/ransack/blob/main/lib/ransack/visitor.rb) converts valid `Sort` nodes into `Arel::Nodes::Ascending` or `Descending` objects, with fallback support for custom scopes named `sort_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.