Recently I rebuilt this website, using Rust as backend programming language and EdgeDB as database. EdgeDB is great, but its Rust support is still weak comparing to other languages. One very important, but missing, feature is to support named parameters in query. Fortunately, there is already a base for the support in the future. In this post, we will exploit that minimum base to let our work done.
Current state
As the time of this writing, if you look into edgedb-tokio, the official Rust client library, documentation and example, you will see that it only supports using positional (numbered) parameters:
let categories: Vec<BlogCategory> = conn.query("SELECT BlogCategory ORDER BY .title OFFSET <int64>$0 LIMIT <int64>$1", &(10, 20)).await?;
That limitation is quite inconvenient, because in real application, which parameters to pass to the query often varies by external input. Like if you are building a page that list products in a store, the users need to be able filter the products by some criteria (brand, price, etc.) and hence, the query to database needs to be dynamic.
You may think about using an IndexMap to hold the to-be-used parameters then deduces the value and the positions from that map. But unfortunately, the arguments
parameter of the query()
function expects a tuple, and in Rust, tuple size must be fixed at compile time, not as in Python, so this solution is not applicable.
Solution
Fortunately, when surfing the reference documentation, checking the signature of query()
function:
pub async fn query<R, A>(
&self,
query: &str,
arguments: &A
) -> Result<Vec<R>, Error>
where
A: QueryArgs,
R: QueryResult,
I clicked to the link of QueryArgs
and saw this at the very last of the page:
impl QueryArgs for Value
Well, this must be a hint and open a chance for using named parameters.
Follow the Value
type and I found that, is has some variants which can be candidate for holding named parameters:
pub enum Value {
// Other variants
Object { shape: ObjectShape, fields: Vec<Option<Value>> },
SparseObject(SparseObject),
NamedTuple { shape: NamedTupleShape, fields: Vec<Value> },
// Other variants
}
After some experiments (facing with the errors and debugging), I finally find out that Value::Object
can be used to hold named parameters and pass to query
. But it is quite verbose to build Value::Object
, due to its complex structure.
The most convenient way is to convert from a map to it. So let's define a function:
use edgedb_protocol::value::Value as EValue;
use edgedb_protocol::codec::{ShapeElement, ObjectShape};
use edgedb_protocol::common::Cardinality;
pub fn edge_object_from_pairs<N, V>(iter: impl IntoIterator<Item = (N, (V, Cardinality))>) -> EValue
where
N: ToString,
V: Into<Option<EValue>>,
{
let mut elements = Vec::new();
let mut fields: Vec<Option<EValue>> = Vec::new();
for (key, (val, cardinality)) in iter.into_iter() {
elements.push(create_shape_element(key, cardinality));
fields.push(val.into());
}
EValue::Object {
shape: ObjectShape::new(elements),
fields,
}
}
pub fn create_shape_element<N: ToString>(name: N, cardinality: Cardinality) -> ShapeElement {
ShapeElement {
name: name.to_string(),
cardinality: Some(cardinality),
flag_link: false,
flag_link_property: false,
flag_implicit: false,
}
}
We can then use the utility as:
use edgedb_protocol::common::Cardinality as Cd;
// offset (i64), limit (i64) are data from external source
let pairs = indexmap! {
offset => (Some(EValue::Int64(offset)), Cd::One),
limit => (Some(EValue::Int64(limit)), Cd::One)
}
let args = edge_object_from_pairs(pairs);
let categories: Vec<BlogCategory> = conn.query("SELECT BlogCategory ORDER BY .title OFFSET <int64>$offset LIMIT <int64>$limit", &args).await?;
You can use any map type. Here I use IndexMap
just because I'm from Python and familiar with its dict
behaviour.
Some notes regarding to Cardinality
:
-
If your passed value can be
None
(not fixedlySome
), useCardinality::AtMostOne
, and the "type cast" expression must be<optional ...>
, e.g.<optional str>$content
. -
If your passed value is fixedly
None
, like when you want to clear the data of some object field in the database, the corresponding element inValue::Object.fields
vector must beNone
(rule for cardinality is the same as above). -
In contrast to what we may think, even if we are setting value for
multi
property, the cardinality for parameter is stillCardinality::One
, notCardinality::Many
, norCardinality::Cardinality
. For example, we have:type BlogPost { multi link categories: BlogCategory; }
and we want to update
BlogPost.categories
field with this query:categories := ( SELECT BlogCategory FILTER .id IN array_unpack(<array<uuid>>$categories) )
we still use
Cardinality::One
for$categories
.
So I have given you a way to fully exploit EdgeDB. Hope your experience with EdgeDB will be good. The conversion function above, I don't extract and publish as a library, because I still hope that EdgeDB authors will provide a more elegant way to build Value::Object
(with proc macro, for example).
Update
This method is no longer fully working with EdgeDB v5. For EdgeDB v5, it is recommended to use HashMap<&str, ValueOpt>
, which was added after this article was written.