qail_core/transpiler/nosql/
mongo.rs1use 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 let filter = build_query_filter(cmd);
36 if filter != "{}" {
37 stages.push(format!("{{ \"$match\": {} }}", filter));
38 }
39
40 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); let lookup = format!(
48 "{{ \"$lookup\": {{ \"from\": \"{}\", \"localField\": \"_id\", \"foreignField\": \"{}\", \"as\": \"{}\" }} }}",
49 target, pk, target
50 );
51 stages.push(lookup);
52 }
53
54 let proj = build_projection(cmd);
57 if proj != "{}" {
58 stages.push(format!("{{ \"$project\": {} }}", proj));
59 }
60
61 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 let mut mongo = format!("db.{}.find({}, {})", cmd.table, query, projection);
92
93 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 let mut update_doc = String::from("{ $set: { ");
122 let mut first = true;
123
124 for cage in &cmd.cages {
125 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 for cage in &cmd.cages {
158 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 let query = build_query_filter(cmd);
184
185 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", };
239
240 let col_str = match &cond.left {
241 Expr::Named(name) => name.clone(),
242 expr => expr.to_string(),
243 };
244
245 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}