use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{de::DeserializeOwned, Serialize};
use sqlx::postgres::{PgPool, PgPoolOptions, PgQueryResult, PgRow};
use sqlx::{Executor, Postgres, Row, Transaction as SqlxTransaction};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tracing::{debug, error, info, instrument, warn};
use super::config::PostgreSQLConfig;
use super::error::{StorageError, StorageResult};
use super::traits::{RelationalStorage, SqlParam, SqlResult, Storage};
use super::types::{
Batch, Operator, Operation, Query, SortDirection, StorageBackend, StorageEntry, StorageHealth,
StorageMetadata, StorageStats, Transaction, TransactionState,
};
#[derive(Debug, Clone)]
pub struct PostgreSQLStorage {
pool: PgPool,
config: PostgreSQLConfig,
stats: Arc<RwLock<StorageStats>>,
transactions: Arc<RwLock<HashMap<String, TransactionContext>>>,
initialized: Arc<RwLock<bool>>,
}
#[derive(Debug)]
struct TransactionContext {
transaction: Transaction,
operations: Vec<Operation>,
}
impl PostgreSQLStorage {
#[instrument(skip(config))]
pub async fn new(config: PostgreSQLConfig) -> StorageResult<Self> {
info!("Creating PostgreSQL storage backend");
let pool = PgPoolOptions::new()
.min_connections(2)
.max_connections(10)
.acquire_timeout(config.connect_timeout)
.idle_timeout(Some(std::time::Duration::from_secs(600)))
.max_lifetime(Some(std::time::Duration::from_secs(1800)))
.connect(&config.connection_url())
.await
.map_err(|e| {
error!("Failed to create connection pool: {}", e);
StorageError::ConnectionError {
source: Box::new(e),
}
})?;
info!("PostgreSQL connection pool created successfully");
Ok(Self {
pool,
config,
stats: Arc::new(RwLock::new(StorageStats::new())),
transactions: Arc::new(RwLock::new(HashMap::new())),
initialized: Arc::new(RwLock::new(false)),
})
}
pub fn pool(&self) -> &PgPool {
&self.pool
}
#[instrument(skip(self))]
async fn initialize_schema(&self) -> StorageResult<()> {
info!("Initializing PostgreSQL schema");
let start = std::time::Instant::now();
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS storage_entries (
table_name VARCHAR(255) NOT NULL,
key VARCHAR(255) NOT NULL,
value JSONB NOT NULL,
version BIGINT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ,
metadata JSONB,
PRIMARY KEY (table_name, key)
)
"#,
)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to create storage_entries table: {}", e);
StorageError::QueryError {
message: format!("Failed to create table: {}", e),
}
})?;
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_expires_at
ON storage_entries(table_name, expires_at)
WHERE expires_at IS NOT NULL
"#,
)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to create expires_at index: {}", e);
StorageError::QueryError {
message: format!("Failed to create index: {}", e),
}
})?;
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_updated_at
ON storage_entries(table_name, updated_at)
"#,
)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to create updated_at index: {}", e);
StorageError::QueryError {
message: format!("Failed to create index: {}", e),
}
})?;
sqlx::query(
r#"
CREATE INDEX IF NOT EXISTS idx_version
ON storage_entries(table_name, key, version)
"#,
)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to create version index: {}", e);
StorageError::QueryError {
message: format!("Failed to create index: {}", e),
}
})?;
let duration = start.elapsed();
info!("Schema initialized in {:?}", duration);
Ok(())
}
fn build_where_clause(&self, query: &Query) -> (String, Vec<String>) {
let mut conditions = Vec::new();
let mut params = Vec::new();
let mut param_index = 1;
conditions.push(format!("table_name = ${}", param_index));
params.push(query.table.clone());
param_index += 1;
for filter in &query.filters {
let condition = match filter.operator {
Operator::Equal => format!("value->>'{}' = ${}", filter.field, param_index),
Operator::NotEqual => format!("value->>'{}' != ${}", filter.field, param_index),
Operator::GreaterThan => format!("(value->>'{}')::numeric > ${}", filter.field, param_index),
Operator::GreaterThanOrEqual => {
format!("(value->>'{}')::numeric >= ${}", filter.field, param_index)
}
Operator::LessThan => format!("(value->>'{}')::numeric < ${}", filter.field, param_index),
Operator::LessThanOrEqual => {
format!("(value->>'{}')::numeric <= ${}", filter.field, param_index)
}
Operator::Like => format!("value->>'{}' LIKE ${}", filter.field, param_index),
Operator::NotLike => format!("value->>'{}' NOT LIKE ${}", filter.field, param_index),
Operator::IsNull => format!("value->>'{}' IS NULL", filter.field),
Operator::IsNotNull => format!("value->>'{}' IS NOT NULL", filter.field),
Operator::In | Operator::NotIn => {
continue;
}
};
conditions.push(condition);
if !matches!(filter.operator, Operator::IsNull | Operator::IsNotNull) {
params.push(self.value_to_string(&filter.value));
param_index += 1;
}
}
let where_clause = if conditions.is_empty() {
String::new()
} else {
format!("WHERE {}", conditions.join(" AND "))
};
(where_clause, params)
}
fn build_order_by_clause(&self, query: &Query) -> String {
if query.sort.is_empty() {
return String::new();
}
let order_items: Vec<String> = query
.sort
.iter()
.map(|sort| {
let direction = match sort.direction {
SortDirection::Ascending => "ASC",
SortDirection::Descending => "DESC",
};
format!("value->>'{}' {}", sort.field, direction)
})
.collect();
format!("ORDER BY {}", order_items.join(", "))
}
fn build_limit_offset_clause(&self, query: &Query) -> String {
let mut clause = String::new();
if let Some(limit) = query.limit {
clause.push_str(&format!(" LIMIT {}", limit));
}
if let Some(offset) = query.offset {
clause.push_str(&format!(" OFFSET {}", offset));
}
clause
}
fn value_to_string(&self, value: &super::types::Value) -> String {
match value {
super::types::Value::String(s) => s.clone(),
super::types::Value::Integer(i) => i.to_string(),
super::types::Value::Float(f) => f.to_string(),
super::types::Value::Boolean(b) => b.to_string(),
super::types::Value::DateTime(dt) => dt.to_rfc3339(),
super::types::Value::List(_) => "[]".to_string(),
super::types::Value::Null => "null".to_string(),
}
}
fn parse_row<T: DeserializeOwned>(&self, row: &PgRow) -> StorageResult<StorageEntry<T>> {
let key: String = row.try_get("key").map_err(|e| StorageError::QueryError {
message: format!("Failed to get key: {}", e),
})?;
let value_json: serde_json::Value = row.try_get("value").map_err(|e| StorageError::QueryError {
message: format!("Failed to get value: {}", e),
})?;
let value: T = serde_json::from_value(value_json.clone()).map_err(|e| {
StorageError::DeserializationError {
source: Box::new(e),
}
})?;
let version: i64 = row.try_get("version").map_err(|e| StorageError::QueryError {
message: format!("Failed to get version: {}", e),
})?;
let created_at: DateTime<Utc> = row.try_get("created_at").map_err(|e| StorageError::QueryError {
message: format!("Failed to get created_at: {}", e),
})?;
let updated_at: DateTime<Utc> = row.try_get("updated_at").map_err(|e| StorageError::QueryError {
message: format!("Failed to get updated_at: {}", e),
})?;
let expires_at: Option<DateTime<Utc>> = row
.try_get("expires_at")
.ok();
let metadata_json: Option<serde_json::Value> = row
.try_get("metadata")
.ok();
let metadata: HashMap<String, String> = if let Some(json) = metadata_json {
serde_json::from_value(json).unwrap_or_default()
} else {
HashMap::new()
};
Ok(StorageEntry {
key,
value,
version: version as u64,
created_at,
updated_at,
expires_at,
metadata,
})
}
async fn execute_with_retry<F, T>(&self, operation: F) -> StorageResult<T>
where
F: Fn() -> futures::future::BoxFuture<'static, StorageResult<T>>,
{
let max_retries = 3;
let mut last_error = None;
for attempt in 0..max_retries {
match operation().await {
Ok(result) => return Ok(result),
Err(e) if e.is_retryable() && attempt < max_retries - 1 => {
warn!("Retryable error on attempt {}: {}", attempt + 1, e);
last_error = Some(e);
tokio::time::sleep(std::time::Duration::from_millis(100 * (attempt + 1) as u64))
.await;
}
Err(e) => return Err(e),
}
}
Err(last_error.unwrap_or_else(|| StorageError::InternalError {
message: "Retry loop failed without error".to_string(),
}))
}
async fn record_operation(&self, success: bool, duration: std::time::Duration, bytes: u64) {
let mut stats = self.stats.write().await;
if success {
stats.record_success(duration.as_millis() as f64, bytes);
} else {
stats.record_failure();
}
}
#[instrument(skip(self))]
async fn cleanup_expired_entries(&self) -> StorageResult<usize> {
debug!("Cleaning up expired entries");
let result = sqlx::query(
r#"
DELETE FROM storage_entries
WHERE expires_at IS NOT NULL AND expires_at < NOW()
"#,
)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to cleanup expired entries: {}", e);
StorageError::QueryError {
message: format!("Failed to cleanup: {}", e),
}
})?;
let count = result.rows_affected() as usize;
if count > 0 {
info!("Cleaned up {} expired entries", count);
}
Ok(count)
}
}
#[async_trait]
impl Storage for PostgreSQLStorage {
fn backend(&self) -> StorageBackend {
StorageBackend::PostgreSQL
}
#[instrument(skip(self))]
async fn metadata(&self) -> StorageResult<StorageMetadata> {
debug!("Fetching storage metadata");
let version_row: (String,) = sqlx::query_as("SELECT version()")
.fetch_one(&self.pool)
.await
.map_err(|e| StorageError::QueryError {
message: format!("Failed to get version: {}", e),
})?;
let version = version_row.0;
let capabilities = vec![
"transactions".to_string(),
"acid_compliance".to_string(),
"relational_queries".to_string(),
"jsonb_storage".to_string(),
"full_text_search".to_string(),
"indexes".to_string(),
"constraints".to_string(),
"versioning".to_string(),
"batch_operations".to_string(),
];
let health = self.health_check().await?;
Ok(StorageMetadata {
backend: StorageBackend::PostgreSQL,
version,
capabilities,
status: health,
last_health_check: Utc::now(),
})
}
#[instrument(skip(self))]
async fn health_check(&self) -> StorageResult<StorageHealth> {
debug!("Performing health check");
match sqlx::query("SELECT 1").execute(&self.pool).await {
Ok(_) => {
let pool_size = self.pool.size();
let idle_conns = self.pool.num_idle();
if pool_size > 0 && idle_conns > 0 {
debug!("Health check passed: {} connections, {} idle", pool_size, idle_conns);
Ok(StorageHealth::Healthy)
} else {
warn!("Health check degraded: {} connections, {} idle", pool_size, idle_conns);
Ok(StorageHealth::Degraded)
}
}
Err(e) => {
error!("Health check failed: {}", e);
Ok(StorageHealth::Unhealthy)
}
}
}
#[instrument(skip(self))]
async fn stats(&self) -> StorageResult<StorageStats> {
debug!("Fetching storage statistics");
let stats = self.stats.read().await.clone();
let mut updated_stats = stats;
updated_stats.active_connections = self.pool.size() as usize;
Ok(updated_stats)
}
#[instrument(skip(self, value))]
async fn insert<T: Serialize + Send + Sync>(
&self,
table: &str,
key: &str,
value: &T,
) -> StorageResult<()> {
debug!("Inserting entry: table={}, key={}", table, key);
let start = std::time::Instant::now();
let value_json = serde_json::to_value(value).map_err(|e| StorageError::SerializationError {
source: Box::new(e),
})?;
let result = sqlx::query(
r#"
INSERT INTO storage_entries (table_name, key, value, version, created_at, updated_at)
VALUES ($1, $2, $3, 1, NOW(), NOW())
ON CONFLICT (table_name, key) DO NOTHING
"#,
)
.bind(table)
.bind(key)
.bind(&value_json)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to insert entry: {}", e);
StorageError::QueryError {
message: format!("Insert failed: {}", e),
}
})?;
if result.rows_affected() == 0 {
return Err(StorageError::AlreadyExists {
entity: table.to_string(),
key: key.to_string(),
});
}
let duration = start.elapsed();
let bytes = value_json.to_string().len() as u64;
self.record_operation(true, duration, bytes).await;
debug!("Entry inserted successfully in {:?}", duration);
Ok(())
}
#[instrument(skip(self, value))]
async fn insert_with_ttl<T: Serialize + Send + Sync>(
&self,
table: &str,
key: &str,
value: &T,
ttl_seconds: i64,
) -> StorageResult<()> {
debug!("Inserting entry with TTL: table={}, key={}, ttl={}s", table, key, ttl_seconds);
let start = std::time::Instant::now();
let value_json = serde_json::to_value(value).map_err(|e| StorageError::SerializationError {
source: Box::new(e),
})?;
let expires_at = Utc::now() + chrono::Duration::seconds(ttl_seconds);
let result = sqlx::query(
r#"
INSERT INTO storage_entries (table_name, key, value, version, created_at, updated_at, expires_at)
VALUES ($1, $2, $3, 1, NOW(), NOW(), $4)
ON CONFLICT (table_name, key) DO NOTHING
"#,
)
.bind(table)
.bind(key)
.bind(&value_json)
.bind(expires_at)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to insert entry with TTL: {}", e);
StorageError::QueryError {
message: format!("Insert with TTL failed: {}", e),
}
})?;
if result.rows_affected() == 0 {
return Err(StorageError::AlreadyExists {
entity: table.to_string(),
key: key.to_string(),
});
}
let duration = start.elapsed();
let bytes = value_json.to_string().len() as u64;
self.record_operation(true, duration, bytes).await;
debug!("Entry with TTL inserted successfully in {:?}", duration);
Ok(())
}
#[instrument(skip(self))]
async fn get<T: DeserializeOwned + Serialize + Send + Sync>(
&self,
table: &str,
key: &str,
) -> StorageResult<Option<StorageEntry<T>>> {
debug!("Getting entry: table={}, key={}", table, key);
let start = std::time::Instant::now();
let row_result = sqlx::query(
r#"
SELECT key, value, version, created_at, updated_at, expires_at, metadata
FROM storage_entries
WHERE table_name = $1 AND key = $2
AND (expires_at IS NULL OR expires_at > NOW())
"#,
)
.bind(table)
.bind(key)
.fetch_optional(&self.pool)
.await
.map_err(|e| {
error!("Failed to get entry: {}", e);
StorageError::QueryError {
message: format!("Get failed: {}", e),
}
})?;
let duration = start.elapsed();
let result = if let Some(row) = row_result {
let entry = self.parse_row::<T>(&row)?;
let bytes = serde_json::to_string(&entry.value).unwrap_or_default().len() as u64;
self.record_operation(true, duration, bytes).await;
Some(entry)
} else {
self.record_operation(true, duration, 0).await;
None
};
debug!("Get operation completed in {:?}", duration);
Ok(result)
}
#[instrument(skip(self, value))]
async fn update<T: Serialize + Send + Sync>(
&self,
table: &str,
key: &str,
value: &T,
) -> StorageResult<()> {
debug!("Updating entry: table={}, key={}", table, key);
let start = std::time::Instant::now();
let value_json = serde_json::to_value(value).map_err(|e| StorageError::SerializationError {
source: Box::new(e),
})?;
let result = sqlx::query(
r#"
UPDATE storage_entries
SET value = $3, version = version + 1, updated_at = NOW()
WHERE table_name = $1 AND key = $2
"#,
)
.bind(table)
.bind(key)
.bind(&value_json)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to update entry: {}", e);
StorageError::QueryError {
message: format!("Update failed: {}", e),
}
})?;
if result.rows_affected() == 0 {
return Err(StorageError::NotFound {
entity: table.to_string(),
key: key.to_string(),
});
}
let duration = start.elapsed();
let bytes = value_json.to_string().len() as u64;
self.record_operation(true, duration, bytes).await;
debug!("Entry updated successfully in {:?}", duration);
Ok(())
}
#[instrument(skip(self, value))]
async fn update_versioned<T: Serialize + Send + Sync>(
&self,
table: &str,
key: &str,
value: &T,
expected_version: u64,
) -> StorageResult<()> {
debug!(
"Updating entry with version check: table={}, key={}, expected_version={}",
table, key, expected_version
);
let start = std::time::Instant::now();
let value_json = serde_json::to_value(value).map_err(|e| StorageError::SerializationError {
source: Box::new(e),
})?;
let result = sqlx::query(
r#"
UPDATE storage_entries
SET value = $3, version = version + 1, updated_at = NOW()
WHERE table_name = $1 AND key = $2 AND version = $4
"#,
)
.bind(table)
.bind(key)
.bind(&value_json)
.bind(expected_version as i64)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to update entry with version: {}", e);
StorageError::QueryError {
message: format!("Versioned update failed: {}", e),
}
})?;
if result.rows_affected() == 0 {
return Err(StorageError::ConstraintViolation {
message: format!("Version mismatch or entry not found: expected version {}", expected_version),
});
}
let duration = start.elapsed();
let bytes = value_json.to_string().len() as u64;
self.record_operation(true, duration, bytes).await;
debug!("Versioned entry updated successfully in {:?}", duration);
Ok(())
}
#[instrument(skip(self))]
async fn delete(&self, table: &str, key: &str) -> StorageResult<()> {
debug!("Deleting entry: table={}, key={}", table, key);
let start = std::time::Instant::now();
let result = sqlx::query(
r#"
DELETE FROM storage_entries
WHERE table_name = $1 AND key = $2
"#,
)
.bind(table)
.bind(key)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to delete entry: {}", e);
StorageError::QueryError {
message: format!("Delete failed: {}", e),
}
})?;
if result.rows_affected() == 0 {
return Err(StorageError::NotFound {
entity: table.to_string(),
key: key.to_string(),
});
}
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("Entry deleted successfully in {:?}", duration);
Ok(())
}
#[instrument(skip(self))]
async fn exists(&self, table: &str, key: &str) -> StorageResult<bool> {
debug!("Checking if entry exists: table={}, key={}", table, key);
let start = std::time::Instant::now();
let result: (bool,) = sqlx::query_as(
r#"
SELECT EXISTS(
SELECT 1 FROM storage_entries
WHERE table_name = $1 AND key = $2
AND (expires_at IS NULL OR expires_at > NOW())
)
"#,
)
.bind(table)
.bind(key)
.fetch_one(&self.pool)
.await
.map_err(|e| {
error!("Failed to check existence: {}", e);
StorageError::QueryError {
message: format!("Exists check failed: {}", e),
}
})?;
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("Exists check completed in {:?}: {}", duration, result.0);
Ok(result.0)
}
#[instrument(skip(self, batch))]
async fn execute_batch(&self, batch: Batch) -> StorageResult<()> {
debug!("Executing batch operation with {} operations", batch.operations.len());
if batch.operations.is_empty() {
return Ok(());
}
let start = std::time::Instant::now();
let mut tx = self.pool.begin().await.map_err(|e| {
error!("Failed to begin transaction: {}", e);
StorageError::TransactionError {
message: format!("Failed to begin transaction: {}", e),
}
})?;
for operation in &batch.operations {
match operation {
Operation::Insert { table, data } => {
let entry: serde_json::Value = serde_json::from_slice(data).map_err(|e| {
StorageError::DeserializationError {
source: Box::new(e),
}
})?;
let key = entry.get("key")
.and_then(|k| k.as_str())
.ok_or_else(|| StorageError::QueryError {
message: "Missing key in insert operation".to_string(),
})?;
sqlx::query(
r#"
INSERT INTO storage_entries (table_name, key, value, version, created_at, updated_at)
VALUES ($1, $2, $3, 1, NOW(), NOW())
ON CONFLICT (table_name, key) DO NOTHING
"#,
)
.bind(table)
.bind(key)
.bind(&entry)
.execute(&mut *tx)
.await
.map_err(|e| StorageError::QueryError {
message: format!("Batch insert failed: {}", e),
})?;
}
Operation::Update { table, key, data } => {
let value: serde_json::Value = serde_json::from_slice(data).map_err(|e| {
StorageError::DeserializationError {
source: Box::new(e),
}
})?;
sqlx::query(
r#"
UPDATE storage_entries
SET value = $3, version = version + 1, updated_at = NOW()
WHERE table_name = $1 AND key = $2
"#,
)
.bind(table)
.bind(key)
.bind(&value)
.execute(&mut *tx)
.await
.map_err(|e| StorageError::QueryError {
message: format!("Batch update failed: {}", e),
})?;
}
Operation::Delete { table, key } => {
sqlx::query(
r#"
DELETE FROM storage_entries
WHERE table_name = $1 AND key = $2
"#,
)
.bind(table)
.bind(key)
.execute(&mut *tx)
.await
.map_err(|e| StorageError::QueryError {
message: format!("Batch delete failed: {}", e),
})?;
}
}
}
tx.commit().await.map_err(|e| {
error!("Failed to commit batch transaction: {}", e);
StorageError::TransactionError {
message: format!("Failed to commit: {}", e),
}
})?;
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
info!("Batch operation completed in {:?}", duration);
Ok(())
}
#[instrument(skip(self, query))]
async fn query<T: DeserializeOwned + Send + Sync + 'static>(&self, query: Query) -> StorageResult<Vec<StorageEntry<T>>> {
debug!("Executing query on table: {}", query.table);
let start = std::time::Instant::now();
let (where_clause, _params) = self.build_where_clause(&query);
let order_by_clause = self.build_order_by_clause(&query);
let limit_offset_clause = self.build_limit_offset_clause(&query);
let sql = format!(
r#"
SELECT key, value, version, created_at, updated_at, expires_at, metadata
FROM storage_entries
{}
{}
{}
"#,
where_clause, order_by_clause, limit_offset_clause
);
let mut query_builder = sqlx::query(&sql);
query_builder = query_builder.bind(&query.table);
let rows = query_builder.fetch_all(&self.pool).await.map_err(|e| {
error!("Failed to execute query: {}", e);
StorageError::QueryError {
message: format!("Query failed: {}", e),
}
})?;
let mut results = Vec::new();
for row in rows {
let entry = self.parse_row::<T>(&row)?;
results.push(entry);
}
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("Query returned {} results in {:?}", results.len(), duration);
Ok(results)
}
#[instrument(skip(self, query))]
async fn count(&self, query: Query) -> StorageResult<usize> {
debug!("Counting entries for table: {}", query.table);
let start = std::time::Instant::now();
let (where_clause, _params) = self.build_where_clause(&query);
let sql = format!(
r#"
SELECT COUNT(*) as count
FROM storage_entries
{}
"#,
where_clause
);
let result: (i64,) = sqlx::query_as(&sql)
.bind(&query.table)
.fetch_one(&self.pool)
.await
.map_err(|e| {
error!("Failed to count entries: {}", e);
StorageError::QueryError {
message: format!("Count failed: {}", e),
}
})?;
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("Count completed in {:?}: {}", duration, result.0);
Ok(result.0 as usize)
}
#[instrument(skip(self))]
async fn begin_transaction(&self) -> StorageResult<Transaction> {
debug!("Beginning transaction");
let transaction = Transaction::new();
let context = TransactionContext {
transaction: transaction.clone(),
operations: Vec::new(),
};
let mut transactions = self.transactions.write().await;
transactions.insert(transaction.id.clone(), context);
info!("Transaction started: {}", transaction.id);
Ok(transaction)
}
#[instrument(skip(self, transaction))]
async fn commit_transaction(&self, mut transaction: Transaction) -> StorageResult<()> {
debug!("Committing transaction: {}", transaction.id);
let mut transactions = self.transactions.write().await;
let context = transactions.remove(&transaction.id).ok_or_else(|| {
StorageError::TransactionError {
message: format!("Transaction not found: {}", transaction.id),
}
})?;
let mut tx = self.pool.begin().await.map_err(|e| {
error!("Failed to begin SQL transaction: {}", e);
StorageError::TransactionError {
message: format!("Failed to begin transaction: {}", e),
}
})?;
for operation in &context.operations {
match operation {
Operation::Insert { table, data } => {
let entry: serde_json::Value = serde_json::from_slice(data).map_err(|e| {
StorageError::DeserializationError {
source: Box::new(e),
}
})?;
let key = entry.get("key")
.and_then(|k| k.as_str())
.ok_or_else(|| StorageError::QueryError {
message: "Missing key in insert operation".to_string(),
})?;
sqlx::query(
r#"
INSERT INTO storage_entries (table_name, key, value, version, created_at, updated_at)
VALUES ($1, $2, $3, 1, NOW(), NOW())
"#,
)
.bind(table)
.bind(key)
.bind(&entry)
.execute(&mut *tx)
.await
.map_err(|e| StorageError::QueryError {
message: format!("Transaction insert failed: {}", e),
})?;
}
Operation::Update { table, key, data } => {
let value: serde_json::Value = serde_json::from_slice(data).map_err(|e| {
StorageError::DeserializationError {
source: Box::new(e),
}
})?;
sqlx::query(
r#"
UPDATE storage_entries
SET value = $3, version = version + 1, updated_at = NOW()
WHERE table_name = $1 AND key = $2
"#,
)
.bind(table)
.bind(key)
.bind(&value)
.execute(&mut *tx)
.await
.map_err(|e| StorageError::QueryError {
message: format!("Transaction update failed: {}", e),
})?;
}
Operation::Delete { table, key } => {
sqlx::query(
r#"
DELETE FROM storage_entries
WHERE table_name = $1 AND key = $2
"#,
)
.bind(table)
.bind(key)
.execute(&mut *tx)
.await
.map_err(|e| StorageError::QueryError {
message: format!("Transaction delete failed: {}", e),
})?;
}
}
}
tx.commit().await.map_err(|e| {
error!("Failed to commit SQL transaction: {}", e);
StorageError::TransactionError {
message: format!("Failed to commit: {}", e),
}
})?;
transaction.state = TransactionState::Committed;
info!("Transaction committed: {}", transaction.id);
Ok(())
}
#[instrument(skip(self, transaction))]
async fn rollback_transaction(&self, mut transaction: Transaction) -> StorageResult<()> {
debug!("Rolling back transaction: {}", transaction.id);
let mut transactions = self.transactions.write().await;
transactions.remove(&transaction.id);
transaction.state = TransactionState::RolledBack;
info!("Transaction rolled back: {}", transaction.id);
Ok(())
}
#[instrument(skip(self))]
async fn scan_prefix(&self, table: &str, prefix: &str) -> StorageResult<Vec<String>> {
debug!("Scanning keys with prefix: table={}, prefix={}", table, prefix);
let start = std::time::Instant::now();
let pattern = format!("{}%", prefix);
let rows: Vec<(String,)> = sqlx::query_as(
r#"
SELECT key
FROM storage_entries
WHERE table_name = $1 AND key LIKE $2
AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY key
"#,
)
.bind(table)
.bind(&pattern)
.fetch_all(&self.pool)
.await
.map_err(|e| {
error!("Failed to scan prefix: {}", e);
StorageError::QueryError {
message: format!("Prefix scan failed: {}", e),
}
})?;
let keys: Vec<String> = rows.into_iter().map(|(key,)| key).collect();
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("Prefix scan returned {} keys in {:?}", keys.len(), duration);
Ok(keys)
}
#[instrument(skip(self, keys))]
async fn get_multi<T: DeserializeOwned + Serialize + Send + Sync>(
&self,
table: &str,
keys: Vec<String>,
) -> StorageResult<Vec<Option<StorageEntry<T>>>> {
debug!("Getting multiple entries: table={}, count={}", table, keys.len());
if keys.is_empty() {
return Ok(Vec::new());
}
let start = std::time::Instant::now();
let mut results = Vec::new();
for key in keys {
let entry = self.get::<T>(table, &key).await?;
results.push(entry);
}
let duration = start.elapsed();
debug!("Multi-get completed in {:?}", duration);
Ok(results)
}
#[instrument(skip(self, keys))]
async fn delete_multi(&self, table: &str, keys: Vec<String>) -> StorageResult<()> {
debug!("Deleting multiple entries: table={}, count={}", table, keys.len());
if keys.is_empty() {
return Ok(());
}
let start = std::time::Instant::now();
let placeholders: Vec<String> = (1..=keys.len())
.map(|i| format!("${}", i + 1))
.collect();
let sql = format!(
r#"
DELETE FROM storage_entries
WHERE table_name = $1 AND key IN ({})
"#,
placeholders.join(", ")
);
let mut query = sqlx::query(&sql).bind(table);
for key in keys {
query = query.bind(key);
}
let result = query.execute(&self.pool).await.map_err(|e| {
error!("Failed to delete multiple entries: {}", e);
StorageError::QueryError {
message: format!("Multi-delete failed: {}", e),
}
})?;
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
info!("Deleted {} entries in {:?}", result.rows_affected(), duration);
Ok(())
}
#[instrument(skip(self, new_value))]
async fn compare_and_swap<T: Serialize + Send + Sync>(
&self,
table: &str,
key: &str,
expected_version: u64,
new_value: &T,
) -> StorageResult<bool> {
debug!(
"Compare-and-swap: table={}, key={}, expected_version={}",
table, key, expected_version
);
let start = std::time::Instant::now();
let value_json = serde_json::to_value(new_value).map_err(|e| {
StorageError::SerializationError {
source: Box::new(e),
}
})?;
let result = sqlx::query(
r#"
UPDATE storage_entries
SET value = $3, version = version + 1, updated_at = NOW()
WHERE table_name = $1 AND key = $2 AND version = $4
"#,
)
.bind(table)
.bind(key)
.bind(&value_json)
.bind(expected_version as i64)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to compare-and-swap: {}", e);
StorageError::QueryError {
message: format!("CAS failed: {}", e),
}
})?;
let success = result.rows_affected() > 0;
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("Compare-and-swap completed in {:?}: {}", duration, success);
Ok(success)
}
#[instrument(skip(self))]
async fn get_modified_since<T: DeserializeOwned + Send + Sync + 'static>(
&self,
table: &str,
since: DateTime<Utc>,
) -> StorageResult<Vec<StorageEntry<T>>> {
debug!("Getting entries modified since: table={}, since={}", table, since);
let start = std::time::Instant::now();
let rows = sqlx::query(
r#"
SELECT key, value, version, created_at, updated_at, expires_at, metadata
FROM storage_entries
WHERE table_name = $1 AND updated_at > $2
AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY updated_at ASC
"#,
)
.bind(table)
.bind(since)
.fetch_all(&self.pool)
.await
.map_err(|e| {
error!("Failed to get modified entries: {}", e);
StorageError::QueryError {
message: format!("Get modified since failed: {}", e),
}
})?;
let mut results = Vec::new();
for row in rows {
let entry = self.parse_row::<T>(&row)?;
results.push(entry);
}
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("Found {} modified entries in {:?}", results.len(), duration);
Ok(results)
}
#[instrument(skip(self))]
async fn get_expiring_before<T: DeserializeOwned + Send + Sync + 'static>(
&self,
table: &str,
before: DateTime<Utc>,
) -> StorageResult<Vec<StorageEntry<T>>> {
debug!("Getting entries expiring before: table={}, before={}", table, before);
let start = std::time::Instant::now();
let rows = sqlx::query(
r#"
SELECT key, value, version, created_at, updated_at, expires_at, metadata
FROM storage_entries
WHERE table_name = $1 AND expires_at IS NOT NULL AND expires_at < $2
ORDER BY expires_at ASC
"#,
)
.bind(table)
.bind(before)
.fetch_all(&self.pool)
.await
.map_err(|e| {
error!("Failed to get expiring entries: {}", e);
StorageError::QueryError {
message: format!("Get expiring before failed: {}", e),
}
})?;
let mut results = Vec::new();
for row in rows {
let entry = self.parse_row::<T>(&row)?;
results.push(entry);
}
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("Found {} expiring entries in {:?}", results.len(), duration);
Ok(results)
}
#[instrument(skip(self))]
async fn cleanup_expired(&self, table: &str) -> StorageResult<usize> {
debug!("Cleaning up expired entries in table: {}", table);
let start = std::time::Instant::now();
let result = sqlx::query(
r#"
DELETE FROM storage_entries
WHERE table_name = $1 AND expires_at IS NOT NULL AND expires_at < NOW()
"#,
)
.bind(table)
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to cleanup expired entries: {}", e);
StorageError::QueryError {
message: format!("Cleanup failed: {}", e),
}
})?;
let count = result.rows_affected() as usize;
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
info!("Cleaned up {} expired entries in {:?}", count, duration);
Ok(count)
}
#[instrument(skip(self))]
async fn compact(&self) -> StorageResult<()> {
info!("Running VACUUM on database");
let start = std::time::Instant::now();
sqlx::query("VACUUM ANALYZE storage_entries")
.execute(&self.pool)
.await
.map_err(|e| {
error!("Failed to vacuum database: {}", e);
StorageError::QueryError {
message: format!("Vacuum failed: {}", e),
}
})?;
let duration = start.elapsed();
info!("Database vacuumed in {:?}", duration);
Ok(())
}
#[instrument(skip(self))]
async fn flush(&self) -> StorageResult<()> {
debug!("Flushing pending writes (no-op for PostgreSQL)");
Ok(())
}
#[instrument(skip(self))]
async fn initialize(&mut self) -> StorageResult<()> {
info!("Initializing PostgreSQL storage");
let mut initialized = self.initialized.write().await;
if *initialized {
warn!("Storage already initialized");
return Ok(());
}
self.initialize_schema().await?;
if self.config.enable_migrations {
info!("Running migrations");
}
*initialized = true;
info!("PostgreSQL storage initialized successfully");
Ok(())
}
#[instrument(skip(self))]
async fn shutdown(&mut self) -> StorageResult<()> {
info!("Shutting down PostgreSQL storage");
self.pool.close().await;
let mut initialized = self.initialized.write().await;
*initialized = false;
info!("PostgreSQL storage shutdown complete");
Ok(())
}
}
#[async_trait]
impl RelationalStorage for PostgreSQLStorage {
#[instrument(skip(self, params))]
async fn execute_sql(&self, sql: &str, params: Vec<SqlParam>) -> StorageResult<SqlResult> {
debug!("Executing SQL: {}", sql);
let start = std::time::Instant::now();
let mut query = sqlx::query(sql);
for param in params {
query = match param {
SqlParam::String(s) => query.bind(s),
SqlParam::Integer(i) => query.bind(i),
SqlParam::Float(f) => query.bind(f),
SqlParam::Boolean(b) => query.bind(b),
SqlParam::DateTime(dt) => query.bind(dt),
SqlParam::Null => query.bind(None::<String>),
};
}
let result = query.execute(&self.pool).await.map_err(|e| {
error!("Failed to execute SQL: {}", e);
StorageError::QueryError {
message: format!("SQL execution failed: {}", e),
}
})?;
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("SQL executed in {:?}, rows affected: {}", duration, result.rows_affected());
Ok(SqlResult {
rows_affected: result.rows_affected(),
})
}
#[instrument(skip(self, params))]
async fn query_sql<T: DeserializeOwned + Send + Sync>(
&self,
sql: &str,
params: Vec<SqlParam>,
) -> StorageResult<Vec<T>> {
debug!("Querying SQL: {}", sql);
let start = std::time::Instant::now();
let mut query = sqlx::query(sql);
for param in params {
query = match param {
SqlParam::String(s) => query.bind(s),
SqlParam::Integer(i) => query.bind(i),
SqlParam::Float(f) => query.bind(f),
SqlParam::Boolean(b) => query.bind(b),
SqlParam::DateTime(dt) => query.bind(dt),
SqlParam::Null => query.bind(None::<String>),
};
}
let rows = query.fetch_all(&self.pool).await.map_err(|e| {
error!("Failed to query SQL: {}", e);
StorageError::QueryError {
message: format!("SQL query failed: {}", e),
}
})?;
let mut results = Vec::new();
for row in rows {
let value_json: serde_json::Value = row.try_get("value").map_err(|e| {
StorageError::QueryError {
message: format!("Failed to get value column: {}", e),
}
})?;
let result: T = serde_json::from_value(value_json).map_err(|e| {
StorageError::DeserializationError {
source: Box::new(e),
}
})?;
results.push(result);
}
let duration = start.elapsed();
self.record_operation(true, duration, 0).await;
debug!("SQL query returned {} rows in {:?}", results.len(), duration);
Ok(results)
}
#[instrument(skip(self, columns))]
async fn create_index(
&self,
table: &str,
index_name: &str,
columns: Vec<String>,
unique: bool,
) -> StorageResult<()> {
info!("Creating index: {} on table: {}", index_name, table);
let unique_clause = if unique { "UNIQUE" } else { "" };
let columns_clause = columns.join(", ");
let sql = format!(
"CREATE {} INDEX IF NOT EXISTS {} ON {} ({})",
unique_clause, index_name, table, columns_clause
);
sqlx::query(&sql).execute(&self.pool).await.map_err(|e| {
error!("Failed to create index: {}", e);
StorageError::QueryError {
message: format!("Index creation failed: {}", e),
}
})?;
info!("Index created successfully: {}", index_name);
Ok(())
}
#[instrument(skip(self))]
async fn drop_index(&self, index_name: &str) -> StorageResult<()> {
info!("Dropping index: {}", index_name);
let sql = format!("DROP INDEX IF EXISTS {}", index_name);
sqlx::query(&sql).execute(&self.pool).await.map_err(|e| {
error!("Failed to drop index: {}", e);
StorageError::QueryError {
message: format!("Index drop failed: {}", e),
}
})?;
info!("Index dropped successfully: {}", index_name);
Ok(())
}
#[instrument(skip(self))]
async fn vacuum(&self, table: &str) -> StorageResult<()> {
info!("Vacuuming table: {}", table);
let sql = format!("VACUUM ANALYZE {}", table);
sqlx::query(&sql).execute(&self.pool).await.map_err(|e| {
error!("Failed to vacuum table: {}", e);
StorageError::QueryError {
message: format!("Vacuum failed: {}", e),
}
})?;
info!("Table vacuumed successfully: {}", table);
Ok(())
}
#[instrument(skip(self))]
async fn analyze(&self, table: &str) -> StorageResult<()> {
info!("Analyzing table: {}", table);
let sql = format!("ANALYZE {}", table);
sqlx::query(&sql).execute(&self.pool).await.map_err(|e| {
error!("Failed to analyze table: {}", e);
StorageError::QueryError {
message: format!("Analyze failed: {}", e),
}
})?;
info!("Table analyzed successfully: {}", table);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct TestData {
id: String,
value: i32,
name: String,
}
async fn create_test_storage() -> PostgreSQLStorage {
let config = PostgreSQLConfig {
host: "localhost".to_string(),
port: 5432,
database: "test_llm_optimizer".to_string(),
username: "postgres".to_string(),
password: "postgres".to_string(),
..Default::default()
};
let mut storage = PostgreSQLStorage::new(config)
.await
.expect("Failed to create storage");
storage.initialize().await.expect("Failed to initialize");
storage
}
#[tokio::test]
async fn test_backend_type() {
let storage = create_test_storage().await;
assert_eq!(storage.backend(), StorageBackend::PostgreSQL);
}
#[tokio::test]
async fn test_health_check() {
let storage = create_test_storage().await;
let health = storage.health_check().await.expect("Health check failed");
assert!(matches!(health, StorageHealth::Healthy | StorageHealth::Degraded));
}
#[tokio::test]
async fn test_insert_and_get() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test1".to_string(),
value: 42,
name: "Test Entry".to_string(),
};
storage
.insert("test_table", "test_key1", &test_data)
.await
.expect("Insert failed");
let entry: Option<StorageEntry<TestData>> = storage
.get("test_table", "test_key1")
.await
.expect("Get failed");
assert!(entry.is_some());
let entry = entry.unwrap();
assert_eq!(entry.key, "test_key1");
assert_eq!(entry.value.id, "test1");
assert_eq!(entry.value.value, 42);
assert_eq!(entry.version, 1);
}
#[tokio::test]
async fn test_insert_duplicate_fails() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test2".to_string(),
value: 100,
name: "Duplicate Test".to_string(),
};
storage
.insert("test_table", "dup_key", &test_data)
.await
.expect("First insert failed");
let result = storage.insert("test_table", "dup_key", &test_data).await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), StorageError::AlreadyExists { .. }));
}
#[tokio::test]
async fn test_update() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test3".to_string(),
value: 10,
name: "Original".to_string(),
};
storage
.insert("test_table", "update_key", &test_data)
.await
.expect("Insert failed");
let updated_data = TestData {
id: "test3".to_string(),
value: 20,
name: "Updated".to_string(),
};
storage
.update("test_table", "update_key", &updated_data)
.await
.expect("Update failed");
let entry: Option<StorageEntry<TestData>> = storage
.get("test_table", "update_key")
.await
.expect("Get failed");
assert!(entry.is_some());
let entry = entry.unwrap();
assert_eq!(entry.value.value, 20);
assert_eq!(entry.value.name, "Updated");
assert_eq!(entry.version, 2);
}
#[tokio::test]
async fn test_update_versioned() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test4".to_string(),
value: 100,
name: "Versioned".to_string(),
};
storage
.insert("test_table", "version_key", &test_data)
.await
.expect("Insert failed");
let updated_data = TestData {
id: "test4".to_string(),
value: 200,
name: "Versioned Updated".to_string(),
};
storage
.update_versioned("test_table", "version_key", &updated_data, 1)
.await
.expect("Versioned update failed");
let result = storage
.update_versioned("test_table", "version_key", &updated_data, 1)
.await;
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), StorageError::ConstraintViolation { .. }));
}
#[tokio::test]
async fn test_delete() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test5".to_string(),
value: 50,
name: "To Delete".to_string(),
};
storage
.insert("test_table", "delete_key", &test_data)
.await
.expect("Insert failed");
storage
.delete("test_table", "delete_key")
.await
.expect("Delete failed");
let entry: Option<StorageEntry<TestData>> = storage
.get("test_table", "delete_key")
.await
.expect("Get failed");
assert!(entry.is_none());
}
#[tokio::test]
async fn test_exists() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test6".to_string(),
value: 60,
name: "Exists Test".to_string(),
};
storage
.insert("test_table", "exists_key", &test_data)
.await
.expect("Insert failed");
let exists = storage
.exists("test_table", "exists_key")
.await
.expect("Exists check failed");
assert!(exists);
let not_exists = storage
.exists("test_table", "nonexistent_key")
.await
.expect("Exists check failed");
assert!(!not_exists);
}
#[tokio::test]
async fn test_insert_with_ttl() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test7".to_string(),
value: 70,
name: "TTL Test".to_string(),
};
storage
.insert_with_ttl("test_table", "ttl_key", &test_data, 2)
.await
.expect("Insert with TTL failed");
let entry: Option<StorageEntry<TestData>> = storage
.get("test_table", "ttl_key")
.await
.expect("Get failed");
assert!(entry.is_some());
let entry = entry.unwrap();
assert!(entry.expires_at.is_some());
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
let expired_entry: Option<StorageEntry<TestData>> = storage
.get("test_table", "ttl_key")
.await
.expect("Get failed");
assert!(expired_entry.is_none());
}
#[tokio::test]
async fn test_scan_prefix() {
let storage = create_test_storage().await;
for i in 0..5 {
let test_data = TestData {
id: format!("prefix_{}", i),
value: i,
name: format!("Prefix {}", i),
};
storage
.insert("test_table", &format!("prefix_{}", i), &test_data)
.await
.expect("Insert failed");
}
let keys = storage
.scan_prefix("test_table", "prefix_")
.await
.expect("Scan prefix failed");
assert!(keys.len() >= 5);
assert!(keys.iter().all(|k| k.starts_with("prefix_")));
}
#[tokio::test]
async fn test_compare_and_swap() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test8".to_string(),
value: 80,
name: "CAS Test".to_string(),
};
storage
.insert("test_table", "cas_key", &test_data)
.await
.expect("Insert failed");
let new_data = TestData {
id: "test8".to_string(),
value: 81,
name: "CAS Updated".to_string(),
};
let success = storage
.compare_and_swap("test_table", "cas_key", 1, &new_data)
.await
.expect("CAS failed");
assert!(success);
let failure = storage
.compare_and_swap("test_table", "cas_key", 1, &new_data)
.await
.expect("CAS failed");
assert!(!failure);
}
#[tokio::test]
async fn test_batch_operations() {
let storage = create_test_storage().await;
let mut batch = Batch::new(10);
for i in 0..5 {
let test_data = TestData {
id: format!("batch_{}", i),
value: i,
name: format!("Batch {}", i),
};
let data = serde_json::to_vec(&test_data).unwrap();
batch.add(Operation::Insert {
table: "test_table".to_string(),
data,
}).expect("Failed to add to batch");
}
storage
.execute_batch(batch)
.await
.expect("Batch execution failed");
for i in 0..5 {
let entry: Option<StorageEntry<TestData>> = storage
.get("test_table", &format!("batch_{}", i))
.await
.expect("Get failed");
assert!(entry.is_some());
}
}
#[tokio::test]
async fn test_transaction_commit() {
let storage = create_test_storage().await;
let transaction = storage
.begin_transaction()
.await
.expect("Begin transaction failed");
storage
.commit_transaction(transaction)
.await
.expect("Commit failed");
}
#[tokio::test]
async fn test_transaction_rollback() {
let storage = create_test_storage().await;
let transaction = storage
.begin_transaction()
.await
.expect("Begin transaction failed");
storage
.rollback_transaction(transaction)
.await
.expect("Rollback failed");
}
#[tokio::test]
async fn test_get_modified_since() {
let storage = create_test_storage().await;
let now = Utc::now();
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
let test_data = TestData {
id: "test9".to_string(),
value: 90,
name: "Modified Test".to_string(),
};
storage
.insert("test_table", "modified_key", &test_data)
.await
.expect("Insert failed");
let entries: Vec<StorageEntry<TestData>> = storage
.get_modified_since("test_table", now)
.await
.expect("Get modified since failed");
assert!(!entries.is_empty());
}
#[tokio::test]
async fn test_cleanup_expired() {
let storage = create_test_storage().await;
let test_data = TestData {
id: "test10".to_string(),
value: 100,
name: "Cleanup Test".to_string(),
};
storage
.insert_with_ttl("test_table", "cleanup_key", &test_data, 1)
.await
.expect("Insert with TTL failed");
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
let count = storage
.cleanup_expired("test_table")
.await
.expect("Cleanup failed");
assert!(count >= 1);
}
#[tokio::test]
async fn test_metadata() {
let storage = create_test_storage().await;
let metadata = storage.metadata().await.expect("Metadata failed");
assert_eq!(metadata.backend, StorageBackend::PostgreSQL);
assert!(!metadata.capabilities.is_empty());
assert!(metadata.version.contains("PostgreSQL"));
}
#[tokio::test]
async fn test_stats() {
let storage = create_test_storage().await;
let stats = storage.stats().await.expect("Stats failed");
assert!(stats.active_connections > 0);
}
}