1use std::sync::Arc;
11
12use async_trait::async_trait;
13use http::StatusCode;
14use serde_json::{json, Map, Value};
15use tokio::sync::Mutex as AsyncMutex;
16
17use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
18use fakecloud_persistence::SnapshotStore;
19
20use crate::query;
21use crate::shared::{
22 database_arn, endpoint_address, new_id, now_epoch, scheduled_query_arn, table_arn, table_key,
23};
24use crate::state::{
25 AccountSettings, BatchLoadTask, Database, ScheduledQuery, SharedTimestreamState, StoredRecord,
26 Table, TimestreamData, MAX_RECORDS_PER_TABLE,
27};
28
29pub const TIMESTREAM_ACTIONS: &[&str] = &[
32 "CreateDatabase",
34 "DeleteDatabase",
35 "DescribeDatabase",
36 "ListDatabases",
37 "UpdateDatabase",
38 "CreateTable",
39 "DeleteTable",
40 "DescribeTable",
41 "ListTables",
42 "UpdateTable",
43 "WriteRecords",
44 "CreateBatchLoadTask",
45 "DescribeBatchLoadTask",
46 "ListBatchLoadTasks",
47 "ResumeBatchLoadTask",
48 "Query",
50 "CancelQuery",
51 "PrepareQuery",
52 "CreateScheduledQuery",
53 "DeleteScheduledQuery",
54 "DescribeScheduledQuery",
55 "ExecuteScheduledQuery",
56 "ListScheduledQueries",
57 "UpdateScheduledQuery",
58 "DescribeAccountSettings",
59 "UpdateAccountSettings",
60 "DescribeEndpoints",
62 "ListTagsForResource",
63 "TagResource",
64 "UntagResource",
65];
66
67fn is_mutating_action(action: &str) -> bool {
70 if action == "Query" {
71 return false;
72 }
73 const READ_PREFIXES: &[&str] = &["Describe", "List", "Prepare", "Cancel"];
74 !READ_PREFIXES.iter().any(|p| action.starts_with(p))
75}
76
77pub struct TimestreamService {
78 state: SharedTimestreamState,
79 snapshot_store: Option<Arc<dyn SnapshotStore>>,
80 snapshot_lock: Arc<AsyncMutex<()>>,
81}
82
83impl TimestreamService {
84 pub fn new(state: SharedTimestreamState) -> Self {
85 Self {
86 state,
87 snapshot_store: None,
88 snapshot_lock: Arc::new(AsyncMutex::new(())),
89 }
90 }
91
92 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
93 self.snapshot_store = Some(store);
94 self
95 }
96
97 async fn save(&self) {
98 crate::persistence::save_snapshot(
99 &self.state,
100 self.snapshot_store.clone(),
101 &self.snapshot_lock,
102 )
103 .await;
104 }
105
106 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
108 let store = self.snapshot_store.clone()?;
109 let state = self.state.clone();
110 let lock = self.snapshot_lock.clone();
111 Some(Arc::new(move || {
112 let state = state.clone();
113 let store = store.clone();
114 let lock = lock.clone();
115 Box::pin(async move {
116 crate::persistence::save_snapshot(&state, Some(store), &lock).await;
117 })
118 }))
119 }
120
121 fn with_account_mut<R>(&self, req: &AwsRequest, f: impl FnOnce(&mut TimestreamData) -> R) -> R {
122 let mut guard = self.state.write();
123 let acct = guard.get_or_create(&req.account_id);
124 f(acct)
125 }
126}
127
128#[async_trait]
129impl AwsService for TimestreamService {
130 fn service_name(&self) -> &str {
131 "timestream"
132 }
133
134 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
135 let action = req.action.clone();
136 if let Err(msg) = crate::validate::validate_input(&action, &req.json_body()) {
137 return Err(validation_exception(msg));
138 }
139 let result = self.dispatch(&action, &req);
140 if is_mutating_action(&action)
141 && matches!(result.as_ref(), Ok(resp) if resp.status.is_success())
142 {
143 self.save().await;
144 }
145 result
146 }
147
148 fn supported_actions(&self) -> &[&str] {
149 TIMESTREAM_ACTIONS
150 }
151}
152
153impl TimestreamService {
154 fn dispatch(&self, action: &str, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
155 let body = req.json_body();
156 match action {
157 "CreateDatabase" => self.create_database(req, &body),
159 "DeleteDatabase" => self.delete_database(req, &body),
160 "DescribeDatabase" => self.describe_database(req, &body),
161 "ListDatabases" => self.list_databases(req, &body),
162 "UpdateDatabase" => self.update_database(req, &body),
163 "CreateTable" => self.create_table(req, &body),
165 "DeleteTable" => self.delete_table(req, &body),
166 "DescribeTable" => self.describe_table(req, &body),
167 "ListTables" => self.list_tables(req, &body),
168 "UpdateTable" => self.update_table(req, &body),
169 "WriteRecords" => self.write_records(req, &body),
171 "CreateBatchLoadTask" => self.create_batch_load_task(req, &body),
173 "DescribeBatchLoadTask" => self.describe_batch_load_task(req, &body),
174 "ListBatchLoadTasks" => self.list_batch_load_tasks(req, &body),
175 "ResumeBatchLoadTask" => self.resume_batch_load_task(req, &body),
176 "Query" => self.query(req, &body),
178 "CancelQuery" => self.cancel_query(req, &body),
179 "PrepareQuery" => self.prepare_query(req, &body),
180 "CreateScheduledQuery" => self.create_scheduled_query(req, &body),
182 "DeleteScheduledQuery" => self.delete_scheduled_query(req, &body),
183 "DescribeScheduledQuery" => self.describe_scheduled_query(req, &body),
184 "ExecuteScheduledQuery" => self.execute_scheduled_query(req, &body),
185 "ListScheduledQueries" => self.list_scheduled_queries(req, &body),
186 "UpdateScheduledQuery" => self.update_scheduled_query(req, &body),
187 "DescribeAccountSettings" => self.describe_account_settings(req),
189 "UpdateAccountSettings" => self.update_account_settings(req, &body),
190 "DescribeEndpoints" => self.describe_endpoints(req),
192 "ListTagsForResource" => self.list_tags_for_resource(req, &body),
193 "TagResource" => self.tag_resource(req, &body),
194 "UntagResource" => self.untag_resource(req, &body),
195 _ => Err(AwsServiceError::action_not_implemented(
196 self.service_name(),
197 action,
198 )),
199 }
200 }
201}
202
203fn err(code: &str, msg: impl Into<String>) -> AwsServiceError {
206 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, code, msg.into())
207}
208
209fn validation_exception(msg: impl Into<String>) -> AwsServiceError {
210 err("ValidationException", msg)
211}
212
213fn resource_not_found(msg: impl Into<String>) -> AwsServiceError {
214 err("ResourceNotFoundException", msg)
215}
216
217fn conflict(msg: impl Into<String>) -> AwsServiceError {
218 err("ConflictException", msg)
219}
220
221fn ok(value: Value) -> Result<AwsResponse, AwsServiceError> {
222 Ok(AwsResponse::ok_json(value))
223}
224
225fn empty_ok() -> Result<AwsResponse, AwsServiceError> {
226 ok(json!({}))
227}
228
229fn str_member<'a>(body: &'a Value, name: &str) -> Option<&'a str> {
232 body.get(name).and_then(Value::as_str)
233}
234
235fn required_str(body: &Value, name: &str) -> Result<String, AwsServiceError> {
236 str_member(body, name)
237 .filter(|s| !s.is_empty())
238 .map(str::to_string)
239 .ok_or_else(|| validation_exception(format!("{name} is required.")))
240}
241
242fn tag_list(map: &std::collections::BTreeMap<String, String>) -> Value {
244 Value::Array(
245 map.iter()
246 .map(|(k, v)| json!({ "Key": k, "Value": v }))
247 .collect(),
248 )
249}
250
251fn absorb_tags(tags: Option<&Value>, into: &mut std::collections::BTreeMap<String, String>) {
253 if let Some(arr) = tags.and_then(Value::as_array) {
254 for t in arr {
255 if let (Some(k), Some(v)) = (
256 t.get("Key").and_then(Value::as_str),
257 t.get("Value").and_then(Value::as_str),
258 ) {
259 into.insert(k.to_string(), v.to_string());
260 }
261 }
262 }
263}
264
265impl Database {
266 fn to_wire(&self) -> Value {
267 let mut o = Map::new();
268 o.insert("Arn".into(), json!(self.arn));
269 o.insert("DatabaseName".into(), json!(self.name));
270 o.insert("TableCount".into(), json!(self.table_count));
271 if let Some(k) = &self.kms_key_id {
272 o.insert("KmsKeyId".into(), json!(k));
273 }
274 o.insert("CreationTime".into(), json!(self.creation_time));
275 o.insert("LastUpdatedTime".into(), json!(self.last_updated_time));
276 Value::Object(o)
277 }
278}
279
280impl Table {
281 fn to_wire(&self) -> Value {
282 let mut o = Map::new();
283 o.insert("Arn".into(), json!(self.arn));
284 o.insert("TableName".into(), json!(self.name));
285 o.insert("DatabaseName".into(), json!(self.database_name));
286 o.insert("TableStatus".into(), json!(self.status));
287 o.insert(
288 "RetentionProperties".into(),
289 self.retention_properties.clone(),
290 );
291 if let Some(m) = &self.magnetic_store_write_properties {
292 o.insert("MagneticStoreWriteProperties".into(), m.clone());
293 }
294 o.insert("Schema".into(), self.schema.clone());
295 o.insert("CreationTime".into(), json!(self.creation_time));
296 o.insert("LastUpdatedTime".into(), json!(self.last_updated_time));
297 Value::Object(o)
298 }
299}
300
301impl TimestreamService {
304 fn create_database(
305 &self,
306 req: &AwsRequest,
307 body: &Value,
308 ) -> Result<AwsResponse, AwsServiceError> {
309 let name = required_str(body, "DatabaseName")?;
310 let kms = str_member(body, "KmsKeyId").map(str::to_string);
311 let region = req.region.clone();
312 let account = req.account_id.clone();
313 let tags = body.get("Tags").cloned();
314 self.with_account_mut(req, |data| {
315 if data.databases.contains_key(&name) {
316 return Err(conflict(format!(
317 "Database '{name}' already exists in this account."
318 )));
319 }
320 let now = now_epoch();
321 let arn = database_arn(®ion, &account, &name);
322 let db = Database {
323 name: name.clone(),
324 arn: arn.clone(),
325 kms_key_id: kms.or_else(|| {
326 Some(format!(
327 "arn:aws:kms:{region}:{account}:key/timestream-default"
328 ))
329 }),
330 table_count: 0,
331 creation_time: now,
332 last_updated_time: now,
333 };
334 let wire = db.to_wire();
335 data.databases.insert(name.clone(), db);
336 let entry = data.tags.entry(arn).or_default();
337 absorb_tags(tags.as_ref(), entry);
338 ok(json!({ "Database": wire }))
339 })
340 }
341
342 fn describe_database(
343 &self,
344 req: &AwsRequest,
345 body: &Value,
346 ) -> Result<AwsResponse, AwsServiceError> {
347 let name = required_str(body, "DatabaseName")?;
348 let guard = self.state.read();
349 let data = match guard.get(&req.account_id) {
350 Some(d) => d,
351 None => {
352 return Err(resource_not_found(format!(
353 "Database '{name}' does not exist."
354 )))
355 }
356 };
357 let db = data
358 .databases
359 .get(&name)
360 .ok_or_else(|| resource_not_found(format!("Database '{name}' does not exist.")))?;
361 ok(json!({ "Database": db.to_wire() }))
362 }
363
364 fn list_databases(
365 &self,
366 req: &AwsRequest,
367 body: &Value,
368 ) -> Result<AwsResponse, AwsServiceError> {
369 let max = body
370 .get("MaxResults")
371 .and_then(Value::as_u64)
372 .unwrap_or(100)
373 .max(1) as usize;
374 let start = decode_token(str_member(body, "NextToken"));
375 let guard = self.state.read();
376 let dbs: Vec<Value> = guard
377 .get(&req.account_id)
378 .map(|d| d.databases.values().map(Database::to_wire).collect())
379 .unwrap_or_default();
380 let (page, next) = paginate(&dbs, start, max);
381 let mut out = Map::new();
382 out.insert("Databases".into(), Value::Array(page));
383 if let Some(n) = next {
384 out.insert("NextToken".into(), json!(n));
385 }
386 ok(Value::Object(out))
387 }
388
389 fn update_database(
390 &self,
391 req: &AwsRequest,
392 body: &Value,
393 ) -> Result<AwsResponse, AwsServiceError> {
394 let name = required_str(body, "DatabaseName")?;
395 let kms = str_member(body, "KmsKeyId").map(str::to_string);
396 self.with_account_mut(req, |data| {
397 let db = data
398 .databases
399 .get_mut(&name)
400 .ok_or_else(|| resource_not_found(format!("Database '{name}' does not exist.")))?;
401 if let Some(k) = kms {
402 db.kms_key_id = Some(k);
403 }
404 db.last_updated_time = now_epoch();
405 ok(json!({ "Database": db.to_wire() }))
406 })
407 }
408
409 fn delete_database(
410 &self,
411 req: &AwsRequest,
412 body: &Value,
413 ) -> Result<AwsResponse, AwsServiceError> {
414 let name = required_str(body, "DatabaseName")?;
415 self.with_account_mut(req, |data| {
416 if !data.databases.contains_key(&name) {
417 return Err(resource_not_found(format!(
418 "Database '{name}' does not exist."
419 )));
420 }
421 let has_tables = data.tables.keys().any(|k| {
422 k.split_once('\u{1}')
423 .map(|(d, _)| d == name)
424 .unwrap_or(false)
425 });
426 if has_tables {
427 return Err(validation_exception(format!(
428 "The database '{name}' cannot be deleted because it still has tables."
429 )));
430 }
431 data.databases.remove(&name);
432 empty_ok()
433 })
434 }
435}
436
437fn default_retention() -> Value {
440 json!({
441 "MemoryStoreRetentionPeriodInHours": 6,
442 "MagneticStoreRetentionPeriodInDays": 73000
443 })
444}
445
446fn default_schema() -> Value {
447 json!({ "CompositePartitionKey": [ { "Type": "MEASURE" } ] })
448}
449
450fn default_magnetic() -> Value {
451 json!({ "EnableMagneticStoreWrites": false })
452}
453
454impl TimestreamService {
455 fn create_table(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
456 let database = required_str(body, "DatabaseName")?;
457 let table = required_str(body, "TableName")?;
458 let region = req.region.clone();
459 let account = req.account_id.clone();
460 let retention = body
461 .get("RetentionProperties")
462 .cloned()
463 .unwrap_or_else(default_retention);
464 let magnetic = body
465 .get("MagneticStoreWriteProperties")
466 .cloned()
467 .unwrap_or_else(default_magnetic);
468 let schema = body.get("Schema").cloned().unwrap_or_else(default_schema);
469 let tags = body.get("Tags").cloned();
470 let key = table_key(&database, &table);
471 self.with_account_mut(req, |data| {
472 if !data.databases.contains_key(&database) {
473 return Err(resource_not_found(format!(
474 "Database '{database}' does not exist."
475 )));
476 }
477 if data.tables.contains_key(&key) {
478 return Err(conflict(format!(
479 "Table '{table}' already exists in database '{database}'."
480 )));
481 }
482 let now = now_epoch();
483 let arn = table_arn(®ion, &account, &database, &table);
484 let t = Table {
485 name: table.clone(),
486 database_name: database.clone(),
487 arn: arn.clone(),
488 status: "ACTIVE".to_string(),
489 retention_properties: retention,
490 magnetic_store_write_properties: Some(magnetic),
491 schema,
492 creation_time: now,
493 last_updated_time: now,
494 };
495 let wire = t.to_wire();
496 data.tables.insert(key.clone(), t);
497 data.records.entry(key).or_default();
498 if let Some(db) = data.databases.get_mut(&database) {
499 db.table_count += 1;
500 db.last_updated_time = now;
501 }
502 let entry = data.tags.entry(arn).or_default();
503 absorb_tags(tags.as_ref(), entry);
504 ok(json!({ "Table": wire }))
505 })
506 }
507
508 fn describe_table(
509 &self,
510 req: &AwsRequest,
511 body: &Value,
512 ) -> Result<AwsResponse, AwsServiceError> {
513 let database = required_str(body, "DatabaseName")?;
514 let table = required_str(body, "TableName")?;
515 let key = table_key(&database, &table);
516 let guard = self.state.read();
517 let t = guard
518 .get(&req.account_id)
519 .and_then(|d| d.tables.get(&key))
520 .ok_or_else(|| {
521 resource_not_found(format!(
522 "Table '{table}' does not exist in database '{database}'."
523 ))
524 })?;
525 ok(json!({ "Table": t.to_wire() }))
526 }
527
528 fn list_tables(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
529 let db_filter = str_member(body, "DatabaseName").map(str::to_string);
530 let max = body
531 .get("MaxResults")
532 .and_then(Value::as_u64)
533 .unwrap_or(100)
534 .max(1) as usize;
535 let start = decode_token(str_member(body, "NextToken"));
536 let guard = self.state.read();
537 let tables: Vec<Value> = guard
538 .get(&req.account_id)
539 .map(|d| {
540 d.tables
541 .values()
542 .filter(|t| {
543 db_filter
544 .as_ref()
545 .map(|f| &t.database_name == f)
546 .unwrap_or(true)
547 })
548 .map(Table::to_wire)
549 .collect()
550 })
551 .unwrap_or_default();
552 let (page, next) = paginate(&tables, start, max);
553 let mut out = Map::new();
554 out.insert("Tables".into(), Value::Array(page));
555 if let Some(n) = next {
556 out.insert("NextToken".into(), json!(n));
557 }
558 ok(Value::Object(out))
559 }
560
561 fn update_table(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
562 let database = required_str(body, "DatabaseName")?;
563 let table = required_str(body, "TableName")?;
564 let key = table_key(&database, &table);
565 let retention = body.get("RetentionProperties").cloned();
566 let magnetic = body.get("MagneticStoreWriteProperties").cloned();
567 let schema = body.get("Schema").cloned();
568 self.with_account_mut(req, |data| {
569 let t = data.tables.get_mut(&key).ok_or_else(|| {
570 resource_not_found(format!(
571 "Table '{table}' does not exist in database '{database}'."
572 ))
573 })?;
574 if let Some(r) = retention {
575 t.retention_properties = r;
576 }
577 if let Some(m) = magnetic {
578 t.magnetic_store_write_properties = Some(m);
579 }
580 if let Some(s) = schema {
581 t.schema = s;
582 }
583 t.last_updated_time = now_epoch();
584 ok(json!({ "Table": t.to_wire() }))
585 })
586 }
587
588 fn delete_table(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
589 let database = required_str(body, "DatabaseName")?;
590 let table = required_str(body, "TableName")?;
591 let key = table_key(&database, &table);
592 self.with_account_mut(req, |data| {
593 if data.tables.remove(&key).is_none() {
594 return Err(resource_not_found(format!(
595 "Table '{table}' does not exist in database '{database}'."
596 )));
597 }
598 data.records.remove(&key);
599 if let Some(db) = data.databases.get_mut(&database) {
600 db.table_count = (db.table_count - 1).max(0);
601 db.last_updated_time = now_epoch();
602 }
603 empty_ok()
604 })
605 }
606}
607
608fn time_unit_multiplier(unit: &str) -> Option<i128> {
611 match unit {
612 "MILLISECONDS" => Some(1_000_000),
613 "SECONDS" => Some(1_000_000_000),
614 "MICROSECONDS" => Some(1_000),
615 "NANOSECONDS" => Some(1),
616 _ => None,
617 }
618}
619
620const MEASURE_VALUE_TYPES: &[&str] = &[
621 "DOUBLE",
622 "BIGINT",
623 "VARCHAR",
624 "BOOLEAN",
625 "TIMESTAMP",
626 "MULTI",
627];
628
629impl TimestreamService {
630 fn write_records(
631 &self,
632 req: &AwsRequest,
633 body: &Value,
634 ) -> Result<AwsResponse, AwsServiceError> {
635 let database = required_str(body, "DatabaseName")?;
636 let table = required_str(body, "TableName")?;
637 let key = table_key(&database, &table);
638 let common = body.get("CommonAttributes");
639 let records = body
640 .get("Records")
641 .and_then(Value::as_array)
642 .ok_or_else(|| validation_exception("Records is required."))?;
643
644 let mut prepared: Vec<StoredRecord> = Vec::with_capacity(records.len());
646 let mut rejected: Vec<Value> = Vec::new();
647 for (idx, rec) in records.iter().enumerate() {
648 match prepare_record(rec, common) {
649 Ok(sr) => prepared.push(sr),
650 Err(reason) => rejected.push(json!({
651 "RecordIndex": idx,
652 "Reason": reason,
653 })),
654 }
655 }
656
657 if !rejected.is_empty() {
658 let rejected_json = serde_json::to_string(&rejected).unwrap_or_else(|_| "[]".into());
659 return Err(AwsServiceError::aws_error_with_fields(
660 StatusCode::BAD_REQUEST,
661 "RejectedRecordsException",
662 "One or more records have been rejected. See RejectedRecords for details.",
663 vec![("RejectedRecords".to_string(), rejected_json)],
664 ));
665 }
666
667 let total = prepared.len();
668 {
669 let mut guard = self.state.write();
670 let data = guard.get_or_create(&req.account_id);
671 if !data.tables.contains_key(&key) {
672 return Err(resource_not_found(format!(
673 "Table '{table}' does not exist in database '{database}'."
674 )));
675 }
676 let buf = data.records.entry(key).or_default();
677 buf.extend(prepared);
678 if buf.len() > MAX_RECORDS_PER_TABLE {
679 let overflow = buf.len() - MAX_RECORDS_PER_TABLE;
680 buf.drain(0..overflow);
681 }
682 }
683
684 ok(json!({
685 "RecordsIngested": {
686 "Total": total,
687 "MemoryStore": total,
688 "MagneticStore": 0
689 }
690 }))
691 }
692}
693
694fn prepare_record(rec: &Value, common: Option<&Value>) -> Result<StoredRecord, String> {
697 let get = |field: &str| -> Option<String> {
698 rec.get(field)
699 .and_then(Value::as_str)
700 .or_else(|| common.and_then(|c| c.get(field)).and_then(Value::as_str))
701 .map(str::to_string)
702 };
703
704 let mut dimensions: Vec<(String, String)> = Vec::new();
706 let mut push_dims = |v: Option<&Value>| -> Result<(), String> {
707 if let Some(arr) = v.and_then(Value::as_array) {
708 for d in arr {
709 let name = d
710 .get("Name")
711 .and_then(Value::as_str)
712 .ok_or("A dimension is missing its Name.")?;
713 let value = d
714 .get("Value")
715 .and_then(Value::as_str)
716 .ok_or("A dimension is missing its Value.")?;
717 dimensions.push((name.to_string(), value.to_string()));
718 }
719 }
720 Ok(())
721 };
722 push_dims(common.and_then(|c| c.get("Dimensions")))?;
723 push_dims(rec.get("Dimensions"))?;
724
725 let measure_value_type = get("MeasureValueType").unwrap_or_else(|| "DOUBLE".to_string());
726 if !MEASURE_VALUE_TYPES.contains(&measure_value_type.as_str()) {
727 return Err(format!(
728 "MeasureValueType '{measure_value_type}' is not a valid measure value type."
729 ));
730 }
731
732 let measure_name = get("MeasureName").unwrap_or_default();
733 if measure_name.is_empty() {
734 return Err("MeasureName is required.".to_string());
735 }
736
737 let measure_value = if measure_value_type == "MULTI" {
738 let mv = rec
739 .get("MeasureValues")
740 .or_else(|| common.and_then(|c| c.get("MeasureValues")));
741 match mv.and_then(Value::as_array) {
742 Some(arr) if !arr.is_empty() => Value::Array(arr.clone()).to_string(),
743 _ => {
744 return Err("A MULTI measure record must carry non-empty MeasureValues.".to_string())
745 }
746 }
747 } else {
748 match get("MeasureValue") {
749 Some(v) => v,
750 None => return Err("MeasureValue is required for a scalar measure.".to_string()),
751 }
752 };
753
754 let time_str = get("Time").ok_or("Time is required.")?;
755 let time_unit = get("TimeUnit").unwrap_or_else(|| "MILLISECONDS".to_string());
756 let mult = time_unit_multiplier(&time_unit)
757 .ok_or_else(|| format!("TimeUnit '{time_unit}' is not a valid time unit."))?;
758 let time_val: i128 = time_str
759 .parse()
760 .map_err(|_| format!("Time '{time_str}' is not a valid integer."))?;
761
762 Ok(StoredRecord {
763 time_nanos: time_val * mult,
764 dimensions,
765 measure_name,
766 measure_value,
767 measure_value_type,
768 })
769}
770
771impl TimestreamService {
774 fn query(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
775 let query_string = required_str(body, "QueryString")?;
776 let plan =
777 query::parse_query(&query_string).map_err(|e| validation_exception(e.message()))?;
778 let key = table_key(&plan.database, &plan.table);
779
780 let result = {
781 let guard = self.state.read();
782 let data = guard.get(&req.account_id);
783 let table_exists = data.map(|d| d.tables.contains_key(&key)).unwrap_or(false);
784 if !table_exists {
785 return Err(validation_exception(format!(
786 "The table \"{}\".\"{}\" does not exist.",
787 plan.database, plan.table
788 )));
789 }
790 let empty: Vec<StoredRecord> = Vec::new();
791 let records = data.and_then(|d| d.records.get(&key)).unwrap_or(&empty);
792 query::execute(&plan, records)
793 };
794
795 let max_rows = body
797 .get("MaxRows")
798 .and_then(Value::as_u64)
799 .map(|n| n.max(1) as usize);
800 let start = decode_token(str_member(body, "NextToken"));
801 let (rows, next) = match max_rows {
802 Some(m) => paginate(&result.rows, start, m),
803 None => (result.rows.clone(), None),
804 };
805
806 let mut out = Map::new();
807 out.insert("QueryId".into(), json!(new_id()));
808 out.insert("ColumnInfo".into(), Value::Array(result.columns));
809 out.insert("Rows".into(), Value::Array(rows));
810 out.insert(
811 "QueryStatus".into(),
812 json!({
813 "ProgressPercentage": 100.0,
814 "CumulativeBytesScanned": 0,
815 "CumulativeBytesMetered": 0
816 }),
817 );
818 if let Some(n) = next {
819 out.insert("NextToken".into(), json!(n));
820 }
821 ok(Value::Object(out))
822 }
823
824 fn cancel_query(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
825 let query_id = required_str(body, "QueryId")?;
826 self.with_account_mut(req, |data| {
827 data.active_queries.remove(&query_id);
828 });
829 ok(json!({ "CancellationMessage": "Query cancelled successfully." }))
830 }
831
832 fn prepare_query(
833 &self,
834 req: &AwsRequest,
835 body: &Value,
836 ) -> Result<AwsResponse, AwsServiceError> {
837 let query_string = required_str(body, "QueryString")?;
838 let plan =
839 query::parse_query(&query_string).map_err(|e| validation_exception(e.message()))?;
840 let key = table_key(&plan.database, &plan.table);
841
842 let columns = {
843 let guard = self.state.read();
844 let data = guard.get(&req.account_id);
845 let empty: Vec<StoredRecord> = Vec::new();
846 let records = data.and_then(|d| d.records.get(&key)).unwrap_or(&empty);
847 let result = query::execute(&plan, records);
848 if result.columns.is_empty() {
849 vec![
850 json!({ "Name": "measure_name", "Type": { "ScalarType": "VARCHAR" } }),
851 json!({ "Name": "time", "Type": { "ScalarType": "TIMESTAMP" } }),
852 ]
853 } else {
854 result.columns
855 }
856 };
857
858 let select_columns: Vec<Value> = columns
860 .into_iter()
861 .map(|c| {
862 json!({
863 "Name": c.get("Name").cloned().unwrap_or(Value::Null),
864 "Type": c.get("Type").cloned().unwrap_or(Value::Null),
865 "DatabaseName": plan.database,
866 "TableName": plan.table,
867 "Aliased": false
868 })
869 })
870 .collect();
871
872 let mut params: Vec<Value> = Vec::new();
874 let mut seen: Vec<String> = Vec::new();
875 for cap in regex_param().find_iter(&query_string) {
876 let name = cap.as_str().to_string();
877 if !seen.contains(&name) {
878 seen.push(name.clone());
879 params.push(json!({ "Name": name, "Type": { "ScalarType": "UNKNOWN" } }));
880 }
881 }
882
883 ok(json!({
884 "QueryString": query_string,
885 "Columns": select_columns,
886 "Parameters": params
887 }))
888 }
889}
890
891fn regex_param() -> &'static regex::Regex {
892 use std::sync::OnceLock;
893 static R: OnceLock<regex::Regex> = OnceLock::new();
894 R.get_or_init(|| regex::Regex::new(r"@[A-Za-z_][A-Za-z0-9_]*").unwrap())
895}
896
897impl ScheduledQuery {
900 fn to_summary(&self) -> Value {
901 let mut o = Map::new();
902 o.insert("Arn".into(), json!(self.arn));
903 o.insert("Name".into(), json!(self.name));
904 o.insert("State".into(), json!(self.state));
905 o.insert("CreationTime".into(), json!(self.creation_time));
906 if let Some(t) = self.previous_invocation_time {
907 o.insert("PreviousInvocationTime".into(), json!(t));
908 }
909 if let Some(e) = &self.error_report_configuration {
910 o.insert("ErrorReportConfiguration".into(), e.clone());
911 }
912 Value::Object(o)
913 }
914
915 fn to_description(&self) -> Value {
916 let mut o = Map::new();
917 o.insert("Arn".into(), json!(self.arn));
918 o.insert("Name".into(), json!(self.name));
919 o.insert("QueryString".into(), json!(self.query_string));
920 o.insert("State".into(), json!(self.state));
921 o.insert("CreationTime".into(), json!(self.creation_time));
922 o.insert(
923 "ScheduleConfiguration".into(),
924 self.schedule_configuration.clone(),
925 );
926 o.insert(
927 "NotificationConfiguration".into(),
928 self.notification_configuration.clone(),
929 );
930 if let Some(t) = &self.target_configuration {
931 o.insert("TargetConfiguration".into(), t.clone());
932 }
933 if let Some(r) = &self.scheduled_query_execution_role_arn {
934 o.insert("ScheduledQueryExecutionRoleArn".into(), json!(r));
935 }
936 if let Some(k) = &self.kms_key_id {
937 o.insert("KmsKeyId".into(), json!(k));
938 }
939 if let Some(e) = &self.error_report_configuration {
940 o.insert("ErrorReportConfiguration".into(), e.clone());
941 }
942 if let Some(t) = self.previous_invocation_time {
943 o.insert("PreviousInvocationTime".into(), json!(t));
944 }
945 Value::Object(o)
946 }
947}
948
949impl TimestreamService {
950 fn create_scheduled_query(
951 &self,
952 req: &AwsRequest,
953 body: &Value,
954 ) -> Result<AwsResponse, AwsServiceError> {
955 let name = required_str(body, "Name")?;
956 let query_string = required_str(body, "QueryString")?;
957 let region = req.region.clone();
958 let account = req.account_id.clone();
959 let schedule = body
960 .get("ScheduleConfiguration")
961 .cloned()
962 .ok_or_else(|| validation_exception("ScheduleConfiguration is required."))?;
963 let notification = body
964 .get("NotificationConfiguration")
965 .cloned()
966 .ok_or_else(|| validation_exception("NotificationConfiguration is required."))?;
967 let error_report = body.get("ErrorReportConfiguration").cloned();
968 let target = body.get("TargetConfiguration").cloned();
969 let role = str_member(body, "ScheduledQueryExecutionRoleArn").map(str::to_string);
970 let kms = str_member(body, "KmsKeyId").map(str::to_string);
971 let tags = body.get("Tags").cloned();
972 let arn = scheduled_query_arn(®ion, &account, &name, &new_id());
973 self.with_account_mut(req, |data| {
974 let sq = ScheduledQuery {
975 arn: arn.clone(),
976 name,
977 query_string,
978 state: "ENABLED".to_string(),
979 creation_time: now_epoch(),
980 schedule_configuration: schedule,
981 notification_configuration: notification,
982 target_configuration: target,
983 scheduled_query_execution_role_arn: role,
984 kms_key_id: kms,
985 error_report_configuration: error_report,
986 previous_invocation_time: None,
987 last_run_status: None,
988 };
989 data.scheduled_queries.insert(arn.clone(), sq);
990 let entry = data.tags.entry(arn.clone()).or_default();
991 absorb_tags(tags.as_ref(), entry);
992 ok(json!({ "Arn": arn }))
993 })
994 }
995
996 fn describe_scheduled_query(
997 &self,
998 req: &AwsRequest,
999 body: &Value,
1000 ) -> Result<AwsResponse, AwsServiceError> {
1001 let arn = required_str(body, "ScheduledQueryArn")?;
1002 let guard = self.state.read();
1003 let sq = guard
1004 .get(&req.account_id)
1005 .and_then(|d| d.scheduled_queries.get(&arn))
1006 .ok_or_else(|| {
1007 resource_not_found(format!("Scheduled query '{arn}' does not exist."))
1008 })?;
1009 ok(json!({ "ScheduledQuery": sq.to_description() }))
1010 }
1011
1012 fn list_scheduled_queries(
1013 &self,
1014 req: &AwsRequest,
1015 body: &Value,
1016 ) -> Result<AwsResponse, AwsServiceError> {
1017 let max = body
1018 .get("MaxResults")
1019 .and_then(Value::as_u64)
1020 .unwrap_or(100)
1021 .max(1) as usize;
1022 let start = decode_token(str_member(body, "NextToken"));
1023 let guard = self.state.read();
1024 let all: Vec<Value> = guard
1025 .get(&req.account_id)
1026 .map(|d| {
1027 d.scheduled_queries
1028 .values()
1029 .map(ScheduledQuery::to_summary)
1030 .collect()
1031 })
1032 .unwrap_or_default();
1033 let (page, next) = paginate(&all, start, max);
1034 let mut out = Map::new();
1035 out.insert("ScheduledQueries".into(), Value::Array(page));
1036 if let Some(n) = next {
1037 out.insert("NextToken".into(), json!(n));
1038 }
1039 ok(Value::Object(out))
1040 }
1041
1042 fn update_scheduled_query(
1043 &self,
1044 req: &AwsRequest,
1045 body: &Value,
1046 ) -> Result<AwsResponse, AwsServiceError> {
1047 let arn = required_str(body, "ScheduledQueryArn")?;
1048 let state = required_str(body, "State")?;
1049 self.with_account_mut(req, |data| {
1050 let sq = data.scheduled_queries.get_mut(&arn).ok_or_else(|| {
1051 resource_not_found(format!("Scheduled query '{arn}' does not exist."))
1052 })?;
1053 sq.state = state;
1054 empty_ok()
1055 })
1056 }
1057
1058 fn delete_scheduled_query(
1059 &self,
1060 req: &AwsRequest,
1061 body: &Value,
1062 ) -> Result<AwsResponse, AwsServiceError> {
1063 let arn = required_str(body, "ScheduledQueryArn")?;
1064 self.with_account_mut(req, |data| {
1065 if data.scheduled_queries.remove(&arn).is_none() {
1066 return Err(resource_not_found(format!(
1067 "Scheduled query '{arn}' does not exist."
1068 )));
1069 }
1070 data.tags.remove(&arn);
1071 empty_ok()
1072 })
1073 }
1074
1075 fn execute_scheduled_query(
1076 &self,
1077 req: &AwsRequest,
1078 body: &Value,
1079 ) -> Result<AwsResponse, AwsServiceError> {
1080 let arn = required_str(body, "ScheduledQueryArn")?;
1081 let invocation = body.get("InvocationTime").and_then(Value::as_f64);
1082 self.with_account_mut(req, |data| {
1083 let sq = data.scheduled_queries.get_mut(&arn).ok_or_else(|| {
1084 resource_not_found(format!("Scheduled query '{arn}' does not exist."))
1085 })?;
1086 sq.previous_invocation_time = invocation.or_else(|| Some(now_epoch()));
1087 sq.last_run_status = Some("MANUAL_TRIGGER_SUCCESS".to_string());
1088 empty_ok()
1089 })
1090 }
1091}
1092
1093impl BatchLoadTask {
1096 fn to_summary(&self) -> Value {
1097 json!({
1098 "TaskId": self.task_id,
1099 "TaskStatus": self.status,
1100 "DatabaseName": self.target_database_name,
1101 "TableName": self.target_table_name,
1102 "CreationTime": self.creation_time,
1103 "LastUpdatedTime": self.last_updated_time,
1104 "ResumableUntil": self.resumable_until
1105 })
1106 }
1107
1108 fn to_description(&self) -> Value {
1109 let mut o = Map::new();
1110 o.insert("TaskId".into(), json!(self.task_id));
1111 o.insert("TaskStatus".into(), json!(self.status));
1112 o.insert(
1113 "TargetDatabaseName".into(),
1114 json!(self.target_database_name),
1115 );
1116 o.insert("TargetTableName".into(), json!(self.target_table_name));
1117 o.insert(
1118 "DataSourceConfiguration".into(),
1119 self.data_source_configuration.clone(),
1120 );
1121 o.insert(
1122 "ReportConfiguration".into(),
1123 self.report_configuration.clone(),
1124 );
1125 if let Some(d) = &self.data_model_configuration {
1126 o.insert("DataModelConfiguration".into(), d.clone());
1127 }
1128 if let Some(v) = self.record_version {
1129 o.insert("RecordVersion".into(), json!(v));
1130 }
1131 o.insert("CreationTime".into(), json!(self.creation_time));
1132 o.insert("LastUpdatedTime".into(), json!(self.last_updated_time));
1133 o.insert("ResumableUntil".into(), json!(self.resumable_until));
1134 Value::Object(o)
1135 }
1136}
1137
1138impl TimestreamService {
1139 fn create_batch_load_task(
1140 &self,
1141 req: &AwsRequest,
1142 body: &Value,
1143 ) -> Result<AwsResponse, AwsServiceError> {
1144 let target_db = required_str(body, "TargetDatabaseName")?;
1145 let target_table = required_str(body, "TargetTableName")?;
1146 let data_source = body
1147 .get("DataSourceConfiguration")
1148 .cloned()
1149 .ok_or_else(|| validation_exception("DataSourceConfiguration is required."))?;
1150 let report = body
1151 .get("ReportConfiguration")
1152 .cloned()
1153 .ok_or_else(|| validation_exception("ReportConfiguration is required."))?;
1154 let data_model = body.get("DataModelConfiguration").cloned();
1155 let record_version = body.get("RecordVersion").and_then(Value::as_i64);
1156 let task_id = new_id();
1157 self.with_account_mut(req, |data| {
1158 let now = now_epoch();
1159 let task = BatchLoadTask {
1160 task_id: task_id.clone(),
1161 status: "CREATED".to_string(),
1162 target_database_name: target_db,
1163 target_table_name: target_table,
1164 data_source_configuration: data_source,
1165 report_configuration: report,
1166 data_model_configuration: data_model,
1167 record_version,
1168 creation_time: now,
1169 last_updated_time: now,
1170 resumable_until: now + 86_400.0,
1171 };
1172 data.batch_load_tasks.insert(task_id.clone(), task);
1173 ok(json!({ "TaskId": task_id }))
1174 })
1175 }
1176
1177 fn describe_batch_load_task(
1178 &self,
1179 req: &AwsRequest,
1180 body: &Value,
1181 ) -> Result<AwsResponse, AwsServiceError> {
1182 let task_id = required_str(body, "TaskId")?;
1183 let guard = self.state.read();
1184 let task = guard
1185 .get(&req.account_id)
1186 .and_then(|d| d.batch_load_tasks.get(&task_id))
1187 .ok_or_else(|| {
1188 resource_not_found(format!("Batch load task '{task_id}' does not exist."))
1189 })?;
1190 ok(json!({ "BatchLoadTaskDescription": task.to_description() }))
1191 }
1192
1193 fn list_batch_load_tasks(
1194 &self,
1195 req: &AwsRequest,
1196 body: &Value,
1197 ) -> Result<AwsResponse, AwsServiceError> {
1198 let status_filter = str_member(body, "TaskStatus").map(str::to_string);
1199 let max = body
1200 .get("MaxResults")
1201 .and_then(Value::as_u64)
1202 .unwrap_or(100)
1203 .max(1) as usize;
1204 let start = decode_token(str_member(body, "NextToken"));
1205 let guard = self.state.read();
1206 let all: Vec<Value> = guard
1207 .get(&req.account_id)
1208 .map(|d| {
1209 d.batch_load_tasks
1210 .values()
1211 .filter(|t| {
1212 status_filter
1213 .as_ref()
1214 .map(|s| &t.status == s)
1215 .unwrap_or(true)
1216 })
1217 .map(BatchLoadTask::to_summary)
1218 .collect()
1219 })
1220 .unwrap_or_default();
1221 let (page, next) = paginate(&all, start, max);
1222 let mut out = Map::new();
1223 out.insert("BatchLoadTasks".into(), Value::Array(page));
1224 if let Some(n) = next {
1225 out.insert("NextToken".into(), json!(n));
1226 }
1227 ok(Value::Object(out))
1228 }
1229
1230 fn resume_batch_load_task(
1231 &self,
1232 req: &AwsRequest,
1233 body: &Value,
1234 ) -> Result<AwsResponse, AwsServiceError> {
1235 let task_id = required_str(body, "TaskId")?;
1236 self.with_account_mut(req, |data| {
1237 let task = data.batch_load_tasks.get_mut(&task_id).ok_or_else(|| {
1238 resource_not_found(format!("Batch load task '{task_id}' does not exist."))
1239 })?;
1240 task.status = "IN_PROGRESS".to_string();
1241 task.last_updated_time = now_epoch();
1242 empty_ok()
1243 })
1244 }
1245}
1246
1247fn query_compute_to_response(req: &Value) -> Value {
1255 let mut out = Map::new();
1256 if let Some(mode) = req.get("ComputeMode") {
1257 out.insert("ComputeMode".into(), mode.clone());
1258 }
1259 if let Some(pc) = req.get("ProvisionedCapacity") {
1260 let target = pc.get("TargetQueryTCU").and_then(Value::as_i64);
1261 let mut pc_out = Map::new();
1262 if let Some(t) = target {
1263 pc_out.insert("ActiveQueryTCU".into(), json!(t));
1264 pc_out.insert(
1265 "LastUpdate".into(),
1266 json!({ "TargetQueryTCU": t, "Status": "SUCCEEDED" }),
1267 );
1268 }
1269 if let Some(n) = pc.get("NotificationConfiguration") {
1270 pc_out.insert("NotificationConfiguration".into(), n.clone());
1271 }
1272 out.insert("ProvisionedCapacity".into(), Value::Object(pc_out));
1273 }
1274 Value::Object(out)
1275}
1276
1277impl AccountSettings {
1278 fn to_wire(&self) -> Value {
1279 let mut o = Map::new();
1280 if let Some(t) = self.max_query_tcu {
1281 o.insert("MaxQueryTCU".into(), json!(t));
1282 }
1283 o.insert("QueryPricingModel".into(), json!(self.query_pricing_model));
1284 if let Some(c) = &self.query_compute {
1285 o.insert("QueryCompute".into(), c.clone());
1286 }
1287 Value::Object(o)
1288 }
1289}
1290
1291impl TimestreamService {
1292 fn describe_account_settings(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1293 let guard = self.state.read();
1294 let settings = guard
1295 .get(&req.account_id)
1296 .map(|d| d.account_settings.clone())
1297 .unwrap_or_default();
1298 ok(settings.to_wire())
1299 }
1300
1301 fn update_account_settings(
1302 &self,
1303 req: &AwsRequest,
1304 body: &Value,
1305 ) -> Result<AwsResponse, AwsServiceError> {
1306 let max_tcu = body.get("MaxQueryTCU").and_then(Value::as_i64);
1307 let pricing = str_member(body, "QueryPricingModel").map(str::to_string);
1308 let compute = body.get("QueryCompute").map(query_compute_to_response);
1313 self.with_account_mut(req, |data| {
1314 if let Some(t) = max_tcu {
1315 data.account_settings.max_query_tcu = Some(t);
1316 }
1317 if let Some(p) = pricing {
1318 data.account_settings.query_pricing_model = p;
1319 }
1320 if compute.is_some() {
1321 data.account_settings.query_compute = compute;
1322 }
1323 ok(data.account_settings.to_wire())
1324 })
1325 }
1326}
1327
1328impl TimestreamService {
1331 fn describe_endpoints(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1332 ok(json!({
1333 "Endpoints": [
1334 {
1335 "Address": endpoint_address(req),
1336 "CachePeriodInMinutes": 1440
1337 }
1338 ]
1339 }))
1340 }
1341
1342 fn list_tags_for_resource(
1343 &self,
1344 req: &AwsRequest,
1345 body: &Value,
1346 ) -> Result<AwsResponse, AwsServiceError> {
1347 let arn = required_str(body, "ResourceARN")?;
1348 let guard = self.state.read();
1349 let tags = guard
1350 .get(&req.account_id)
1351 .and_then(|d| d.tags.get(&arn))
1352 .cloned()
1353 .unwrap_or_default();
1354 ok(json!({ "Tags": tag_list(&tags) }))
1355 }
1356
1357 fn tag_resource(&self, req: &AwsRequest, body: &Value) -> Result<AwsResponse, AwsServiceError> {
1358 let arn = required_str(body, "ResourceARN")?;
1359 let tags = body.get("Tags").cloned();
1360 self.with_account_mut(req, |data| {
1361 let entry = data.tags.entry(arn).or_default();
1362 absorb_tags(tags.as_ref(), entry);
1363 });
1364 empty_ok()
1365 }
1366
1367 fn untag_resource(
1368 &self,
1369 req: &AwsRequest,
1370 body: &Value,
1371 ) -> Result<AwsResponse, AwsServiceError> {
1372 let arn = required_str(body, "ResourceARN")?;
1373 let keys: Vec<String> = body
1374 .get("TagKeys")
1375 .and_then(Value::as_array)
1376 .map(|a| {
1377 a.iter()
1378 .filter_map(|v| v.as_str().map(str::to_string))
1379 .collect()
1380 })
1381 .unwrap_or_default();
1382 self.with_account_mut(req, |data| {
1383 if let Some(entry) = data.tags.get_mut(&arn) {
1384 for k in &keys {
1385 entry.remove(k);
1386 }
1387 }
1388 });
1389 empty_ok()
1390 }
1391}
1392
1393fn decode_token(token: Option<&str>) -> usize {
1397 token.and_then(|t| t.parse::<usize>().ok()).unwrap_or(0)
1398}
1399
1400fn paginate(items: &[Value], start: usize, max: usize) -> (Vec<Value>, Option<String>) {
1403 if start >= items.len() {
1404 return (Vec::new(), None);
1405 }
1406 let end = (start + max).min(items.len());
1407 let page = items[start..end].to_vec();
1408 let next = if end < items.len() {
1409 Some(end.to_string())
1410 } else {
1411 None
1412 };
1413 (page, next)
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419 use fakecloud_core::multi_account::MultiAccountState;
1420 use parking_lot::RwLock;
1421
1422 fn svc() -> TimestreamService {
1423 TimestreamService::new(Arc::new(RwLock::new(MultiAccountState::new(
1424 "000000000000",
1425 "us-east-1",
1426 "http://localhost:4566",
1427 ))))
1428 }
1429
1430 fn req(action: &str, body: Value) -> AwsRequest {
1431 AwsRequest {
1432 service: "timestream".to_string(),
1433 action: action.to_string(),
1434 region: "us-east-1".to_string(),
1435 account_id: "000000000000".to_string(),
1436 request_id: "test".to_string(),
1437 headers: http::HeaderMap::new(),
1438 query_params: std::collections::HashMap::new(),
1439 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1440 body_stream: parking_lot::Mutex::new(None),
1441 path_segments: Vec::new(),
1442 raw_path: String::new(),
1443 raw_query: String::new(),
1444 method: http::Method::POST,
1445 is_query_protocol: false,
1446 access_key_id: None,
1447 principal: None,
1448 }
1449 }
1450
1451 fn call(s: &TimestreamService, action: &str, body: Value) -> Result<Value, AwsServiceError> {
1452 let r = req(action, body);
1453 let resp = s.dispatch(action, &r)?;
1454 Ok(serde_json::from_slice(resp.body.expect_bytes()).unwrap())
1455 }
1456
1457 #[test]
1458 fn database_lifecycle() {
1459 let s = svc();
1460 let out = call(&s, "CreateDatabase", json!({ "DatabaseName": "metrics" })).unwrap();
1461 assert_eq!(out["Database"]["DatabaseName"], "metrics");
1462 assert!(out["Database"]["Arn"]
1463 .as_str()
1464 .unwrap()
1465 .ends_with(":database/metrics"));
1466
1467 let e = call(&s, "CreateDatabase", json!({ "DatabaseName": "metrics" })).unwrap_err();
1469 assert_eq!(e.code(), "ConflictException");
1470
1471 let d = call(&s, "DescribeDatabase", json!({ "DatabaseName": "metrics" })).unwrap();
1472 assert_eq!(d["Database"]["TableCount"], 0);
1473
1474 let l = call(&s, "ListDatabases", json!({})).unwrap();
1475 assert_eq!(l["Databases"].as_array().unwrap().len(), 1);
1476
1477 call(&s, "DeleteDatabase", json!({ "DatabaseName": "metrics" })).unwrap();
1478 let e = call(&s, "DescribeDatabase", json!({ "DatabaseName": "metrics" })).unwrap_err();
1479 assert_eq!(e.code(), "ResourceNotFoundException");
1480 }
1481
1482 #[test]
1483 fn table_requires_database() {
1484 let s = svc();
1485 let e = call(
1486 &s,
1487 "CreateTable",
1488 json!({ "DatabaseName": "nope", "TableName": "t" }),
1489 )
1490 .unwrap_err();
1491 assert_eq!(e.code(), "ResourceNotFoundException");
1492 }
1493
1494 #[test]
1495 fn delete_non_empty_database_is_rejected() {
1496 let s = svc();
1497 call(&s, "CreateDatabase", json!({ "DatabaseName": "m" })).unwrap();
1498 call(
1499 &s,
1500 "CreateTable",
1501 json!({ "DatabaseName": "m", "TableName": "t" }),
1502 )
1503 .unwrap();
1504 let e = call(&s, "DeleteDatabase", json!({ "DatabaseName": "m" })).unwrap_err();
1505 assert_eq!(e.code(), "ValidationException");
1506 }
1507
1508 #[test]
1509 fn write_and_query_round_trip() {
1510 let s = svc();
1511 call(&s, "CreateDatabase", json!({ "DatabaseName": "m" })).unwrap();
1512 call(
1513 &s,
1514 "CreateTable",
1515 json!({ "DatabaseName": "m", "TableName": "t" }),
1516 )
1517 .unwrap();
1518 let w = call(
1519 &s,
1520 "WriteRecords",
1521 json!({
1522 "DatabaseName": "m",
1523 "TableName": "t",
1524 "Records": [
1525 {
1526 "Dimensions": [ { "Name": "host", "Value": "a" } ],
1527 "MeasureName": "cpu",
1528 "MeasureValue": "1.5",
1529 "MeasureValueType": "DOUBLE",
1530 "Time": "1000",
1531 "TimeUnit": "MILLISECONDS"
1532 }
1533 ]
1534 }),
1535 )
1536 .unwrap();
1537 assert_eq!(w["RecordsIngested"]["Total"], 1);
1538
1539 let q = call(
1540 &s,
1541 "Query",
1542 json!({ "QueryString": "SELECT * FROM \"m\".\"t\"" }),
1543 )
1544 .unwrap();
1545 assert_eq!(q["Rows"].as_array().unwrap().len(), 1);
1546 assert!(q["QueryId"].as_str().is_some());
1547
1548 let c = call(
1549 &s,
1550 "Query",
1551 json!({ "QueryString": "SELECT COUNT(*) FROM \"m\".\"t\"" }),
1552 )
1553 .unwrap();
1554 assert_eq!(c["Rows"][0]["Data"][0]["ScalarValue"], "1");
1555 }
1556
1557 #[test]
1558 fn write_records_rejects_bad_record() {
1559 let s = svc();
1560 call(&s, "CreateDatabase", json!({ "DatabaseName": "m" })).unwrap();
1561 call(
1562 &s,
1563 "CreateTable",
1564 json!({ "DatabaseName": "m", "TableName": "t" }),
1565 )
1566 .unwrap();
1567 let e = call(
1568 &s,
1569 "WriteRecords",
1570 json!({
1571 "DatabaseName": "m",
1572 "TableName": "t",
1573 "Records": [ { "MeasureValue": "1", "Time": "1" } ]
1574 }),
1575 )
1576 .unwrap_err();
1577 assert_eq!(e.code(), "RejectedRecordsException");
1578 }
1579
1580 #[test]
1581 fn unsupported_query_is_validation() {
1582 let s = svc();
1583 call(&s, "CreateDatabase", json!({ "DatabaseName": "m" })).unwrap();
1584 call(
1585 &s,
1586 "CreateTable",
1587 json!({ "DatabaseName": "m", "TableName": "t" }),
1588 )
1589 .unwrap();
1590 let e = call(
1591 &s,
1592 "Query",
1593 json!({ "QueryString": "SELECT avg(x) FROM \"m\".\"t\"" }),
1594 )
1595 .unwrap_err();
1596 assert_eq!(e.code(), "ValidationException");
1597 }
1598
1599 #[test]
1600 fn scheduled_query_crud() {
1601 let s = svc();
1602 let out = call(
1603 &s,
1604 "CreateScheduledQuery",
1605 json!({
1606 "Name": "sq",
1607 "QueryString": "SELECT * FROM \"m\".\"t\"",
1608 "ScheduleConfiguration": { "ScheduleExpression": "rate(1 hour)" },
1609 "NotificationConfiguration": { "SnsConfiguration": { "TopicArn": "arn:aws:sns:us-east-1:000000000000:t" } },
1610 "ScheduledQueryExecutionRoleArn": "arn:aws:iam::000000000000:role/r",
1611 "ErrorReportConfiguration": { "S3Configuration": { "BucketName": "b" } }
1612 }),
1613 )
1614 .unwrap();
1615 let arn = out["Arn"].as_str().unwrap().to_string();
1616 let d = call(
1617 &s,
1618 "DescribeScheduledQuery",
1619 json!({ "ScheduledQueryArn": arn }),
1620 )
1621 .unwrap();
1622 assert_eq!(d["ScheduledQuery"]["State"], "ENABLED");
1623 call(
1624 &s,
1625 "UpdateScheduledQuery",
1626 json!({ "ScheduledQueryArn": arn, "State": "DISABLED" }),
1627 )
1628 .unwrap();
1629 let d = call(
1630 &s,
1631 "DescribeScheduledQuery",
1632 json!({ "ScheduledQueryArn": arn }),
1633 )
1634 .unwrap();
1635 assert_eq!(d["ScheduledQuery"]["State"], "DISABLED");
1636 call(
1637 &s,
1638 "DeleteScheduledQuery",
1639 json!({ "ScheduledQueryArn": arn }),
1640 )
1641 .unwrap();
1642 }
1643
1644 #[test]
1645 fn describe_endpoints_echoes_host() {
1646 let s = svc();
1647 let mut r = req("DescribeEndpoints", json!({}));
1648 r.headers.insert("host", "localhost:4566".parse().unwrap());
1649 let resp = s.dispatch("DescribeEndpoints", &r).unwrap();
1650 let v: Value = serde_json::from_slice(resp.body.expect_bytes()).unwrap();
1651 assert_eq!(v["Endpoints"][0]["Address"], "localhost:4566");
1652 assert_eq!(v["Endpoints"][0]["CachePeriodInMinutes"], 1440);
1653 }
1654
1655 #[test]
1656 fn account_settings_round_trip() {
1657 let s = svc();
1658 let d = call(&s, "DescribeAccountSettings", json!({})).unwrap();
1659 assert_eq!(d["QueryPricingModel"], "BYTES_SCANNED");
1660 let u = call(
1661 &s,
1662 "UpdateAccountSettings",
1663 json!({ "MaxQueryTCU": 4, "QueryPricingModel": "COMPUTE_UNITS" }),
1664 )
1665 .unwrap();
1666 assert_eq!(u["MaxQueryTCU"], 4);
1667 assert_eq!(u["QueryPricingModel"], "COMPUTE_UNITS");
1668 }
1669
1670 #[test]
1671 fn tagging_round_trip() {
1672 let s = svc();
1673 let out = call(&s, "CreateDatabase", json!({ "DatabaseName": "m" })).unwrap();
1674 let arn = out["Database"]["Arn"].as_str().unwrap().to_string();
1675 call(
1676 &s,
1677 "TagResource",
1678 json!({ "ResourceARN": arn, "Tags": [ { "Key": "env", "Value": "prod" } ] }),
1679 )
1680 .unwrap();
1681 let t = call(&s, "ListTagsForResource", json!({ "ResourceARN": arn })).unwrap();
1682 assert_eq!(t["Tags"][0]["Key"], "env");
1683 call(
1684 &s,
1685 "UntagResource",
1686 json!({ "ResourceARN": arn, "TagKeys": ["env"] }),
1687 )
1688 .unwrap();
1689 let t = call(&s, "ListTagsForResource", json!({ "ResourceARN": arn })).unwrap();
1690 assert_eq!(t["Tags"].as_array().unwrap().len(), 0);
1691 }
1692
1693 #[test]
1694 fn batch_load_lifecycle() {
1695 let s = svc();
1696 let out = call(
1697 &s,
1698 "CreateBatchLoadTask",
1699 json!({
1700 "TargetDatabaseName": "m",
1701 "TargetTableName": "t",
1702 "DataSourceConfiguration": { "DataSourceS3Configuration": { "BucketName": "b" }, "DataFormat": "CSV" },
1703 "ReportConfiguration": { "ReportS3Configuration": { "BucketName": "r" } }
1704 }),
1705 )
1706 .unwrap();
1707 let task_id = out["TaskId"].as_str().unwrap().to_string();
1708 let d = call(&s, "DescribeBatchLoadTask", json!({ "TaskId": task_id })).unwrap();
1709 assert_eq!(d["BatchLoadTaskDescription"]["TaskStatus"], "CREATED");
1710 let l = call(&s, "ListBatchLoadTasks", json!({})).unwrap();
1711 assert_eq!(l["BatchLoadTasks"].as_array().unwrap().len(), 1);
1712 }
1713}
1714
1715#[cfg(test)]
1716mod async_handle_tests {
1717 use super::*;
1718 use fakecloud_core::multi_account::MultiAccountState;
1719 use parking_lot::RwLock;
1720 use serde_json::json;
1721
1722 fn areq(action: &str, body: Value) -> AwsRequest {
1723 AwsRequest {
1724 service: "timestream".to_string(),
1725 action: action.to_string(),
1726 region: "us-east-1".to_string(),
1727 account_id: "000000000000".to_string(),
1728 request_id: "test".to_string(),
1729 headers: http::HeaderMap::new(),
1730 query_params: std::collections::HashMap::new(),
1731 body: bytes::Bytes::from(serde_json::to_vec(&body).unwrap()),
1732 body_stream: parking_lot::Mutex::new(None),
1733 path_segments: Vec::new(),
1734 raw_path: String::new(),
1735 raw_query: String::new(),
1736 method: http::Method::POST,
1737 is_query_protocol: false,
1738 access_key_id: None,
1739 principal: None,
1740 }
1741 }
1742
1743 #[tokio::test]
1744 async fn handle_describe_endpoints_completes() {
1745 let s = TimestreamService::new(Arc::new(RwLock::new(MultiAccountState::new(
1746 "000000000000",
1747 "us-east-1",
1748 "http://localhost:4566",
1749 ))));
1750 let resp = s
1751 .handle(areq("DescribeEndpoints", json!({})))
1752 .await
1753 .unwrap();
1754 assert!(resp.status.is_success());
1755 }
1756
1757 #[tokio::test]
1758 async fn handle_create_database_completes() {
1759 let s = TimestreamService::new(Arc::new(RwLock::new(MultiAccountState::new(
1760 "000000000000",
1761 "us-east-1",
1762 "http://localhost:4566",
1763 ))));
1764 let resp = s
1765 .handle(areq("CreateDatabase", json!({ "DatabaseName": "m" })))
1766 .await
1767 .unwrap();
1768 assert!(resp.status.is_success());
1769 }
1770}