Vantage Expressions
A composable database-agnostic expression framework for Rust that enables building SQL-injection-safe queries using templates and parameters. Provides the foundation for creating advanced query builders that can implement any query language and database flavor. Implementation is well suited for large distributed code-bases and enterprise-level migration / refactoring projects.
Example Use
use expr;
let where_expr = expr!;
let query_expr = expr!;
The [expr!] macro keeps your parameters separate from the query template, preventing SQL injection
while maintaining readability.
For a complete example with testing using mockbuilder, see expression module documentation.
Features
- [
expr!] macro: SQL-injection safe query building with great support for Rust native types - Dynamic query construction: Compose queries conditionally using [
Expression::from_vec()] - Deferred execution: Embed async API calls or Mutexes directly in queries with [
DeferredFn] - Cross-database type mapping: Handle type conversion when query crosses persistence boundaries.
- Custom SQL constructs: Implement [
Expressive] trait for UNION, CTE, or vendor-specific syntax - Standardized SELECT builders: [
Selectable] trait works across SQL, SurrealDB, MongoDB
A design goal for Vantage Expressions is to assist with Enterprise refactoring - providing powerful mechanisms to perform persistence refactoring without breaking model API and affectign existing code.
Expressions are just one part of Vantage framework. I recommend also looking into:
- vantage-surrealdb - Implements SurrealDB client SDK by integrating it into Vantage, providing many
Expressiveconstructs,Selectableimplementation, Precise type system. - vantage-dataset - Although not directly related to Expressions - DataSet crate provides similar abstraction for abstractign CRUD operatons on remote data sources.
- vantage-table - Provides abstract implementation with schema-compliant data-sources. Although
Tabledoes not rely on expressions directly - it makes sense to implement those throughSelectablequery building. - vantage-core - Implements
VantageError,Returnand some other useful things shared across Vantage crates.
Rust Type Support
Expressions can carry any universal type of your choosing. The [expr!] macro defaults to
serde_json::Value for maximum compatibility, but you can use [Expression<T>] with any type that
suits your database. Use CBOR values for binary protocols, SurrealDB's native types for SurrealQL,
or design custom enum-style types optimized for your specific database's type system.
use expr_any;
use Value as SurrealValue;
use Duration;
// Using SurrealDB native types with Duration
let surreal_query = expr_any!;
This approach is different from a "type binding" employed in crates like SQLx, where all parameters must be provided at-once and without abstraction.
Dynamic Query Building
Expressions can be built dynamically at runtime, allowing for flexible query construction based on
your application logic. use [Expression::from_vec] to join multiple expressions with a separator,
preserving order of all parameters stored within.
let mut conditions = Vecnew;
// Conditionally build WHERE conditions
if let Some = min_age
if let Some = status
if active_only
// Combine conditions using from_vec
let where_clause = from_vec;
let final_query = expr!;
If you design a query builder for a custom query language, you want maximum freedom, even allowing your API to accept user-supplied expression.
Type Mapping
Expressions can be converted between compatible types using the mapping functionality. This is
useful when you need to convert Expression<String> to Expression<Value> or between other
compatible types:
use ;
use Value;
// Create expression with String parameters
let string_expr: = new;
// Convert to Expression<Value> using the map() method
let value_expr: = string_expr.map;
Type mapping handles all expression components automatically:
- Scalar values are converted using the
Intotrait - Nested expressions are converted recursively
- Deferred values are wrapped in conversion closures that execute at runtime
This enables seamless interoperability between different expression types while maintaining type safety.
Cross-Database Queries with Type Mapping
Type mapping becomes particularly powerful when combined with deferred queries across databases with incompatible value types:
use ;
// Database 1 uses String values, Database 2 uses JSON Values
let db1 = new;
let db2 = new;
// Create query for db1 and defer its execution
let string_query = expr!;
let deferred_query = db1.defer;
// Map the deferred String query to JSON Value and execute on db2
let result = db2.execute.await;
The deferred query from db1 is automatically converted from Expression<String> to
Expression<Value> when mapped, enabling cross-database operations even when the databases use
incompatible value types.
Type Mapping
If your database engine uses a custom type system (e.g. SurrealType) but under the hood it would use
CBOR it is sufficient for you to implement Into<cborium::Value>. Now any expression defined for
your custom type can be mapped into cborium::Value automatically.
Here is example of mapping [Expression<String>] into [Expression<Value>]:
// Create expression with String parameters
let string_expr: = new;
// Convert to Expression<Value> using the map() method
let value_expr: = string_expr.map;
Ability to map expression values is important when system must operate across different databases and each database could be implementing their own type system.
Immediate vs deferred execution
vantage-expression does not require you to implement database SDK in a certain way, however, by
implementing a trait [ExprDataSource] your SDK would have 2 foundational methods:
async db.execute(expr) -> result- Execute an [Expression<V>] now and returnResult<V>.db.defer(expr) -> DeferredFn- Wrap query execution into a closure which can be executed withfn.call()
// Create a query expression
let query = expr!;
// Immediate execution - execute now and get result
let count: Value = db.execute.await?;
println!;
// Deferred execution - create a closure for later execution
let deferred_query = db.defer;
// Execute the deferred query when needed
let count_later = deferred_query.call.await?;
match count_later
Other kinds of DeferredFn
A sharp-eyeed reader would notice that count_later actually contains [ExpressiveEnum::Scalar].
As it turns out - resolving deferred query can also return nested expressions, once return results
are known. I'll explore the powerful implications of non-scalar return types later.
There are also other ways to obtain [DeferredFn], for instance you can create it from a mutex:
// Shared state that can change over time
let counter = new;
// Create deferred function from mutex - reads current value when executed
let deferred_count = from_mutex;
let query = expr!;
// Change the value after query construction
*counter.lock.unwrap = 25;
// When executed, the query uses the current value (25), not the original (10)
let result = db.execute.await?;
Associated Expressions
While deferred execution provides powerful async capabilities, sometimes you need a middle ground between immediate execution and full deferral.
Associated expressions combine 3 things - Expression, DataSource reference and Expected type. For
example - imagine a method, get_authenticated_users_email. Should it query and return email or
return Expression? Thin can be both now:
use ;
// Get authenticated user's email with type safety
This can now be used to get Email directly or inside expressions. Also - We using a custom type for Email, so we don't loose on type-safety (check vantage_types) for more info on type mapping support.
// Direct execution with type safety
let email: Email = get_authenticated_users_email.get.await?;
println!;
Using as part of other query:
// Use in other queries via composition
let balance_query = expr!;
let balance = db.execute.await?;
Unlike DeferredFn - you will need a proper data source for AssociatedExpression.
Cross-database query-building
The main purpose of deferred queries is to enable cross-database query building. Assume we start with this query:
SELECT * FROM orders WHERE user_id IN (SELECT id FROM user WHERE status = 'active');
Converting this query into Expression is a sync operations. However if during your database refactor
user table migrates to an external API, this would break significant portion of your code as it
would require async API fetch.
Vantage uses deferred queries to solve this problem:
// API call that fetches user IDs asynchronously
async
// Build query synchronously - no async needed here!
let query = expr!;
// Execute the query - API call happens automatically during execution
let orders = db.execute.await?;
The query building remains synchronous even though get_user_ids() is an async API call. The API is
only invoked when the query is executed, maintaining clean separation between query construction and
execution phases.
The deferred SurrealDB query executes automatically when the PostgreSQL query needs the user IDs, enabling seamless cross-database operations. Result is passed into db.execute() as a bind.
Extensibility
Create custom SQL constructs by implementing the [Expressive] trait:
The execute() method handles all deferred operations and resolves them into final values. This
design allows the use of shared state like Arc<Mutex<T>> inside callbacks, enabling dynamic query
parameters that can change between query construction and execution.
/// A UNION SQL construct
// Usage example with nested queries and stored procedure
let users_query = expr!;
let admins_query = expr!;
let union = new;
let final_query = expr!;
Vantage-expressions provides a solid foundation for implementing query builders for any dialect or
database language. Query builders can implement builders like Select or Insert, use composable
types like Table, Aggregation, Sum, and even implement operations like comparison methods
eq() or ne().
Selectable Trait
A most sophisticated construct usually is a SELECT builder. There can be a separate INSERT or
combined builder - that's up to a DB vendor, but a SELECT builder usually is quite common.
The [Selectable] trait provides a standardized interface for building SELECT-style queries across
different database backends. It defines common operations like filtering, sorting, field selection,
and pagination that are universal to most query languages.
use ;
use SurrealSelect;
// Create a new select query builder
let mut select = new;
// Build query using Selectable trait methods
select.add_source;
select.add_field;
select.add_field;
select.add_expression;
select.add_where_condition;
select.add_where_condition;
select.add_order_by;
select.add_group_by;
select.set_distinct;
select.set_limit;
// Convert to expression and execute
let query_expr: Expression = select.into;
let result = db.execute.await?;
The [Selectable] trait also provides fluent builder-style methods for chaining operations:
let query = new
.with_source
.with_field
.with_field
.with_condition
.with_order
.with_limit;
let results = db.execute.await?;
Database-specific implementations like SurrealSelect implement the [Selectable] trait while
providing their own syntax and features. This allows the same query building patterns to work across
SQL, SurrealDB, MongoDB, and other backends while maintaining database-specific optimizations.
Use of [Expression] across Vantage framework
As you have probably noticed - [Selectable] trait makes use of nested expressions quite
deliberatly. Vantage framework treats expressions as a first class citizen and therefore we want to
expose interface which is powerful and extensive.
This extensibility makes Vantage a cohesive framework, suitable for the use in the Enterprise setting.