Skip to main content

sql_forge

Macro sql_forge 

Source
sql_forge!() { /* proc-macro */ }
Expand description

Builds a parameterized SQL query with compile-time type-checking and a runtime [sqlx::QueryBuilder] for dynamic SQL.

Combines sqlx::query_as! / sqlx::query_scalar! validation (never called at runtime) with QueryBuilder::push_bind for safe value binding.

§Syntax

sql_forge!(
    [DB,]        // optional: sqlx::MySql | sqlx::Postgres | sqlx::Sqlite
    [Model,]     // optional result spec
    SQL,         // string literal
    [params,]    // optional: ( :name = expr, ... )  or  struct_expr
    [(sections),]// optional: ( #name = ..., ... )
    [..batch]    // optional: batch source expression used by {( ... )}
)

Model has three forms:

  • omitted: execute-only query; only .execute(...) is available
  • Type or scalar Type: a single result query
  • ( >key1 = TypeA, >key2 = scalar TypeB ): a grouped multi-result query

The trailing parameter source, section map, and batch source are optional. The batch source may appear alongside the others as a single ..expr argument.

The DB type may be omitted when SQL_FORGE_DB_TYPE is set (e.g. SQL_FORGE_DB_TYPE=sqlx::MySql) or when [package.metadata.sql_forge] db = "..." is set in Cargo.toml. The env var takes priority over Cargo.toml metadata.

§Parameters

Named parameters are written :name in the SQL. At runtime each occurrence is replaced by a push_bind call; at compile time it becomes a database-specific placeholder: ? for MySQL and SQLite, and $1, $2, … for Postgres.

Inline map: bind individual expressions:

sql_forge!(User, "SELECT ... WHERE id <= :max_id", ( :max_id = filter.max_id ))

Struct source: field names are matched to :name placeholders automatically:

sql_forge!(User, "SELECT ... WHERE id <= :max_id LIMIT :limit", filter)

§Sections ({#name})

Sections are runtime SQL slots; each section’s variants are validated at compile time via query_as! / query_scalar!, though not every combination of variants across sections is checked. The section map is a second parenthesised argument starting with #:

sql_forge!(
    User,
    "SELECT * FROM users {#join_org}",
    (
        #join_org = match include_org {
            true  => " JOIN organisations o ON o.id = users.org_id ",
            false => "",
        }
    )
)

A section arm can also carry local parameters as a tuple ("sql", params):

sql_forge!(
    User,
    "SELECT * FROM users {#filter}",
    (
        #filter = (
            " WHERE id <= :max_id AND status = :status ",
            ( :max_id = max_id, :status = "active" ),
        )
    )
)

Multiple placeholders driven by one match use #(a, b) with each arm returning a tuple of the same width:

sql_forge!(
    User,
    "SELECT * FROM users {#join_org} {#filter_org}",
    (
        #(join_org, filter_org) = match include_org {
            true  => (
                " JOIN organisations o ON o.id = users.org_id ",
                (
                    " AND o.active = :active ",
                    ( :active = true ),
                ),
            ),
            false => ("", ""),
        }
    )
)

Grouped section items may themselves use nested match expressions. Those nested matches use smart cycling within the arm rather than a cartesian product. For example, if one grouped arm returns a fixed first item plus two nested binary matches for the second and third items, that arm contributes two aligned variants (0, 0) and (1, 1), not four (0, 0), (0, 1), (1, 0), (1, 1) combinations.

§IN (...) with list parameters

Wrap the placeholder in parentheses to expand a Vec into multiple bound values:

sql_forge!(User, "SELECT * FROM users WHERE id IN (:ids[])", ( :ids = ids ))

Empty lists are not rewritten; IN () is a database syntax error. Guard against empty inputs explicitly, e.g. with a dynamic section:

sql_forge!(
    User,
    "SELECT id, name FROM users WHERE {#filter}",
    (
        #filter = match ids.is_empty() {
            true  => "1 = 0",
            false => ("id IN (:ids[])", ( :ids = ids )),
        }
    )
)

§Batch inserts ({( ... )})

A batch section {( ... )} repeats its content for each item in an iterable source passed as ..expr. Inside the batch, :name refers to a field on the current item. List parameters (:name[]) are not allowed inside batch sections.

§Struct batch

struct BatchItem { name: String, price: i64 }

let items = vec![
    BatchItem { name: "A".into(), price: 100 },
    BatchItem { name: "B".into(), price: 200 },
];

sql_forge!(
    "INSERT INTO products (name, price, stock, category)
     VALUES {(:name, :price, 10, 'Batch')}",
    ..items
)
.execute(&pool)
.await?;

For compile-time checking, the validator expands the batch to 3 fake copies ((?, ?, 10, 'Batch'), (?, ?, 10, 'Batch'), (?, ?, 10, 'Batch')). At runtime the iterable drives the actual number of rows.

§Scalar output

When Model is a primitive (i32, i64, String, etc.) the macro uses query_scalar! for validation and build_query_scalar for execution.

§Multiple results

A result map produces a SqlForgeQueryGroup with one query per key. Each key can be a struct or a primitive (used as a scalar):

sql_forge!(
    (
        >count = i64,
        >items = Item,
    ),
    "SELECT {#fields} FROM items WHERE category_id = :cat",
    ( :cat = category_id ),
    (
        #fields = match {>count} {           // {>key} is true when building
            true  => "COUNT(*) AS total",    // the query for that model/result
            false => "id, name, price",      // key and false otherwise
        }
    )
)

The generated struct has one field per key (group.count, group.items), each implementing SqlForgeQuery<T, Db = DB> and usable with any SQLx executor method (fetch_one, fetch_all, etc.).

§Execute-only (no model)

When the model type is omitted, the macro produces a value implementing SqlForgeQueryExecute. Only .execute(executor) is available and there is no return type to deserialize into. This is useful for INSERT, UPDATE, DELETE, and other DML statements.

sql_forge!(
    "UPDATE products SET stock = stock + 1 WHERE id = :id",
    ( :id = 42i64 ),
)
.execute(&pool)
.await?;

Sections and struct parameter sources work the same way as in model-backed queries:

sql_forge!(
    "UPDATE products SET price = :new_price {#filter}",
    ( #filter = "WHERE category = :cat", ( :cat = "Electronics" ) ),
)
.execute(&pool)
.await?;