Builder (assemble something complex step by step)
When to use
- You need to build complex objects/strings with many optional parts (e.g., SQL with filters, sort, limit).
- You want a fluent API so call sites are readable and order-independent.
- You must keep params separate from SQL (safe binding, no string concat injection).
Avoid when a single function with a few arguments is enough.
Diagram (text)
Client ──> QueryBuilder
├─ select(...)
├─ from_(...)
├─ where(cond, params)
├─ order_by(...)
├─ limit(n)
└─ compile() → (sql, params)
Python example (≤40 lines, type-hinted)
Concrete case: build parameterized SQL for an analytics job.
(Uses ? placeholders for demo—adapt to your DB-API if it uses %s.)
from __future__ import annotations
from dataclasses import dataclass, field
from typing import List, Tuple
@dataclass
class QueryBuilder:
_cols: List[str] = field(default_factory=list)
_table: str = ""
_wheres: List[str] = field(default_factory=list)
_params: List[object] = field(default_factory=list)
_order: str = ""
_limit: int | None = None
def select(self, *cols: str) -> "QueryBuilder":
self._cols.extend(cols or ["*"]); return self
def from_(self, table: str) -> "QueryBuilder":
self._table = table; return self
def where(self, cond: str, *params: object) -> "QueryBuilder":
self._wheres.append(cond); self._params.extend(params); return self
def order_by(self, expr: str) -> "QueryBuilder":
self._order = expr; return self
def limit(self, n: int) -> "QueryBuilder":
self._limit = n; return self
def compile(self) -> Tuple[str, Tuple[object, ...]]:
assert self._table, "FROM is required"
cols = ", ".join(self._cols or ["*"])
sql = [f"SELECT {cols} FROM {self._table}"]
if self._wheres: sql.append("WHERE " + " AND ".join(self._wheres))
if self._order: sql.append(f"ORDER BY {self._order}")
if self._limit is not None: sql.append("LIMIT ?"); self._params.append(self._limit)
return " ".join(sql), tuple(self._params)
Usage
qb = (QueryBuilder()
.select("id", "event_time", "user_id")
.from_("events")
.where("event_time >= ?", "2025-11-01")
.where("tenant_id = ?", 42)
.order_by("event_time DESC")
.limit(100))
sql, params = qb.compile()
# cursor.execute(sql, params)
Tiny pytest (cements it)
def test_builder_outputs_param_sql():
qb = (QueryBuilder().select("id").from_("t")
.where("a = ?", 1).where("b IN (?,?)", 2, 3).limit(10))
sql, p = qb.compile()
assert sql == "SELECT id FROM t WHERE a = ? AND b IN (?,?) LIMIT ?"
assert p == (1, 2, 3, 10)
Trade-offs & pitfalls
- Pros: Readable call sites; safe parameter binding; easy to add options; testable output.
- Cons: More code than handcrafted SQL; can grow heavy if you reimplement a full SQL DSL.
- Pitfalls:
- Mixing raw string interpolation with params → injection risk.
- Letting the builder “execute” queries—keep it focused on building, not running.
- Giant “god” builder—favor a small surface; compose builders for joins, filters, etc.
Pythonic alternatives
- SQL libraries: SQLAlchemy Core, pypika—battle-tested builders; prefer them if allowed.
- Dataclass config + renderer: collect options in a dataclass, then one pure
render(cfg)function. - Generators: stream clauses (
yield "WHERE ...") and join at the end—nice for very dynamic queries.
Mini exercise
Add .group_by(expr: str) and make sure compile() renders it before ORDER BY. Write a quick test to confirm order: WHERE → GROUP BY → ORDER BY → LIMIT.
Checks (quick checklist)
- Builder keeps data and params separate.
- Fluent methods return
self; order of calls doesn’t break output. compile()is pure: returns(sql, params)and doesn’t execute.- Assertions/validation for required parts (e.g.,
FROM). - Tests cover typical clause combinations and parameter positions.




