Builder

The SQL builder provides a fluent, type-safe API for constructing SQL queries programmatically. It supports SELECT, INSERT, UPDATE, DELETE, and complex operations like JOINs, CTEs, and window functions.

Warning

The builder API is experimental and subject to breaking changes in future releases. Use with caution in production code.

Overview

The builder uses method chaining to construct SQL queries:

from sqlspec import sql

query = (
    sql.select("name", "email", "department")
    .from_("users")
    .where("active = TRUE")
    .where(("salary", ">", 50000))
    .order_by("name")
    .limit(10)
)

# Convert to SQL statement
stmt = query.to_statement()

# Execute with driver
result = await driver.execute(stmt)

Builder Factory

class sqlspec.builder.SQLFactory[source]

Bases: object

Factory for creating SQL builders and column expressions.

The main entry point for building SQL queries.

Available as: from sqlspec import sql

Methods:

  • select(*columns) - Create SELECT query

  • insert(table) - Create INSERT query

  • update(table) - Create UPDATE query

  • delete(table) - Create DELETE query

  • merge(dialect=None) - Create MERGE query (PostgreSQL 15+, Oracle, BigQuery)

classmethod detect_sql_type(sql, dialect=None)[source]
Return type:

str

__init__(dialect=None)[source]

Initialize the SQL factory.

Parameters:

dialect (Union[str, Dialect, Type[Dialect], None]) – Default SQL dialect to use for all builders.

__call__(statement, dialect=None)[source]

Create a SelectBuilder from a SQL string, or SQL object for DML with RETURNING.

Parameters:
  • statement (str) – The SQL statement string.

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect.

Return type:

typing.Any

Returns:

SelectBuilder instance for SELECT/WITH statements, SQL object for DML statements with RETURNING clause.

Raises:

SQLBuilderError – If the SQL is not a SELECT/CTE/DML+RETURNING statement.

select(*columns_or_sql, dialect=None)[source]
Return type:

Select

insert(table_or_sql=None, dialect=None)[source]
Return type:

Insert

update(table_or_sql=None, dialect=None)[source]
Return type:

Update

delete(table_or_sql=None, dialect=None)[source]
Return type:

Delete

merge(table_or_sql=None, dialect=None)[source]
Return type:

Merge

property merge_: Merge

Create a new MERGE builder (property shorthand).

Property that returns a new Merge builder instance using the factory’s default dialect. Cleaner syntax alternative to merge() method.

Examples

query = sql.merge_.into(“products”).using(data, alias=”src”) query = sql.merge_.into(“products”, alias=”t”).on(“t.id = src.id”)

Returns:

New Merge builder instance

upsert(table, dialect=None)[source]

Create an upsert builder (MERGE or INSERT ON CONFLICT).

Automatically selects the appropriate builder based on database dialect: - PostgreSQL 15+, Oracle, BigQuery: Returns MERGE builder - SQLite, DuckDB, MySQL: Returns INSERT builder with ON CONFLICT support

Parameters:
  • table (str) – Target table name

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect (uses factory default if not provided)

Return type:

Merge | Insert

Returns:

MERGE builder for supported databases, INSERT builder otherwise

Examples

PostgreSQL/Oracle/BigQuery (uses MERGE):
upsert_query = (

sql.upsert(“products”, dialect=”postgres”) .using([{“id”: 1, “name”: “Product 1”}], alias=”src”) .on(“t.id = src.id”) .when_matched_then_update(name=”src.name”) .when_not_matched_then_insert(id=”src.id”, name=”src.name”)

)

SQLite/DuckDB/MySQL (uses INSERT ON CONFLICT):
upsert_query = (

sql.upsert(“products”, dialect=”sqlite”) .values(id=1, name=”Product 1”) .on_conflict(“id”) .do_update(name=”EXCLUDED.name”)

)

create_table(table_name, dialect=None)[source]

Create a CREATE TABLE builder.

Parameters:
  • table_name (str) – Name of the table to create

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

CreateTable

Returns:

CreateTable builder instance

create_table_as_select(dialect=None)[source]

Create a CREATE TABLE AS SELECT builder.

Parameters:

dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

CreateTableAsSelect

Returns:

CreateTableAsSelect builder instance

create_view(view_name, dialect=None)[source]

Create a CREATE VIEW builder.

Parameters:
  • view_name (str) – Name of the view to create

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

CreateView

Returns:

CreateView builder instance

create_materialized_view(view_name, dialect=None)[source]

Create a CREATE MATERIALIZED VIEW builder.

Parameters:
  • view_name (str) – Name of the materialized view to create

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

CreateMaterializedView

Returns:

CreateMaterializedView builder instance

create_index(index_name, dialect=None)[source]

Create a CREATE INDEX builder.

Parameters:
  • index_name (str) – Name of the index to create

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

CreateIndex

Returns:

CreateIndex builder instance

