How Ransack Translates Search Parameters into SQL Using the Visitor/AST Pattern

Ransack converts search parameters into an abstract syntax tree (AST) of node objects, then uses a Visitor pattern to traverse the tree and generate Arel predicates that ActiveRecord compiles into SQL.

The activerecord-hackery/ransack gem provides a powerful search interface for Rails applications. Understanding how Ransack translates search parameters into SQL using the Visitor/AST pattern reveals how it supports complex queries, custom predicates, and multiple database adapters while maintaining a clean public API.

Building the AST from Search Parameters

Ransack begins by transforming the incoming parameter hash into a tree of node objects. This AST represents the logical structure of the query, including conditions, groupings, and sort orders.

The Search Class and Parameter Parsing

The entry point is Ransack::Search, which receives raw parameters in its initializer and delegates to #build.


# lib/ransack/search.rb

def build(params)
  collapse_multiparameter_attributes!(params).each do |key, value|
    if base.attribute_method?(key)
      base.send("#{key}=", value)  # Creates Grouping/Condition nodes

    end
  end
end

This method iterates through the parameter hash, creating the appropriate node types based on the key names. Simple predicates like name_cont become Condition nodes, while the g key triggers nested Grouping creation.

Grouping Nodes and Logical Structure

The base node is a Ransack::Nodes::Grouping instance that holds child conditions and sub-groupings. In lib/ransack/nodes/grouping.rb, the conditions= setter instantiates Condition objects:


# lib/ransack/nodes/grouping.rb

def conditions=(conditions)
  conditions.each do |attrs|
    condition = Condition.new(@context).build(attrs)
    self.conditions << condition
  end
end

Groupings maintain a combinator property (:and or :or) that determines how to join their children when the Visitor traverses the tree.

Condition Nodes and Predicates

The Condition class in lib/ransack/nodes/condition.rb represents a single predicate comparison. It stores the attribute name, predicate type (eq, cont, gt, etc.), and value. The critical method is arel_predicate, which constructs the actual Arel node:


# lib/ransack/nodes/condition.rb

def arel_predicate
  attributes.map { |attribute|
    format_predicate(attribute)
  }.reduce(combinator_method)
end

This method maps the Ransack predicate to an Arel method (e.g., cont becomes matches), handles multiple attributes with the combinator, and returns an Arel predicate node ready for SQL generation.

Sort Nodes for Ordering

Sort nodes handle the ORDER BY clause. Created via Search#sorts=, they wrap column names and direction. In lib/ransack/nodes/sort.rb, the to_arel method returns an Arel::Nodes::Ordering instance:


# lib/ransack/nodes/sort.rb

def to_arel
  # Returns Arel::Nodes::Ordering instance

  attr.direction == :asc ? attr.asc : attr.desc
end

Traversing the AST with the Visitor Pattern

Once the AST is constructed, Ransack must convert the node tree into Arel objects. This happens through the Visitor pattern implemented in lib/ransack/visitor.rb.

Dispatch Mechanism and Dynamic Method Resolution

The Visitor class uses a dynamic dispatch table to route nodes to specific handler methods:


# lib/ransack/visitor.rb

DISPATCH = Hash.new do |hash, klass|
  hash[klass] = "visit_#{klass.name.gsub(Constants::TWO_COLONS, Constants::UNDERSCORE)}"
end

When Visitor#accept receives a node, it looks up the corresponding visit_* method name based on the node's class name. This allows the Visitor to handle new node types without modification, following the Open/Closed Principle.

Converting Conditions to Arel Predicates

For Condition nodes, the Visitor delegates to the node's own Arel-building logic:


# lib/ransack/visitor.rb

def visit_Ransack_Nodes_Condition(object)
  object.arel_predicate
end

This method simply returns the result of Condition#arel_predicate, which contains the formatted Arel predicate (e.g., Arel::Nodes::Matches for cont predicates).

Handling Logical Combinations (AND/OR)

Grouping nodes require combining their children with the appropriate logical operator:


# lib/ransack/visitor.rb

def visit_Ransack_Nodes_Grouping(object)
  send(DISPATCH[object.combinator], object)
end

def visit_and(object)
  nodes = object.values.map { |o| accept(o) }.compact
  # Combines nodes with Arel::Nodes::And

end

def visit_or(object)
  left, right = object.values.map { |o| accept(o) }
  Arel::Nodes::Or.new(left, right)
end

The Visitor recursively processes child nodes, then combines them using Arel::Nodes::And or Arel::Nodes::Or, preserving the logical structure defined in the search parameters.

Processing Sort Nodes

For sorting, the Visitor handles both standard attribute sorting and custom scope-based sorting:


# lib/ransack/visitor.rb

def visit_Ransack_Nodes_Sort(object)
  # Returns Arel ordering or scope symbol

  object.to_arel
end

From Arel to Executable SQL

The Visitor returns Arel nodes, but the final SQL generation happens through ActiveRecord's query interface.

