Skip to main content

dynoxide/actions/
delete_table.rs

1use crate::actions::vector_lifecycle::VectorIndexLifecycle;
2use crate::actions::{TableDescription, build_table_description};
3use crate::errors::{DynoxideError, Result};
4use crate::storage_backend::StorageBackend;
5use crate::types::{GlobalSecondaryIndex, LocalSecondaryIndex};
6use serde::{Deserialize, Serialize};
7
8/// Internal deserialization struct for detecting missing TableName.
9#[derive(Debug, Default, Deserialize)]
10struct DeleteTableRequestRaw {
11    #[serde(rename = "TableName", default)]
12    table_name: Option<String>,
13}
14
15#[derive(Debug, Default)]
16pub struct DeleteTableRequest {
17    pub table_name: String,
18}
19
20impl<'de> serde::Deserialize<'de> for DeleteTableRequest {
21    fn deserialize<D: serde::Deserializer<'de>>(
22        deserializer: D,
23    ) -> std::result::Result<Self, D::Error> {
24        let raw = DeleteTableRequestRaw::deserialize(deserializer)?;
25
26        if raw.table_name.is_none() {
27            return Err(serde::de::Error::custom(
28                "VALIDATION:The parameter 'TableName' is required but was not present in the request",
29            ));
30        }
31        let table_name = raw.table_name.unwrap();
32
33        // Length check (before pattern, matching DynamoDB ordering)
34        if table_name.len() < 3 || table_name.len() > 255 {
35            return Err(serde::de::Error::custom(
36                "VALIDATION:TableName must be at least 3 characters long and at most 255 characters long",
37            ));
38        }
39
40        // Pattern check (only reached if length is valid)
41        if !table_name
42            .chars()
43            .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
44        {
45            return Err(serde::de::Error::custom(format!(
46                "VALIDATION:1 validation error detected: \
47                 Value '{}' at 'tableName' failed to satisfy constraint: \
48                 Member must satisfy regular expression pattern: [a-zA-Z0-9_.-]+",
49                table_name
50            )));
51        }
52
53        Ok(DeleteTableRequest { table_name })
54    }
55}
56
57#[derive(Debug, Default, Serialize)]
58pub struct DeleteTableResponse {
59    #[serde(rename = "TableDescription")]
60    pub table_description: TableDescription,
61}
62
63pub async fn execute<S: StorageBackend>(
64    storage: &S,
65    request: DeleteTableRequest,
66    lifecycle: &VectorIndexLifecycle,
67) -> Result<DeleteTableResponse> {
68    // Validate table name format before checking existence (DynamoDB validates input first)
69    crate::validation::validate_table_name(&request.table_name)?;
70
71    // Get metadata before deletion (for the response)
72    let meta = storage
73        .get_table_metadata(&request.table_name)
74        .await?
75        .ok_or_else(|| {
76            DynoxideError::ResourceNotFoundException(format!(
77                "Requested resource not found: Table: {} not found",
78                request.table_name
79            ))
80        })?;
81
82    // Check deletion protection
83    if meta.deletion_protection_enabled {
84        return Err(DynoxideError::ValidationException(
85            "Resource cannot be deleted as it is currently protected against deletion. \
86             Disable deletion protection first."
87                .to_string(),
88        ));
89    }
90
91    // A table cannot be dropped underneath an index that is still being
92    // created, and it refuses for as long as the index is creating rather than
93    // for a fixed number of calls: the documented readiness pattern polls
94    // DescribeTable an unbounded number of times. Sits after the deletion
95    // protection check so a protected table still answers its own message
96    // first. Only indexes the table still defines are considered, so a stale
97    // entry for one that has gone cannot refuse a delete.
98    //
99    // Definitions that will not parse read as no indexes, the way every other
100    // reader of this column treats them: build_table_description reports none,
101    // and the drop loop below has always skipped them. A table whose metadata
102    // has become unreadable should stay deletable rather than be pinned by a
103    // guard nothing can now satisfy.
104    let vector_indexes: Vec<crate::types::VectorIndex> = meta
105        .vector_index_definitions
106        .as_ref()
107        .and_then(|json| serde_json::from_str(json).ok())
108        .unwrap_or_default();
109    let vector_phases = lifecycle.phases_of(
110        storage,
111        &request.table_name,
112        vector_indexes.iter().map(|v| v.index_name.as_str()),
113    );
114    if vector_phases.any_creating() {
115        return Err(DynoxideError::ResourceInUseException(
116            "Attempt to change a resource which is still in use: Cannot delete table while \
117             indexes are being created, updated, or deleted."
118                .to_string(),
119        ));
120    }
121
122    // Forget the entries here rather than after the drops below: the guard has
123    // just proved every one of them has settled, so none can affect an answer,
124    // and any of the drops can fail and return early. Left until the end, a
125    // half-finished delete would leak an entry per attempt.
126    lifecycle.forget_table(&request.table_name);
127
128    // Drop GSI tables first
129    if let Some(ref gsi_json) = meta.gsi_definitions
130        && let Ok(gsis) = serde_json::from_str::<Vec<GlobalSecondaryIndex>>(gsi_json)
131    {
132        for gsi in &gsis {
133            storage
134                .drop_gsi_table(&request.table_name, &gsi.index_name)
135                .await?;
136        }
137    }
138
139    // Drop LSI tables
140    if let Some(ref lsi_json) = meta.lsi_definitions
141        && let Ok(lsis) = serde_json::from_str::<Vec<LocalSecondaryIndex>>(lsi_json)
142    {
143        for lsi in &lsis {
144            storage
145                .drop_lsi_table(&request.table_name, &lsi.index_name)
146                .await?;
147        }
148    }
149
150    // Drop vector index shadow tables, from the definitions the guard above
151    // already parsed.
152    for vix in &vector_indexes {
153        storage
154            .drop_vector_table(&request.table_name, &vix.index_name)
155            .await?;
156    }
157
158    // Drop data table
159    storage.drop_data_table(&request.table_name).await?;
160
161    // Delete metadata
162    storage.delete_table_metadata(&request.table_name).await?;
163
164    // Build response with DELETING status. The guard above ruled out a creating
165    // index, so every phase in the set reports ACTIVE.
166    let mut desc = build_table_description(&meta, Some(0), Some(0), &vector_phases);
167    desc.table_status = "DELETING".to_string();
168
169    Ok(DeleteTableResponse {
170        table_description: desc,
171    })
172}