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 `?` for
`query_as!` / `query_scalar!`.
**Inline map** – bind individual expressions:
```rust,ignore
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 key
false => "id, name, price", // 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?;§Caveats
String literals containing :
In this case, the template scanner cannot distinguish
a colon inside a SQL string literal from a :name placeholder. Embedding
"abc:def" directly in the template will fail.
Pass such values as bind parameters instead:
// ❌ sql_forge!(User, r#"WHERE name = "abc:def""#);
// ✅
sql_forge!(User, r#"WHERE name = :name"#, ( :name = "abc:def" ))String literals containing {#
Similarly, a {# sequence inside a
SQL string literal is treated as a section slot. Pass the value as a bind
parameter:
// ❌ sql_forge!(User, r#"WHERE name = "abc{#def""#);
// ✅
sql_forge!(User, r#"WHERE name = :name"#, ( :name = "abc{#def" ))