datafold 0.1.28

A personal database for data sovereignty with AI-powered ingestion and S3 support
Documentation
# DynamoDB Implementation Review

## Overview
This review covers two DynamoDB implementations:
1. **`dynamodb_store.rs`** - Schema storage backend for the schema service
2. **`dynamodb_backend.rs`** - Generic KV store and namespaced store implementations

## Strengths

### 1. Good Architecture
- Clear separation between schema storage and generic KV storage
- Proper use of async/await throughout
- Good abstraction with trait implementations (`KvStore`, `NamespacedStore`)

### 2. Efficient Query Patterns
- Uses `Query` operations instead of `Scan` where possible (in `dynamodb_backend.rs`)
- Proper pagination handling with `last_evaluated_key`
- Efficient prefix scanning using `begins_with` on sort keys

### 3. Multi-tenant Support
- Well-designed partition key strategy using `user_id` for isolation
- Proper key structure: `PK = user_id`, `SK = actual_key`

## Critical Issues

### 1. Missing Retry Logic for Transient Failures ⚠️
**Location**: Both files
**Issue**: No retry logic for transient AWS errors (throttling, service errors)
**Impact**: Operations will fail immediately on transient errors
**Recommendation**: 
```rust
// Add retry with exponential backoff for:
// - ProvisionedThroughputExceededException
// - ThrottlingException
// - Service errors (5xx)
```

### 2. Batch Operations Don't Handle Unprocessed Items ⚠️
**Location**: `dynamodb_backend.rs` lines 190-228, 230-266, 588-620, 622-653
**Issue**: `batch_write_item` and `batch_delete` don't retry unprocessed items
**Impact**: Items may silently fail to write/delete
**Current Code**:
```rust
self.client
    .batch_write_item()
    .set_request_items(Some(requests))
    .send()
    .await
    .map_err(|e| StorageError::DynamoDbError(e.to_string()))?;
```
**Recommendation**: Check `unprocessed_items` and retry with exponential backoff

### 3. No Table Existence Validation in `dynamodb_store.rs` ⚠️
**Location**: `dynamodb_store.rs` - `new()` method
**Issue**: Doesn't verify table exists before operations
**Impact**: Operations will fail at runtime with cryptic errors
**Recommendation**: Add table validation on initialization or handle `ResourceNotFoundException` gracefully

### 4. Inefficient Scan Operations in Schema Store
**Location**: `dynamodb_store.rs` lines 123-158, 161-194
**Issue**: Uses `Scan` operations which are expensive and don't scale well
**Impact**: Performance degradation as table grows, higher costs
**Recommendation**: 
- Consider using GSI (Global Secondary Index) if listing is frequent
- Or use `Query` with a partition key if schemas can be grouped
- Add pagination limits to prevent large scans

### 5. Missing Error Context
**Location**: Both files
**Issue**: Error messages don't include operation context (table name, key, etc.)
**Impact**: Difficult to debug production issues
**Example**:
```rust
// Current
.map_err(|e| FoldDbError::Database(format!("DynamoDB get_item failed: {}", e)))?

// Better
.map_err(|e| FoldDbError::Database(format!(
    "DynamoDB get_item failed for table '{}', key '{}': {}", 
    self.table_name, schema_name, e
)))?
```

### 6. Inconsistent Error Types
**Location**: `dynamodb_store.rs` vs `dynamodb_backend.rs`
**Issue**: 
- `dynamodb_store.rs` uses `FoldDbError`
- `dynamodb_backend.rs` uses `StorageError`
**Impact**: Inconsistent error handling in calling code
**Recommendation**: Standardize on one error type or provide clear conversion

### 7. No Connection Pooling Configuration
**Location**: Both files - client initialization
**Issue**: Uses default AWS SDK configuration without tuning
**Impact**: May not be optimal for high-throughput scenarios
**Recommendation**: Allow configuration of:
- Max connections
- Connection timeout
- Request timeout
- Retry configuration