The ActiveRecord Context Adapter

The Context class in lib/ransack/adapters/active_record/context.rb serves as the bridge between Ransack's AST and ActiveRecord:


# lib/ransack/adapters/active_record/context.rb

def evaluate(search, opts = {})
  viz = Visitor.new
  relation = @object.where(viz.accept(search.base))
  # Handles sorting and scoping...

end

This method instantiates the Visitor, accepts the root grouping node to generate the WHERE clause Arel, and applies it to the ActiveRecord relation.

Compiling the Final Query

When search.result is called, the chain executes:

  1. Context#evaluate creates the Visitor
  2. viz.accept(search.base) traverses the AST and returns an Arel node
  3. @object.where(arel_node) applies the predicate
  4. ActiveRecord's SQL builder converts the Arel tree to a SQL string when the query executes
relation = Article.where(viz.accept(search.base))
sql = relation.to_sql

# => "SELECT \"articles\".* FROM \"articles\" WHERE \"articles\".\"name\" ILIKE '%rails%'"

Complete Example Walkthrough

Here is a comprehensive example demonstrating the full pipeline from parameters to SQL:


# app/models/article.rb

class Article < ApplicationRecord; end

# Build a complex search

search = Article.ransack(
  name_cont: "rails",                    # Condition node

  published_eq: true,                    # Condition node

  g: {                                    # Nested Grouping (OR)

    0 => { title_start: "A", m: "or" },
    1 => { title_end: "Z", m: "or" }
  },
  s: "created_at desc"                   # Sort node

)

# Generate the relation and inspect SQL

relation = search.result
puts relation.to_sql

Generated SQL:

SELECT "articles".* FROM "articles"
WHERE ("articles"."name" ILIKE '%rails%')
  AND ("articles"."published" = TRUE)
  AND (("articles"."title" LIKE 'A%') OR ("articles"."title" LIKE '%Z'))
ORDER BY "articles"."created_at" DESC

Internal transformation steps:

Parameter Node Type Visitor Method Arel Output
name_cont Condition visit_Ransack_Nodes_Condition Arel::Nodes::Matches
published_eq Condition visit_Ransack_Nodes_Condition Arel::Nodes::Equality
g (group) Grouping visit_Ransack_Nodes_Grouping → visit_or Arel::Nodes::Or
s (sort) Sort visit_Ransack_Nodes_Sort Arel::Nodes::Ordering

Summary

  • Ransack converts URL parameters into an AST composed of Grouping, Condition, and Sort nodes defined in lib/ransack/nodes/.
  • The Search class orchestrates AST construction in lib/ransack/search.rb, parsing predicates like name_cont into structured node objects.
  • The Visitor class in lib/ransack/visitor.rb traverses the AST using dynamic dispatch, converting each node into Arel objects via methods like visit_Ransack_Nodes_Condition.
  • Arel nodes are combined using Arel::Nodes::And and Arel::Nodes::Or to preserve logical groupings from the original parameters.
  • The Context adapter in lib/ransack/adapters/active_record/context.rb applies the generated Arel to ActiveRecord relations, which ultimately compile the tree into executable SQL.

Frequently Asked Questions

How does Ransack handle complex nested queries with AND/OR logic?

Ransack represents nested logic using Grouping nodes that store a combinator property (:and or :or). When the Visitor processes a Grouping via visit_Ransack_Nodes_Grouping, it dispatches to visit_and or visit_or, which combine child nodes using Arel::Nodes::And or Arel::Nodes::Or. This allows arbitrarily deep nesting of conditions while preserving the logical structure defined in the search parameters.

What is the role of Arel in Ransack's query generation?

Arel serves as the intermediate representation between Ransack's AST and the final SQL string. Each Condition node implements arel_predicate to return an Arel predicate object (such as Arel::Nodes::Matches for cont queries). The Visitor traverses the tree and aggregates these Arel objects, which are then passed to ActiveRecord's where method. ActiveRecord ultimately compiles the Arel tree into database-specific SQL when the query executes.

How does the Visitor pattern enable database adapter flexibility?

The Visitor class in lib/ransack/visitor.rb uses a dynamic dispatch table (DISPATCH) to map node classes to handler methods like visit_Ransack_Nodes_Condition. This separation of traversal logic from node implementation means Ransack can support different database adapters without modifying the AST node classes. The adapter-specific logic (such as handling SQL syntax variations) resides in the Context class and Arel compilation, while the Visitor focuses solely on tree traversal and Arel construction.

Where does the actual SQL string generation happen in the Ransack pipeline?

The final SQL generation occurs outside of Ransack itself, within ActiveRecord's Arel integration. In lib/ransack/adapters/active_record/context.rb, the evaluate method calls viz.accept(search.base) to obtain an Arel node, then passes it to @object.where(). When the resulting relation is executed (via to_sql or iteration), ActiveRecord's Arel visitors compile the Arel tree into the final SQL string specific to the connected database.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →