1use crate::error::{ErrorData, Result};
2use crate::traits::{Binding, Kv, PutOptions, ScanResult};
3use alien_azure_clients::tables::{
4 AzureTableStorageClient, EntityQueryContinuation, EntityQueryOptions, TableEntity,
5 TableStorageApi,
6};
7use alien_error::{AlienError, Context, IntoAlienError};
8use async_trait::async_trait;
9use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use std::collections::HashMap;
14use std::fmt::{Debug, Formatter};
15
16use super::{validate_key, validate_value};
17
18fn create_table_entity(
21 partition_key: String,
22 row_key: String,
23 value: &[u8],
24 expires_at: Option<DateTime<Utc>>,
25) -> TableEntity {
26 let mut properties = HashMap::new();
27
28 properties.insert("Value".to_string(), Value::String(BASE64.encode(value)));
31
32 properties.insert(
34 "CreatedAt".to_string(),
35 Value::String(Utc::now().to_rfc3339()),
36 );
37
38 if let Some(expiry) = expires_at {
40 properties.insert("ExpiresAt".to_string(), Value::String(expiry.to_rfc3339()));
41 }
42
43 TableEntity {
44 partition_key,
45 row_key,
46 timestamp: None, properties,
48 }
49}
50
51fn extract_value_from_entity(entity: &TableEntity) -> Result<Vec<u8>> {
53 let value_str = entity
54 .properties
55 .get("Value")
56 .and_then(|v| v.as_str())
57 .ok_or_else(|| {
58 AlienError::new(ErrorData::InvalidInput {
59 operation_context: "Azure Table Storage KV extract value".to_string(),
60 details: "Entity missing Value property or not a string".to_string(),
61 field_name: Some("Value".to_string()),
62 })
63 })?;
64
65 BASE64
67 .decode(value_str)
68 .into_alien_error()
69 .context(ErrorData::InvalidInput {
70 operation_context: "Azure Table Storage KV extract value".to_string(),
71 details: "Failed to decode base64 value".to_string(),
72 field_name: Some("Value".to_string()),
73 })
74}
75
76fn is_entity_expired(entity: &TableEntity) -> bool {
78 if let Some(expires_at_value) = entity.properties.get("ExpiresAt") {
79 if let Some(expires_at_str) = expires_at_value.as_str() {
80 if let Ok(expires_at) = DateTime::parse_from_rfc3339(expires_at_str) {
81 return Utc::now() > expires_at.with_timezone(&Utc);
82 }
83 }
84 }
85 false
86}
87
88#[derive(Serialize, Deserialize)]
90#[serde(rename_all = "camelCase")]
91struct CursorState {
92 version: u8,
93 prefix: String,
94 current_partition: u32,
95 continuation: Option<EntityQueryContinuation>,
96}
97
98pub struct AzureTableStorageKv {
100 client: AzureTableStorageClient,
101 resource_group_name: String,
102 account_name: String,
103 table_name: String,
104 num_partitions: u32,
105}
106
107impl Debug for AzureTableStorageKv {
108 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
109 f.debug_struct("AzureTableStorageKv")
110 .field("resource_group_name", &self.resource_group_name)
111 .field("account_name", &self.account_name)
112 .field("table_name", &self.table_name)
113 .field("num_partitions", &self.num_partitions)
114 .finish()
115 }
116}
117
118impl AzureTableStorageKv {
119 pub fn new(
120 client: AzureTableStorageClient,
121 resource_group_name: String,
122 account_name: String,
123 table_name: String,
124 ) -> Self {
125 Self {
126 client,
127 resource_group_name,
128 account_name,
129 table_name,
130 num_partitions: 16, }
132 }
133
134 fn hash_bucket(&self, key: &str) -> u32 {
136 use std::collections::hash_map::DefaultHasher;
137 use std::hash::{Hash, Hasher};
138
139 let mut hasher = DefaultHasher::new();
140 key.hash(&mut hasher);
141 hasher.finish() as u32 % self.num_partitions
142 }
143
144 async fn try_take_over_expired(
150 &self,
151 key: &str,
152 entity: &alien_azure_clients::azure::tables::TableEntity,
153 ) -> Result<bool> {
154 use alien_client_core::ErrorData as CloudErrorData;
155
156 let (partition_key, row_key) = self.split_key(key);
157 let existing = match self
158 .client
159 .get_entity(
160 &self.resource_group_name,
161 &self.account_name,
162 &self.table_name,
163 &partition_key,
164 &row_key,
165 None,
166 )
167 .await
168 {
169 Ok(existing) => existing,
170 Err(e)
173 if matches!(
174 e.error.as_ref(),
175 Some(CloudErrorData::RemoteResourceNotFound { .. })
176 ) =>
177 {
178 return Ok(false);
179 }
180 Err(e) => {
181 return Err(crate::error::map_cloud_client_error(
182 e,
183 format!("Failed to read existing entity for key '{}'", key),
184 Some(key.to_string()),
185 ));
186 }
187 };
188
189 if !is_entity_expired(&existing) {
190 return Ok(false);
191 }
192 let Some(etag) = existing
193 .properties
194 .get("odata.etag")
195 .and_then(|value| value.as_str())
196 else {
197 return Ok(false);
199 };
200
201 match self
202 .client
203 .update_entity(
204 &self.resource_group_name,
205 &self.account_name,
206 &self.table_name,
207 &partition_key,
208 &row_key,
209 entity,
210 Some(alien_azure_clients::azure::tables::ETag::from(etag)),
211 )
212 .await
213 {
214 Ok(_) => Ok(true),
215 Err(e)
217 if matches!(
218 e.error.as_ref(),
219 Some(CloudErrorData::RemoteResourceConflict { .. })
220 | Some(CloudErrorData::RemoteResourceNotFound { .. })
221 ) =>
222 {
223 Ok(false)
224 }
225 Err(e) => Err(crate::error::map_cloud_client_error(
226 e,
227 format!("Failed to take over expired entity for key '{}'", key),
228 Some(key.to_string()),
229 )),
230 }
231 }
232
233 fn split_key(&self, key: &str) -> (String, String) {
234 let partition_key = format!("p{}", self.hash_bucket(key));
236 (partition_key, key.to_string())
237 }
238
239 fn combine_key(&self, _partition_key: &str, row_key: &str) -> String {
241 row_key.to_string() }
243
244 fn encode_cursor(&self, state: &CursorState) -> Result<String> {
246 let json =
247 serde_json::to_vec(state)
248 .into_alien_error()
249 .context(ErrorData::InvalidInput {
250 operation_context: "Azure Table Storage KV cursor encoding".to_string(),
251 details: "Failed to serialize cursor state".to_string(),
252 field_name: Some("cursor".to_string()),
253 })?;
254 Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json))
255 }
256
257 fn decode_cursor(&self, prefix: &str, cursor: &str) -> Result<CursorState> {
259 let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD
260 .decode(cursor)
261 .into_alien_error()
262 .context(ErrorData::InvalidInput {
263 operation_context: "Azure Table Storage KV cursor decoding".to_string(),
264 details: "Invalid cursor encoding".to_string(),
265 field_name: Some("cursor".to_string()),
266 })?;
267 let json =
268 String::from_utf8(decoded)
269 .into_alien_error()
270 .context(ErrorData::InvalidInput {
271 operation_context: "Azure Table Storage KV cursor decoding".to_string(),
272 details: "Invalid cursor UTF-8".to_string(),
273 field_name: Some("cursor".to_string()),
274 })?;
275 let state: CursorState =
276 serde_json::from_str(&json)
277 .into_alien_error()
278 .context(ErrorData::InvalidInput {
279 operation_context: "Azure Table Storage KV cursor decoding".to_string(),
280 details: "Invalid cursor JSON".to_string(),
281 field_name: Some("cursor".to_string()),
282 })?;
283 if state.version != 1
284 || state.prefix != prefix
285 || state.current_partition >= self.num_partitions
286 {
287 return Err(AlienError::new(ErrorData::InvalidInput {
288 operation_context: "Azure Table Storage KV cursor validation".to_string(),
289 details: "Cursor does not belong to this prefix scan".to_string(),
290 field_name: Some("cursor".to_string()),
291 }));
292 }
293 Ok(state)
294 }
295}
296
297impl Binding for AzureTableStorageKv {}
298
299#[async_trait]
300impl Kv for AzureTableStorageKv {
301 async fn get(&self, key: &str) -> Result<Option<Vec<u8>>> {
302 validate_key(key)?;
303
304 let (partition_key, row_key) = self.split_key(key);
305
306 match self
307 .client
308 .get_entity(
309 &self.resource_group_name,
310 &self.account_name,
311 &self.table_name,
312 &partition_key,
313 &row_key,
314 None,
315 )
316 .await
317 {
318 Ok(entity) => {
319 if is_entity_expired(&entity) {
321 return Ok(None); }
323
324 let value = extract_value_from_entity(&entity)?;
325 Ok(Some(value))
326 }
327 Err(e) => {
328 use alien_client_core::ErrorData as CloudErrorData;
329 match e.error.as_ref() {
330 Some(CloudErrorData::RemoteResourceNotFound { .. }) => Ok(None),
331 _ => Err(crate::error::map_cloud_client_error(
332 e,
333 format!("Failed to get entity for key '{}'", key),
334 Some(key.to_string()),
335 )),
336 }
337 }
338 }
339 }
340
341 async fn put(&self, key: &str, value: Vec<u8>, options: Option<PutOptions>) -> Result<bool> {
342 validate_key(key)?;
343 validate_value(&value)?;
344
345 let options = options.unwrap_or_default();
346 let (partition_key, row_key) = self.split_key(key);
347
348 let expires_at = options.ttl.map(|d| Utc::now() + d);
349 let entity =
350 create_table_entity(partition_key.clone(), row_key.clone(), &value, expires_at);
351
352 if options.if_not_exists {
353 match self
354 .client
355 .insert_entity(
356 &self.resource_group_name,
357 &self.account_name,
358 &self.table_name,
359 &entity,
360 )
361 .await
362 {
363 Ok(_) => Ok(true),
364 Err(e) => {
365 use alien_client_core::ErrorData as CloudErrorData;
366 match e.error.as_ref() {
367 Some(CloudErrorData::RemoteResourceConflict { .. }) => {
368 self.try_take_over_expired(key, &entity).await
377 }
378 _ => Err(crate::error::map_cloud_client_error(
379 e,
380 format!("Failed to insert entity for key '{}'", key),
381 Some(key.to_string()),
382 )),
383 }
384 }
385 }
386 } else {
387 self.client
389 .insert_or_replace_entity(
390 &self.resource_group_name,
391 &self.account_name,
392 &self.table_name,
393 &partition_key,
394 &row_key,
395 &entity,
396 )
397 .await
398 .map_err(|e| {
399 crate::error::map_cloud_client_error(
400 e,
401 format!("Failed to upsert entity for key '{}'", key),
402 Some(key.to_string()),
403 )
404 })?;
405 Ok(true)
406 }
407 }
408
409 async fn delete(&self, key: &str) -> Result<()> {
410 validate_key(key)?;
411
412 let (partition_key, row_key) = self.split_key(key);
413
414 match self
416 .client
417 .delete_entity(
418 &self.resource_group_name,
419 &self.account_name,
420 &self.table_name,
421 &partition_key,
422 &row_key,
423 None, )
425 .await
426 {
427 Ok(_) => Ok(()),
428 Err(e) => {
429 use alien_client_core::ErrorData as CloudErrorData;
430 match e.error.as_ref() {
431 Some(CloudErrorData::RemoteResourceNotFound { .. }) => Ok(()), _ => Err(crate::error::map_cloud_client_error(
433 e,
434 format!("Failed to delete entity for key '{}'", key),
435 Some(key.to_string()),
436 )),
437 }
438 }
439 }
440 }
441
442 async fn exists(&self, key: &str) -> Result<bool> {
443 validate_key(key)?;
444
445 let (partition_key, row_key) = self.split_key(key);
446
447 match self
448 .client
449 .get_entity(
450 &self.resource_group_name,
451 &self.account_name,
452 &self.table_name,
453 &partition_key,
454 &row_key,
455 None,
456 )
457 .await
458 {
459 Ok(entity) => {
460 Ok(!is_entity_expired(&entity))
462 }
463 Err(e) => {
464 use alien_client_core::ErrorData as CloudErrorData;
465 match e.error.as_ref() {
466 Some(CloudErrorData::RemoteResourceNotFound { .. }) => Ok(false),
467 _ => Err(crate::error::map_cloud_client_error(
468 e,
469 format!("Failed to check existence of entity for key '{}'", key),
470 Some(key.to_string()),
471 )),
472 }
473 }
474 }
475 }
476
477 async fn scan_prefix(
478 &self,
479 prefix: &str,
480 limit: Option<usize>,
481 cursor: Option<String>,
482 ) -> Result<ScanResult> {
483 validate_key(prefix)?;
484 let limit = limit.unwrap_or(1000);
485 let initial = cursor
486 .as_deref()
487 .map(|cursor| self.decode_cursor(prefix, cursor))
488 .transpose()?
489 .unwrap_or(CursorState {
490 version: 1,
491 prefix: prefix.to_string(),
492 current_partition: 0,
493 continuation: None,
494 });
495 if limit == 0 {
496 return Ok(ScanResult {
497 items: Vec::new(),
498 next_cursor: cursor,
499 });
500 }
501
502 let mut items = Vec::with_capacity(limit);
503 let mut partition_id = initial.current_partition;
504 let mut continuation = initial.continuation;
505
506 while partition_id < self.num_partitions {
507 let partition_key = format!("p{}", partition_id);
508
509 let prefix_end = format!("{}~", prefix); let filter = format!(
513 "(PartitionKey eq '{}') and (RowKey ge '{}') and (RowKey lt '{}')",
514 partition_key, prefix, prefix_end
515 );
516
517 let filter_with_ttl = filter;
519
520 let query_options = EntityQueryOptions {
521 filter: Some(filter_with_ttl),
522 select: None,
523 top: Some(u32::try_from(limit - items.len()).unwrap_or(u32::MAX)),
524 continuation: continuation.clone(),
525 };
526
527 let response = self
528 .client
529 .query_entities(
530 &self.resource_group_name,
531 &self.account_name,
532 &self.table_name,
533 Some(query_options),
534 )
535 .await
536 .map_err(|e| {
537 crate::error::map_cloud_client_error(
538 e,
539 format!("Failed to query entities with prefix '{}'", prefix),
540 Some(prefix.to_string()),
541 )
542 })?;
543
544 for entity in response.entities {
545 if is_entity_expired(&entity) {
546 continue;
547 }
548
549 let key = self.combine_key(&entity.partition_key, &entity.row_key);
550 let value = extract_value_from_entity(&entity)?;
551
552 items.push((key, value));
553 }
554
555 if items.len() == limit {
556 let (next_partition, next_continuation) = match response.continuation {
557 Some(continuation) => (partition_id, Some(continuation)),
558 None if partition_id + 1 < self.num_partitions => (partition_id + 1, None),
559 None => {
560 return Ok(ScanResult {
561 items,
562 next_cursor: None,
563 });
564 }
565 };
566 return Ok(ScanResult {
567 items,
568 next_cursor: Some(self.encode_cursor(&CursorState {
569 version: 1,
570 prefix: prefix.to_string(),
571 current_partition: next_partition,
572 continuation: next_continuation,
573 })?),
574 });
575 }
576
577 if let Some(next) = response.continuation {
578 continuation = Some(next);
579 } else {
580 partition_id += 1;
581 continuation = None;
582 }
583 }
584
585 Ok(ScanResult {
586 items,
587 next_cursor: None,
588 })
589 }
590}