Announcing sql-forge: A compile-time validated superset for sqlx to build dynamic queries cleanly
⚓ Rust 📅 2026-06-02 👤 surdeus 👁️ 1Hi everyone,
I’m excited to share a project I’ve been working on called sql-forge.
If you use sqlx, you already know how amazing its compile-time validation (query!) is for static queries. However, the moment you need dynamic queries, such as conditionally appending WHERE clauses based on optional filters, you are usually forced to downgrade to runtime-checked strings or fight with manual QueryBuilder state management.
I built sql-forge as a superset for sqlx, not an alternative. It introduces a macro-based DSL that allows you to write dynamic SQL with full compile-time verification without the dreaded combinatorial explosion.
The Secret Sauce: Compile-Time Validation for Dynamic Queries
Normally, validating a dynamic query at compile time would require checking every possible combination of active filters, which quickly explodes the number of database roundtrips during cargo build and the size of the expanded source code.
sql-forge solves this using a cycling strategy under the hood:
- At Compile Time: The macro analyzes your template and conditional
#sections. It combines them into an optimized sequence that exercises the structural variations against your live database viasqlxmetadata checks. This ensures that every conditional fragment and parameter binding is syntactically and structurally valid before your code even runs. - At Runtime: The macro expands into a native
sqlx::QueryBuilderworkflow. It evaluates your actual Rust conditions dynamically at runtime, building and executing the exact query needed for that specific execution path with zero overhead.
Under the hood the macro emits a never-called closure containing one or more sqlx::query_as! / sqlx::query_scalar! invocations. Rather than generating the full cartesian product of all section variants (which would explode for many sections), it uses a smart cycling strategy:
- Let each section have m possible variants (match arms).
- Find n_max, the largest m across all sections.
- Generate exactly n_max validator queries.
- Query i (0-based) uses variant
i % mfor each section.
For example, with two sections having 3 and 10 variants respectively, 10 validator queries are generated, instead of 30; the first section cycles (0, 1, 2, 0, 1, 2, 0, 1, 2, 0) while the second uses each of its 10 variants once. This ensures every variant of the widest section appears in exactly one validator query, and every other section is exercised as many times as its own variant count allows, without combinatorial growth.
Collections, Batching, and Multiple Query Results
Beyond simple conditional filters, sql-forge natively handles complex database patterns while keeping everything compile-time safe:
- Collection Binding: You can pass a standard vector of items directly into placeholders for SQL operations like
IN (:list[]). The macro automatically expands this into the correct number of positional parameters at runtime without losing type safety. - Dynamic Batch Inserts: You can feed a variable number of items into a batch insertion template. The macro handles the repeating structure dynamically while ensuring that the schema types match your database columns during compilation.
- Multiple Results and Query Reuse: You can define a single set of dynamic filters and joins, and reuse them to return multiple distinct result sets. For example, you can fetch both a total
COUNT(*)aggregate for pagination and the actual list of non-aggregated fields using the exact same conditions, ensuring perfect consistency between your counts and your data.
Quick Example
Instead of concatenating strings or manually managing QueryBuilder pushes, you can keep your SQL structure visually intact:
// sql-forge acts directly on top of your sqlx workflow
let query = sql_forge!(
User,
r#"
SELECT * FROM users
WHERE 1=1
{#filter_status}
{#filter_name}
"#,
(
#filter_status = match status {
Some(s) => ("AND status = :status", (:status = s)),
None => "",
},
#filter_name = if let Some(n) = name {
("AND name LIKE :name", (:name = format!("%{}%", n)))
} else {
""
}
)
);
// Compiles down to a checked sqlx::QueryBuilder execution!
let users = query.fetch_all(&pool).await?;
Key Advantages
- Full
sqlxIntegration: Since it compiles down tosqlxtypes under the hood, it fits seamlessly into your existing database stack and supports your custom types. - No Combinatorial Explosion: The cycling compiler strategy guarantees your dynamic paths are checked without multiplying build times or spamming your database during compilation.
- Smart Parameter Binding: Map
:param_namedirectly to Rust expressions safely, eliminating SQL injection vectors while keeping queries highly readable.
Feedback Wanted!
The crate is now live on crates.io.
As a macro-heavy crate using advanced AST parsing, I’d love to get the community's thoughts on:
- The Cycling Strategy: How do you feel about this approach to solving the dynamic compile-time validation problem?
- Syntax Comfort: Does this layout feel clean and maintainable for large-scale enterprise queries?
- Edge Cases: Are there complex SQL structures like deeply nested joins that are not covered?
Check out the repository here: github.com/lucasbasquerotto/sql-forge
Looking forward to hearing your critiques and technical insights!
Background
sql-forge started as an internal tool for a production web platform, where we needed dynamic query construction with the same safety guarantees we already enjoyed from sqlx's static queries. After it proved itself stable and solved our real-world needs without issues, we extracted it, generalized the API, and published it as a standalone crate.
It's been running in production as part of that project for a while now with no problems so far.
Looking forward to hearing your critiques and technical insights!
1 post - 1 participant
🏷️ Rust_feed