create_schema(schema_name, dialect=None)[source]

Create a CREATE SCHEMA builder.

Parameters:
  • schema_name (str) – Name of the schema to create

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

CreateSchema

Returns:

CreateSchema builder instance

drop_table(table_name, dialect=None)[source]

Create a DROP TABLE builder.

Parameters:
  • table_name (str) – Name of the table to drop

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

DropTable

Returns:

DropTable builder instance

drop_view(view_name, dialect=None)[source]

Create a DROP VIEW builder.

Parameters:
  • view_name (str) – Name of the view to drop

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

DropView

Returns:

DropView builder instance

drop_index(index_name, dialect=None)[source]

Create a DROP INDEX builder.

Parameters:
  • index_name (str) – Name of the index to drop

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

DropIndex

Returns:

DropIndex builder instance

drop_schema(schema_name, dialect=None)[source]

Create a DROP SCHEMA builder.

Parameters:
  • schema_name (str) – Name of the schema to drop

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

DropSchema

Returns:

DropSchema builder instance

alter_table(table_name, dialect=None)[source]

Create an ALTER TABLE builder.

Parameters:
  • table_name (str) – Name of the table to alter

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

AlterTable

Returns:

AlterTable builder instance

rename_table(old_name, dialect=None)[source]

Create a RENAME TABLE builder.

Parameters:
  • old_name (str) – Current name of the table

  • dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

RenameTable

Returns:

RenameTable builder instance

comment_on(dialect=None)[source]

Create a COMMENT ON builder.

Parameters:

dialect (Union[str, Dialect, Type[Dialect], None]) – Optional SQL dialect

Return type:

CommentOn

Returns:

CommentOn builder instance

copy_from(table, source, *, columns=None, options=None, dialect=None)[source]

Build a COPY … FROM statement.

Return type:

SQL

copy_to(table, target, *, columns=None, options=None, dialect=None)[source]

Build a COPY … TO statement.

Return type:

SQL

copy(table, *, source=None, target=None, columns=None, options=None, dialect=None)[source]

Build a COPY statement, inferring direction from provided arguments.

Return type:

SQL

column(name, table=None)[source]

Create a column reference.

Parameters:
  • name (str) – Column name.

  • table (str | None) – Optional table name.

Return type:

Column

Returns:

Column object that supports method chaining and operator overloading.

property case_: Case

Create a CASE expression builder.

Returns:

Case builder instance for CASE expression building.

Example

