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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Returning self from each mutator method is what enables a fluent, chainable API.
- 2Separating accumulation from rendering lets you build state incrementally and generate output only when needed.
- 3Keeping bindings apart from the SQL string preserves safe parameterized queries.
Related explainers
ruby
class Order class InvalidTransition < StandardError; end TRANSITIONS = {
A state machine for order transitions in Ruby
state-machine
data-driven
error-handling
Intermediate
8 steps
ruby
class CreateOrderItems < ActiveRecord::Migration[7.1] def change create_table :order_items do |t| t.references :order, null: false, foreign_key: { on_delete: :cascade }
Enforcing order-item integrity in Rails
migrations
foreign-keys
validations
Intermediate
7 steps
ruby
class Registration < ApplicationRecord belongs_to :event validates :email, presence: true, format: { with: URI::MailTo::EMAIL_REGEXP }
Validating registrations in Rails
validations
i18n
error-handling
Intermediate
8 steps
ruby
class ApacheLogParser LINE_PATTERN = /\A(?<ip>\S+)\s\S+\s\S+\s\[(?<time>[^\]]+)\]\s"(?<method>[A-Z]+)\s(?<path>\S+)\s(?<protocol>[^"]+)"\s(?<status>\d{3})\s(?<bytes>\d+|-)/ TIME_FORMAT = "%d/%b/%Y:%H:%M:%S %z"
Parsing Apache logs with named captures
regex
named-captures
parsing
Intermediate
6 steps
rust
use axum::{ extract::State, routing::{get, post, MethodRouter}, Json, Router,
Self-documenting routes in Axum
builder-pattern
closures
shared-state
Intermediate
8 steps
ruby
require "csv" def parse_transaction_row(line) fields = CSV.parse_line(line, headers: false, skip_blanks: true)
Parsing one CSV transaction row in Ruby
parsing
type-coercion
error-handling
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-a-chainable-sql-query-builder-works-explained-ruby-a222/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.