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