Skip to main content

mongreldb_kit_core/
query.rs

1//! Query AST for the language-neutral Kit model.
2//!
3//! This module defines expression and statement trees. Execution is left to
4//! the storage-backed `mongreldb-kit` crate; `mongreldb-kit-core` only
5//! validates structure and provides serialization.
6
7use serde::{Deserialize, Serialize};
8
9/// A scalar literal.
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum Literal {
13    Null,
14    Bool(bool),
15    Int(i64),
16    Float(f64),
17    Text(String),
18    Json(serde_json::Value),
19}
20
21/// A query expression.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum Expr {
25    /// Reference to a table column.
26    Column(String),
27    /// A constant value.
28    Literal(Literal),
29    /// Logical conjunction.
30    And(Vec<Expr>),
31    /// Logical disjunction.
32    Or(Vec<Expr>),
33    /// Logical negation.
34    Not(Box<Expr>),
35    /// Equality.
36    Eq(Box<Expr>, Box<Expr>),
37    /// Inequality.
38    Ne(Box<Expr>, Box<Expr>),
39    /// Greater than.
40    Gt(Box<Expr>, Box<Expr>),
41    /// Greater than or equal.
42    Gte(Box<Expr>, Box<Expr>),
43    /// Less than.
44    Lt(Box<Expr>, Box<Expr>),
45    /// Less than or equal.
46    Lte(Box<Expr>, Box<Expr>),
47    /// Membership in a list.
48    In(Box<Expr>, Vec<Literal>),
49    /// Non-membership in a list.
50    NotIn(Box<Expr>, Vec<Literal>),
51    /// IS NULL test.
52    IsNull(Box<Expr>),
53    /// IS NOT NULL test.
54    IsNotNull(Box<Expr>),
55    /// SQL `LIKE` pattern match (`%`/`_` wildcards).
56    Like(Box<Expr>, String),
57    /// Case-sensitive substring containment. Equivalent to `LIKE '%needle%'`
58    /// but without treating `%`/`_` in the needle as wildcards.
59    Contains(Box<Expr>, String),
60    /// Membership in the rows produced by a sub-`SELECT` (its first projected
61    /// column). The subquery is evaluated against the same execution context, so
62    /// it may read other tables or materialized CTEs.
63    InSubquery(Box<Expr>, Box<Select>),
64    /// `EXISTS (subquery)` — true when the subquery yields at least one row.
65    Exists(Box<Select>),
66    /// `NOT EXISTS (subquery)` — true when the subquery yields no rows.
67    NotExists(Box<Select>),
68}
69
70/// Sort direction for `ORDER BY`.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
72#[serde(rename_all = "snake_case")]
73pub enum Direction {
74    #[default]
75    Asc,
76    Desc,
77}
78
79/// An `ORDER BY` clause.
80#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
81pub struct OrderBy {
82    pub expr: Expr,
83    pub direction: Direction,
84}
85
86/// A `SELECT` statement.
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
88pub struct Select {
89    pub table: String,
90    pub columns: Vec<Expr>,
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub filter: Option<Expr>,
93    #[serde(default, skip_serializing_if = "Vec::is_empty")]
94    pub order_by: Vec<OrderBy>,
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub limit: Option<usize>,
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub offset: Option<usize>,
99}
100
101/// An `INSERT` statement.
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103pub struct Insert {
104    pub table: String,
105    pub values: serde_json::Map<String, serde_json::Value>,
106    #[serde(default, skip_serializing_if = "Vec::is_empty")]
107    pub returning: Vec<String>,
108}
109
110/// An `UPDATE` statement.
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
112pub struct Update {
113    pub table: String,
114    pub set: serde_json::Map<String, serde_json::Value>,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub filter: Option<Expr>,
117    #[serde(default, skip_serializing_if = "Vec::is_empty")]
118    pub returning: Vec<String>,
119    /// Optional single-row target: the primary key value to update.
120    /// Mutually exclusive with `filter`.
121    #[serde(default, skip_serializing_if = "Option::is_none")]
122    pub pk: Option<serde_json::Value>,
123}
124
125/// A `DELETE` statement.
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127pub struct Delete {
128    pub table: String,
129    #[serde(default, skip_serializing_if = "Option::is_none")]
130    pub filter: Option<Expr>,
131    #[serde(default, skip_serializing_if = "Vec::is_empty")]
132    pub returning: Vec<String>,
133    /// Optional single-row target: the primary key value to delete.
134    /// Mutually exclusive with `filter`.
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub pk: Option<serde_json::Value>,
137}
138
139/// An `UPSERT` statement.
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141pub struct Upsert {
142    pub table: String,
143    pub values: serde_json::Map<String, serde_json::Value>,
144    pub on_conflict: OnConflict,
145    #[serde(default, skip_serializing_if = "Vec::is_empty")]
146    pub returning: Vec<String>,
147}
148
149/// SQL-style conflict behavior for `UPSERT`.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151#[serde(rename_all = "snake_case")]
152pub enum OnConflict {
153    DoNothing,
154    DoUpdate(serde_json::Map<String, serde_json::Value>),
155}
156
157/// Top-level query statement.
158#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
159#[serde(rename_all = "snake_case")]
160pub enum Query {
161    Select(Select),
162    Insert(Insert),
163    Update(Update),
164    Delete(Delete),
165    Upsert(Upsert),
166    Aggregate(AggregateQuery),
167    Join(JoinQuery),
168}
169
170/// An aggregate function.
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "snake_case")]
173pub enum AggFunc {
174    Count,
175    Sum,
176    Min,
177    Max,
178    Avg,
179}
180
181/// A single aggregate output column, e.g. `SUM(total) AS total_sum`.
182#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
183pub struct Aggregate {
184    pub func: AggFunc,
185    /// Source column. `None` means `COUNT(*)` (only valid for `Count`).
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub column: Option<String>,
188    /// Output column name in each result row.
189    pub alias: String,
190    /// `DISTINCT` modifier, e.g. `COUNT(DISTINCT col)`. Requires a `column`; it
191    /// is a no-op for `MIN`/`MAX`. Defaults to `false` so existing serialized
192    /// queries deserialize unchanged.
193    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
194    pub distinct: bool,
195}
196
197/// A grouped/aggregated query. Rows are scanned, optionally filtered, grouped by
198/// `group_by` key columns, reduced with `aggregates`, and finally filtered by
199/// `having`. With no `group_by`, the whole (filtered) table is one group.
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct AggregateQuery {
202    pub table: String,
203    #[serde(default, skip_serializing_if = "Option::is_none")]
204    pub filter: Option<Expr>,
205    #[serde(default, skip_serializing_if = "Vec::is_empty")]
206    pub group_by: Vec<String>,
207    pub aggregates: Vec<Aggregate>,
208    /// Predicate applied to each produced group row. Column references resolve to
209    /// the group key columns or aggregate aliases.
210    #[serde(default, skip_serializing_if = "Option::is_none")]
211    pub having: Option<Expr>,
212}
213
214/// The kind of join to perform.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
216#[serde(rename_all = "snake_case")]
217pub enum JoinKind {
218    #[default]
219    Inner,
220    Left,
221    Cross,
222}
223
224/// One joined table in a [`JoinQuery`].
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226pub struct Join {
227    #[serde(default)]
228    pub kind: JoinKind,
229    pub table: String,
230    /// Optional alias; defaults to the table name when omitted.
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub alias: Option<String>,
233    /// Join predicate. Column references must be qualified (`alias.column`).
234    /// Ignored (and may be omitted) for `Cross` joins.
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub on: Option<Expr>,
237}
238
239/// A nested-loop join query. The result is a list of combined rows; each combined
240/// row is a JSON object keyed by table alias whose values are that source's row
241/// object (or JSON `null` for an unmatched right side of a `LEFT` join). Column
242/// references in `filter`/`order_by`/join `on` must be qualified (`alias.column`).
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
244pub struct JoinQuery {
245    pub table: String,
246    #[serde(default, skip_serializing_if = "Option::is_none")]
247    pub alias: Option<String>,
248    #[serde(default, skip_serializing_if = "Vec::is_empty")]
249    pub joins: Vec<Join>,
250    #[serde(default, skip_serializing_if = "Option::is_none")]
251    pub filter: Option<Expr>,
252    #[serde(default, skip_serializing_if = "Vec::is_empty")]
253    pub order_by: Vec<OrderBy>,
254    #[serde(default, skip_serializing_if = "Option::is_none")]
255    pub limit: Option<usize>,
256    #[serde(default, skip_serializing_if = "Option::is_none")]
257    pub offset: Option<usize>,
258}
259
260/// A common table expression: a named, materialized subquery. A later query can
261/// read from `name` as if it were a table.
262#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
263pub struct Cte {
264    pub name: String,
265    pub query: Box<Select>,
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    fn select_serializes_to_expected_shape() {
274        let q = Query::Select(Select {
275            table: "users".into(),
276            columns: vec![Expr::Column("id".into()), Expr::Column("email".into())],
277            filter: Some(Expr::Eq(
278                Box::new(Expr::Column("id".into())),
279                Box::new(Expr::Literal(Literal::Int(1))),
280            )),
281            order_by: vec![OrderBy {
282                expr: Expr::Column("id".into()),
283                direction: Direction::Desc,
284            }],
285            limit: Some(10),
286            offset: Some(5),
287        });
288
289        let json = serde_json::to_value(&q).unwrap();
290        assert_eq!(json["select"]["table"], "users");
291        assert_eq!(json["select"]["columns"][0]["column"], "id");
292        assert_eq!(json["select"]["filter"]["eq"][0]["column"], "id");
293        assert_eq!(json["select"]["filter"]["eq"][1]["literal"]["int"], 1);
294        assert_eq!(json["select"]["order_by"][0]["direction"], "desc");
295        assert_eq!(json["select"]["limit"], 10);
296    }
297
298    #[test]
299    fn query_roundtrips() {
300        let q = Query::Select(Select {
301            table: "t".into(),
302            columns: vec![Expr::Column("x".into())],
303            filter: Some(Expr::And(vec![
304                Expr::Not(Box::new(Expr::IsNull(Box::new(Expr::Column("y".into()))))),
305                Expr::Like(Box::new(Expr::Column("z".into())), "%test%".into()),
306            ])),
307            order_by: vec![],
308            limit: None,
309            offset: None,
310        });
311        let json = serde_json::to_string(&q).unwrap();
312        let back: Query = serde_json::from_str(&json).unwrap();
313        assert_eq!(q, back);
314    }
315
316    #[test]
317    fn extended_exprs_and_queries_roundtrip() {
318        let sub = Select {
319            table: "orders".into(),
320            columns: vec![Expr::Column("user_id".into())],
321            filter: Some(Expr::Gt(
322                Box::new(Expr::Column("total".into())),
323                Box::new(Expr::Literal(Literal::Int(100))),
324            )),
325            order_by: vec![],
326            limit: None,
327            offset: None,
328        };
329        let select = Select {
330            table: "users".into(),
331            columns: vec![],
332            filter: Some(Expr::And(vec![
333                Expr::Contains(Box::new(Expr::Column("email".into())), "@x".into()),
334                Expr::InSubquery(Box::new(Expr::Column("id".into())), Box::new(sub.clone())),
335                Expr::Exists(Box::new(sub.clone())),
336                Expr::Not(Box::new(Expr::NotExists(Box::new(sub.clone())))),
337            ])),
338            order_by: vec![],
339            limit: None,
340            offset: None,
341        };
342        let q = Query::Select(select);
343        let back: Query = serde_json::from_str(&serde_json::to_string(&q).unwrap()).unwrap();
344        assert_eq!(q, back);
345
346        let agg = Query::Aggregate(AggregateQuery {
347            table: "orders".into(),
348            filter: None,
349            group_by: vec!["user_id".into()],
350            aggregates: vec![
351                Aggregate {
352                    func: AggFunc::Count,
353                    column: None,
354                    alias: "n".into(),
355                    distinct: false,
356                },
357                Aggregate {
358                    func: AggFunc::Sum,
359                    column: Some("total".into()),
360                    alias: "total_sum".into(),
361                    distinct: false,
362                },
363            ],
364            having: Some(Expr::Gt(
365                Box::new(Expr::Column("n".into())),
366                Box::new(Expr::Literal(Literal::Int(1))),
367            )),
368        });
369        let back: Query = serde_json::from_str(&serde_json::to_string(&agg).unwrap()).unwrap();
370        assert_eq!(agg, back);
371
372        let join = Query::Join(JoinQuery {
373            table: "users".into(),
374            alias: None,
375            joins: vec![Join {
376                kind: JoinKind::Left,
377                table: "orders".into(),
378                alias: None,
379                on: Some(Expr::Eq(
380                    Box::new(Expr::Column("users.id".into())),
381                    Box::new(Expr::Column("orders.user_id".into())),
382                )),
383            }],
384            filter: None,
385            order_by: vec![],
386            limit: None,
387            offset: None,
388        });
389        let back: Query = serde_json::from_str(&serde_json::to_string(&join).unwrap()).unwrap();
390        assert_eq!(join, back);
391
392        let cte = Cte {
393            name: "big".into(),
394            query: Box::new(sub),
395        };
396        let back: Cte = serde_json::from_str(&serde_json::to_string(&cte).unwrap()).unwrap();
397        assert_eq!(cte, back);
398    }
399
400    #[test]
401    fn insert_and_delete_roundtrip() {
402        let mut values = serde_json::Map::new();
403        values.insert("name".into(), serde_json::Value::String("alice".into()));
404
405        let q = Query::Insert(Insert {
406            table: "users".into(),
407            values,
408            returning: vec![],
409        });
410        let back: Query = serde_json::from_str(&serde_json::to_string(&q).unwrap()).unwrap();
411        assert_eq!(q, back);
412
413        let d = Query::Delete(Delete {
414            table: "users".into(),
415            filter: Some(Expr::In(
416                Box::new(Expr::Column("id".into())),
417                vec![Literal::Int(1), Literal::Int(2)],
418            )),
419            returning: vec![],
420            pk: None,
421        });
422        let back: Query = serde_json::from_str(&serde_json::to_string(&d).unwrap()).unwrap();
423        assert_eq!(d, back);
424    }
425}