### 8. Race Condition in Table Creation
**Location**: `dynamodb_backend.rs` lines 366-445
**Issue**: Multiple processes creating the same table simultaneously could cause issues
**Current**: Checks for `ResourceInUseException` but doesn't wait for table to be ACTIVE
**Impact**: First operation after table creation might fail
**Recommendation**: 
```rust
// Wait for table to be ACTIVE before returning
let waiter = self.client.wait_until_table_exists()
    .table_name(table_name)
    .send()
    .await;
```

### 9. Missing Timestamp Updates in `put_schema`
**Location**: `dynamodb_store.rs` lines 91-120
**Issue**: Both `CreatedAt` and `UpdatedAt` are set to current time on every put
**Impact**: `CreatedAt` should only be set on first creation
**Recommendation**: Use conditional expression or separate creation timestamp logic

### 10. No Validation of Attribute Values
**Location**: Both files
**Issue**: No validation that attribute values fit DynamoDB limits
**Impact**: 
- String values > 400KB will fail
- Binary values > 400KB will fail
- Item size > 400KB will fail
**Recommendation**: Add validation or chunking for large values

## Medium Priority Issues

### 11. Missing Metrics/Monitoring
**Issue**: No instrumentation for operation latency, error rates, throttling
**Recommendation**: Add metrics for:
- Operation duration
- Error counts by type
- Throttling events
- Batch operation success rates

### 12. Inefficient `clear_all_schemas` Implementation
**Location**: `dynamodb_store.rs` lines 197-211
**Issue**: Deletes items one by one instead of using batch operations
**Impact**: Slow for large tables
**Recommendation**: Use `batch_write_item` with delete requests

### 13. No Support for Conditional Writes
**Location**: `dynamodb_store.rs` - `put_schema`
**Issue**: No way to prevent overwriting existing schemas
**Impact**: Accidental overwrites possible
**Recommendation**: Add optional `condition_expression` parameter

### 14. Hardcoded Batch Size
**Location**: `dynamodb_backend.rs` - multiple locations
**Issue**: Batch size of 25 is hardcoded (DynamoDB limit)
**Impact**: Not configurable, but this is actually correct
**Note**: This is fine, but could be a constant for clarity

### 15. Missing GSI Support
**Issue**: No Global Secondary Index support for alternative query patterns
**Impact**: Limited query flexibility
**Recommendation**: Consider adding GSI support if needed for future queries

## Code Quality Issues

### 16. Inconsistent Error Handling Patterns
**Location**: Both files
**Issue**: Mix of `.map_err()` and direct error construction
**Recommendation**: Use consistent error handling utility functions

### 17. Missing Documentation
**Issue**: Some methods lack documentation about:
- DynamoDB costs
- Performance characteristics
- Error conditions
- Retry behavior
**Recommendation**: Add comprehensive doc comments

### 18. Test Coverage
**Location**: `dynamodb_store.rs` lines 219-291
**Issue**: Tests are marked `#[ignore]` and require manual setup
**Impact**: No automated testing
**Recommendation**: 
- Add LocalStack integration tests
- Add unit tests with mocked clients
- Add integration tests in CI/CD

### 19. No Rate Limiting Protection
**Issue**: No client-side rate limiting
**Impact**: Could overwhelm DynamoDB and hit throttling
**Recommendation**: Add rate limiting or use AWS SDK's built-in retry with backoff

### 20. Missing Timeout Configuration
**Issue**: No explicit timeout configuration
**Impact**: Operations could hang indefinitely
**Recommendation**: Add configurable timeouts

## Security Considerations

### 21. No Encryption at Rest Mention
**Issue**: No documentation about encryption requirements
**Recommendation**: Document that DynamoDB encryption should be enabled at table level

### 22. Credential Handling
**Location**: `dynamodb_store.rs` line 50
**Issue**: Uses default AWS credential chain
**Impact**: Relies on environment/IAM roles (this is actually correct)
**Note**: This is fine, but should be documented

## Performance Optimizations

