qail_core/transpiler/nosql/
dynamo.rs

1use crate::ast::*;
2
3pub trait ToDynamo {
4    fn to_dynamo(&self) -> String;
5}
6
7impl ToDynamo for Qail {
8    fn to_dynamo(&self) -> String {
9        match self.action {
10            Action::Get => build_get_item(self),
11            Action::Add | Action::Put => build_put_item(self),
12            Action::Set => build_update_item(self),
13            Action::Del => build_delete_item(self),
14            Action::Make => build_create_table(self),
15            Action::Drop => format!("{{ \"TableName\": \"{}\" }}", self.table), // DeleteTable input
16            _ => format!(
17                "{{ \"error\": \"Action {:?} not supported\" }}",
18                self.action
19            ),
20        }
21    }
22}
23
24fn build_get_item(cmd: &Qail) -> String {
25
26    let mut parts = Vec::new();
27    parts.push(format!("\"TableName\": \"{}\"", cmd.table));
28
29    let filter = build_expression(cmd);
30    if !filter.0.is_empty() {
31        parts.push(format!("\"FilterExpression\": \"{}\"", filter.0));
32        parts.push(format!("\"ExpressionAttributeValues\": {{ {} }}", filter.1));
33    }
34
35    for cage in &cmd.cages {
36        if let CageKind::Filter = cage.kind {
37            for cond in &cage.conditions {
38                if let Expr::Named(name) = &cond.left {
39                    match name.as_str() {
40                        "gsi" | "index" => {
41                            let index_name = match &cond.value {
42                                Value::String(s) => s.clone(),
43                                _ => cond.value.to_string().replace("'", ""),
44                            };
45                            parts.push(format!("\"IndexName\": \"{}\"", index_name));
46                        }
47                        "consistency" | "consistent" => {
48                            // STRONG -> true. EVENTUAL -> false.
49                            let val = cond.value.to_string().to_uppercase();
50                            if val.contains("STRONG") || val.contains("TRUE") {
51                                parts.push("\"ConsistentRead\": true".to_string());
52                            } else {
53                                parts.push("\"ConsistentRead\": false".to_string());
54                            }
55                        }
56                        _ => {}
57                    }
58                }
59            }
60        }
61    }
62
63    if !cmd.columns.is_empty() {
64        let cols: Vec<String> = cmd
65            .columns
66            .iter()
67            .map(|c| match c {
68                Expr::Named(n) => n.clone(),
69                _ => "".to_string(),
70            })
71            .collect();
72        parts.push(format!("\"ProjectionExpression\": \"{}\"", cols.join(", ")));
73    }
74
75    if let Some(n) = get_limit(cmd) {
76        parts.push(format!("\"Limit\": {}", n))
77    }
78
79    format!("{{ {} }}", parts.join(", "))
80}
81
82fn build_put_item(cmd: &Qail) -> String {
83    let mut parts = Vec::new();
84    parts.push(format!("\"TableName\": \"{}\"", cmd.table));
85
86    let item = build_item_json(cmd);
87    parts.push(format!("\"Item\": {{ {} }}", item));
88
89    format!("{{ {} }}", parts.join(", "))
90}
91
92fn build_update_item(cmd: &Qail) -> String {
93    let mut parts = Vec::new();
94    parts.push(format!("\"TableName\": \"{}\"", cmd.table));
95
96    let key = build_key_from_filter(cmd);
97    parts.push(format!("\"Key\": {{ {} }}", key));
98
99    let update = build_update_expression(cmd);
100    parts.push(format!("\"UpdateExpression\": \"{}\"", update.0));
101    parts.push(format!("\"ExpressionAttributeValues\": {{ {} }}", update.1));
102
103    format!("{{ {} }}", parts.join(", "))
104}
105
106fn build_delete_item(cmd: &Qail) -> String {
107    let mut parts = Vec::new();
108    parts.push(format!("\"TableName\": \"{}\"", cmd.table));
109
110    // Key logic
111    let key = build_key_from_filter(cmd);
112    parts.push(format!("\"Key\": {{ {} }}", key));
113
114    format!("{{ {} }}", parts.join(", "))
115}
116
117fn build_expression(cmd: &Qail) -> (String, String) {
118    let mut expr_parts = Vec::new();
119    let mut values_parts = Vec::new();
120    let mut counter = 0;
121
122    for cage in &cmd.cages {
123        if let CageKind::Filter = cage.kind {
124            for cond in &cage.conditions {
125                let col_name = match &cond.left {
126                    Expr::Named(name) => name.clone(),
127                    expr => expr.to_string(),
128                };
129
130                if matches!(
131                    col_name.as_str(),
132                    "gsi" | "index" | "consistency" | "consistent"
133                ) {
134                    continue;
135                }
136
137                counter += 1;
138                let placeholder = format!(":v{}", counter);
139                let op = match cond.op {
140                    Operator::Eq => "=",
141                    Operator::Ne => "<>",
142                    Operator::Gt => ">",
143                    Operator::Lt => "<",
144                    Operator::Gte => ">=",
145                    Operator::Lte => "<=",
146                    _ => "=",
147                };
148
149                expr_parts.push(format!("{} {} {}", col_name, op, placeholder));
150
151                let val_json = value_to_dynamo(&cond.value);
152                values_parts.push(format!("\"{}\": {}", placeholder, val_json));
153            }
154        }
155    }
156
157    (expr_parts.join(" AND "), values_parts.join(", "))
158}
159
160fn build_item_json(cmd: &Qail) -> String {
161    let mut parts = Vec::new();
162    for cage in &cmd.cages {
163        match cage.kind {
164            CageKind::Payload | CageKind::Filter => {
165                for cond in &cage.conditions {
166                    let val = value_to_dynamo(&cond.value);
167                    let col_str = match &cond.left {
168                        Expr::Named(name) => name.clone(),
169                        expr => expr.to_string(),
170                    };
171                    parts.push(format!("\"{}\": {}", col_str, val));
172                }
173            }
174            _ => {}
175        }
176    }
177    parts.join(", ")
178}
179
180fn build_key_from_filter(cmd: &Qail) -> String {
181    for cage in &cmd.cages {
182        if let CageKind::Filter = cage.kind
183            && let Some(cond) = cage.conditions.first()
184        {
185            let val = value_to_dynamo(&cond.value);
186            let col_str = match &cond.left {
187                Expr::Named(name) => name.clone(),
188                expr => expr.to_string(),
189            };
190            return format!("\"{}\": {}", col_str, val);
191        }
192    }
193    "\"pk\": { \"S\": \"unknown\" }".to_string()
194}
195
196fn build_update_expression(cmd: &Qail) -> (String, String) {
197    let mut sets = Vec::new();
198    let mut vals = Vec::new();
199    let mut counter = 100; // Offset to avoid collision with filters
200
201    for cage in &cmd.cages {
202        if let CageKind::Payload = cage.kind {
203            for cond in &cage.conditions {
204                counter += 1;
205                let placeholder = format!(":u{}", counter);
206                let col_str = match &cond.left {
207                    Expr::Named(name) => name.clone(),
208                    expr => expr.to_string(),
209                };
210                sets.push(format!("{} = {}", col_str, placeholder));
211
212                let val = value_to_dynamo(&cond.value);
213                vals.push(format!("\"{}\": {}", placeholder, val));
214            }
215        }
216    }
217
218    (format!("SET {}", sets.join(", ")), vals.join(", "))
219}
220
221fn get_limit(cmd: &Qail) -> Option<usize> {
222    for cage in &cmd.cages {
223        if let CageKind::Limit(n) = cage.kind {
224            return Some(n);
225        }
226    }
227    None
228}
229
230fn build_create_table(cmd: &Qail) -> String {
231    let mut attr_defs = Vec::new();
232    let mut key_schema = Vec::new();
233
234    for col in &cmd.columns {
235        if let Expr::Def {
236            name,
237            data_type,
238            constraints,
239        } = col
240            && constraints.contains(&Constraint::PrimaryKey)
241        {
242            let dtype = match data_type.as_str() {
243                "int" | "i32" | "float" => "N",
244                _ => "S",
245            };
246            attr_defs.push(format!(
247                "{{ \"AttributeName\": \"{}\", \"AttributeType\": \"{}\" }}",
248                name, dtype
249            ));
250            key_schema.push(format!(
251                "{{ \"AttributeName\": \"{}\", \"KeyType\": \"HASH\" }}",
252                name
253            ));
254        }
255    }
256
257    if key_schema.is_empty() {
258        attr_defs.push("{ \"AttributeName\": \"id\", \"AttributeType\": \"S\" }".to_string());
259        key_schema.push("{ \"AttributeName\": \"id\", \"KeyType\": \"HASH\" }".to_string());
260    }
261
262    format!(
263        "{{ \"TableName\": \"{}\", \"KeySchema\": [{}], \"AttributeDefinitions\": [{}], \"BillingMode\": \"PAY_PER_REQUEST\" }}",
264        cmd.table,
265        key_schema.join(", "),
266        attr_defs.join(", ")
267    )
268}
269
270fn value_to_dynamo(v: &Value) -> String {
271    match v {
272        Value::String(s) => format!("{{ \"S\": \"{}\" }}", s),
273        Value::Int(n) => format!("{{ \"N\": \"{}\" }}", n),
274        Value::Float(n) => format!("{{ \"N\": \"{}\" }}", n),
275        Value::Bool(b) => format!("{{ \"BOOL\": {} }}", b),
276        Value::Null => "{ \"NULL\": true }".to_string(),
277        _ => "{ \"S\": \"unknown\" }".to_string(),
278    }
279}