1use crate::ast::*;
2
3fn json_string(value: &str) -> String {
4 serde_json::to_string(value).unwrap_or_else(|_| "\"\"".to_string())
5}
6
7#[derive(Default)]
8struct DynamoExpression {
9 expression: String,
10 values: String,
11 names: Vec<(String, String)>,
12}
13
14fn attribute_names_json(names: &[(String, String)]) -> String {
15 names
16 .iter()
17 .map(|(placeholder, name)| format!("{}: {}", json_string(placeholder), json_string(name)))
18 .collect::<Vec<_>>()
19 .join(", ")
20}
21
22pub trait ToDynamo {
24 fn to_dynamo(&self) -> String;
26}
27
28impl ToDynamo for Qail {
29 fn to_dynamo(&self) -> String {
30 let result = match self.action {
31 Action::Get => build_get_item(self),
32 Action::Add | Action::Put => build_put_item(self),
33 Action::Set => build_update_item(self),
34 Action::Del => build_delete_item(self),
35 Action::Make => Ok(build_create_table(self)),
36 Action::Drop => Ok(format!("{{ \"TableName\": {} }}", json_string(&self.table))), _ => {
38 return format!(
39 "{{ \"error\": {} }}",
40 json_string(&format!("Action {:?} not supported", self.action))
41 );
42 }
43 };
44
45 result.unwrap_or_else(|err| dynamo_error(&err))
46 }
47}
48
49fn dynamo_error(message: &str) -> String {
50 format!("{{ \"error\": {} }}", json_string(message))
51}
52
53fn build_get_item(cmd: &Qail) -> Result<String, String> {
54 let mut parts = Vec::new();
55 parts.push(format!("\"TableName\": {}", json_string(&cmd.table)));
56
57 let mut filter = build_expression(cmd)?;
58 if !filter.expression.is_empty() {
59 parts.push(format!(
60 "\"FilterExpression\": {}",
61 json_string(&filter.expression)
62 ));
63 parts.push(format!(
64 "\"ExpressionAttributeValues\": {{ {} }}",
65 filter.values
66 ));
67 }
68
69 for cage in &cmd.cages {
70 if let CageKind::Filter = cage.kind {
71 for cond in &cage.conditions {
72 if let Expr::Named(name) = &cond.left {
73 match name.as_str() {
74 "gsi" | "index" => {
75 let index_name = match &cond.value {
76 Value::String(s) => s.clone(),
77 _ => {
78 return Err("DynamoDB index name must be provided as a string"
79 .to_string());
80 }
81 };
82 parts.push(format!("\"IndexName\": {}", json_string(&index_name)));
83 }
84 "consistency" | "consistent" => {
85 if consistent_read_value(&cond.value)? {
86 parts.push("\"ConsistentRead\": true".to_string());
87 } else {
88 parts.push("\"ConsistentRead\": false".to_string());
89 }
90 }
91 _ => {}
92 }
93 }
94 }
95 }
96 }
97
98 if !cmd.columns.is_empty() {
99 let mut cols = Vec::new();
100 for (idx, col) in cmd.columns.iter().enumerate() {
101 if let Expr::Named(n) = col {
102 let placeholder = format!("#p{}", idx + 1);
103 cols.push(placeholder.clone());
104 filter.names.push((placeholder, n.clone()));
105 }
106 }
107 if !cols.is_empty() {
108 parts.push(format!(
109 "\"ProjectionExpression\": {}",
110 json_string(&cols.join(", "))
111 ));
112 }
113 }
114
115 if !filter.names.is_empty() {
116 parts.push(format!(
117 "\"ExpressionAttributeNames\": {{ {} }}",
118 attribute_names_json(&filter.names)
119 ));
120 }
121
122 if let Some(n) = get_limit(cmd) {
123 parts.push(format!("\"Limit\": {}", n))
124 }
125
126 Ok(format!("{{ {} }}", parts.join(", ")))
127}
128
129fn build_put_item(cmd: &Qail) -> Result<String, String> {
130 let mut parts = Vec::new();
131 parts.push(format!("\"TableName\": {}", json_string(&cmd.table)));
132
133 let item = build_item_json(cmd)?;
134 parts.push(format!("\"Item\": {{ {} }}", item));
135
136 Ok(format!("{{ {} }}", parts.join(", ")))
137}
138
139fn build_update_item(cmd: &Qail) -> Result<String, String> {
140 let mut parts = Vec::new();
141 parts.push(format!("\"TableName\": {}", json_string(&cmd.table)));
142
143 let key = build_key_from_filter(cmd)?;
144 parts.push(format!("\"Key\": {{ {} }}", key));
145
146 let update = build_update_expression(cmd)?;
147 parts.push(format!(
148 "\"UpdateExpression\": {}",
149 json_string(&update.expression)
150 ));
151 parts.push(format!(
152 "\"ExpressionAttributeValues\": {{ {} }}",
153 update.values
154 ));
155 if !update.names.is_empty() {
156 parts.push(format!(
157 "\"ExpressionAttributeNames\": {{ {} }}",
158 attribute_names_json(&update.names)
159 ));
160 }
161
162 Ok(format!("{{ {} }}", parts.join(", ")))
163}
164
165fn build_delete_item(cmd: &Qail) -> Result<String, String> {
166 let mut parts = Vec::new();
167 parts.push(format!("\"TableName\": {}", json_string(&cmd.table)));
168
169 let key = build_key_from_filter(cmd)?;
171 parts.push(format!("\"Key\": {{ {} }}", key));
172
173 Ok(format!("{{ {} }}", parts.join(", ")))
174}
175
176fn build_expression(cmd: &Qail) -> Result<DynamoExpression, String> {
177 let mut expr_parts = Vec::new();
178 let mut values_parts = Vec::new();
179 let mut names = Vec::new();
180 let mut counter = 0;
181
182 for cage in &cmd.cages {
183 if let CageKind::Filter = cage.kind {
184 for cond in &cage.conditions {
185 let Expr::Named(name) = &cond.left else {
186 return Err(format!(
187 "DynamoDB filters require named fields, got expression `{}`",
188 cond.left
189 ));
190 };
191
192 if matches!(
193 name.as_str(),
194 "gsi" | "index" | "consistency" | "consistent"
195 ) {
196 continue;
197 }
198
199 counter += 1;
200 let placeholder = format!(":v{}", counter);
201 let name_placeholder = format!("#f{}", counter);
202 let op = match cond.op {
203 Operator::Eq => "=",
204 Operator::Ne => "<>",
205 Operator::Gt => ">",
206 Operator::Lt => "<",
207 Operator::Gte => ">=",
208 Operator::Lte => "<=",
209 _ => {
210 return Err(format!(
211 "unsupported DynamoDB filter operator {:?}",
212 cond.op
213 ));
214 }
215 };
216
217 expr_parts.push(format!("{} {} {}", name_placeholder, op, placeholder));
218 names.push((name_placeholder, name.clone()));
219
220 let val_json = value_to_dynamo(&cond.value)?;
221 values_parts.push(format!("{}: {}", json_string(&placeholder), val_json));
222 }
223 }
224 }
225
226 Ok(DynamoExpression {
227 expression: expr_parts.join(" AND "),
228 values: values_parts.join(", "),
229 names,
230 })
231}
232
233fn build_item_json(cmd: &Qail) -> Result<String, String> {
234 let mut parts = Vec::new();
235 for cage in &cmd.cages {
236 match cage.kind {
237 CageKind::Payload | CageKind::Filter => {
238 for cond in &cage.conditions {
239 let val = value_to_dynamo(&cond.value)?;
240 let Expr::Named(name) = &cond.left else {
241 return Err(format!(
242 "DynamoDB item fields must be named, got expression `{}`",
243 cond.left
244 ));
245 };
246 parts.push(format!("{}: {}", json_string(name), val));
247 }
248 }
249 _ => {}
250 }
251 }
252
253 if parts.is_empty() {
254 return Err("DynamoDB put item requires at least one item field".to_string());
255 }
256
257 Ok(parts.join(", "))
258}
259
260fn build_key_from_filter(cmd: &Qail) -> Result<String, String> {
261 for cage in &cmd.cages {
262 if let CageKind::Filter = cage.kind {
263 for cond in &cage.conditions {
264 let Expr::Named(name) = &cond.left else {
265 return Err(format!(
266 "DynamoDB key fields must be named, got expression `{}`",
267 cond.left
268 ));
269 };
270 if matches!(
271 name.as_str(),
272 "gsi" | "index" | "consistency" | "consistent"
273 ) {
274 continue;
275 }
276 if cond.op != Operator::Eq {
277 return Err("DynamoDB key filters must use equality".to_string());
278 }
279 let val = value_to_dynamo(&cond.value)?;
280 return Ok(format!("{}: {}", json_string(name), val));
281 }
282 }
283 }
284 Err("DynamoDB update/delete requires an equality key filter".to_string())
285}
286
287fn build_update_expression(cmd: &Qail) -> Result<DynamoExpression, String> {
288 let mut sets = Vec::new();
289 let mut vals = Vec::new();
290 let mut names = Vec::new();
291 let mut counter = 100; for cage in &cmd.cages {
294 if let CageKind::Payload = cage.kind {
295 for cond in &cage.conditions {
296 counter += 1;
297 let placeholder = format!(":u{}", counter);
298 let Expr::Named(name) = &cond.left else {
299 return Err(format!(
300 "DynamoDB update fields must be named, got expression `{}`",
301 cond.left
302 ));
303 };
304 let name_placeholder = format!("#u{}", counter);
305 sets.push(format!("{} = {}", name_placeholder, placeholder));
306 names.push((name_placeholder, name.clone()));
307
308 let val = value_to_dynamo(&cond.value)?;
309 vals.push(format!("{}: {}", json_string(&placeholder), val));
310 }
311 }
312 }
313
314 if sets.is_empty() {
315 return Err("DynamoDB update requires at least one payload field".to_string());
316 }
317
318 Ok(DynamoExpression {
319 expression: format!("SET {}", sets.join(", ")),
320 values: vals.join(", "),
321 names,
322 })
323}
324
325fn get_limit(cmd: &Qail) -> Option<usize> {
326 for cage in &cmd.cages {
327 if let CageKind::Limit(n) = cage.kind {
328 return Some(n);
329 }
330 }
331 None
332}
333
334fn build_create_table(cmd: &Qail) -> String {
335 let mut attr_defs = Vec::new();
336 let mut key_schema = Vec::new();
337
338 for col in &cmd.columns {
339 if let Expr::Def {
340 name,
341 data_type,
342 constraints,
343 } = col
344 && constraints.contains(&Constraint::PrimaryKey)
345 {
346 let dtype = match data_type.as_str() {
347 "int" | "i32" | "float" => "N",
348 _ => "S",
349 };
350 attr_defs.push(format!(
351 "{{ \"AttributeName\": {}, \"AttributeType\": {} }}",
352 json_string(name),
353 json_string(dtype)
354 ));
355 key_schema.push(format!(
356 "{{ \"AttributeName\": {}, \"KeyType\": \"HASH\" }}",
357 json_string(name)
358 ));
359 }
360 }
361
362 if key_schema.is_empty() {
363 attr_defs.push("{ \"AttributeName\": \"id\", \"AttributeType\": \"S\" }".to_string());
364 key_schema.push("{ \"AttributeName\": \"id\", \"KeyType\": \"HASH\" }".to_string());
365 }
366
367 format!(
368 "{{ \"TableName\": {}, \"KeySchema\": [{}], \"AttributeDefinitions\": [{}], \"BillingMode\": \"PAY_PER_REQUEST\" }}",
369 json_string(&cmd.table),
370 key_schema.join(", "),
371 attr_defs.join(", ")
372 )
373}
374
375fn consistent_read_value(value: &Value) -> Result<bool, String> {
376 match value {
377 Value::Bool(value) => Ok(*value),
378 Value::String(value) => match value.to_ascii_uppercase().as_str() {
379 "STRONG" | "TRUE" => Ok(true),
380 "EVENTUAL" | "FALSE" => Ok(false),
381 _ => Err("DynamoDB consistency must be STRONG, EVENTUAL, true, or false".to_string()),
382 },
383 other => Err(format!(
384 "DynamoDB consistency must be a bool or string, got {other}"
385 )),
386 }
387}
388
389fn value_to_dynamo(v: &Value) -> Result<String, String> {
390 match v {
391 Value::String(s) => Ok(format!("{{ \"S\": {} }}", json_string(s))),
392 Value::Int(n) => Ok(format!("{{ \"N\": \"{}\" }}", n)),
393 Value::Float(n) if n.is_finite() => Ok(format!("{{ \"N\": \"{}\" }}", n)),
394 Value::Float(_) => {
395 Err("non-finite floats cannot be encoded as DynamoDB numbers".to_string())
396 }
397 Value::Bool(b) => Ok(format!("{{ \"BOOL\": {} }}", b)),
398 Value::Null | Value::NullUuid => Ok("{ \"NULL\": true }".to_string()),
399 Value::Uuid(uuid) => Ok(format!("{{ \"S\": {} }}", json_string(&uuid.to_string()))),
400 Value::Timestamp(ts) => Ok(format!("{{ \"S\": {} }}", json_string(ts))),
401 Value::Array(values) => {
402 let values: Result<Vec<String>, String> = values.iter().map(value_to_dynamo).collect();
403 Ok(format!("{{ \"L\": [{}] }}", values?.join(", ")))
404 }
405 Value::Vector(values) => {
406 let values: Result<Vec<String>, String> = values
407 .iter()
408 .map(|value| {
409 if value.is_finite() {
410 Ok(format!("{{ \"N\": \"{}\" }}", value))
411 } else {
412 Err(
413 "non-finite vector values cannot be encoded as DynamoDB numbers"
414 .to_string(),
415 )
416 }
417 })
418 .collect();
419 Ok(format!("{{ \"L\": [{}] }}", values?.join(", ")))
420 }
421 Value::Json(json) => serde_json::from_str::<serde_json::Value>(json)
422 .map_err(|err| format!("invalid JSON value for DynamoDB attribute: {err}"))
423 .and_then(|value| json_value_to_dynamo(&value)),
424 other => Err(format!("unsupported DynamoDB attribute value: {other}")),
425 }
426}
427
428fn json_value_to_dynamo(value: &serde_json::Value) -> Result<String, String> {
429 match value {
430 serde_json::Value::Null => Ok("{ \"NULL\": true }".to_string()),
431 serde_json::Value::Bool(value) => Ok(format!("{{ \"BOOL\": {} }}", value)),
432 serde_json::Value::Number(value) => {
433 Ok(format!("{{ \"N\": {} }}", json_string(&value.to_string())))
434 }
435 serde_json::Value::String(value) => Ok(format!("{{ \"S\": {} }}", json_string(value))),
436 serde_json::Value::Array(values) => {
437 let values: Result<Vec<String>, String> =
438 values.iter().map(json_value_to_dynamo).collect();
439 Ok(format!("{{ \"L\": [{}] }}", values?.join(", ")))
440 }
441 serde_json::Value::Object(values) => {
442 let values: Result<Vec<String>, String> = values
443 .iter()
444 .map(|(key, value)| {
445 Ok(format!(
446 "{}: {}",
447 json_string(key),
448 json_value_to_dynamo(value)?
449 ))
450 })
451 .collect();
452 Ok(format!("{{ \"M\": {{ {} }} }}", values?.join(", ")))
453 }
454 }
455}