Light
Dark
System
v3latest
v3latest
v2
v1

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.

Copy
#[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:

Copy
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:

Copy
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(())
    }
}
Light
Dark
System

We use ChatGPT with additional context from our documentation to answer your questions. Not all answers will be accurate. Please join our Discord if you need more help.