edgedb / edgedb-rust

The official Rust binding for EdgeDB
https://edgedb.com
Apache License 2.0
215 stars 25 forks source link

Compile time checked queries #161

Open melkir opened 2 years ago

melkir commented 2 years ago

Would it be possible to support something similar to sqlx?

I think it could be really nice to have this feature when it comes to have a great developer experience 🙂

https://github.com/launchbadge/sqlx#sqlx-is-not-an-orm

tailhook commented 2 years ago

Sure. This is in our to do list. We don't have a timeline yet, though.

imbolc commented 1 year ago

Just to mention it's the only thing preventing me from switching from Postgres :)

xlash123 commented 5 months ago

Adding my +1 for support of this feature. Compile-time guarantees are my biggest reason for using Rust, and I would love to see EdgeDB take full advantage of that. As a newbie to EdgeDB, this would also be extremely helpful for getting me getting used to the query language.

MarioIshac commented 3 months ago

Also adding +1, this feature would provide a great end-to-end experience with the static typing of Rust and EdgeDB.

ifiokjr commented 2 months ago

@melkir @imbolc @tailhook @MarioIshac I've just published a tool I've been using internally this week.

https://github.com/ifiokjr/edgedb_codegen

It's still very early and I'm eager for feedback.

The types are currently generated from a running instance of the edgedb instance using the environment variable EDGEDB_INSTANCE. Once this is in place it will automatically verify the queries written.

Inline Queries

use edgedb_codegen::edgedb_query;
use edgedb_errors::Error;
use edgedb_tokio::create_client;

// Creates a module called `simple` with a function called `query` and structs
// for the `Input` and `Output`.
edgedb_query!(
    simple,
    "select {hello := \"world\", custom := <str>$custom }"
);

#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = create_client().await?;
    let input = simple::Input::builder().custom("custom").build();

    // For queries the following code can be used.
    let output = simple::query(&client, &input).await?;

    Ok(())
}

The macro above generates the following code:

pub mod simple {
    use ::edgedb_codegen::exports as e;
    #[doc = r" Execute the desired query."]
    #[cfg(feature = "query")]
    pub async fn query(
        client: &e::edgedb_tokio::Client,
        props: &Input,
    ) -> core::result::Result<Output, e::edgedb_errors::Error> {
        client.query_required_single(QUERY, props).await
    }
    #[doc = r" Compose the query as part of a larger transaction."]
    #[cfg(feature = "query")]
    pub async fn transaction(
        conn: &mut e::edgedb_tokio::Transaction,
        props: &Input,
    ) -> core::result::Result<Output, e::edgedb_errors::Error> {
        conn.query_required_single(QUERY, props).await
    }
    #[derive(Clone, Debug, e :: typed_builder :: TypedBuilder)]
    #[cfg_attr(feature = "serde", derive(e::serde::Serialize, e::serde::Deserialize))]
    #[cfg_attr(feature = "query", derive(e::edgedb_derive::Queryable))]
    pub struct Input {
        #[builder(setter(into))]
        pub custom: String,
    }
    impl e::edgedb_protocol::query_arg::QueryArgs for Input {
        fn encode(
            &self,
            encoder: &mut e::edgedb_protocol::query_arg::Encoder,
        ) -> core::result::Result<(), e::edgedb_errors::Error> {
            let map = e::edgedb_protocol::named_args! { "custom" => self . custom . clone () , };
            map.encode(encoder)
        }
    }
    #[derive(Clone, Debug, e :: typed_builder :: TypedBuilder)]
    #[cfg_attr(feature = "serde", derive(e::serde::Serialize, e::serde::Deserialize))]
    #[cfg_attr(feature = "query", derive(e::edgedb_derive::Queryable))]
    pub struct Output {
        #[builder(setter(into))]
        pub hello: String,
        #[builder(setter(into))]
        pub custom: String,
    }
    #[doc = r" The original query string provided to the macro. Can be reused in your codebase."]
    pub const QUERY: &str = "select { hello := \"world\", custom := <str>$custom }";
}

Query Files

Define a query file in the queries directory of your crate called select_user.edgeql.

# queries/select_user.edgeql

select User {
  name,
  bio,
  slug,
} filter .slug = <str>$slug;

Then use the edgedb_query macro to import the query.

use edgedb_codegen::edgedb_query;
use edgedb_errors::Error;
use edgedb_tokio::create_client;

// Creates a module called `select_user` with public functions `transaction` and
// `query` as well as structs for the `Input` and `Output`.
edgedb_query!(select_user);

#[tokio::main]
async fn main() -> Result<(), Error> {
    let client = create_client().await?;

    // Generated code can be run inside a transaction.
    let result = client
        .transaction(|mut txn| {
            async move {
                let input = select_user::Input::builder().slug("test").build();
                let output = select_user::transaction(&mut txn, &input).await?;
                Ok(output)
            }
        })
        .await?;

    Ok(())
}