Using the Queryable
macro
The easiest way to unpack an EdgeDB query result is the built-in
Queryable
macro from the edgedb-derive
crate. This turns queries
directly into Rust types without having to match on a Value
(more in
the section on the Value
enum), cast to JSON, etc.
#[derive(Debug, Deserialize, Queryable)]
pub struct QueryableAccount {
pub username: String,
pub id: Uuid,
}
let query = "select account {
username,
id
};";
let as_queryable_account: QueryableAccount = client
.query_required_single(query, &())
.await?;
Field order within the shape of the query matters when using the
Queryable
macro. In the example below, we run a query with the order
id, username
instead of username, id
as defined in the struct:
let query = "select account {
id,
username
};";
let wrong_order: Result<QueryableAccount, _> = client
.query_required_single(query, &())
.await;
assert!(
format!("{wrong_order:?}")
.contains(r#"WrongField { unexpected: "id", expected: "username""#);
);
You can use cargo expand with the nightly compiler to see the code
generated by the Queryable
macro, but the minimal example repo also
contains a somewhat cleaned up version of the generated Queryable
code:
use edgedb_protocol::{
descriptors::{Descriptor, TypePos},
errors::DecodeError,
queryable::{Decoder, DescriptorContext, DescriptorMismatch, Queryable},
serialization::decode::DecodeTupleLike,
};
// The code below shows the code generated from the Queryable macro in a
// more readable form (with macro-generated qualified paths replaced with
// use statements).
#[derive(Debug)]
pub struct IsAStruct {
pub name: String,
pub number: i16,
pub is_ok: bool,
}
impl Queryable for IsAStruct {
fn decode(decoder: &Decoder, buf: &[u8]) -> Result<Self, DecodeError> {
let nfields = 3usize
+ if decoder.has_implicit_id { 1 } else { 0 }
+ if decoder.has_implicit_tid { 1 } else { 0 }
+ if decoder.has_implicit_tname { 1 } else { 0 };
let mut elements = DecodeTupleLike::new_object(buf, nfields)?;
if decoder.has_implicit_tid {
elements.skip_element()?;
}
if decoder.has_implicit_tname {
elements.skip_element()?;
}
if decoder.has_implicit_id {
elements.skip_element()?;
}
let name = Queryable::decode_optional(decoder, elements.read()?)?;
let number = Queryable::decode_optional(decoder, elements.read()?)?;
let is_ok = Queryable::decode_optional(decoder, elements.read()?)?;
Ok(IsAStruct {
name,
number,
is_ok,
})
}
fn check_descriptor(
ctx: &DescriptorContext,
type_pos: TypePos,
) -> Result<(), DescriptorMismatch> {
let desc = ctx.get(type_pos)?;
let shape = match desc {
Descriptor::ObjectShape(shape) => shape,
_ => return Err(ctx.wrong_type(desc, "str")),
};
let mut idx = 0;
if ctx.has_implicit_tid {
if !shape.elements[idx].flag_implicit {
return Err(ctx.expected("implicit __tid__"));
}
idx += 1;
}
if ctx.has_implicit_tname {
if !shape.elements[idx].flag_implicit {
return Err(ctx.expected("implicit __tname__"));
}
idx += 1;
}
if ctx.has_implicit_id {
if !shape.elements[idx].flag_implicit {
return Err(ctx.expected("implicit id"));
}
idx += 1;
}
let el = &shape.elements[idx];
if el.name != "name" {
return Err(ctx.wrong_field("name", &el.name));
}
idx += 1;
<String as Queryable>::check_descriptor(ctx, el.type_pos)?;
let el = &shape.elements[idx];
if el.name != "number" {
return Err(ctx.wrong_field("number", &el.name));
}
idx += 1;
<i16 as Queryable>::check_descriptor(ctx, el.type_pos)?;
let el = &shape.elements[idx];
if el.name != "is_ok" {
return Err(ctx.wrong_field("is_ok", &el.name));
}
idx += 1;
<bool as Queryable>::check_descriptor(ctx, el.type_pos)?;
if shape.elements.len() != idx {
return Err(ctx.field_number(shape.elements.len(), idx));
}
Ok(())
}
}