### 23. Projection Expressions
**Location**: `dynamodb_backend.rs` line 128
**Good**: Uses `projection_expression` in `exists()` to reduce data transfer
**Recommendation**: Apply to other operations where only keys are needed

### 24. Parallel Batch Operations
**Issue**: Batch operations are sequential
**Recommendation**: Consider parallelizing batch chunks (with rate limiting)

## Recommendations Summary

### High Priority
1. ✅ Add retry logic with exponential backoff
2. ✅ Handle unprocessed items in batch operations
3. ✅ Add table existence validation
4. ✅ Replace scans with queries where possible
5. ✅ Add error context to all error messages

### Medium Priority
6. Add metrics/monitoring
7. Optimize `clear_all_schemas` with batch deletes
8. Add conditional write support
9. Wait for table ACTIVE status after creation
10. Fix `CreatedAt` timestamp logic

### Low Priority
11. Add comprehensive documentation
12. Improve test coverage
13. Add timeout configuration
14. Consider GSI support for future needs

## Code Examples for Fixes

### Example 1: Retry Logic
```rust
use aws_sdk_dynamodb::error::SdkError;
use aws_sdk_dynamodb::operation::get_item::GetItemError;

async fn get_with_retry(&self, key: &str) -> FoldDbResult<Option<Schema>> {
    let mut retries = 0;
    let max_retries = 3;
    
    loop {
        match self.get_schema_internal(key).await {
            Ok(result) => return Ok(result),
            Err(e) => {
                if retries >= max_retries {
                    return Err(e);
                }
                
                // Check if retryable
                if is_retryable_error(&e) {
                    let delay = exponential_backoff(retries);
                    tokio::time::sleep(delay).await;
                    retries += 1;
                    continue;
                }
                return Err(e);
            }
        }
    }
}

fn is_retryable_error(e: &FoldDbError) -> bool {
    match e {
        FoldDbError::Database(msg) => {
            msg.contains("ProvisionedThroughputExceededException") ||
            msg.contains("ThrottlingException") ||
            msg.contains("ServiceUnavailable")
        }
        _ => false
    }
}
```

### Example 2: Handle Unprocessed Items
```rust
async fn batch_put_with_retry(&self, items: Vec<(Vec<u8>, Vec<u8>)>) -> StorageResult<()> {
    let mut remaining = items;
    let mut retries = 0;
    let max_retries = 5;
    
    while !remaining.is_empty() && retries < max_retries {
        let result = self.batch_put_internal(&remaining).await?;
        
        if let Some(unprocessed) = result.unprocessed_items {
            remaining = unprocessed_to_items(unprocessed)?;
            let delay = exponential_backoff(retries);
            tokio::time::sleep(delay).await;
            retries += 1;
        } else {
            break;
        }
    }
    
    if !remaining.is_empty() {
        return Err(StorageError::DynamoDbError(
            format!("Failed to process {} items after {} retries", 
                    remaining.len(), max_retries)
        ));
    }
    
    Ok(())
}
```

### Example 3: Table Validation
```rust
pub async fn new(config: DynamoDbConfig) -> FoldDbResult<Self> {
    let aws_config = aws_config::defaults(aws_config::BehaviorVersion::latest())
        .region(aws_sdk_dynamodb::config::Region::new(config.region.clone()))
        .load()
        .await;

    let client = DynamoClient::new(&aws_config);
    
    // Validate table exists
    client
        .describe_table()
        .table_name(&config.table_name)
        .send()
        .await
        .map_err(|e| FoldDbError::Config(format!(
            "DynamoDB table '{}' does not exist or is not accessible: {}", 
            config.table_name, e
        )))?;

    Ok(Self {
        client,
        table_name: config.table_name,
    })
}
```

## Conclusion

The DynamoDB implementation is functional but needs improvements in:
- **Reliability**: Retry logic and unprocessed item handling
- **Performance**: Replace scans with queries, optimize batch operations
- **Observability**: Add metrics and better error context
- **Robustness**: Table validation, timeout configuration, error handling

The architecture is sound, but production readiness requires addressing the critical issues listed above.