```python case_expr = (

sql.case_.when(“x = 1”, “one”) .when(“x = 2”, “two”) .else_(“other”) .end()

) aliased_case = (

sql.case_.when(“status = ‘active’”, 1) .else_(0) .as_(“is_active”)

property row_number_: WindowFunctionBuilder

Create a ROW_NUMBER() window function builder.

property rank_: WindowFunctionBuilder

Create a RANK() window function builder.

property dense_rank_: WindowFunctionBuilder

Create a DENSE_RANK() window function builder.

property lag_: WindowFunctionBuilder

Create a LAG() window function builder.

property lead_: WindowFunctionBuilder

Create a LEAD() window function builder.

property exists_: SubqueryBuilder

Create an EXISTS subquery builder.

property in_: SubqueryBuilder

Create an IN subquery builder.

property any_: SubqueryBuilder

Create an ANY subquery builder.

property all_: SubqueryBuilder

Create an ALL subquery builder.

property inner_join_: JoinBuilder

Create an INNER JOIN builder.

property left_join_: JoinBuilder

Create a LEFT JOIN builder.

property right_join_: JoinBuilder

Create a RIGHT JOIN builder.

property full_join_: JoinBuilder

Create a FULL OUTER JOIN builder.

property cross_join_: JoinBuilder

Create a CROSS JOIN builder.

property lateral_join_: JoinBuilder

Create a LATERAL JOIN builder.

Returns:

JoinBuilder configured for LATERAL JOIN

Example

```python query = (

sql.select(“u.name”, “arr.value”) .from_(“users u”) .join(sql.lateral_join_(“UNNEST(u.tags)”).on(“true”))

property left_lateral_join_: JoinBuilder

Create a LEFT LATERAL JOIN builder.

Returns:

JoinBuilder configured for LEFT LATERAL JOIN

property cross_lateral_join_: JoinBuilder

Create a CROSS LATERAL JOIN builder.

Returns:

JoinBuilder configured for CROSS LATERAL JOIN

__getattr__(name)[source]

Dynamically create column references.

Parameters:

name (str) – Column name.

Return type:

Column

Returns:

Column object for the given name.

Note

Special SQL constructs like case_, row_number_, etc. are handled as properties for type safety.

static raw(sql_fragment, **parameters)[source]

Create a raw SQL expression from a string fragment with optional parameters.

Parameters:
  • sql_fragment (str) – Raw SQL string to parse into an expression.

  • **parameters (Any) – Named parameters for parameter binding.

Return type:

Expression | SQL

Returns:

SQLGlot expression from the parsed SQL fragment (if no parameters). SQL statement object (if parameters provided).

Raises:

SQLBuilderError – If the SQL fragment cannot be parsed.

Example

```python expr = sql.raw(“COALESCE(name, ‘Unknown’)”)

stmt = sql.raw(

“LOWER(name) LIKE LOWER(:pattern)”, pattern=f”%{query}%”

)

expr = sql.raw(

“price BETWEEN :min_price AND :max_price”, min_price=100, max_price=500,

)

query = sql.select(

“name”, sql.raw(

“ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC)”

),

).from_(“employees”) ```

count(column='*', distinct=False)[source]

Create a COUNT expression.

Parameters:
  • column (Union[str, Expression, ExpressionWrapper, Case, Column]) – Column to count (default “*”).

  • distinct (bool) – Whether to use COUNT DISTINCT.

Return type:

AggregateExpression

Returns:

COUNT expression.

count_distinct(column)[source]

Create a COUNT(DISTINCT column) expression.

Parameters:

column (Union[str, Expression, ExpressionWrapper, Case]) – Column to count distinct values.

Return type:

AggregateExpression

Returns:

COUNT DISTINCT expression.

static sum(column, distinct=False)[source]

Create a SUM expression.

Parameters:
  • column (Union[str, Expression, ExpressionWrapper, Case]) – Column to sum.

  • distinct (bool) – Whether to use SUM DISTINCT.

Return type:

AggregateExpression

Returns:

SUM expression.

static avg(column)[source]

Create an AVG expression.

Parameters:

column (Union[str, Expression, ExpressionWrapper, Case]) – Column to average.

Return type:

AggregateExpression

Returns:

AVG expression.

static max(column)[source]

Create a MAX expression.

Parameters:

column (Union[str, Expression, ExpressionWrapper, Case]) – Column to find maximum.

Return type:

AggregateExpression

Returns:

MAX expression.

static min(column)[source]

Create a MIN expression.

Parameters:

column (Union[str, Expression, ExpressionWrapper, Case]) – Column to find minimum.

Return type:

AggregateExpression

Returns:

MIN expression.

static rollup(*columns)[source]

Create a ROLLUP expression for GROUP BY clauses.

Parameters:

*columns (str | Expression) – Columns to include in the rollup.

Return type:

FunctionExpression

Returns:

ROLLUP expression.

Example

```python query = (

sql.select(“product”, “region”, sql.sum(“sales”)) .from_(“sales_data”) .group_by(sql.rollup(“product”, “region”))

static cube(*columns)[source]

Create a CUBE expression for GROUP BY clauses.

Parameters:

*columns (str | Expression) – Columns to include in the cube.

Return type:

FunctionExpression

Returns:

CUBE expression.

Example

```python query = (

sql.select(“product”, “region”, sql.sum(“sales”)) .from_(“sales_data”) .group_by(sql.cube(“product”, “region”))

static grouping_sets(*column_sets)[source]

Create a GROUPING SETS expression for GROUP BY clauses.

Parameters:

*column_sets (tuple[str, ...] | list[str]) – Sets of columns to group by.

Return type:

FunctionExpression

Returns:

GROUPING SETS expression.

Example

```python query = (

sql.select(“product”, “region”, sql.sum(“sales”)) .from_(“sales_data”) .group_by(

sql.grouping_sets((“product”,), (“region”,), ())

)

static any(values)[source]

Create an ANY expression for use with comparison operators.

Parameters:

values (list[Any] | Expression | str) – Values, expression, or subquery for the ANY clause.

Return type:

FunctionExpression

Returns:

ANY expression.

Example

```python subquery = sql.select(“user_id”).from_(“active_users”) query = (

sql.select(“*”) .from_(“users”) .where(sql.id.eq(sql.any(subquery)))

static not_any_(values)[source]

Create a NOT ANY expression for use with comparison operators.

Parameters:

values (list[Any] | Expression | str) – Values, expression, or subquery for the NOT ANY clause.

Return type:

FunctionExpression

Returns:

NOT ANY expression.

Example

```python subquery = sql.select(“user_id”).from_(“blocked_users”) query = (

sql.select(“*”) .from_(“users”) .where(sql.id.neq(sql.not_any(subquery)))

static concat(*expressions)[source]

Create a CONCAT expression.

Parameters:

*expressions (str | Expression) – Expressions to concatenate.

Return type:

StringExpression

Returns:

CONCAT expression.

static upper(column)[source]

Create an UPPER expression.

Parameters:

column (str | Expression) – Column to convert to uppercase.

Return type:

StringExpression

Returns:

UPPER expression.

static lower(column)[source]

Create a LOWER expression.

Parameters:

column (str | Expression) – Column to convert to lowercase.

Return type:

StringExpression

Returns:

LOWER expression.

static length(column)[source]

Create a LENGTH expression.

Parameters:

column (str | Expression) – Column to get length of.

Return type:

StringExpression

Returns:

LENGTH expression.

static round(column, decimals=0)[source]

Create a ROUND expression.

Parameters:
  • column (str | Expression) – Column to round.

  • decimals (int) – Number of decimal places.

Return type:

MathExpression

Returns:

ROUND expression.

static to_literal(value)[source]

Convert a Python value to a SQLGlot literal expression.

Uses SQLGlot’s built-in exp.convert() function for literal creation. Handles all Python primitive types: - None -> exp.Null (renders as NULL) - bool -> exp.Boolean (renders as TRUE/FALSE or 1/0 based on dialect) - int/float -> exp.Literal with is_number=True - str -> exp.Literal with is_string=True - exp.Expression -> returned as-is (passthrough)

Parameters:

value (Any) – Python value or SQLGlot expression to convert.

Return type:

FunctionExpression

Returns:

SQLGlot expression representing the literal value.

static decode(column, *args)[source]

Create a DECODE expression (Oracle-style conditional logic).

DECODE compares column to each search value and returns the corresponding result. If no match is found, returns the default value (if provided) or NULL.

Parameters:
  • column (str | Expression) – Column to compare.

  • *args (str | Expression | Any) – Alternating search values and results, with optional default at the end. Format: search1, result1, search2, result2, …, [default]

Raises:

ValueError – If fewer than two search/result pairs are provided.

Return type:

FunctionExpression

Returns:

CASE expression equivalent to DECODE.

Example

```python sql.decode(

“status”, “A”, “Active”, “I”, “Inactive”, “Unknown”

static cast(column, data_type)[source]

Create a CAST expression for type conversion.

Parameters:
  • column (str | Expression) – Column or expression to cast.

  • data_type (str) – Target data type (e.g., ‘INT’, ‘VARCHAR(100)’, ‘DECIMAL(10,2)’).

Return type:

ConversionExpression

Returns:

CAST expression.

static coalesce(*expressions)[source]

Create a COALESCE expression.

Parameters:

*expressions (str | Expression) – Expressions to coalesce.

Return type:

ConversionExpression

Returns:

COALESCE expression.

static nvl(column, substitute_value)[source]

Create an NVL (Oracle-style) expression using COALESCE.

Parameters:
  • column (str | Expression) – Column to check for NULL.

  • substitute_value (str | Expression | Any) – Value to use if column is NULL.

Return type:

ConversionExpression

Returns:

COALESCE expression equivalent to NVL.

static nvl2(column, value_if_not_null, value_if_null)[source]

Create an NVL2 (Oracle-style) expression using CASE.

NVL2 returns value_if_not_null if column is not NULL, otherwise returns value_if_null.

Parameters:
  • column (str | Expression) – Column to check for NULL.

  • value_if_not_null (str | Expression | Any) – Value to use if column is NOT NULL.

  • value_if_null (str | Expression | Any) – Value to use if column is NULL.

Return type:

ConversionExpression

Returns:

CASE expression equivalent to NVL2.

Example

`python sql.nvl2("salary", "Has Salary", "No Salary") `

static bulk_insert(table_name, column_count, placeholder_style='?')[source]

Create bulk INSERT expression for executemany operations.

For bulk loading operations like CSV ingestion where an INSERT expression with placeholders for executemany() is needed.

Parameters:
  • table_name (str) – Name of the table to insert into

  • column_count (int) – Number of columns (for placeholder generation)

  • placeholder_style (str) – Placeholder style (“?” for SQLite/PostgreSQL, “%s” for MySQL, “:1” for Oracle)

Return type:

FunctionExpression

Returns:

INSERT expression with placeholders for bulk operations

Example

```python from sqlspec import sql

insert_expr = sql.bulk_insert(“my_table”, 3)

insert_expr = sql.bulk_insert(

“my_table”, 3, placeholder_style=”%s”

)

insert_expr = sql.bulk_insert(

“my_table”, 3, placeholder_style=”:1”

truncate(table_name)[source]

Create a TRUNCATE TABLE builder.

Parameters:

table_name (str) – Name of the table to truncate

Return type:

Truncate

Returns:

TruncateTable builder instance

Example

```python from sqlspec import sql

truncate_sql = sql.truncate_table(“my_table”).build().sql

truncate_sql = (

sql.truncate_table(“my_table”) .cascade() .restart_identity() .build() .sql

static case()[source]

Create a CASE expression builder.

Return type:

Case

Returns:

CaseExpressionBuilder for building CASE expressions.

row_number(partition_by=None, order_by=None)[source]

Create a ROW_NUMBER() window function.

Parameters:
Return type:

FunctionExpression

Returns:

ROW_NUMBER window function expression.

rank(partition_by=None, order_by=None)[source]

Create a RANK() window function.

Parameters:
Return type:

FunctionExpression

Returns:

RANK window function expression.

dense_rank(partition_by=None, order_by=None)[source]

Create a DENSE_RANK() window function.

Parameters:
Return type:

FunctionExpression

Returns:

DENSE_RANK window function expression.

SELECT Queries

class sqlspec.builder.Select[source]

Bases: QueryBuilder, WhereClauseMixin, OrderByClauseMixin, LimitOffsetClauseMixin, SelectClauseMixin, JoinClauseMixin, HavingClauseMixin, SetOperationMixin, CommonTableExpressionMixin, PivotClauseMixin, UnpivotClauseMixin

Builder for SELECT queries.

Provides a fluent interface for constructing SQL SELECT statements with parameter binding and validation.

Example

>>> class User(BaseModel):
...     id: int
...     name: str
>>> builder = Select("id", "name").from_("users")
>>> result = driver.execute(builder)

Builder for SELECT statements with support for:

  • Column selection

  • FROM clauses

  • JOINs (INNER, LEFT, RIGHT, FULL)

  • WHERE conditions

  • GROUP BY

  • HAVING

  • ORDER BY

  • LIMIT/OFFSET

  • CTEs (WITH)

  • Window functions

  • Subqueries

__init__(*columns, **kwargs)[source]

Initialize SELECT with optional columns.

Parameters:
  • *columns (str) – Column names to select (e.g., “id”, “name”, “u.email”)

  • **kwargs (Any) – Additional QueryBuilder arguments (dialect, schema, etc.)

Examples

Select(“id”, “name”) # Shorthand for Select().select(“id”, “name”) Select() # Same as Select() - start empty

with_hint(hint, *, location='statement', table=None, dialect=None)[source]

Attach an optimizer or dialect-specific hint to the query.

Parameters:
  • hint (str) – The raw hint string (e.g., ‘INDEX(users idx_users_name)’).

  • location (str) – Where to apply the hint (‘statement’, ‘table’).

  • table (str | None) – Table name if the hint is for a specific table.

  • dialect (str | None) – Restrict the hint to a specific dialect (optional).

Return type:

Self

Returns:

The current builder instance for method chaining.

build(dialect=None)[source]

Builds the SQL query string and parameters with hint injection.

Parameters:

dialect (Union[str, Dialect, Type[Dialect], None]) – Optional dialect override for SQL generation.

Returns:

A dataclass containing the SQL string and parameters.

Return type:

SafeQuery

for_update(*, skip_locked=False, nowait=False, of=None)[source]

Add FOR UPDATE clause to SELECT statement for row-level locking.

Parameters:
  • skip_locked (bool) – Skip rows that are already locked (SKIP LOCKED)

  • nowait (bool) – Return immediately if row is locked (NOWAIT)

  • of (str | list[str] | None) – Table names/aliases to lock (FOR UPDATE OF table)

Return type:

Self

Returns:

Self for method chaining

for_share(*, skip_locked=False, nowait=False, of=None)[source]

Add FOR SHARE clause for shared row-level locking.

Parameters:
  • skip_locked (bool) – Skip rows that are already locked (SKIP LOCKED)

  • nowait (bool) – Return immediately if row is locked (NOWAIT)

  • of (str | list[str] | None) – Table names/aliases to lock (FOR SHARE OF table)

Return type:

Self

Returns:

Self for method chaining

for_key_share()[source]

Add FOR KEY SHARE clause (PostgreSQL-specific).

FOR KEY SHARE is like FOR SHARE, but the lock is weaker: SELECT FOR UPDATE is blocked, but not SELECT FOR NO KEY UPDATE.

Return type:

Self

Returns:

Self for method chaining

for_no_key_update()[source]

Add FOR NO KEY UPDATE clause (PostgreSQL-specific).

FOR NO KEY UPDATE is like FOR UPDATE, but the lock is weaker: it does not block SELECT FOR KEY SHARE commands that attempt to acquire a share lock on the same rows.

Return type:

Self

Returns:

Self for method chaining

Basic SELECT

# Simple select
query = sql.select("*").from_("users")

# Specific columns
query = sql.select("id", "name", "email").from_("users")

# Column aliases
query = sql.select(
    "id",
    "name AS full_name",
    "COUNT(*) OVER () AS total_count"
).from_("users")

WHERE Conditions

Multiple ways to specify conditions:

# Raw SQL string
query = sql.select("*").from_("users").where("active = TRUE")

# Tuple format (column, operator, value)
query = sql.select("*").from_("users").where(("age", ">=", 18))

# Multiple conditions (AND)
query = (
    sql.select("*")
    .from_("users")
    .where("active = TRUE")
    .where(("age", ">=", 18))
    .where(("department", "=", "Engineering"))
)

# OR conditions
query = sql.select("*").from_("users").where_or(
    ("role", "=", "admin"),
    ("role", "=", "moderator")
)

JOINs

# INNER JOIN
query = (
    sql.select("u.name", "o.total", "p.name as product")
    .from_("users u")
    .inner_join("orders o", "u.id = o.user_id")
    .inner_join("products p", "o.product_id = p.id")
)

# LEFT JOIN
query = (
    sql.select("u.name", "COUNT(o.id) as order_count")
    .from_("users u")
    .left_join("orders o", "u.id = o.user_id")
    .group_by("u.id", "u.name")
)

# Multiple JOIN types
query = (
    sql.select("*")
    .from_("users u")
    .inner_join("orders o", "u.id = o.user_id")
    .left_join("reviews r", "o.id = r.order_id")
    .right_join("products p", "o.product_id = p.id")
)

Aggregation

# GROUP BY with aggregates
query = (
    sql.select(
        "department",
        "COUNT(*) as employee_count",
        "AVG(salary) as avg_salary",
        "MAX(salary) as max_salary"
    )
    .from_("users")
    .group_by("department")
    .having("COUNT(*) > 5")
    .order_by("avg_salary DESC")
)

CTEs (WITH Clauses)

# Simple CTE
high_earners = (
    sql.select("id", "name", "salary")
    .from_("users")
    .where(("salary", ">", 100000))
)

query = (
    sql.select("*")
    .with_("high_earners", high_earners)
    .from_("high_earners")
    .order_by("salary DESC")
)

# Multiple CTEs
dept_stats = sql.select(
    "department",
    "AVG(salary) as avg_salary"
).from_("users").group_by("department")

user_ranks = sql.select(
    "id", "name", "salary",
    "ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as rank"
).from_("users")

query = (
    sql.select("u.name", "u.salary", "d.avg_salary")
    .with_("dept_stats", dept_stats)
    .with_("user_ranks", user_ranks)
    .from_("user_ranks u")
    .inner_join("dept_stats d", "u.department = d.department")
    .where("u.rank <= 3")
)

Subqueries

# Subquery in WHERE
high_value_customers = (
    sql.select("user_id")
    .from_("orders")
    .group_by("user_id")
    .having("SUM(total_amount) > 10000")
)

query = (
    sql.select("*")
    .from_("users")
    .where_in("id", high_value_customers)
)

# EXISTS subquery
has_orders = (
    sql.select("1")
    .from_("orders")
    .where("orders.user_id = users.id")
)

query = (
    sql.select("*")
    .from_("users")
    .where_exists(has_orders)
)

INSERT Queries

class sqlspec.builder.Insert[source]

Bases: QueryBuilder, ReturningClauseMixin, InsertValuesMixin, InsertFromSelectMixin, InsertIntoClauseMixin

Builder for INSERT statements.

Constructs SQL INSERT queries with parameter binding and validation.

Builder for INSERT statements.

__init__(table=None, **kwargs)[source]

Initialize INSERT with optional table.

Parameters:
  • table (str | None) – Target table name

  • **kwargs (Any) – Additional QueryBuilder arguments

get_insert_expression()[source]

Get the insert expression (public API).

Return type:

Insert

values_from_dict(data)[source]

Adds a row of values from a dictionary.

This is a convenience method that automatically sets columns based on the dictionary keys and values based on the dictionary values.

Parameters:

data (Mapping[str, typing.Any]) – A mapping of column names to values.

Return type:

Self

Returns:

The current builder instance for method chaining.

Raises:

SQLBuilderError – If into() has not been called to set the table.

values_from_dicts(data)[source]

Adds multiple rows of values from a sequence of dictionaries.

This is a convenience method for bulk inserts from structured data.

Parameters:

data (Sequence[Mapping[str, typing.Any]]) – A sequence of mappings, each representing a row of data.

Return type:

Self

Returns:

The current builder instance for method chaining.

Raises:

SQLBuilderError – If into() has not been called to set the table, or if dictionaries have inconsistent keys.

on_conflict(*columns)[source]

Adds an ON CONFLICT clause with specified columns.

Parameters:

*columns (str) – Column names that define the conflict. If no columns provided, creates an ON CONFLICT without specific columns (catches all conflicts).

Return type:

ConflictBuilder

Returns:

A ConflictBuilder instance for chaining conflict resolution methods.

Example

```python sql.insert(“users”).values(id=1, name=”John”).on_conflict(

“id”

).do_nothing()

sql.insert(“users”).values(…).on_conflict(

“email”, “username”

).do_update(updated_at=sql.raw(“NOW()”))

sql.insert(“users”).values(…).on_conflict().do_nothing() ```

on_conflict_do_nothing(*columns)[source]

Adds an ON CONFLICT DO NOTHING clause (convenience method).

Parameters:

*columns (str) – Column names that define the conflict. If no columns provided, creates an ON CONFLICT without specific columns.

Return type:

Insert

Returns:

The current builder instance for method chaining.

Note

This is a convenience method. For more control, use on_conflict().do_nothing().

on_duplicate_key_update(**kwargs)[source]

Adds MySQL-style ON DUPLICATE KEY UPDATE clause.

Parameters:

**kwargs (Any) – Column-value pairs to update on duplicate key.

Return type:

Insert

Returns:

The current builder instance for method chaining.

Note

This method creates MySQL-specific ON DUPLICATE KEY UPDATE syntax. For PostgreSQL, use on_conflict() instead.

# Insert single row
query = (
    sql.insert("users")
    .values(name="Alice", email="[email protected]", age=30)
)

# Insert multiple rows
query = (
    sql.insert("users")
    .columns("name", "email", "age")
    .values(
        ("Alice", "[email protected]", 30),
        ("Bob", "[email protected]", 25),
        ("Charlie", "[email protected]", 35)
    )
)

# INSERT with RETURNING
query = (
    sql.insert("users")
    .values(name="Alice", email="[email protected]")
    .returning("id", "created_at")
)

# INSERT from SELECT
query = (
    sql.insert("archived_users")
    .columns("id", "name", "email")
    .from_select(
        sql.select("id", "name", "email")
        .from_("users")
        .where("last_login < NOW() - INTERVAL '1 year'")
    )
)

UPDATE Queries

class sqlspec.builder.Update[source]

Bases: QueryBuilder, WhereClauseMixin, ReturningClauseMixin, UpdateSetClauseMixin, UpdateFromClauseMixin, UpdateTableClauseMixin

Builder for UPDATE statements.

Constructs SQL UPDATE statements with parameter binding and validation.

Example

```python update_query = (

Update() .table(“users”) .set_(name=”John Doe”) .set_(email=”john@example.com”) .where(“id = 1”)

)

update_query = (

Update(“users”).set_(name=”John Doe”).where(“id = 1”)

)

update_query = (

Update() .table(“users”) .set_(status=”active”) .where_eq(“id”, 123)

)

update_query = (

Update() .table(“users”, “u”) .set_(name=”Updated Name”) .from_(“profiles”, “p”) .where(“u.id = p.user_id AND p.is_verified = true”)

Builder for UPDATE statements.

__init__(table=None, **kwargs)[source]

Initialize UPDATE with optional table.

Parameters:
  • table (str | None) – Target table name

  • **kwargs (Any) – Additional QueryBuilder arguments

join(table, on, alias=None, join_type='INNER')[source]

Add JOIN clause to the UPDATE statement.

Parameters:
  • table (str | Expression | Select) – The table name, expression, or subquery to join.

  • on (str | Expression) – The JOIN condition.

  • alias (str | None) – Optional alias for the joined table.

  • join_type (str) – Type of join (INNER, LEFT, RIGHT, FULL).

Return type:

Self

Returns:

The current builder instance for method chaining.

Raises:

SQLBuilderError – If the current expression is not an UPDATE statement.

build(dialect=None)[source]

Build the UPDATE query with validation.

Parameters:

dialect (Union[str, Dialect, Type[Dialect], None]) – Optional dialect override for SQL generation.

Returns:

The built query with SQL and parameters.

Return type:

SafeQuery

Raises:

SQLBuilderError – If no table is set or expression is not an UPDATE.

# Simple update
query = (
    sql.update("users")
    .set(active=False)
    .where(("last_login", "<", "2024-01-01"))
)

# Multiple columns
query = (
    sql.update("users")
    .set(
        active=True,
        last_login="NOW()",
        login_count="login_count + 1"
    )
    .where(("id", "=", 123))
)

# UPDATE with JOIN
query = (
    sql.update("users u")
    .set(premium=True)
    .from_("orders o")
    .where("u.id = o.user_id")
    .where(("o.total_amount", ">", 10000))
)

# UPDATE with RETURNING
query = (
    sql.update("users")
    .set(salary="salary * 1.1")
    .where(("department", "=", "Engineering"))
    .returning("id", "name", "salary")
)

DELETE Queries

class sqlspec.builder.Delete[source]

Bases: QueryBuilder, WhereClauseMixin, ReturningClauseMixin, DeleteFromClauseMixin

Builder for DELETE statements.

Constructs SQL DELETE statements with parameter binding and validation. Does not support JOIN operations to maintain cross-dialect compatibility.

Builder for DELETE statements.

__init__(table=None, **kwargs)[source]

Initialize DELETE with optional table.

Parameters:
  • table (str | None) – Target table name

  • **kwargs (Any) – Additional QueryBuilder arguments

build(dialect=None)[source]

Build the DELETE query with validation.

Parameters:

dialect (Union[str, Dialect, Type[Dialect], None]) – Optional dialect override for SQL generation.

Returns:

The built query with SQL and parameters.

Return type:

SafeQuery

Raises:

SQLBuilderError – If the table is not specified.

# Simple delete
query = sql.delete("users").where(("active", "=", False))

# Delete with multiple conditions
query = (
    sql.delete("users")
    .where("last_login < NOW() - INTERVAL '2 years'")
    .where(("email_verified", "=", False))
)

# DELETE with RETURNING
query = (
    sql.delete("users")
    .where(("id", "=", 123))
    .returning("id", "name", "email")
)

MERGE Queries (UPSERT)

class sqlspec.builder.Merge[source]

Bases: QueryBuilder, MergeUsingClauseMixin, MergeOnClauseMixin, MergeMatchedClauseMixin, MergeNotMatchedClauseMixin, MergeIntoClauseMixin, MergeNotMatchedBySourceClauseMixin

Builder for MERGE statements.

Constructs SQL MERGE statements (also known as UPSERT in some databases) with parameter binding and validation.

Builder for MERGE statements (INSERT or UPDATE based on condition).

Database Support:

  • ✅ PostgreSQL 15+

  • ✅ Oracle 9i+

  • ✅ BigQuery

  • ❌ MySQL (use INSERT … ON DUPLICATE KEY UPDATE)

  • ❌ SQLite (use INSERT … ON CONFLICT)

__init__(target_table=None, **kwargs)[source]

Initialize MERGE with optional target table.

Parameters:
  • target_table (str | None) – Target table name

  • **kwargs (Any) – Additional QueryBuilder arguments

build(dialect=None)[source]

Build MERGE statement with dialect validation.

Parameters:

dialect (Union[str, Dialect, Type[Dialect], None]) – Optional dialect override for SQL generation.

Return type:

typing.Any

Returns:

Built statement object.

Raises:

DialectNotSupportedError – If dialect does not support MERGE.

Basic MERGE

# Simple upsert from dict
query = (
    sql.merge(dialect="postgres")
    .into("products", alias="t")
    .using({"id": 1, "name": "Product A", "price": 19.99}, alias="src")
    .on("t.id = src.id")
    .when_matched_then_update(name="src.name", price="src.price")
    .when_not_matched_then_insert(id="src.id", name="src.name", price="src.price")
)

# MERGE from table source
query = (
    sql.merge(dialect="postgres")
    .into("products", alias="t")
    .using("staging_products", alias="s")
    .on("t.id = s.id")
    .when_matched_then_update(name="s.name", price="s.price")
    .when_not_matched_then_insert(columns=["id", "name", "price"])
)

Conditional MERGE

# Update only if condition met
query = (
    sql.merge(dialect="postgres")
    .into("products", alias="t")
    .using(staging_data, alias="src")
    .on("t.id = src.id")
    .when_matched_then_update(
        condition="t.price < src.price",
        price="src.price"
    )
    .when_not_matched_then_insert(id="src.id", name="src.name", price="src.price")
)

# Delete matched rows conditionally
query = (
    sql.merge(dialect="postgres")
    .into("products", alias="t")
    .using({"id": 1, "discontinued": True}, alias="src")
    .on("t.id = src.id")
    .when_matched_then_delete(condition="src.discontinued = TRUE")
)

SQL Server Extensions

SQL Server supports additional WHEN NOT MATCHED BY SOURCE clauses:

# SQL Server: Handle rows in target not in source
query = (
    sql.merge(dialect="tsql")
    .into("products", alias="t")
    .using(current_products, alias="src")
    .on("t.id = src.id")
    .when_matched_then_update(name="src.name", price="src.price")
    .when_not_matched_then_insert(id="src.id", name="src.name", price="src.price")
    .when_not_matched_by_source_then_delete()  # Delete obsolete products
)

NULL Value Handling

MERGE automatically handles NULL values in source data:

# NULL values are properly typed
query = (
    sql.merge(dialect="postgres")
    .into("products", alias="t")
    .using({"id": 1, "name": "Updated", "price": None}, alias="src")
    .on("t.id = src.id")
    .when_matched_then_update(name="src.name", price="src.price")
)
# Sets price to NULL if matched

Note

When all values for a column are NULL, PostgreSQL defaults to NUMERIC type. For other column types, provide at least one non-NULL value for accurate type inference.

Query Mixins

Base Classes

Filter Integration

The builder integrates with SQLSpec’s filter system:

from sqlspec.core import LimitOffsetFilter, SearchFilter, OrderByFilter

# Base query
query = sql.select("*").from_("users")

# Apply filters
filtered = query.append_filter(
    SearchFilter("name", "John"),
    LimitOffsetFilter(10, 0),
    OrderByFilter("created_at", "desc")
)

# Execute
stmt = filtered.to_statement()

Statement Conversion

Convert builder to executable SQL:

# Build query
query = sql.select("*").from_("users").limit(10)

# Convert to SQL statement
stmt = query.to_statement()

# SQL object has .sql and .parameters
print(stmt.sql)  # "SELECT * FROM users LIMIT 10"
print(stmt.parameters)  # None or parameter dict

# Execute with driver
async with db.provide_session(config) as session:
    result = await session.execute(stmt)

See Also