Followed the introduction of 3 query builders (SQL, MongoDB and SurrealDB query dialects), I still felt that my architecture is still uncertain. While implementing builders, I wasn't sure if I should re-use existing expression engines or build a new one or if I should go for Owned or Lazy expressions. Also I wasn't sure how expression engines would interact with multiple databases.
My latest PR is a step to resolve all those issues, introducing a clean way to implement Expression Engines with the Expressive protocol
(https://github.com/romaninsh/vantage/pull/58)
Implementing custom Expression Engines
My PR contains a "mock" expression engine, but I intend to re-shape OwnedExpression
into a proper implementation of expressive protocol
. It is possible though to create new expression engines relatively easily.
- Implement necessary traits:
impl Expressive<MyExpr> for MyExpr {
fn into_expressive() -> IntoExpressive<MyExpr>;
fn expr(..) -> MyExpr;
}
impl DataSource<MyExpr> for MyDB {
async fn execute(expr) -> Value;
fn defer(expr) -> (async Fn->Value);
}
- Implement or derive Debug trait
- Implement From for IntoExpressive
The standard way for Rust to cast types:
impl<MyExpr> From<Identifier> for IntoExpressive<MyExpr> {
...
}
.. allows Builders to use a wide range of types to to shift weight of implementation into expression engine
Once your expression engine is ready - you can use your Expressive
type. Define my_expr!
macro to simplify your code.
let my_expr = my_expr!("select {}", Identifier::new("foo"));
Nesting Expressions
Expressive protocol requires implementations to allow nested templates and also support closures for deferred values. If a particular database is incapable of using expressions - nested templates can also be converted into deferred values.
Creating and manipulating expressions must always be sync(), but it may carry some closures
to be evaluated later. Obviously if the database can do logic server-side we prefer that.
Here are the examples:
// nesting expressions:
let expr = my_expr!("NOT({})", my_expr!("true OR false"));
// deferred closures:
let expr = my_expr!("{} + 5", async { json!(5) } );
Executing Queries
To execute a query, you need a database driver that implements DataSource<MyExpr>
. Yeah! you can support multiple expression engines inside the same datasource.
let db = DataBase::new(..);
let result = db.execute(expr).await;
Calling execute()
will evaluate deferred closures first, then will run expression and return result.
There is however, one more feature - ability to defer expressions:
let db1 = DataBase::new(..);
let db2 = GeoDB::new(..);
let subquery = my_expr!(
"select country_id from countries where ip={}",
ip
);
let expr = my_expr!(
"select * from data where country = {}",
db2.defer(subquery)
)
let result = db1.execute(expr).await;
In this scenario, we are still working with a single await, however query would be performed across 2 databases. Result of subquery will be automatically inserted into expression which will be executed too.
As you might have noticed - we can even use different implementation of expressive protocol here.
Lazy expressions and snapshots
Previously I implemented OwnedExpression and LazyExpression, but now I can make them interact since they both will be implementing the same protocol.
Builder implementations may decide which expressions engine to use and would still be compatible with everything around them.
Into<> can even allow you to cast between different expression engines if they are compatible or automatically defer if they are not.
Now I can now go back to the SelectQuery builders and polish them next.
Top comments (0)