ruby 48 lines · 8 steps

How a chainable SQL query builder works

A Ruby class that accumulates query parts through chained calls and assembles them into a SQL string on demand.

Explained by highlit
1class QueryBuilder
2 def initialize(table)
3 @table = table
4 @wheres = []
5 @orders = []
6 @limit = nil
7 @offset = nil
8 @selects = ['*']
9 end
10 
11 def select(*columns)
12 @selects = columns.flatten.map(&:to_s) unless columns.empty?
13 self
14 end
15 
16 def where(condition, *bindings)
17 @wheres << { sql: condition, bindings: bindings }
18 self
19 end
20 
21 def order(column, direction = :asc)
22 @orders << "#{column} #{direction.to_s.upcase}"
23 self
24 end
25 
26 def limit(count)
27 @limit = count.to_i
28 self
29 end
30 
31 def offset(count)
32 @offset = count.to_i
33 self
34 end
35 
36 def to_sql
37 sql = "SELECT #{@selects.join(', ')} FROM #{@table}"
38 sql << " WHERE #{@wheres.map { |w| w[:sql] }.join(' AND ')}" unless @wheres.empty?
39 sql << " ORDER BY #{@orders.join(', ')}" unless @orders.empty?
40 sql << " LIMIT #{@limit}" if @limit
41 sql << " OFFSET #{@offset}" if @offset
42 sql
43 end
44 
45 def bindings
46 @wheres.flat_map { |w| w[:bindings] }
47 end
48end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Returning self from each mutator method is what enables a fluent, chainable API.
  2. 2Separating accumulation from rendering lets you build state incrementally and generate output only when needed.
  3. 3Keeping bindings apart from the SQL string preserves safe parameterized queries.

Related explainers

Share this explainer

Here's the card — post it anywhere.

How a chainable SQL query builder works — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code