qail_core/transpiler/nosql/
dynamo.rs1use 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), _ => 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 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 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; 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}