--- title: Querying EdgeDB with named parameters in Rust date: 2023-08-26 04:47:43.029524 UTC --- Recently I rebuilt this website, using Rust as backend programming language and [EdgeDB](https://www.edgedb.com/) 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](https://www.edgedb.com/docs/edgeql/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](https://crates.io/crates/edgedb-tokio), the official Rust client library, [documentation](https://www.edgedb.com/docs/clients/rust/arguments) and [example](https://github.com/edgedb/edgedb-rust/blob/master/edgedb-tokio/examples/query_args.rs), you will see that it only supports using positional (numbered) parameters: ```rust let categories: Vec = conn.query("SELECT BlogCategory ORDER BY .title OFFSET $0 LIMIT $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](https://docs.rs/edgedb-tokio/0.5.0/edgedb_tokio/struct.Client.html#method.query), checking the signature of `query()` function: ```rust pub async fn query( &self, query: &str, arguments: &A ) -> Result, Error> where A: QueryArgs, R: QueryResult, ``` I clicked to the link of [`QueryArgs`](https://docs.rs/edgedb-protocol/0.6.0/edgedb_protocol/query_arg/trait.QueryArgs.html#implementors) and saw this at the very last of the page: ```rust impl QueryArgs for Value ``` Well, this must be a hint and open a chance for using named parameters. Follow the [`Value`](https://docs.rs/edgedb-protocol/0.6.0/edgedb_protocol/value/enum.Value.html) type and I found that, is has some variants which can be candidate for holding named parameters: ```rust pub enum Value { // Other variants Object { shape: ObjectShape, fields: Vec> }, SparseObject(SparseObject), NamedTuple { shape: NamedTupleShape, fields: Vec }, // 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: ```rust use edgedb_protocol::value::Value as EValue; use edgedb_protocol::codec::{ShapeElement, ObjectShape}; use edgedb_protocol::common::Cardinality; pub fn edge_object_from_pairs(iter: impl IntoIterator) -> EValue where N: ToString, V: Into>, { let mut elements = Vec::new(); let mut fields: Vec> = 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(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: ```rust 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 = conn.query("SELECT BlogCategory ORDER BY .title OFFSET $offset LIMIT $limit", &args).await?; ``` You can use any map type. Here I use [`IndexMap`](https://crates.io/crates/indexmap) just because I'm from Python and familiar with its `dict` behaviour. Some notes regarding to [`Cardinality`](https://docs.rs/edgedb-protocol/0.6.0/edgedb_protocol/common/enum.Cardinality.html): - If your passed value can be `None` (not fixedly `Some`), use `Cardinality::AtMostOne`, and the "type cast" expression must be ``, e.g. `$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 in `Value::Object.fields` vector must be `None` (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 still `Cardinality::One`, not `Cardinality::Many`, nor `Cardinality::Cardinality`. For example, we have: ```edgeql type BlogPost { multi link categories: BlogCategory; } ``` and we want to update `BlogPost.categories` field with this query: ```edgeql categories := ( SELECT BlogCategory FILTER .id IN array_unpack(>$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.