1use std::collections::HashMap;
4use std::sync::Arc;
5use std::sync::Mutex as StdMutex;
6
7use async_trait::async_trait;
8use http::{Method, StatusCode};
9use serde_json::{json, Map, Value};
10use tokio::sync::Mutex;
11use tokio::time::{Duration, Instant};
12
13use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
14use fakecloud_rds::SharedRdsState;
15
16const SUPPORTED_ACTIONS: &[&str] = &[
17 "ExecuteStatement",
18 "BatchExecuteStatement",
19 "BeginTransaction",
20 "CommitTransaction",
21 "RollbackTransaction",
22 "ExecuteSql",
23];
24
25const TXN_IDLE_TIMEOUT: Duration = Duration::from_secs(180);
31
32struct DbConn {
35 engine: String,
36 host: String,
37 port: u16,
38 user: String,
39 password: String,
40 db_name: String,
41 schema: Option<String>,
44}
45
46enum HeldConn {
50 Pg(tokio_postgres::Client),
51 MySql(mysql_async::Conn),
52}
53
54struct TxnEntry {
59 conn: Mutex<Option<HeldConn>>,
61 last_used: StdMutex<Instant>,
62}
63
64impl TxnEntry {
65 fn touch(&self) {
66 if let Ok(mut t) = self.last_used.lock() {
67 *t = Instant::now();
68 }
69 }
70}
71
72type TxnMap = Arc<Mutex<HashMap<String, Arc<TxnEntry>>>>;
73
74pub struct RdsDataService {
75 rds_state: SharedRdsState,
76 transactions: TxnMap,
79}
80
81impl RdsDataService {
82 pub fn new(rds_state: SharedRdsState) -> Self {
83 let transactions: TxnMap = Arc::new(Mutex::new(HashMap::new()));
84 if let Ok(handle) = tokio::runtime::Handle::try_current() {
88 handle.spawn(reap_idle_transactions(transactions.clone()));
89 }
90 Self {
91 rds_state,
92 transactions,
93 }
94 }
95
96 fn resolve_action(req: &AwsRequest) -> Option<&'static str> {
98 if req.method != Method::POST {
99 return None;
100 }
101 match req.path_segments.first().map(String::as_str)? {
102 "Execute" => Some("ExecuteStatement"),
103 "BatchExecute" => Some("BatchExecuteStatement"),
104 "BeginTransaction" => Some("BeginTransaction"),
105 "CommitTransaction" => Some("CommitTransaction"),
106 "RollbackTransaction" => Some("RollbackTransaction"),
107 "ExecuteSql" => Some("ExecuteSql"),
108 _ => None,
109 }
110 }
111
112 fn resolve_conn(&self, account_id: &str, resource_arn: &str) -> Option<DbConn> {
116 let cluster_id = resource_arn
117 .rsplit_once(":cluster:")
118 .map(|(_, id)| id.to_string());
119 let accounts = self.rds_state.read();
120 let state = accounts.get(account_id)?;
121 let inst = state.instances.values().find(|i| {
122 i.db_instance_arn == resource_arn
123 || cluster_id
124 .as_deref()
125 .is_some_and(|c| i.db_cluster_identifier.as_deref() == Some(c))
126 })?;
127 Some(DbConn {
128 engine: inst.engine.clone(),
129 host: inst.endpoint_address.clone(),
134 port: inst.host_port,
135 user: inst.master_username.clone(),
136 password: inst.master_user_password.clone(),
137 db_name: inst.db_name.clone().unwrap_or_default(),
138 schema: None,
139 })
140 }
141
142 fn require_conn(&self, req: &AwsRequest, body: &Value) -> Result<DbConn, AwsServiceError> {
147 let resource_arn = body
148 .get("resourceArn")
149 .and_then(Value::as_str)
150 .ok_or_else(|| bad_request("resourceArn is required"))?;
151 if body.get("secretArn").and_then(Value::as_str).is_none() {
154 return Err(bad_request("secretArn is required"));
155 }
156 let mut conn = self
157 .resolve_conn(&req.account_id, resource_arn)
158 .ok_or_else(|| {
159 bad_request(format!(
160 "HttpEndpoint is not enabled for resource {resource_arn} (no such DB)"
161 ))
162 })?;
163 if let Some(db) = body.get("database").and_then(Value::as_str) {
164 if !db.is_empty() {
165 conn.db_name = db.to_string();
166 }
167 }
168 conn.schema = body
169 .get("schema")
170 .and_then(Value::as_str)
171 .filter(|s| !s.is_empty())
172 .map(str::to_string);
173 Ok(conn)
174 }
175
176 async fn execute_statement(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
177 let body = req.json_body();
178 let sql = body
179 .get("sql")
180 .and_then(Value::as_str)
181 .ok_or_else(|| bad_request("sql is required"))?;
182 let include_metadata = body
183 .get("includeResultMetadata")
184 .and_then(Value::as_bool)
185 .unwrap_or(false);
186 let format_json = body
187 .get("formatRecordsAs")
188 .and_then(Value::as_str)
189 .map(|f| f == "JSON")
190 .unwrap_or(false);
191 let params = parse_parameters(body.get("parameters"));
192
193 if let Some(txid) = body.get("transactionId").and_then(Value::as_str) {
197 self.require_conn(req, &body)?;
200 let entry = self.txn_entry(txid).await?;
201 entry.touch();
202 let mut guard = entry.conn.lock().await;
205 let held = guard.as_mut().ok_or_else(|| not_found_txn(txid))?;
206 let value = match held {
207 HeldConn::Pg(client) => {
208 pg_run(client, sql, ¶ms, include_metadata, format_json).await?
209 }
210 HeldConn::MySql(conn) => {
211 my_run(conn, sql, ¶ms, include_metadata, format_json).await?
212 }
213 };
214 return Ok(AwsResponse::ok_json(value));
215 }
216
217 let conn = self.require_conn(req, &body)?;
218 let engine = conn.engine.to_lowercase();
219 let value = if engine.contains("postgres") {
220 let client = pg_connect(&conn).await?;
221 pg_run(&client, sql, ¶ms, include_metadata, format_json).await?
222 } else if is_mysql(&engine) {
223 let mut c = my_connect(&conn).await?;
224 let v = my_run(&mut c, sql, ¶ms, include_metadata, format_json).await;
225 let _ = c.disconnect().await;
226 v?
227 } else {
228 return Err(unsupported_engine(&conn.engine));
229 };
230 Ok(AwsResponse::ok_json(value))
231 }
232
233 async fn begin_transaction(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
234 let body = req.json_body();
235 let conn = self.require_conn(req, &body)?;
236 let engine = conn.engine.to_lowercase();
237
238 let held = if engine.contains("postgres") {
239 let client = pg_connect(&conn).await?;
240 client
241 .batch_execute("BEGIN")
242 .await
243 .map_err(|e| bad_request(format!("could not begin transaction: {e}")))?;
244 HeldConn::Pg(client)
245 } else if is_mysql(&engine) {
246 use mysql_async::prelude::Queryable;
247 let mut c = my_connect(&conn).await?;
248 c.query_drop("START TRANSACTION")
249 .await
250 .map_err(|e| bad_request(format!("could not begin transaction: {e}")))?;
251 HeldConn::MySql(c)
252 } else {
253 return Err(unsupported_engine(&conn.engine));
254 };
255
256 let txid = gen_transaction_id();
257 let entry = Arc::new(TxnEntry {
258 conn: Mutex::new(Some(held)),
259 last_used: StdMutex::new(Instant::now()),
260 });
261 self.transactions.lock().await.insert(txid.clone(), entry);
262 Ok(AwsResponse::ok_json(json!({ "transactionId": txid })))
263 }
264
265 async fn txn_entry(&self, txid: &str) -> Result<Arc<TxnEntry>, AwsServiceError> {
268 self.transactions
269 .lock()
270 .await
271 .get(txid)
272 .cloned()
273 .ok_or_else(|| not_found_txn(txid))
274 }
275
276 async fn finish_transaction(
279 &self,
280 req: &AwsRequest,
281 sql_keyword: &str,
282 status: &str,
283 ) -> Result<AwsResponse, AwsServiceError> {
284 let body = req.json_body();
285 let txid = body
286 .get("transactionId")
287 .and_then(Value::as_str)
288 .ok_or_else(|| bad_request("transactionId is required"))?;
289 let entry = self
290 .transactions
291 .lock()
292 .await
293 .remove(txid)
294 .ok_or_else(|| not_found_txn(txid))?;
295 let held = entry
298 .conn
299 .lock()
300 .await
301 .take()
302 .ok_or_else(|| not_found_txn(txid))?;
303 finish_held(held, sql_keyword).await?;
304 Ok(AwsResponse::ok_json(json!({ "transactionStatus": status })))
305 }
306
307 async fn batch_execute_statement(
308 &self,
309 req: &AwsRequest,
310 ) -> Result<AwsResponse, AwsServiceError> {
311 let body = req.json_body();
312 let sql = body
313 .get("sql")
314 .and_then(Value::as_str)
315 .ok_or_else(|| bad_request("sql is required"))?;
316 let sets = parse_parameter_sets(body.get("parameterSets"));
317
318 if let Some(txid) = body.get("transactionId").and_then(Value::as_str) {
321 self.require_conn(req, &body)?;
322 let entry = self.txn_entry(txid).await?;
323 entry.touch();
324 let mut guard = entry.conn.lock().await;
325 let held = guard.as_mut().ok_or_else(|| not_found_txn(txid))?;
326 let mut results = Vec::with_capacity(sets.len().max(1));
327 match held {
328 HeldConn::Pg(client) => {
329 for set in run_each(&sets) {
330 pg_run(client, sql, set, false, false).await?;
331 results.push(json!({ "generatedFields": [] }));
332 }
333 }
334 HeldConn::MySql(conn) => {
335 for set in run_each(&sets) {
336 my_run(conn, sql, set, false, false).await?;
337 results.push(json!({ "generatedFields": [] }));
338 }
339 }
340 }
341 return Ok(AwsResponse::ok_json(json!({ "updateResults": results })));
342 }
343
344 let conn = self.require_conn(req, &body)?;
345 let engine = conn.engine.to_lowercase();
346 let mut results = Vec::with_capacity(sets.len().max(1));
347 if engine.contains("postgres") {
348 let client = pg_connect(&conn).await?;
349 for set in run_each(&sets) {
350 pg_run(&client, sql, set, false, false).await?;
351 results.push(json!({ "generatedFields": [] }));
352 }
353 } else if is_mysql(&engine) {
354 let mut c = my_connect(&conn).await?;
355 for set in run_each(&sets) {
356 if let Err(e) = my_run(&mut c, sql, set, false, false).await {
357 let _ = c.disconnect().await;
358 return Err(e);
359 }
360 results.push(json!({ "generatedFields": [] }));
361 }
362 let _ = c.disconnect().await;
363 } else {
364 return Err(unsupported_engine(&conn.engine));
365 }
366 Ok(AwsResponse::ok_json(json!({ "updateResults": results })))
367 }
368}
369
370#[async_trait]
371impl AwsService for RdsDataService {
372 fn service_name(&self) -> &str {
373 "rds-data"
374 }
375
376 fn supported_actions(&self) -> &[&str] {
377 SUPPORTED_ACTIONS
378 }
379
380 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
381 let Some(action) = Self::resolve_action(&req) else {
382 return Err(AwsServiceError::aws_error(
383 StatusCode::NOT_FOUND,
384 "ResourceNotFoundException",
385 format!("Unknown operation: {} {}", req.method, req.raw_path),
386 ));
387 };
388 match action {
389 "ExecuteStatement" => self.execute_statement(&req).await,
390 "BatchExecuteStatement" => self.batch_execute_statement(&req).await,
391 "BeginTransaction" => self.begin_transaction(&req).await,
392 "CommitTransaction" => {
393 self.finish_transaction(&req, "COMMIT", "Transaction Committed")
394 .await
395 }
396 "RollbackTransaction" => {
397 self.finish_transaction(&req, "ROLLBACK", "Rollback Complete")
398 .await
399 }
400 other => Err(AwsServiceError::aws_error(
404 StatusCode::BAD_REQUEST,
405 "BadRequestException",
406 format!("rds-data operation {other} is not supported"),
407 )),
408 }
409 }
410}
411
412fn bad_request(msg: impl Into<String>) -> AwsServiceError {
413 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "BadRequestException", msg.into())
414}
415
416fn not_found_txn(txid: &str) -> AwsServiceError {
417 AwsServiceError::aws_error(
418 StatusCode::BAD_REQUEST,
419 "BadRequestException",
420 format!("Transaction {txid} is not found"),
421 )
422}
423
424fn unsupported_engine(engine: &str) -> AwsServiceError {
425 bad_request(format!("RDS Data API is not supported for engine {engine}"))
426}
427
428fn is_mysql(engine_lower: &str) -> bool {
429 engine_lower.contains("mysql") || engine_lower.contains("maria")
430}
431
432fn gen_transaction_id() -> String {
434 use rand::RngCore;
435 let mut bytes = [0u8; 48];
436 rand::thread_rng().fill_bytes(&mut bytes);
437 b64_encode(&bytes)
438}
439
440async fn finish_held(held: HeldConn, sql_keyword: &str) -> Result<(), AwsServiceError> {
443 match held {
444 HeldConn::Pg(client) => {
445 client
446 .batch_execute(sql_keyword)
447 .await
448 .map_err(|e| bad_request(format!("{e}")))?;
449 }
450 HeldConn::MySql(mut conn) => {
451 use mysql_async::prelude::Queryable;
452 conn.query_drop(sql_keyword)
453 .await
454 .map_err(|e| bad_request(format!("{e}")))?;
455 let _ = conn.disconnect().await;
456 }
457 }
458 Ok(())
459}
460
461async fn reap_idle_transactions(transactions: TxnMap) {
465 let mut tick = tokio::time::interval(Duration::from_secs(30));
466 loop {
467 tick.tick().await;
468 let now = Instant::now();
469 let stale: Vec<(String, Arc<TxnEntry>)> = {
470 let map = transactions.lock().await;
471 map.iter()
472 .filter(|(_, e)| {
473 e.last_used
474 .lock()
475 .map(|t| now.duration_since(*t) > TXN_IDLE_TIMEOUT)
476 .unwrap_or(false)
477 })
478 .map(|(id, e)| (id.clone(), e.clone()))
479 .collect()
480 };
481 for (id, entry) in stale {
482 transactions.lock().await.remove(&id);
483 if let Some(held) = entry.conn.lock().await.take() {
484 let _ = finish_held(held, "ROLLBACK").await;
485 }
486 }
487 }
488}
489
490enum SqlValue {
492 Null,
493 Bool(bool),
494 Long(i64),
495 Double(f64),
496 Text(String),
497 Blob(Vec<u8>),
498}
499
500type Params = Vec<(String, SqlValue)>;
501
502fn parse_parameters(params: Option<&Value>) -> Params {
504 let Some(arr) = params.and_then(Value::as_array) else {
505 return Vec::new();
506 };
507 arr.iter()
508 .filter_map(|p| {
509 let name = p.get("name").and_then(Value::as_str)?.to_string();
510 let v = p.get("value")?;
511 let sv = if v.get("isNull").and_then(Value::as_bool) == Some(true) {
512 SqlValue::Null
513 } else if let Some(b) = v.get("booleanValue").and_then(Value::as_bool) {
514 SqlValue::Bool(b)
515 } else if let Some(n) = v.get("longValue").and_then(Value::as_i64) {
516 SqlValue::Long(n)
517 } else if let Some(d) = v.get("doubleValue").and_then(Value::as_f64) {
518 SqlValue::Double(d)
519 } else if let Some(s) = v.get("stringValue").and_then(Value::as_str) {
520 SqlValue::Text(s.to_string())
521 } else if let Some(b) = v.get("blobValue").and_then(Value::as_str) {
522 SqlValue::Blob(b64_decode(b))
524 } else {
525 SqlValue::Null
526 };
527 Some((name, sv))
528 })
529 .collect()
530}
531
532fn parse_parameter_sets(sets: Option<&Value>) -> Vec<Params> {
534 let Some(arr) = sets.and_then(Value::as_array) else {
535 return Vec::new();
536 };
537 arr.iter().map(|s| parse_parameters(Some(s))).collect()
538}
539
540fn run_each(sets: &[Params]) -> Vec<&[(String, SqlValue)]> {
543 if sets.is_empty() {
544 vec![&[]]
545 } else {
546 sets.iter().map(Vec::as_slice).collect()
547 }
548}
549
550fn inline_params(
557 sql: &str,
558 params: &[(String, SqlValue)],
559 lit: impl Fn(&SqlValue) -> String,
560) -> String {
561 let mut out = String::with_capacity(sql.len());
562 let bytes = sql.as_bytes();
563 let mut i = 0;
564 while i < bytes.len() {
565 if bytes[i] == b':' && i + 1 < bytes.len() && is_ident_start(bytes[i + 1]) {
566 let mut j = i + 1;
567 while j < bytes.len() && is_ident_char(bytes[j]) {
568 j += 1;
569 }
570 let name = &sql[i + 1..j];
571 if let Some((_, v)) = params.iter().find(|(n, _)| n == name) {
572 out.push_str(&lit(v));
573 i = j;
574 continue;
575 }
576 }
577 let ch = sql[i..]
582 .chars()
583 .next()
584 .expect("byte index is on a char boundary");
585 out.push(ch);
586 i += ch.len_utf8();
587 }
588 out
589}
590
591fn sql_quote(s: &str) -> String {
592 format!("'{}'", s.replace('\'', "''"))
593}
594
595fn quote_ident(s: &str) -> String {
598 format!("\"{}\"", s.replace('"', "\"\""))
599}
600
601fn pg_literal(v: &SqlValue) -> String {
603 match v {
604 SqlValue::Null => "NULL".to_string(),
605 SqlValue::Bool(b) => if *b { "TRUE" } else { "FALSE" }.to_string(),
606 SqlValue::Long(n) => n.to_string(),
607 SqlValue::Double(d) => d.to_string(),
608 SqlValue::Text(s) => sql_quote(s),
609 SqlValue::Blob(b) => format!("'\\x{}'::bytea", hex(b)),
610 }
611}
612
613fn mysql_literal(v: &SqlValue) -> String {
615 match v {
616 SqlValue::Null => "NULL".to_string(),
617 SqlValue::Bool(b) => if *b { "1" } else { "0" }.to_string(),
618 SqlValue::Long(n) => n.to_string(),
619 SqlValue::Double(d) => d.to_string(),
620 SqlValue::Text(s) => sql_quote(s),
621 SqlValue::Blob(b) => format!("x'{}'", hex(b)),
622 }
623}
624
625fn returns_rows(sql: &str) -> bool {
627 let head = sql.trim_start().to_ascii_uppercase();
628 head.starts_with("SELECT")
629 || head.starts_with("WITH")
630 || head.starts_with("SHOW")
631 || head.starts_with("VALUES")
632 || head.starts_with("TABLE")
633 || head.starts_with("EXPLAIN")
634 || head.contains(" RETURNING ")
635}
636
637fn hex(b: &[u8]) -> String {
638 let mut s = String::with_capacity(b.len() * 2);
639 for byte in b {
640 s.push_str(&format!("{byte:02x}"));
641 }
642 s
643}
644
645fn is_ident_start(b: u8) -> bool {
646 b.is_ascii_alphabetic() || b == b'_'
647}
648fn is_ident_char(b: u8) -> bool {
649 b.is_ascii_alphanumeric() || b == b'_'
650}
651
652async fn pg_connect(conn: &DbConn) -> Result<tokio_postgres::Client, AwsServiceError> {
653 use tokio_postgres::NoTls;
654 let cs = format!(
655 "host={} port={} user={} password={} dbname={}",
656 conn.host, conn.port, conn.user, conn.password, conn.db_name
657 );
658 let (client, connection) = tokio_postgres::connect(&cs, NoTls)
659 .await
660 .map_err(|e| bad_request(format!("could not connect to database: {e}")))?;
661 tokio::spawn(async move {
662 let _ = connection.await;
663 });
664 if let Some(schema) = &conn.schema {
665 client
666 .batch_execute(&format!("SET search_path TO {}", quote_ident(schema)))
667 .await
668 .map_err(|e| bad_request(format!("could not set schema: {e}")))?;
669 }
670 Ok(client)
671}
672
673async fn pg_run(
674 client: &tokio_postgres::Client,
675 sql: &str,
676 params: &[(String, SqlValue)],
677 include_metadata: bool,
678 format_json: bool,
679) -> Result<Value, AwsServiceError> {
680 let stmt = inline_params(sql, params, pg_literal);
681 let no_params: &[&(dyn tokio_postgres::types::ToSql + Sync)] = &[];
682
683 let mut out = Map::new();
684 if !returns_rows(&stmt) {
687 let affected = client
688 .execute(stmt.as_str(), no_params)
689 .await
690 .map_err(|e| bad_request(format!("{e}")))?;
691 out.insert("numberOfRecordsUpdated".into(), json!(affected));
692 out.insert("records".into(), json!([]));
693 return Ok(Value::Object(out));
694 }
695
696 let rows = client
697 .query(stmt.as_str(), no_params)
698 .await
699 .map_err(|e| bad_request(format!("{e}")))?;
700 if rows.is_empty() {
701 out.insert("numberOfRecordsUpdated".into(), json!(0));
702 if format_json {
703 out.insert("formattedRecords".into(), json!("[]"));
704 } else {
705 out.insert("records".into(), json!([]));
706 }
707 return Ok(Value::Object(out));
708 }
709
710 let cols = rows[0].columns();
711 if include_metadata {
712 let md: Vec<Value> = cols
713 .iter()
714 .map(|c| {
715 json!({
716 "name": c.name(),
717 "label": c.name(),
718 "typeName": c.type_().name(),
719 "nullable": 2, })
721 })
722 .collect();
723 out.insert("columnMetadata".into(), Value::Array(md));
724 }
725
726 let mut records: Vec<Value> = Vec::with_capacity(rows.len());
727 let mut json_rows: Vec<Map<String, Value>> = Vec::new();
728 for row in &rows {
729 let mut rec: Vec<Value> = Vec::with_capacity(cols.len());
730 let mut jr = Map::new();
731 for (i, col) in cols.iter().enumerate() {
732 let field = pg_field(row, i, col.type_());
733 if format_json {
734 jr.insert(col.name().to_string(), field_to_plain(&field));
735 }
736 rec.push(field);
737 }
738 records.push(Value::Array(rec));
739 if format_json {
740 json_rows.push(jr);
741 }
742 }
743 if format_json {
744 out.insert(
745 "formattedRecords".into(),
746 json!(serde_json::to_string(&json_rows).unwrap_or_default()),
747 );
748 } else {
749 out.insert("records".into(), Value::Array(records));
750 }
751 Ok(Value::Object(out))
752}
753
754fn pg_timestamp_text(base: impl std::fmt::Display, micros: u32) -> String {
757 if micros == 0 {
758 return base.to_string();
759 }
760 let frac = format!("{micros:06}");
761 let frac = frac.trim_end_matches('0');
762 format!("{base}.{frac}")
763}
764
765fn pg_field(row: &tokio_postgres::Row, i: usize, ty: &tokio_postgres::types::Type) -> Value {
766 use tokio_postgres::types::Type;
767 macro_rules! opt {
768 ($t:ty, $key:expr, $conv:expr) => {{
769 let v: Option<$t> = row.try_get(i).unwrap_or(None);
770 match v {
771 Some(x) => json!({ $key: $conv(x) }),
772 None => json!({ "isNull": true }),
773 }
774 }};
775 }
776 macro_rules! strv {
782 ($t:ty, $conv:expr) => {{
783 let v: Option<$t> = row.try_get(i).unwrap_or(None);
784 match v {
785 Some(x) => json!({ "stringValue": $conv(x) }),
786 None => json!({ "isNull": true }),
787 }
788 }};
789 }
790 match *ty {
791 Type::BOOL => opt!(bool, "booleanValue", |x| x),
792 Type::INT2 => opt!(i16, "longValue", |x| x as i64),
793 Type::INT4 => opt!(i32, "longValue", |x| x as i64),
794 Type::INT8 => opt!(i64, "longValue", |x: i64| x),
795 Type::FLOAT4 => opt!(f32, "doubleValue", |x| x as f64),
796 Type::FLOAT8 => opt!(f64, "doubleValue", |x: f64| x),
797 Type::NUMERIC => strv!(rust_decimal::Decimal, |d: rust_decimal::Decimal| d
798 .to_string()),
799 Type::UUID => strv!(uuid::Uuid, |u: uuid::Uuid| u.to_string()),
800 Type::JSON | Type::JSONB => {
801 strv!(serde_json::Value, |j: serde_json::Value| j.to_string())
802 }
803 Type::TIMESTAMP => strv!(chrono::NaiveDateTime, |t: chrono::NaiveDateTime| {
804 pg_timestamp_text(
805 t.format("%Y-%m-%d %H:%M:%S"),
806 t.and_utc().timestamp_subsec_micros(),
807 )
808 }),
809 Type::TIMESTAMPTZ => strv!(chrono::DateTime<chrono::Utc>, |t: chrono::DateTime<
810 chrono::Utc,
811 >| format!(
812 "{}{}",
813 pg_timestamp_text(t.format("%Y-%m-%d %H:%M:%S"), t.timestamp_subsec_micros()),
814 t.format("%:z")
815 )),
816 Type::DATE => strv!(chrono::NaiveDate, |d: chrono::NaiveDate| d.to_string()),
817 Type::TIME => strv!(chrono::NaiveTime, |t: chrono::NaiveTime| t.to_string()),
818 Type::BYTEA => {
819 let v: Option<Vec<u8>> = row.try_get(i).unwrap_or(None);
820 match v {
821 Some(b) => json!({ "blobValue": b64_encode(&b) }),
822 None => json!({ "isNull": true }),
823 }
824 }
825 _ => {
826 let v: Option<String> = row.try_get(i).unwrap_or(None);
827 match v {
828 Some(s) => json!({ "stringValue": s }),
829 None => json!({ "isNull": true }),
830 }
831 }
832 }
833}
834
835async fn my_connect(conn: &DbConn) -> Result<mysql_async::Conn, AwsServiceError> {
836 use mysql_async::OptsBuilder;
837 let opts = OptsBuilder::default()
838 .ip_or_hostname(conn.host.as_str())
839 .tcp_port(conn.port)
840 .user(Some(&conn.user))
841 .pass(Some(&conn.password))
842 .db_name(Some(&conn.db_name));
843 mysql_async::Conn::new(opts)
844 .await
845 .map_err(|e| bad_request(format!("could not connect to database: {e}")))
846}
847
848async fn my_run(
849 c: &mut mysql_async::Conn,
850 sql: &str,
851 params: &[(String, SqlValue)],
852 include_metadata: bool,
853 format_json: bool,
854) -> Result<Value, AwsServiceError> {
855 use mysql_async::prelude::Queryable;
856 use mysql_async::{Column, Row};
857
858 let stmt = inline_params(sql, params, mysql_literal);
859 let mut out = Map::new();
860
861 if !returns_rows(&stmt) {
865 c.query_drop(stmt.as_str())
866 .await
867 .map_err(|e| bad_request(format!("{e}")))?;
868 out.insert("numberOfRecordsUpdated".into(), json!(c.affected_rows()));
869 let generated: Option<u64> = c
874 .query_first("SELECT LAST_INSERT_ID()")
875 .await
876 .ok()
877 .flatten()
878 .filter(|id: &u64| *id != 0);
879 if let Some(id) = generated {
880 out.insert(
881 "generatedFields".into(),
882 json!([{ "longValue": id as i64 }]),
883 );
884 }
885 out.insert("records".into(), json!([]));
886 return Ok(Value::Object(out));
887 }
888
889 let rows: Vec<Row> = c
890 .query(stmt.as_str())
891 .await
892 .map_err(|e| bad_request(format!("{e}")))?;
893 let affected = c.affected_rows();
894
895 if rows.is_empty() {
896 out.insert("numberOfRecordsUpdated".into(), json!(affected));
897 if format_json {
898 out.insert("formattedRecords".into(), json!("[]"));
899 } else {
900 out.insert("records".into(), json!([]));
901 }
902 return Ok(Value::Object(out));
903 }
904
905 let cols: std::sync::Arc<[Column]> = rows[0].columns();
906 if include_metadata {
907 let md: Vec<Value> = cols
908 .iter()
909 .map(|c| {
910 json!({
911 "name": c.name_str().to_string(),
912 "label": c.name_str().to_string(),
913 "typeName": format!("{:?}", c.column_type()),
914 "nullable": 2,
915 })
916 })
917 .collect();
918 out.insert("columnMetadata".into(), Value::Array(md));
919 }
920
921 let mut records: Vec<Value> = Vec::with_capacity(rows.len());
922 let mut json_rows: Vec<Map<String, Value>> = Vec::new();
923 for row in &rows {
924 let mut rec: Vec<Value> = Vec::with_capacity(cols.len());
925 let mut jr = Map::new();
926 for (i, col) in cols.iter().enumerate() {
927 let field = my_field(row, i, col.column_type());
928 if format_json {
929 jr.insert(col.name_str().to_string(), field_to_plain(&field));
930 }
931 rec.push(field);
932 }
933 records.push(Value::Array(rec));
934 if format_json {
935 json_rows.push(jr);
936 }
937 }
938 if format_json {
939 out.insert(
940 "formattedRecords".into(),
941 json!(serde_json::to_string(&json_rows).unwrap_or_default()),
942 );
943 } else {
944 out.insert("records".into(), Value::Array(records));
945 }
946 Ok(Value::Object(out))
947}
948
949fn my_field(row: &mysql_async::Row, i: usize, ty: mysql_async::consts::ColumnType) -> Value {
950 use mysql_async::Value as V;
951 match row.as_ref(i) {
952 Some(V::NULL) | None => json!({ "isNull": true }),
953 Some(V::Int(n)) => json!({ "longValue": n }),
955 Some(V::UInt(n)) => json!({ "longValue": *n as i64 }),
956 Some(V::Float(f)) => json!({ "doubleValue": *f as f64 }),
957 Some(V::Double(d)) => json!({ "doubleValue": d }),
958 Some(V::Bytes(b)) => my_bytes_field(b, ty),
962 Some(V::Date(y, mo, d, h, mi, s, us)) => {
965 json!({ "stringValue": format_mysql_datetime(*y, *mo, *d, *h, *mi, *s, *us) })
966 }
967 Some(V::Time(neg, days, h, mi, s, us)) => {
968 json!({ "stringValue": format_mysql_time(*neg, *days, *h, *mi, *s, *us) })
969 }
970 }
971}
972
973fn my_bytes_field(b: &[u8], ty: mysql_async::consts::ColumnType) -> Value {
978 use mysql_async::consts::ColumnType::*;
979 let as_str = std::str::from_utf8(b).ok();
980 match ty {
981 MYSQL_TYPE_TINY | MYSQL_TYPE_SHORT | MYSQL_TYPE_LONG | MYSQL_TYPE_LONGLONG
982 | MYSQL_TYPE_INT24 | MYSQL_TYPE_YEAR => {
983 if let Some(n) = as_str.and_then(|s| s.parse::<i64>().ok()) {
984 return json!({ "longValue": n });
985 }
986 }
987 MYSQL_TYPE_FLOAT | MYSQL_TYPE_DOUBLE => {
988 if let Some(d) = as_str.and_then(|s| s.parse::<f64>().ok()) {
989 return json!({ "doubleValue": d });
990 }
991 }
992 _ => {}
993 }
994 match as_str {
995 Some(s) => json!({ "stringValue": s }),
996 None => json!({ "blobValue": b64_encode(b) }),
997 }
998}
999
1000#[allow(clippy::too_many_arguments)]
1003fn format_mysql_datetime(
1004 year: u16,
1005 month: u8,
1006 day: u8,
1007 hour: u8,
1008 min: u8,
1009 sec: u8,
1010 micros: u32,
1011) -> String {
1012 let date = format!("{year:04}-{month:02}-{day:02}");
1013 if hour == 0 && min == 0 && sec == 0 && micros == 0 {
1014 return date;
1015 }
1016 if micros == 0 {
1017 format!("{date} {hour:02}:{min:02}:{sec:02}")
1018 } else {
1019 format!("{date} {hour:02}:{min:02}:{sec:02}.{micros:06}")
1020 }
1021}
1022
1023fn format_mysql_time(neg: bool, days: u32, hours: u8, mins: u8, secs: u8, micros: u32) -> String {
1025 let sign = if neg { "-" } else { "" };
1026 let total_hours = days * 24 + hours as u32;
1027 if micros == 0 {
1028 format!("{sign}{total_hours:02}:{mins:02}:{secs:02}")
1029 } else {
1030 format!("{sign}{total_hours:02}:{mins:02}:{secs:02}.{micros:06}")
1031 }
1032}
1033
1034fn field_to_plain(field: &Value) -> Value {
1036 let o = field.as_object();
1037 if o.and_then(|m| m.get("isNull")).is_some() {
1038 return Value::Null;
1039 }
1040 for k in [
1041 "stringValue",
1042 "longValue",
1043 "doubleValue",
1044 "booleanValue",
1045 "blobValue",
1046 ] {
1047 if let Some(v) = o.and_then(|m| m.get(k)) {
1048 return v.clone();
1049 }
1050 }
1051 Value::Null
1052}
1053
1054fn b64_encode(b: &[u8]) -> String {
1055 use base64::Engine;
1056 base64::engine::general_purpose::STANDARD.encode(b)
1057}
1058fn b64_decode(s: &str) -> Vec<u8> {
1059 use base64::Engine;
1060 base64::engine::general_purpose::STANDARD
1061 .decode(s)
1062 .unwrap_or_default()
1063}