Skip to main content

fakecloud_timestream/
service.rs

1//! Amazon Timestream awsJson1_0 dispatch + operation handlers.
2//!
3//! Requests carry `X-Amz-Target: Timestream_20181101.<Operation>` for BOTH the
4//! Timestream Write and Timestream Query SDK clients (they share the target
5//! prefix); dispatch keys off `req.action`. Every operation runs model-driven
6//! input validation first, then real, account-partitioned, persisted behavior
7//! over one shared store: databases and tables created on the write side are
8//! read back by `Query` on the query side.
9
10use 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
29/// Every operation name across the Timestream Write + Query Smithy models
30/// (30 distinct operations; the four shared ops appear once).
31pub const TIMESTREAM_ACTIONS: &[&str] = &[
32    // Write control plane
33    "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 plane
49    "Query",
50    "CancelQuery",
51    "PrepareQuery",
52    "CreateScheduledQuery",
53    "DeleteScheduledQuery",
54    "DescribeScheduledQuery",
55    "ExecuteScheduledQuery",
56    "ListScheduledQueries",
57    "UpdateScheduledQuery",
58    "DescribeAccountSettings",
59    "UpdateAccountSettings",
60    // Shared
61    "DescribeEndpoints",
62    "ListTagsForResource",
63    "TagResource",
64    "UntagResource",
65];
66
67/// Read-only verbs; any other action mutates persisted state and triggers a
68/// snapshot after success.
69fn 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    /// Persist hook for the CloudFormation provisioner; `None` in memory mode.
107    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            // Databases
158            "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            // Tables
164            "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            // Ingestion
170            "WriteRecords" => self.write_records(req, &body),
171            // Batch load
172            "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
177            "Query" => self.query(req, &body),
178            "CancelQuery" => self.cancel_query(req, &body),
179            "PrepareQuery" => self.prepare_query(req, &body),
180            // Scheduled queries
181            "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            // Account settings
188            "DescribeAccountSettings" => self.describe_account_settings(req),
189            "UpdateAccountSettings" => self.update_account_settings(req, &body),
190            // Endpoints + tagging
191            "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
203// ---- error / response helpers --------------------------------------------
204
205fn 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
229// ---- small member helpers -------------------------------------------------
230
231fn 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
242/// Render an ARN-keyed tag map as a `TagList` (`[{Key,Value}]`).
243fn 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
251/// Merge a `TagList` body member into an ARN-keyed tag map.
252fn 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
301// ---- databases ------------------------------------------------------------
302
303impl 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(&region, &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
437// ---- tables ---------------------------------------------------------------
438
439fn 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(&region, &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
608// ---- ingestion ------------------------------------------------------------
609
610fn 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        // Build (or reject) every record before touching state.
645        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
694/// Merge `CommonAttributes` under a record and validate it. Returns the
695/// normalized [`StoredRecord`] or a human-readable rejection reason.
696fn 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    // Dimensions: common dimensions first, then record dimensions.
705    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
771// ---- query ----------------------------------------------------------------
772
773impl 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        // Paginate the row list with a round-tripping offset NextToken.
796        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        // Reshape ColumnInfo into SelectColumn (adds Database/Table/Aliased).
859        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        // Named parameters (`@name`) become ParameterMapping entries.
873        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
897// ---- scheduled queries ----------------------------------------------------
898
899impl 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(&region, &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
1093// ---- batch load -----------------------------------------------------------
1094
1095impl 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
1247// ---- account settings -----------------------------------------------------
1248
1249/// Map an `UpdateAccountSettings` request `QueryComputeRequest` onto the
1250/// `QueryComputeResponse` shape the API returns: `ProvisionedCapacity`'s
1251/// request-only `TargetQueryTCU` becomes the response's `ActiveQueryTCU`, and a
1252/// settled `LastUpdate` is synthesized. `ComputeMode` and
1253/// `NotificationConfiguration` pass through.
1254fn 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        // The request carries a `QueryComputeRequest`
1309        // (`ProvisionedCapacity.TargetQueryTCU`); the response expects a
1310        // `QueryComputeResponse` (`ProvisionedCapacity.ActiveQueryTCU` + a
1311        // `LastUpdate`). Map it so a `Describe` / `Update` echo is shape-valid.
1312        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
1328// ---- endpoints + tagging --------------------------------------------------
1329
1330impl 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
1393// ---- pagination -----------------------------------------------------------
1394
1395/// Decode a numeric offset NextToken; a missing/garbage token starts at 0.
1396fn decode_token(token: Option<&str>) -> usize {
1397    token.and_then(|t| t.parse::<usize>().ok()).unwrap_or(0)
1398}
1399
1400/// Slice `items[start..]` to at most `max` entries, returning the page plus the
1401/// next offset token when more remain.
1402fn 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        // Duplicate is a conflict.
1468        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}