Getting Started
Purpose
sqlexpr is a query-building library for people who know SQL. It does not validate your SQL — it will happily let you construct illegal or nonsensical queries. Its purpose is to let you build queries that vary based on application logic without resorting to ugly and difficult-to-follow string manipulation.
Rather than concatenating SQL strings with if statements, you construct a
query object and call methods to add clauses. This makes it straightforward to
build queries where the columns, joins, filters, and grouping vary based on
runtime conditions:
qb = SQL.select().from_table('orders').column('id').column('total')
if include_customer:
qb.join_table('customers', 'c', 'c.id = orders.customer_id')
qb.column('c.name', 'customer_name')
if status_filter:
qb.where('status', '=', status_filter)
See SqlSelect for the full SELECT API.
Real-World Example
A more compelling example: a simplified Cypher-to-SQL translator that parses
graph query syntax and dynamically builds JOIN clauses. The translator calls
add_query_joins(qb) on a base query, adding joins and conditions based on
the parsed graph pattern. The same base query produces different SQL depending
on the input:
expr = parse_cypher("match (:Person)")
qb = expr.add_query_joins(SQL.select().from_table('ev').column('*'))
SELECT *
FROM
ev
JOIN n _n0 ON _n0.event_id = ev.id
AND _n0.entity ->> 'label' = %s
-- args: ['Person']
A relationship pattern adds multiple joins with direction constraints:
expr = parse_cypher("match (a:Person)-[r:KNOWS]->(b:Person)")
qb = expr.add_query_joins(SQL.select().from_table('ev').column('*'))
SELECT *
FROM
ev
JOIN n a ON a.event_id = ev.id
AND a.entity ->> 'label' = %s
JOIN n b ON b.event_id = ev.id
AND b.entity ->> 'label' = %s
JOIN r r ON r.event_id = ev.id
AND r.start_source_id = a.source_id
AND r.end_source_id = b.source_id
AND r.entity ->> 'type' = %s
-- args: ['Person', 'Person', 'KNOWS']
Boolean expressions compose naturally — an OR condition on a node property:
expr = parse_cypher("match (p:Person) where p.age < 18 or p.age > 65")
qb = expr.add_query_joins(SQL.select().from_table('ev').column('*'))
SELECT *
FROM
ev
JOIN n p ON p.event_id = ev.id
AND p.entity ->> 'label' = %s
AND ((p.entity -> 'properties' ->> 'age')::int < %s
OR (p.entity -> 'properties' ->> 'age')::int > %s)
-- args: ['Person', 18, 65]
Longer paths produce more joins, all correctly cross-referenced:
expr = parse_cypher("match (a:Person)-[r1:KNOWS]->(b:Person)-[r2:WORKS_AT]->(c:Company)")
qb = expr.add_query_joins(SQL.select().from_table('ev').column('*'))
SELECT *
FROM
ev
JOIN n a ON a.event_id = ev.id
AND a.entity ->> 'label' = %s
JOIN n b ON b.event_id = ev.id
AND b.entity ->> 'label' = %s
JOIN n c ON c.event_id = ev.id
AND c.entity ->> 'label' = %s
JOIN r r1 ON r1.event_id = ev.id
AND r1.start_source_id = a.source_id
AND r1.end_source_id = b.source_id
AND r1.entity ->> 'type' = %s
JOIN r r2 ON r2.event_id = ev.id
AND r2.start_source_id = b.source_id
AND r2.end_source_id = c.source_id
AND r2.entity ->> 'type' = %s
-- args: ['Person', 'Person', 'Company', 'KNOWS', 'WORKS_AT']
The translator code never constructs SQL strings directly — it calls
.join_table(), .add() on SQL.and_expr(), and SQL.value() for
parameters. The formatter handles indentation, alignment, and parameterization
automatically.
Design
All interaction with sqlexpr goes through a
dialect class — PG, MYSQL, SQLITE,
ORACLE, or MSSQL. Each dialect class provides attributes that
reference the correct builder classes for that database. Calling an attribute
(e.g. SQL.select()) instantiates a builder whose methods construct the
query.
The API uses method chaining — most methods return self so calls can be
composed fluently. Methods can be called in any order; the formatter handles
emitting clauses in the correct SQL sequence regardless of the order they were
added.
The SQL output follows the formatting style produced by pganalyze/format where appropriate, with some additions for clarity such as wrapping long SELECT column lists and breaking boolean expressions across lines.
Installation
Add the sqlexpr package to your project. It requires Python 3.8+.
Basic Usage
All query builders are accessed through a dialect class
(PG, MYSQL, SQLITE, ORACLE, or MSSQL). We recommend
aliasing your dialect as SQL so that application code is
dialect-agnostic:
from sqlexpr import PG as SQL # PostgreSQL
from sqlexpr import MYSQL as SQL # MySQL
from sqlexpr import SQLITE as SQL # SQLite
Each dialect class is a namespace whose attributes are the builder classes
for that database — so SQL.select() instantiates the SELECT builder,
SQL.insert() the INSERT builder, and so on. See
Dialect Classes for the complete list of
attributes on each dialect class, with links to the method API for each
builder.
The examples throughout this documentation use SQL, which refers to
whichever dialect class you imported above.
Building a query returns an object that you pass to SQL.format() to get the final SQL string and arguments:
qb = (SQL.select()
.from_table('users')
.column('name')
.column('email')
.where('active', '=', True))
fmt = SQL.format(qb)
sql = fmt.get_sql() # The SQL string with %s placeholders
args = fmt.get_args() # The parameter values
The formatter produces readable, indented SQL:
SELECT name, email
FROM users
WHERE active = %s
Executing Queries
The get_sqlargs() method returns a tuple of (sql, args) that can be
unpacked directly into a DB-API cursor’s execute() method:
cur.execute(*SQL.format(qb).get_sqlargs())
Parameter Styles
Each dialect class uses the correct placeholder style for its
database. Alternative styles are available where the driver supports them.
The format method you call depends on which dialect you imported as SQL:
# PostgreSQL (from sqlexpr import PG as SQL)
SQL.format(qb) # %s placeholders, list args
SQL.format_pyformat(qb) # %(name)s placeholders, dict args
# MySQL (from sqlexpr import MYSQL as SQL)
SQL.format(qb) # %s placeholders, list args
SQL.format_pyformat(qb) # %(name)s placeholders, dict args
# SQLite (from sqlexpr import SQLITE as SQL)
SQL.format(qb) # ? placeholders, list args
# Oracle (from sqlexpr import ORACLE as SQL)
SQL.format(qb) # :name placeholders, dict args
SQL.format_numeric(qb) # :1, :2 placeholders, list args
# MSSQL (from sqlexpr import MSSQL as SQL)
SQL.format(qb) # %s placeholders, list args
Named styles (named and pyformat) require named_value(name, value)
instead of raw values. See Formatting for full details and examples.
Comments
Any statement can have a SQL comment attached via
.comment(). The comment is emitted as--prefixed lines before the statement:This works on all statement types — SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, CREATE INDEX, and DROP TABLE.
Comments are particularly useful when composing complex queries with nested subqueries built up through conditional logic. When a query is assembled across multiple functions or branches, the generated SQL can be difficult to trace back to the code that produced it. Adding a comment to each subquery or conditional branch makes the final SQL self-documenting — you can see in the database logs or query plans exactly which code path produced each part of the statement.