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:
Context#evaluatecreates the Visitorviz.accept(search.base)traverses the AST and returns an Arel node@object.where(arel_node)applies the predicate- 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, andSortnodes defined inlib/ransack/nodes/. - The
Searchclass orchestrates AST construction inlib/ransack/search.rb, parsing predicates likename_continto structured node objects. - The
Visitorclass inlib/ransack/visitor.rbtraverses the AST using dynamic dispatch, converting each node into Arel objects via methods likevisit_Ransack_Nodes_Condition. - Arel nodes are combined using
Arel::Nodes::AndandArel::Nodes::Orto preserve logical groupings from the original parameters. - The
Contextadapter inlib/ransack/adapters/active_record/context.rbapplies 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →