edgedb_composable_query/composable/
mod.rs

1//! # edgedb_composable_query::composable
2//!
3//! Beware: it's very much a work-in-progress. Pre-0.1. There're todo!()'s in the code, etc.
4//! I'm still figuring out the semantics.
5//! If you're interested in this working for your use-cases, please
6//! try it and file the issues at: <https://github.com/valyagolev/edgedb-composable-query/issues>.
7//! But don't use it seriously yet; it might panic, and *will* change the API.
8//!
9//! # EdgedbComposableQuery Examples
10//!
11//! If you have this schema:
12//!
13//! ```edgedb
14//! module default {
15//! type Inner {
16//!     required req: str;
17//!     opt: str;
18//! }
19//! type Outer {
20//!     inner: Inner;
21//!
22//!     some_field: str;
23//!     required other_field: str;
24//! }
25//! ```
26//!
27//! Here're some of the ways to use this module:
28//!
29//! ```
30//! # tokio_test::block_on(async {
31//! use edgedb_protocol::model::Uuid;
32//! use edgedb_composable_query::{query, EdgedbObject, Ref};
33//! use edgedb_composable_query::composable::{EdgedbComposableQuery, EdgedbComposableSelector, run_query};
34//!
35//! let conn = edgedb_tokio::create_client().await?;
36//!
37//! // You can define specific queries directly:
38//!
39//! #[derive(Debug, PartialEq, Eq, EdgedbObject,
40//!          EdgedbComposableSelector, EdgedbComposableQuery)]
41//! #[select("select Inner limit 1")]
42//! struct OneInnerQuery {
43//!    req: String,
44//!    opt: Option<String>,
45//! }
46//!
47//! assert!(
48//!     run_query::<OneInnerQuery>(&conn, ()).await.is_ok()
49//! );
50//!
51//! // If you want to compose them, typically it's better to extract the selector:
52//!
53//! #[derive(Debug, PartialEq, Eq, EdgedbObject, EdgedbComposableSelector)]
54//! struct InnerSelector {
55//!   req: String,
56//!   opt: Option<String>,
57//! }
58//!
59//! #[derive(Debug, PartialEq, Eq, EdgedbComposableQuery)]
60//! #[select("select Inner limit 1")]
61//! struct OneInnerBySelector(InnerSelector);
62//!
63//! assert!(
64//!     run_query::<OneInnerBySelector>(&conn, ()).await.is_ok()
65//! );
66//!
67//! #[derive(Debug, PartialEq, Eq, EdgedbComposableQuery)]
68//! #[params(id: Uuid)]
69//! #[select("select Inner filter .id = id")]
70//! struct OneInnerBySelectorById(Option<InnerSelector>);
71//!
72//! assert!(
73//!     run_query::<OneInnerBySelectorById>(&conn, (
74//!         Uuid::parse_str("9be70fb0-8240-11ee-9175-cff95b46d325").unwrap(),
75//!     )).await.unwrap().is_some()
76//! );
77//!
78//! #[derive(Debug, PartialEq, Eq, EdgedbComposableQuery)]
79//! #[select("select Inner limit 10")]
80//! struct ManyInnersBySelector(Vec<InnerSelector>);
81//!
82//! // Queries can have parameters:
83//! // (And remember to use Ref<T>)
84//!
85//! #[derive(Debug, PartialEq, Eq, EdgedbObject,
86//!          EdgedbComposableSelector, EdgedbComposableQuery)]
87//! #[params(id: Uuid)]
88//! #[select("select Outer filter .id = id limit 1")]
89//! struct OuterByIdQuery {
90//!     inner: Option<Ref<InnerSelector>>,
91//!
92//!     some_field: Option<String>,
93//!     other_field: String,
94//! }
95//!
96//! # anyhow::Ok(())
97//! # }).unwrap();
98//! ```
99
100use crate::{EdgedbObject, EdgedbPrim, EdgedbQueryArgs, EdgedbSetValue, Ref};
101
102pub use edgedb_composable_query_derive::{EdgedbComposableQuery, EdgedbComposableSelector};
103use edgedb_tokio::Client;
104use nonempty::NonEmpty;
105
106use crate::Result;
107
108pub enum ComposableQueryResultKind {
109    Field,
110    Selector,
111    FreeObject,
112}
113
114/// Derivable trait. Must have named fields, each is either another selector, or a primitive, or a `Vec/Option/NonEmpty` of those.
115pub trait EdgedbComposableSelector {
116    const RESULT_TYPE: ComposableQueryResultKind;
117
118    /// should't add `{` and `}` around the selector
119    fn format_selector(fmt: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error>;
120
121    fn format_subquery(fmt: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
122        match Self::RESULT_TYPE {
123            ComposableQueryResultKind::Field => {
124                return Ok(());
125            }
126            ComposableQueryResultKind::Selector => fmt.write_str(": {\n")?,
127            ComposableQueryResultKind::FreeObject => fmt.write_str(" := {\n")?,
128        };
129
130        Self::format_selector(fmt)?;
131
132        fmt.write_str("\n}")
133    }
134}
135
136impl<T: EdgedbPrim> EdgedbComposableSelector for T {
137    const RESULT_TYPE: ComposableQueryResultKind = ComposableQueryResultKind::Field;
138
139    fn format_selector(fmt: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
140        Ok(())
141    }
142
143    fn format_subquery(fmt: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
144        Ok(())
145    }
146}
147
148impl<T: EdgedbComposableSelector> EdgedbComposableSelector for Vec<T> {
149    const RESULT_TYPE: ComposableQueryResultKind = T::RESULT_TYPE;
150
151    fn format_selector(fmt: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
152        T::format_selector(fmt)
153    }
154}
155
156impl<T: EdgedbComposableSelector> EdgedbComposableSelector for Option<T> {
157    const RESULT_TYPE: ComposableQueryResultKind = T::RESULT_TYPE;
158
159    fn format_selector(fmt: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
160        T::format_selector(fmt)
161    }
162}
163
164impl<T: EdgedbComposableSelector> EdgedbComposableSelector for NonEmpty<T> {
165    const RESULT_TYPE: ComposableQueryResultKind = T::RESULT_TYPE;
166
167    fn format_selector(fmt: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
168        T::format_selector(fmt)
169    }
170}
171
172impl<T: EdgedbComposableSelector + EdgedbObject> EdgedbComposableSelector for Ref<T> {
173    const RESULT_TYPE: ComposableQueryResultKind = ComposableQueryResultKind::Selector;
174
175    fn format_selector(fmt: &mut impl std::fmt::Write) -> Result<(), std::fmt::Error> {
176        fmt.write_str("\tid,\n")?;
177
178        T::format_selector(fmt)?;
179
180        Ok(())
181    }
182}
183
184/// Derivable trait. Can have parameters. Either an object with named fields, or can be a wrapper around a selector, or `Option<selector>`, or `Vec<selector>`, or `NonEmpty<selector>``.
185pub trait EdgedbComposableQuery {
186    const ARG_NAMES: &'static [&'static str];
187
188    type ArgTypes: EdgedbQueryArgs;
189    type ReturnType: EdgedbSetValue;
190
191    fn format_query(
192        fmt: &mut impl std::fmt::Write,
193        args: &::std::collections::HashMap<&str, String>,
194    ) -> Result<(), std::fmt::Error>;
195
196    fn query() -> String {
197        let mut buf = String::new();
198        // let args = (0..Self::ARG_NAMES.len())
199        //     .map(|i| (format!("${}", i)))
200        //     .collect();
201
202        let args = Self::ARG_NAMES
203            .iter()
204            .enumerate()
205            .map(|(i, n)| (*n, format!("${i}")))
206            .collect();
207
208        Self::format_query(&mut buf, &args).unwrap();
209        buf
210    }
211}
212
213/// use this to run an [`EdgedbComposableQuery`]
214pub async fn run_query<T: EdgedbComposableQuery>(
215    client: &Client,
216    args: T::ArgTypes,
217) -> Result<T::ReturnType>
218where
219    <T as EdgedbComposableQuery>::ArgTypes: Send,
220{
221    let query_s = T::query();
222
223    crate::query(client, &query_s, args).await
224}
225
226#[cfg(test)]
227mod test {
228    use edgedb_protocol::model::Uuid;
229
230    use crate::composable::EdgedbComposableQuery;
231    use crate::composable::EdgedbComposableSelector;
232    use crate::{EdgedbObject, Ref};
233
234    #[derive(
235        Debug, PartialEq, Eq, EdgedbObject, EdgedbComposableSelector, EdgedbComposableQuery,
236    )]
237    #[select("select Inner limit 1")]
238    struct InnerQuery {
239        req: String,
240        opt: Option<String>,
241    }
242
243    #[derive(Debug, PartialEq, Eq, EdgedbObject, EdgedbComposableSelector)]
244    struct InnerSelector {
245        req: String,
246        opt: Option<String>,
247    }
248
249    #[derive(Debug, PartialEq, Eq, EdgedbComposableQuery)]
250    #[select("select Inner limit 1")]
251    struct OneInnerBySelector(InnerSelector);
252
253    #[derive(Debug, PartialEq, Eq, EdgedbComposableQuery)]
254    #[params(id: Uuid)]
255    #[select("select Inner filter .id = id")]
256    struct OneInnerBySelectorById(InnerSelector);
257
258    #[derive(Debug, PartialEq, Eq, EdgedbComposableQuery)]
259    #[select("select Inner limit 10")]
260    struct ManyInnersBySelector(Vec<InnerSelector>);
261
262    #[derive(
263        Debug, PartialEq, Eq, EdgedbObject, EdgedbComposableSelector, EdgedbComposableQuery,
264    )]
265    #[select("select Outer limit 1")]
266    struct OuterQuery {
267        inner: Option<InnerSelector>,
268
269        some_field: Option<String>,
270        other_field: String,
271    }
272
273    #[derive(
274        Debug, PartialEq, Eq, EdgedbObject, EdgedbComposableSelector, EdgedbComposableQuery,
275    )]
276    #[select("select Outer limit 1")]
277    struct OuterQueryWithRef {
278        inner: Option<Ref<InnerSelector>>,
279
280        some_field: Option<String>,
281        other_field: String,
282    }
283
284    #[test]
285    fn selector_tests() {
286        // let mut buf = String::new();
287        // InnerQuery::format_selector(&mut buf).unwrap();
288        // insta::assert_snapshot!(buf);
289
290        // let mut buf = String::new();
291        // OuterQuery::format_selector(&mut buf).unwrap();
292        // insta::assert_snapshot!(buf);
293
294        // let mut buf = String::new();
295        // OuterQueryWithRef::format_selector(&mut buf).unwrap();
296        // insta::assert_snapshot!(buf);
297
298        let mut buf = String::new();
299        InnerSelector::format_selector(&mut buf).unwrap();
300        insta::assert_snapshot!(buf);
301
302        let mut buf = String::new();
303        Option::<InnerSelector>::format_selector(&mut buf).unwrap();
304        insta::assert_snapshot!(buf);
305    }
306
307    #[test]
308    fn query_tests() {
309        insta::assert_snapshot!(InnerQuery::query());
310        insta::assert_snapshot!(OuterQuery::query());
311        insta::assert_snapshot!(OuterQueryWithRef::query());
312
313        insta::assert_snapshot!(OneInnerBySelector::query());
314        insta::assert_snapshot!(OneInnerBySelectorById::query());
315        insta::assert_snapshot!(ManyInnersBySelector::query());
316    }
317}