qail_core/transpiler/nosql/
mongo.rs

1use crate::ast::*;
2
3pub trait ToMongo {
4    fn to_mongo(&self) -> String;
5}
6
7impl ToMongo for Qail {
8    fn to_mongo(&self) -> String {
9        match self.action {
10            Action::Get => {
11                if !self.joins.is_empty() {
12                    build_aggregate(self)
13                } else {
14                    build_find(self)
15                }
16            }
17            Action::Set => build_update(self),
18            Action::Add => build_insert(self),
19            Action::Put => build_upsert(self),
20            Action::Del => build_delete(self),
21            Action::Make => format!("db.createCollection(\"{}\")", self.table),
22            Action::Drop => format!("db.{}.drop()", self.table),
23            Action::TxnStart => "session.startTransaction()".to_string(),
24            Action::TxnCommit => "session.commitTransaction()".to_string(),
25            Action::TxnRollback => "session.abortTransaction()".to_string(),
26            _ => format!("// Action {:?} not supported for MongoDB yet", self.action),
27        }
28    }
29}
30
31fn build_aggregate(cmd: &Qail) -> String {
32    let mut stages = Vec::new();
33
34    // 1. $match
35    let filter = build_query_filter(cmd);
36    if filter != "{}" {
37        stages.push(format!("{{ \"$match\": {} }}", filter));
38    }
39
40    // 2. $lookup
41    for join in &cmd.joins {
42        let target = &join.table;
43        let source_singular = cmd.table.trim_end_matches('s');
44        let pk = format!("{}_id", source_singular); // users -> user_id
45
46        // from: orders, localField: _id, foreignField: user_id, as: orders
47        let lookup = format!(
48            "{{ \"$lookup\": {{ \"from\": \"{}\", \"localField\": \"_id\", \"foreignField\": \"{}\", \"as\": \"{}\" }} }}",
49            target, pk, target
50        );
51        stages.push(lookup);
52    }
53
54    // 3. $project & Add Fields logic if needed?
55    // For now simple projection if columns exist
56    let proj = build_projection(cmd);
57    if proj != "{}" {
58        stages.push(format!("{{ \"$project\": {} }}", proj));
59    }
60
61    // 4. Sort, Skip, Limit
62    for cage in &cmd.cages {
63        match &cage.kind {
64            CageKind::Sort(order) => {
65                let val = match order {
66                    SortOrder::Asc | SortOrder::AscNullsFirst | SortOrder::AscNullsLast => 1,
67                    SortOrder::Desc | SortOrder::DescNullsFirst | SortOrder::DescNullsLast => -1,
68                };
69                if let Some(cond) = cage.conditions.first() {
70                    let col_str = match &cond.left {
71                        Expr::Named(name) => name.clone(),
72                        expr => expr.to_string(),
73                    };
74                    stages.push(format!("{{ \"$sort\": {{ \"{}\": {} }} }}", col_str, val));
75                }
76            }
77            CageKind::Offset(n) => stages.push(format!("{{ \"$skip\": {} }}", n)),
78            CageKind::Limit(n) => stages.push(format!("{{ \"$limit\": {} }}", n)),
79            _ => {}
80        }
81    }
82
83    format!("db.{}.aggregate([{}])", cmd.table, stages.join(", "))
84}
85
86fn build_find(cmd: &Qail) -> String {
87    let query = build_query_filter(cmd);
88    let projection = build_projection(cmd);
89
90    // Base: db.collection.find(query, projection)
91    let mut mongo = format!("db.{}.find({}, {})", cmd.table, query, projection);
92
93    // Sort, Limit, Skip logic
94    for cage in &cmd.cages {
95        match &cage.kind {
96            CageKind::Limit(n) => mongo.push_str(&format!(".limit({})", n)),
97            CageKind::Offset(n) => mongo.push_str(&format!(".skip({})", n)),
98            CageKind::Sort(order) => {
99                let val = match order {
100                    SortOrder::Asc | SortOrder::AscNullsFirst | SortOrder::AscNullsLast => 1,
101                    SortOrder::Desc | SortOrder::DescNullsFirst | SortOrder::DescNullsLast => -1,
102                };
103                if let Some(cond) = cage.conditions.first() {
104                    let col_str = match &cond.left {
105                        Expr::Named(name) => name.clone(),
106                        expr => expr.to_string(),
107                    };
108                    mongo.push_str(&format!(".sort({{ \"{}\": {} }})", col_str, val));
109                }
110            }
111            _ => {}
112        }
113    }
114
115    mongo
116}
117
118fn build_update(cmd: &Qail) -> String {
119    let query = build_query_filter(cmd);
120    // Payload logic for $set would go here
121    let mut update_doc = String::from("{ $set: { ");
122    let mut first = true;
123
124    for cage in &cmd.cages {
125        // In current parser, [key=val] updates come as Filter cages
126        match cage.kind {
127            CageKind::Payload | CageKind::Filter => {
128                for cond in &cage.conditions {
129                    if !first {
130                        update_doc.push_str(", ");
131                    }
132                    let col_str = match &cond.left {
133                        Expr::Named(name) => name.clone(),
134                        expr => expr.to_string(),
135                    };
136                    update_doc.push_str(&format!(
137                        "\"{}\": {}",
138                        col_str,
139                        value_to_json(&cond.value)
140                    ));
141                    first = false;
142                }
143            }
144            _ => {}
145        }
146    }
147    update_doc.push_str(" } }");
148
149    format!("db.{}.updateMany({}, {})", cmd.table, query, update_doc)
150}
151
152fn build_insert(cmd: &Qail) -> String {
153    let mut doc = String::from("{ ");
154    let mut first = true;
155
156    // Assuming cages contain the payload for insert
157    for cage in &cmd.cages {
158        // In current parser, [key=val] inserts come as Filter cages
159        match cage.kind {
160            CageKind::Payload | CageKind::Filter => {
161                for cond in &cage.conditions {
162                    if !first {
163                        doc.push_str(", ");
164                    }
165                    let col_str = match &cond.left {
166                        Expr::Named(name) => name.clone(),
167                        expr => expr.to_string(),
168                    };
169                    doc.push_str(&format!("\"{}\": {}", col_str, value_to_json(&cond.value)));
170                    first = false;
171                }
172            }
173            _ => {}
174        }
175    }
176    doc.push_str(" }");
177
178    format!("db.{}.insertOne({})", cmd.table, doc)
179}
180
181fn build_upsert(cmd: &Qail) -> String {
182    // Similar to update but with upsert: true
183    let query = build_query_filter(cmd);
184
185    // Payload logic for $set
186    let mut update_doc = String::from("{ $set: { ");
187    let mut first = true;
188
189    for cage in &cmd.cages {
190        match cage.kind {
191            CageKind::Payload | CageKind::Filter => {
192                for cond in &cage.conditions {
193                    if !first {
194                        update_doc.push_str(", ");
195                    }
196                    let col_str = match &cond.left {
197                        Expr::Named(name) => name.clone(),
198                        expr => expr.to_string(),
199                    };
200                    update_doc.push_str(&format!(
201                        "\"{}\": {}",
202                        col_str,
203                        value_to_json(&cond.value)
204                    ));
205                    first = false;
206                }
207            }
208            _ => {}
209        }
210    }
211    update_doc.push_str(" } }");
212
213    format!(
214        "db.{}.updateOne({}, {}, {{ \"upsert\": true }})",
215        cmd.table, query, update_doc
216    )
217}
218
219fn build_delete(cmd: &Qail) -> String {
220    let query = build_query_filter(cmd);
221    format!("db.{}.deleteMany({})", cmd.table, query)
222}
223
224fn build_query_filter(cmd: &Qail) -> String {
225    let mut query_parts = Vec::new();
226
227    for cage in &cmd.cages {
228        if let CageKind::Filter = cage.kind {
229            for cond in &cage.conditions {
230                let op = match cond.op {
231                    Operator::Eq => "$eq",
232                    Operator::Ne => "$ne",
233                    Operator::Gt => "$gt",
234                    Operator::Lt => "$lt",
235                    Operator::Gte => "$gte",
236                    Operator::Lte => "$lte",
237                    _ => "$eq", // Fallback
238                };
239
240                let col_str = match &cond.left {
241                    Expr::Named(name) => name.clone(),
242                    expr => expr.to_string(),
243                };
244
245                // If simple equality, clean syntax { key: val }
246                if let Operator::Eq = cond.op {
247                    query_parts.push(format!("\"{}\": {}", col_str, value_to_json(&cond.value)));
248                } else {
249                    query_parts.push(format!(
250                        "\"{}\": {{ \"{}\": {} }}",
251                        col_str,
252                        op,
253                        value_to_json(&cond.value)
254                    ));
255                }
256            }
257        }
258    }
259
260    if query_parts.is_empty() {
261        return "{}".to_string();
262    }
263
264    format!("{{ {} }}", query_parts.join(", "))
265}
266
267fn build_projection(cmd: &Qail) -> String {
268    if cmd.columns.is_empty() {
269        return "{}".to_string();
270    }
271
272    let mut proj = String::from("{ ");
273    for (i, col) in cmd.columns.iter().enumerate() {
274        if i > 0 {
275            proj.push_str(", ");
276        }
277        if let Expr::Named(name) = col {
278            proj.push_str(&format!("\"{}\": 1", name));
279        }
280    }
281    proj.push_str(" }");
282    proj
283}
284
285fn value_to_json(v: &Value) -> String {
286    match v {
287        Value::String(s) => format!("\"{}\"", s),
288        Value::Int(n) => n.to_string(),
289        Value::Float(n) => n.to_string(),
290        Value::Bool(b) => b.to_string(),
291        Value::Null => "null".to_string(),
292        Value::Param(i) => format!("\"$param{}\"", i),
293        _ => "\"unknown\"".to_string(),
294    }
295}