Expressions and Queries
In Vantage, query is a dynamic representation of a SQL query. You already saw how sql::Table
is
creating sql::Query
, now it's time to learn how sql::Query
works.
Query owes it's flexibility to Expressions
or more specifically to a Chunk
trait. Any type
implementing Chunk
can be part of a Query. Expression
is just a simplest implementation of
Chunk
trait.
The main reason for using Expression
is separation of SQL statements and its parameters. Treating
SQL as a string introduces a possibility for SQL injections:
#![allow(unused)] fn main() { let query = format!( "SELECT * FROM product WHERE name = \"{}\"", user_name ); }
What if user_name
contains "
character? Expression is able to handle this:
#![allow(unused)] fn main() { let expression = Expression::new( "SELECT * FROM product WHERE name = {}", vec![json!("DeLorian Doughnut")]); writeln!(expression.preview()); }
Expression holds statement
and parameters
separatelly. Here are some methods of Expression
:
expr!()
- macro for creating new expressionnew()
- constructor, used by the macroExpression::empty()
- an empty expressionsql()
- return SQL statement (without parameters)sql_final()
- returns SQL but will replace {} placeholders with $1, $2 as requested by an underlying SQL access libraryparams()
- return array of parameterspreview()
- will insert parameters into the statement and show it for preview purposes. Do not use for executing queries!split()
- returns statement and parameters as a tuplefrom_vec()
- combines multiple expressions into one using delimeter (like concat_ws)
expr!()
macro
Parameters in expressions can have of several types, like i64 or String or &str:
#![allow(unused)] fn main() { let expression = expr!("INSERT INTO user (name, age) VALUES ({}, {})", "John", 30); }
This macro relies on serde_json::json!
macro to convert parameters to serde_json::Value
.
ExpressionArc
Expression
implements Chunk
trait, however it can only hold static parameters. Sometimes
we want our expressions to be able to hold other Chunk
types. This is where ExpressionArc
comes in:
ExpressionArc
is similar to Expression
but can contain Arc<Box
#![allow(unused)] fn main() { let expression = expr_arc!("INSERT INTO user (name, age) VALUES ({}, {})", "John", 30); }
Now, we can also pass nested expressions to expr_arc!
:
#![allow(unused)] fn main() { let expression = expr_arc!("INSERT INTO {} (name, age) VALUES ({}, {})", expr!("user"), expr!("John"), 30); }
Overview of ExpressionArc methods:
from_vec()
- combines multipleChunk
s into single expression using a delimiter (like concat_ws)fx()
- handy way to create a function call:fx!("UPPER", vec!["Hello"])
Just like Expression
, ExpressionArc
implements Chunk
, so can be nested. This feature is crucial
for building queries.
Query type
A Query
will consists of many parts, each being a Chunk
. When query needs to be rendered, it will
render all of its parts recursively:
#![allow(unused)] fn main() { // example from Query::render_delete() Ok(expr_arc!( format!("DELETE FROM {}{{}}", table), self.where_conditions.render_chunk() ).render_chunk()) }
Obviously you can extend this easily or even have your own version of Query
. Generally it's not needed,
as Query
is very flexible and diverse. It can also hold other queries recursively.
Locate bakery_model/examples/3-query-builder.rs
for an example of a super-complex query syntax.
Query Overview:
Let me establish a pattern first:
set_table()
- sets table for a queryset_source()
- similar toset_table()
but QuerySource can be a table, another query or an expressionwith_table()
- similar toset_table()
but returns a modified Self
As with Table
type earlier - set_table()
adn set_source()
are implemented as part of dyn-safe SqlQuery
trait.
On other hand with_table()
is only implemented by Query
struct.
Here are some other methods:
new()
- returns a blank queryset_distinct()
,with_distinct()
- includesDISTINCT
keyword into a queryset_type()
,with_type()
- sets query type (INSERT, UPDATE, DELETE, SELECT)add_with()
,with_with()
- adds a WITH subquery to a queryadd_field()
,with_field()
- adds a field to a querywith_field_arc()
- acceptsArc<Box<dyn Chunk>>
as a fieldwith_column_field()
- simplified way to add a table columns to a querywithout_fields()
- removes all fields from a querywith_where_condition()
,with_having_condition()
- adds a condition to a query.with_condition()
,with_condition_arc()
- acceptsimpl Chunk
andArc<Box<dyn Chunk>>
as a conditionwith_join()
- adds a join to a querywith_group_by()
,add_group_by()
- adds a group by to a querywith_order_by()
,add_order_by()
- adds an order by to a querywith_set_field()
,set_field_value()
- sets a field value for INSERT, UPDATE or REPLACE queries
Query
relies on several sub-types: QuerySource
, QueryConditions
, JoinQuery
etc.
How Query uses Expressions ?
Lets look at some examples, which combine Expressions and Query:
#![allow(unused)] fn main() { let expr1 = expr!("name = {}", "John"); let expr2 = expr!("age > {}", 30); let query = Query::new() .with_table("users", None) .with_column_field("id") .with_column_field("name") .with_condition(expr1) .with_condition(expr2); writeln!(query.preview()); // renders into: SELECT id, name FROM users WHERE name = 'John' AND age > 30 }
Lets continue to build out and make query
part of a bigger query2
:
#![allow(unused)] fn main() { // query is from example above let query2 = Query::new() .with_table("orders", None) .with_condition(expr_arc!("user_id in {}", query .clone() .without_fields() .with_column_field("id") )); writeln!(query2.preview()); // renders into: SELECT * FROM orders WHERE user_id in (SELECT id FROM users WHERE name = 'John' AND age > 30) }
Summary
Vantage's query system leverages Expressions
and the Chunk
trait to build dynamic, safe, and
composable SQL queries. Expressions
separate SQL statements from parameters, preventing injection
risks, while ExpressionArc
extends this flexibility by supporting nested expressions.
Queries
are constructed from multiple Chunk
s, allowing complex operations like subqueries,
joins, and conditions to be rendered recursively.
Query
methods like with_table
, with_field
, and with_condition
make query building very
simple and customizable, while macros like expr!
and expr_arc!
simplify additional ways to
extend queries.
Next, I'll explain how Expressions
and Query
can be part of Table
field expressions.