Overview
The SQL generation system converts filter expressions and ordering specifications into database-specific SQL queries. It supports both PostgreSQL and MySQL dialects with proper parameter binding to prevent SQL injection.
SqlFilterBuilder
Basic SQL Generation
use bomboni_request :: sql :: { SqlFilterBuilder , SqlDialect };
use bomboni_request :: filter :: Filter ;
use bomboni_request :: testing :: schema :: RequestItem ;
let schema = RequestItem :: get_schema ();
let filter = Filter :: parse ( r#"NOT task.deleted AND user.age >= 30"# ) . unwrap ();
// Generate PostgreSQL SQL
let ( sql , args ) = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema )
. build ( & filter )
. unwrap ();
assert_eq! ( sql , r#"NOT ("task"."deleted") AND "user"."age" >= $1"# );
assert_eq! ( args [ 0 ], bomboni_request :: value :: Value :: Integer ( 30 ));
Supported Dialects
use bomboni_request :: sql :: SqlDialect ;
let dialect = SqlDialect :: Postgres ;
// Uses $1, $2, $3... for parameters
// Uses double quotes for identifiers: "table"."column"
Field Mapping with SqlRenameMap
Map schema field names to database column names:
use bomboni_request :: sql :: { SqlFilterBuilder , SqlDialect , SqlRenameMap };
use bomboni_request :: filter :: Filter ;
use bomboni_request :: testing :: schema :: RequestItem ;
use bomboni_macros :: btree_map_into;
let schema = RequestItem :: get_schema ();
let filter = Filter :: parse ( r#"NOT task.deleted AND user.age >= 30"# ) . unwrap ();
let rename_map = SqlRenameMap {
members : btree_map_into! {
"user" => "u" , // Alias user table to 'u'
"task.userId" => "user_id" , // Map camelCase to snake_case
},
functions : std :: collections :: BTreeMap :: new (),
};
let ( sql , args ) = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema )
. set_rename_map ( & rename_map )
. build ( & filter )
. unwrap ();
assert_eq! ( sql , r#"NOT ("task"."deleted") AND "u"."age" >= $1"# );
assert_eq! ( args [ 0 ], bomboni_request :: value :: Value :: Integer ( 30 ));
Rename Map Structure
Maps schema field names to database column/table names. Supports both simple fields and nested paths.
Maps function names to SQL function names (for custom filter functions).
Parameter Binding
All values are properly parameterized to prevent SQL injection:
use bomboni_request :: sql :: { SqlFilterBuilder , SqlDialect };
use bomboni_request :: filter :: Filter ;
use bomboni_request :: testing :: schema :: UserItem ;
let schema = UserItem :: get_schema ();
let filter = Filter :: parse ( r#"displayName = "Alice" AND age >= 18"# ) . unwrap ();
let ( sql , args ) = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema )
. build ( & filter )
. unwrap ();
assert_eq! ( sql , r#""displayName" = $1 AND "age" >= $2"# );
assert_eq! ( args . len (), 2 );
assert_eq! ( args [ 0 ], bomboni_request :: value :: Value :: String ( "Alice" . into ()));
assert_eq! ( args [ 1 ], bomboni_request :: value :: Value :: Integer ( 18 ));
Using Bound Parameters
use sqlx :: PgPool ;
let ( sql , args ) = builder . build ( & filter ) . unwrap ();
let query = sqlx :: query ( & format! ( "SELECT * FROM users WHERE {}" , sql ));
for arg in args {
query = match arg {
Value :: Integer ( i ) => query . bind ( i ),
Value :: String ( s ) => query . bind ( s ),
Value :: Boolean ( b ) => query . bind ( b ),
// ... handle other types
_ => query ,
};
}
let users = query . fetch_all ( & pool ) . await ? ;
SQL Ordering Builder
Convert ordering specifications to SQL ORDER BY clauses:
use bomboni_request :: sql :: { SqlDialect , ordering :: SqlOrderingBuilder };
use bomboni_request :: ordering :: Ordering ;
use bomboni_request :: testing :: schema :: UserItem ;
let schema = UserItem :: get_schema ();
let ordering = Ordering :: parse ( "displayName desc, age asc" ) . unwrap ();
let sql = SqlOrderingBuilder :: new ( SqlDialect :: Postgres , & schema )
. build ( & ordering )
. unwrap ();
assert_eq! ( sql , r#""displayName" DESC, "age" ASC"# );
With Rename Map
use bomboni_request :: sql :: SqlRenameMap ;
use bomboni_macros :: btree_map_into;
let rename_map = SqlRenameMap {
members : btree_map_into! {
"displayName" => "display_name" ,
},
functions : std :: collections :: BTreeMap :: new (),
};
let sql = SqlOrderingBuilder :: new ( SqlDialect :: Postgres , & schema )
. set_rename_map ( & rename_map )
. build ( & ordering )
. unwrap ();
assert_eq! ( sql , r#""display_name" DESC, "age" ASC"# );
Complete SQL Query Example
use bomboni_request :: {
filter :: Filter ,
ordering :: Ordering ,
sql :: { SqlFilterBuilder , SqlDialect , SqlRenameMap , ordering :: SqlOrderingBuilder },
};
use bomboni_macros :: btree_map_into;
// Define schema and rename map
let schema = UserItem :: get_schema ();
let rename_map = SqlRenameMap {
members : btree_map_into! {
"displayName" => "display_name" ,
"userId" => "user_id" ,
},
functions : std :: collections :: BTreeMap :: new (),
};
// Parse filter and ordering
let filter = Filter :: parse ( r#"displayName = "Alice" AND age >= 18"# ) . unwrap ();
let ordering = Ordering :: parse ( "age desc, displayName asc" ) . unwrap ();
// Generate SQL
let ( where_clause , args ) = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema )
. set_rename_map ( & rename_map )
. build ( & filter )
. unwrap ();
let order_clause = SqlOrderingBuilder :: new ( SqlDialect :: Postgres , & schema )
. set_rename_map ( & rename_map )
. build ( & ordering )
. unwrap ();
let sql = format! (
"SELECT * FROM users WHERE {} ORDER BY {} LIMIT $3" ,
where_clause ,
order_clause
);
// sql = SELECT * FROM users WHERE "display_name" = $1 AND "age" >= $2 ORDER BY "age" DESC, "display_name" ASC LIMIT $3
// args = ["Alice", 18]
Advanced Features
Parameter Offset
When combining multiple SQL builders, adjust parameter numbering:
let mut builder = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema );
builder . set_document_offset ( 5 ); // Start parameters at $6
let ( sql , args ) = builder . build ( & filter ) . unwrap ();
// Parameters will be $6, $7, $8... instead of $1, $2, $3...
Case-Insensitive Matching
Enable case-insensitive string comparisons:
let mut builder = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema );
builder . case_insensitive_like ();
let filter = Filter :: parse ( r#"name:"alice""# ) . unwrap ();
let ( sql , args ) = builder . build ( & filter ) . unwrap ();
// Generates: LOWER("name") LIKE LOWER($1)
Custom Argument Style
use bomboni_request :: sql :: SqlArgumentStyle ;
let mut builder = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema );
builder . set_argument_style ( SqlArgumentStyle :: Indexed { prefix : "?" . into () });
// Use ? instead of $1, $2, $3...
Integration with List Queries
Combine SQL generation with pagination:
use bomboni_request :: query :: list :: ListQueryBuilder ;
// Build list query (handles parsing and validation)
let query = list_builder . build (
Some ( 20 ),
page_token . as_deref (),
filter . as_deref (),
order_by . as_deref (),
) . unwrap ();
// Generate SQL
let ( where_clause , args ) = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema )
. set_rename_map ( & rename_map )
. build ( & query . filter)
. unwrap ();
let order_clause = SqlOrderingBuilder :: new ( SqlDialect :: Postgres , & schema )
. set_rename_map ( & rename_map )
. build ( & query . ordering)
. unwrap ();
// Build complete query
let sql = format! (
"SELECT * FROM users WHERE {} ORDER BY {} LIMIT {}" ,
where_clause ,
order_clause ,
query . page_size + 1 // Fetch N+1 to detect next page
);
Type Conversions
The SQL builders handle type conversions automatically:
Value Type PostgreSQL MySQL Integer BIGINTBIGINTFloat DOUBLE PRECISIONDOUBLEString TEXTVARCHARBoolean BOOLEANTINYINT(1)Timestamp TIMESTAMPTIMESTAMP
Error Handling
use bomboni_request :: filter :: error :: FilterError ;
match SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema ) . build ( & filter ) {
Ok (( sql , args )) => {
// Use SQL and args
}
Err ( FilterError :: UnknownMember ( field )) => {
eprintln! ( "Unknown field in filter: {}" , field );
}
Err ( FilterError :: InvalidType { actual , expected }) => {
eprintln! ( "Type mismatch: expected {:?}, got {:?}" , expected , actual );
}
Err ( e ) => {
eprintln! ( "SQL generation error: {:?}" , e );
}
}
Best Practices
Always Use Parameter Binding
Never interpolate values directly into SQL strings. The builders handle this automatically.
Define Rename Maps
Create rename maps to bridge schema field names (often camelCase) with database column names (often snake_case)
Validate Before SQL Generation
Use schema validation before SQL generation to catch errors early
Use Database-Specific Features
Take advantage of dialect-specific optimizations when available
The : (has/contains) operator is not fully supported for SQL generation as different databases handle array/JSON containment differently. Consider implementing custom handling for this operator.
Common Patterns
Table Joins
let rename_map = SqlRenameMap {
members : btree_map_into! {
"user" => "u" ,
"task" => "t" ,
"task.userId" => "t.user_id" ,
"user.displayName" => "u.display_name" ,
},
functions : std :: collections :: BTreeMap :: new (),
};
let filter = Filter :: parse ( r#"user.displayName = "Alice" AND task.deleted = false"# ) . unwrap ();
let ( sql , args ) = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema )
. set_rename_map ( & rename_map )
. build ( & filter )
. unwrap ();
let query = format! (
"SELECT t.* FROM tasks t JOIN users u ON t.user_id = u.id WHERE {}" ,
sql
);
// Use page token to continue from last item
if let Some ( page_token ) = & query . page_token {
// page_token.filter contains continuation condition
let ( cursor_sql , cursor_args ) = SqlFilterBuilder :: new ( SqlDialect :: Postgres , & schema )
. set_rename_map ( & rename_map )
. set_document_offset ( args . len ()) // Continue parameter numbering
. build ( & page_token . filter)
. unwrap ();
let sql = format! (
"SELECT * FROM users WHERE ({}) AND ({}) ORDER BY {} LIMIT {}" ,
where_clause ,
cursor_sql ,
order_clause ,
query . page_size + 1
);
}
Filtering Learn about filter expressions
Ordering Understand ordering specifications
Pagination Implement paginated queries