llm_optimizer_processor/state/
postgres_backend.rs

1//! PostgreSQL-based persistent state backend
2//!
3//! This module provides a production-ready PostgreSQL state backend for distributed
4//! state management. It supports connection pooling, transactions, JSONB metadata,
5//! automatic schema migrations, and advanced features like checkpointing and TTL.
6//!
7//! ## Features
8//!
9//! - **Connection Pooling**: Managed by sqlx PgPool for optimal performance
10//! - **Async Operations**: Full async/await support throughout
11//! - **JSONB Metadata**: Store additional context with state entries
12//! - **Automatic Migrations**: Schema versioning and automatic upgrades
13//! - **Transactions**: ACID guarantees with BEGIN/COMMIT/ROLLBACK
14//! - **TTL Support**: Automatic expiration and cleanup of old entries
15//! - **Checkpointing**: Point-in-time snapshots for backup and recovery
16//! - **Query Optimization**: Indexes, prepared statements, and hints
17//! - **Production Ready**: SSL, retries, timeouts, and health checks
18//!
19//! ## Example
20//!
21//! ```rust,no_run
22//! use processor::state::{PostgresStateBackend, PostgresConfig, StateBackend};
23//!
24//! #[tokio::main]
25//! async fn main() -> anyhow::Result<()> {
26//!     // Configure PostgreSQL backend
27//!     let config = PostgresConfig::new("postgresql://user:pass@localhost/dbname")
28//!         .with_pool_size(10, 50)
29//!         .with_ssl_mode("require")
30//!         .with_timeout(std::time::Duration::from_secs(30));
31//!
32//!     // Initialize backend (runs migrations automatically)
33//!     let backend = PostgresStateBackend::new(config).await?;
34//!
35//!     // Use like any other state backend
36//!     backend.put(b"window:123", b"aggregated_data").await?;
37//!
38//!     // Store with metadata
39//!     backend.put_with_metadata(
40//!         b"window:123",
41//!         b"data",
42//!         serde_json::json!({"source": "kafka", "partition": 5})
43//!     ).await?;
44//!
45//!     // Create checkpoint
46//!     let checkpoint_id = backend.create_checkpoint().await?;
47//!
48//!     // Cleanup expired entries
49//!     let deleted = backend.cleanup_expired().await?;
50//!     println!("Deleted {} expired entries", deleted);
51//!
52//!     Ok(())
53//! }
54//! ```
55
56use async_trait::async_trait;
57use serde::{Deserialize, Serialize};
58use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode};
59use sqlx::{PgPool, Row};
60use std::path::Path;
61use std::str::FromStr;
62use std::sync::Arc;
63use std::time::Duration;
64use tokio::sync::RwLock;
65use tracing::{debug, info, trace, warn};
66use uuid::Uuid;
67
68use super::backend::StateBackend;
69use crate::error::{StateError, StateResult};
70
71/// PostgreSQL backend configuration
72#[derive(Debug, Clone)]
73pub struct PostgresConfig {
74    /// Database connection URL (e.g., "postgresql://user:pass@host/db")
75    pub database_url: String,
76    /// Minimum number of connections in the pool
77    pub min_connections: u32,
78    /// Maximum number of connections in the pool
79    pub max_connections: u32,
80    /// Table name for state storage (default: "state_entries")
81    pub table_name: String,
82    /// Schema name (default: "public")
83    pub schema_name: String,
84    /// Connection timeout
85    pub connect_timeout: Duration,
86    /// Query timeout
87    pub query_timeout: Duration,
88    /// SSL mode
89    pub ssl_mode: String,
90    /// Maximum retry attempts for transient errors
91    pub max_retries: u32,
92    /// Retry backoff duration
93    pub retry_backoff: Duration,
94    /// Enable statement logging
95    pub log_statements: bool,
96}
97
98impl Default for PostgresConfig {
99    fn default() -> Self {
100        Self {
101            database_url: "postgresql://postgres:postgres@localhost/optimizer".to_string(),
102            min_connections: 2,
103            max_connections: 10,
104            table_name: "state_entries".to_string(),
105            schema_name: "public".to_string(),
106            connect_timeout: Duration::from_secs(30),
107            query_timeout: Duration::from_secs(60),
108            ssl_mode: "prefer".to_string(),
109            max_retries: 3,
110            retry_backoff: Duration::from_millis(100),
111            log_statements: false,
112        }
113    }
114}
115
116impl PostgresConfig {
117    /// Create a new configuration with the given database URL
118    ///
119    /// # Arguments
120    ///
121    /// * `database_url` - PostgreSQL connection string
122    ///
123    /// # Example
124    ///
125    /// ```
126    /// use processor::state::PostgresConfig;
127    ///
128    /// let config = PostgresConfig::new("postgresql://user:pass@localhost/mydb");
129    /// ```
130    pub fn new<S: Into<String>>(database_url: S) -> Self {
131        Self {
132            database_url: database_url.into(),
133            ..Default::default()
134        }
135    }
136
137    /// Set the connection pool size
138    ///
139    /// # Arguments
140    ///
141    /// * `min` - Minimum connections
142    /// * `max` - Maximum connections
143    pub fn with_pool_size(mut self, min: u32, max: u32) -> Self {
144        self.min_connections = min;
145        self.max_connections = max;
146        self
147    }
148
149    /// Set the table name for state storage
150    pub fn with_table_name<S: Into<String>>(mut self, name: S) -> Self {
151        self.table_name = name.into();
152        self
153    }
154
155    /// Set the schema name
156    pub fn with_schema_name<S: Into<String>>(mut self, name: S) -> Self {
157        self.schema_name = name.into();
158        self
159    }
160
161    /// Set connection and query timeouts
162    pub fn with_timeout(mut self, timeout: Duration) -> Self {
163        self.connect_timeout = timeout;
164        self.query_timeout = timeout;
165        self
166    }
167
168    /// Set SSL mode (disable, allow, prefer, require, verify-ca, verify-full)
169    pub fn with_ssl_mode<S: Into<String>>(mut self, mode: S) -> Self {
170        self.ssl_mode = mode.into();
171        self
172    }
173
174    /// Set retry configuration
175    pub fn with_retries(mut self, max_retries: u32, backoff: Duration) -> Self {
176        self.max_retries = max_retries;
177        self.retry_backoff = backoff;
178        self
179    }
180
181    /// Enable or disable statement logging
182    pub fn with_statement_logging(mut self, enabled: bool) -> Self {
183        self.log_statements = enabled;
184        self
185    }
186}
187
188/// Statistics for the PostgreSQL backend
189#[derive(Debug, Clone, Default, Serialize, Deserialize)]
190pub struct PostgresStats {
191    /// Number of get operations
192    pub get_count: u64,
193    /// Number of put operations
194    pub put_count: u64,
195    /// Number of delete operations
196    pub delete_count: u64,
197    /// Number of queries executed
198    pub query_count: u64,
199    /// Number of transactions committed
200    pub transaction_count: u64,
201    /// Number of retry attempts
202    pub retry_count: u64,
203    /// Total bytes written
204    pub bytes_written: u64,
205    /// Total bytes read
206    pub bytes_read: u64,
207}
208
209/// Checkpoint metadata
210#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct CheckpointInfo {
212    /// Unique checkpoint identifier
213    pub checkpoint_id: Uuid,
214    /// Timestamp when checkpoint was created
215    pub created_at: chrono::DateTime<chrono::Utc>,
216    /// Number of rows in checkpoint
217    pub row_count: i64,
218    /// Total data size in bytes
219    pub data_size: i64,
220    /// Optional metadata
221    pub metadata: Option<serde_json::Value>,
222}
223
224/// PostgreSQL state backend
225///
226/// This backend uses PostgreSQL for persistent, distributed state management.
227/// It provides ACID guarantees, connection pooling, automatic migrations,
228/// and advanced features like JSONB metadata and checkpointing.
229pub struct PostgresStateBackend {
230    /// Connection pool
231    pool: PgPool,
232    /// Configuration
233    config: PostgresConfig,
234    /// Statistics
235    stats: Arc<RwLock<PostgresStats>>,
236}
237
238impl PostgresStateBackend {
239    /// Create a new PostgreSQL state backend
240    ///
241    /// This initializes the connection pool and runs automatic schema migrations.
242    ///
243    /// # Arguments
244    ///
245    /// * `config` - PostgreSQL configuration
246    ///
247    /// # Returns
248    ///
249    /// * `Ok(PostgresStateBackend)` - Successfully initialized backend
250    /// * `Err(StateError)` - Failed to initialize
251    ///
252    /// # Example
253    ///
254    /// ```rust,no_run
255    /// # use processor::state::{PostgresStateBackend, PostgresConfig};
256    /// # #[tokio::main]
257    /// # async fn main() -> anyhow::Result<()> {
258    /// let config = PostgresConfig::new("postgresql://localhost/test");
259    /// let backend = PostgresStateBackend::new(config).await?;
260    /// # Ok(())
261    /// # }
262    /// ```
263    pub async fn new(config: PostgresConfig) -> StateResult<Self> {
264        info!("Initializing PostgreSQL state backend");
265        info!("Database: {}", Self::sanitize_url(&config.database_url));
266        info!("Pool size: {} - {}", config.min_connections, config.max_connections);
267
268        // Parse SSL mode
269        let ssl_mode = Self::parse_ssl_mode(&config.ssl_mode)?;
270
271        // Parse connection options
272        let connect_options = PgConnectOptions::from_str(&config.database_url)
273            .map_err(|e| StateError::StorageError {
274                backend_type: "postgres".to_string(),
275                details: format!("Invalid database URL: {}", e),
276            })?
277            .ssl_mode(ssl_mode);
278
279        // Create connection pool
280        let pool = PgPoolOptions::new()
281            .min_connections(config.min_connections)
282            .max_connections(config.max_connections)
283            .acquire_timeout(config.connect_timeout)
284            .connect_with(connect_options)
285            .await
286            .map_err(|e| StateError::StorageError {
287                backend_type: "postgres".to_string(),
288                details: format!("Failed to create connection pool: {}", e),
289            })?;
290
291        info!("Connection pool created successfully");
292
293        let backend = Self {
294            pool,
295            config,
296            stats: Arc::new(RwLock::new(PostgresStats::default())),
297        };
298
299        // Run migrations
300        backend.run_migrations().await?;
301
302        info!("PostgreSQL state backend initialized successfully");
303
304        Ok(backend)
305    }
306
307    /// Run database migrations
308    ///
309    /// This applies all pending schema migrations from the migrations directory.
310    async fn run_migrations(&self) -> StateResult<()> {
311        info!("Running database migrations");
312
313        // Read migration files
314        let migration_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("migrations");
315
316        if !migration_dir.exists() {
317            warn!("Migrations directory not found: {:?}", migration_dir);
318            return Ok(());
319        }
320
321        let mut migrations = vec![];
322        let mut entries = tokio::fs::read_dir(&migration_dir)
323            .await
324            .map_err(|e| StateError::StorageError {
325                backend_type: "postgres".to_string(),
326                details: format!("Failed to read migrations directory: {}", e),
327            })?;
328
329        while let Some(entry) = entries.next_entry().await.map_err(|e| StateError::StorageError {
330            backend_type: "postgres".to_string(),
331            details: format!("Failed to read migration entry: {}", e),
332        })? {
333            let path = entry.path();
334            if path.extension().and_then(|s| s.to_str()) == Some("sql") {
335                migrations.push(path);
336            }
337        }
338
339        // Sort migrations by filename
340        migrations.sort();
341
342        // Execute each migration
343        for migration_path in migrations {
344            let filename = migration_path
345                .file_name()
346                .and_then(|s| s.to_str())
347                .unwrap_or("unknown");
348
349            info!("Applying migration: {}", filename);
350
351            let sql = tokio::fs::read_to_string(&migration_path)
352                .await
353                .map_err(|e| StateError::StorageError {
354                    backend_type: "postgres".to_string(),
355                    details: format!("Failed to read migration file {}: {}", filename, e),
356                })?;
357
358            sqlx::raw_sql(&sql)
359                .execute(&self.pool)
360                .await
361                .map_err(|e| StateError::StorageError {
362                    backend_type: "postgres".to_string(),
363                    details: format!("Failed to execute migration {}: {}", filename, e),
364                })?;
365
366            debug!("Migration applied successfully: {}", filename);
367        }
368
369        info!("All migrations applied successfully");
370        Ok(())
371    }
372
373    /// Put a value with metadata
374    ///
375    /// # Arguments
376    ///
377    /// * `key` - The key to store
378    /// * `value` - The value to store
379    /// * `metadata` - JSON metadata
380    ///
381    /// # Example
382    ///
383    /// ```rust,no_run
384    /// # use processor::state::{PostgresStateBackend, PostgresConfig, StateBackend};
385    /// # #[tokio::main]
386    /// # async fn main() -> anyhow::Result<()> {
387    /// # let backend = PostgresStateBackend::new(PostgresConfig::default()).await?;
388    /// backend.put_with_metadata(
389    ///     b"window:123",
390    ///     b"data",
391    ///     serde_json::json!({"partition": 5, "source": "kafka"})
392    /// ).await?;
393    /// # Ok(())
394    /// # }
395    /// ```
396    pub async fn put_with_metadata(
397        &self,
398        key: &[u8],
399        value: &[u8],
400        metadata: serde_json::Value,
401    ) -> StateResult<()> {
402        trace!("Putting key with metadata to PostgreSQL: {} bytes", key.len());
403
404        let key = key.to_vec();
405        let value = value.to_vec();
406        let metadata = metadata.clone();
407
408        self.retry_operation(move || {
409            let key = key.clone();
410            let value = value.clone();
411            let metadata = metadata.clone();
412            async move {
413                let key_len = key.len();
414                let value_len = value.len();
415
416                sqlx::query(
417                    r#"
418                    INSERT INTO state_entries (key, value, metadata, updated_at)
419                    VALUES ($1, $2, $3, NOW())
420                    ON CONFLICT (key) DO UPDATE
421                    SET value = EXCLUDED.value,
422                        metadata = EXCLUDED.metadata,
423                        updated_at = NOW()
424                    "#,
425                )
426                .bind(key)
427                .bind(value)
428                .bind(metadata)
429            .execute(&self.pool)
430            .await
431            .map_err(|e| StateError::StorageError {
432                backend_type: "postgres".to_string(),
433                details: format!("Put with metadata failed: {}", e),
434            })?;
435
436            let mut stats = self.stats.write().await;
437            stats.put_count += 1;
438            stats.bytes_written += (key_len + value_len) as u64;
439            stats.query_count += 1;
440
441            Ok(())
442            }
443        })
444        .await
445    }
446
447    /// Put a value with TTL
448    ///
449    /// # Arguments
450    ///
451    /// * `key` - The key to store
452    /// * `value` - The value to store
453    /// * `ttl` - Time to live duration
454    pub async fn put_with_ttl(&self, key: &[u8], value: &[u8], ttl: Duration) -> StateResult<()> {
455        trace!("Putting key with TTL to PostgreSQL: {} bytes, ttl: {:?}", key.len(), ttl);
456
457        self.retry_operation(|| async {
458            let expires_at = chrono::Utc::now() + chrono::Duration::from_std(ttl).unwrap();
459
460            sqlx::query(
461                r#"
462                INSERT INTO state_entries (key, value, expires_at, updated_at)
463                VALUES ($1, $2, $3, NOW())
464                ON CONFLICT (key) DO UPDATE
465                SET value = EXCLUDED.value,
466                    expires_at = EXCLUDED.expires_at,
467                    updated_at = NOW()
468                "#,
469            )
470            .bind(key)
471            .bind(value)
472            .bind(expires_at)
473            .execute(&self.pool)
474            .await
475            .map_err(|e| StateError::StorageError {
476                backend_type: "postgres".to_string(),
477                details: format!("Put with TTL failed: {}", e),
478            })?;
479
480            let mut stats = self.stats.write().await;
481            stats.put_count += 1;
482            stats.bytes_written += (key.len() + value.len()) as u64;
483            stats.query_count += 1;
484
485            Ok(())
486        })
487        .await
488    }
489
490    /// Clean up expired entries
491    ///
492    /// This removes all entries where expires_at is in the past.
493    ///
494    /// # Returns
495    ///
496    /// Number of entries deleted
497    pub async fn cleanup_expired(&self) -> StateResult<usize> {
498        debug!("Cleaning up expired entries");
499
500        self.retry_operation(|| async {
501            let result = sqlx::query("SELECT cleanup_expired_state_entries()")
502                .fetch_one(&self.pool)
503                .await
504                .map_err(|e| StateError::StorageError {
505                    backend_type: "postgres".to_string(),
506                    details: format!("Cleanup failed: {}", e),
507                })?;
508
509            let count: i64 = result.try_get(0).map_err(|e| StateError::StorageError {
510                backend_type: "postgres".to_string(),
511                details: format!("Failed to get cleanup count: {}", e),
512            })?;
513
514            let mut stats = self.stats.write().await;
515            stats.query_count += 1;
516
517            info!("Cleaned up {} expired entries", count);
518            Ok(count as usize)
519        })
520        .await
521    }
522
523    /// Create a checkpoint of the current state
524    ///
525    /// This creates a point-in-time snapshot of all state entries.
526    ///
527    /// # Returns
528    ///
529    /// Checkpoint UUID
530    pub async fn create_checkpoint(&self) -> StateResult<Uuid> {
531        info!("Creating state checkpoint");
532
533        self.retry_operation(|| async {
534            let mut tx = self.pool.begin().await.map_err(|e| StateError::TransactionFailed {
535                operation: "begin".to_string(),
536                reason: e.to_string(),
537            })?;
538
539            // Get all current state
540            let rows = sqlx::query("SELECT key, value, metadata FROM state_entries")
541                .fetch_all(&mut *tx)
542                .await
543                .map_err(|e| StateError::StorageError {
544                    backend_type: "postgres".to_string(),
545                    details: format!("Failed to fetch state: {}", e),
546                })?;
547
548            let mut checkpoint_data = Vec::new();
549            let mut total_size = 0i64;
550
551            for row in &rows {
552                let key: Vec<u8> = row.try_get("key").map_err(|e| StateError::StorageError {
553                    backend_type: "postgres".to_string(),
554                    details: format!("Failed to get key: {}", e),
555                })?;
556                let value: Vec<u8> = row.try_get("value").map_err(|e| StateError::StorageError {
557                    backend_type: "postgres".to_string(),
558                    details: format!("Failed to get value: {}", e),
559                })?;
560
561                total_size += (key.len() + value.len()) as i64;
562
563                checkpoint_data.push(serde_json::json!({
564                    "key": hex::encode(&key),
565                    "value": hex::encode(&value),
566                }));
567            }
568
569            let checkpoint_id = Uuid::new_v4();
570            let checkpoint_json = serde_json::to_value(checkpoint_data).map_err(|e| {
571                StateError::SerializationFailed {
572                    key: "checkpoint".to_string(),
573                    reason: e.to_string(),
574                }
575            })?;
576
577            // Store checkpoint
578            sqlx::query(
579                r#"
580                INSERT INTO state_checkpoints (checkpoint_id, checkpoint_data, row_count, data_size)
581                VALUES ($1, $2, $3, $4)
582                "#,
583            )
584            .bind(checkpoint_id)
585            .bind(checkpoint_json)
586            .bind(rows.len() as i64)
587            .bind(total_size)
588            .execute(&mut *tx)
589            .await
590            .map_err(|e| StateError::StorageError {
591                backend_type: "postgres".to_string(),
592                details: format!("Failed to store checkpoint: {}", e),
593            })?;
594
595            tx.commit().await.map_err(|e| StateError::TransactionFailed {
596                operation: "commit".to_string(),
597                reason: e.to_string(),
598            })?;
599
600            let mut stats = self.stats.write().await;
601            stats.transaction_count += 1;
602
603            info!("Checkpoint created: {} ({} entries, {} bytes)", checkpoint_id, rows.len(), total_size);
604            Ok(checkpoint_id)
605        })
606        .await
607    }
608
609    /// Restore from a checkpoint
610    ///
611    /// # Arguments
612    ///
613    /// * `checkpoint_id` - UUID of the checkpoint to restore
614    pub async fn restore_checkpoint(&self, checkpoint_id: Uuid) -> StateResult<()> {
615        info!("Restoring from checkpoint: {}", checkpoint_id);
616
617        self.retry_operation(|| async {
618            let mut tx = self.pool.begin().await.map_err(|e| StateError::TransactionFailed {
619                operation: "begin".to_string(),
620                reason: e.to_string(),
621            })?;
622
623            // Get checkpoint
624            let row = sqlx::query("SELECT checkpoint_data FROM state_checkpoints WHERE checkpoint_id = $1")
625                .bind(checkpoint_id)
626                .fetch_optional(&mut *tx)
627                .await
628                .map_err(|e| StateError::StorageError {
629                    backend_type: "postgres".to_string(),
630                    details: format!("Failed to fetch checkpoint: {}", e),
631                })?
632                .ok_or_else(|| StateError::RestoreFailed {
633                    checkpoint_id: checkpoint_id.to_string(),
634                    reason: "Checkpoint not found".to_string(),
635                })?;
636
637            let checkpoint_data: serde_json::Value = row.try_get("checkpoint_data").map_err(|e| {
638                StateError::DeserializationFailed {
639                    key: checkpoint_id.to_string(),
640                    reason: e.to_string(),
641                }
642            })?;
643
644            // Clear existing state
645            sqlx::query("DELETE FROM state_entries")
646                .execute(&mut *tx)
647                .await
648                .map_err(|e| StateError::StorageError {
649                    backend_type: "postgres".to_string(),
650                    details: format!("Failed to clear state: {}", e),
651                })?;
652
653            // Restore entries
654            if let Some(entries) = checkpoint_data.as_array() {
655                for entry in entries {
656                    let key_hex = entry["key"].as_str().ok_or_else(|| StateError::DeserializationFailed {
657                        key: "checkpoint_entry".to_string(),
658                        reason: "Missing key field".to_string(),
659                    })?;
660                    let value_hex = entry["value"].as_str().ok_or_else(|| StateError::DeserializationFailed {
661                        key: "checkpoint_entry".to_string(),
662                        reason: "Missing value field".to_string(),
663                    })?;
664
665                    let key = hex::decode(key_hex).map_err(|e| StateError::DeserializationFailed {
666                        key: "key".to_string(),
667                        reason: e.to_string(),
668                    })?;
669                    let value = hex::decode(value_hex).map_err(|e| StateError::DeserializationFailed {
670                        key: "value".to_string(),
671                        reason: e.to_string(),
672                    })?;
673
674                    sqlx::query("INSERT INTO state_entries (key, value) VALUES ($1, $2)")
675                        .bind(&key)
676                        .bind(&value)
677                        .execute(&mut *tx)
678                        .await
679                        .map_err(|e| StateError::StorageError {
680                            backend_type: "postgres".to_string(),
681                            details: format!("Failed to insert entry: {}", e),
682                        })?;
683                }
684            }
685
686            tx.commit().await.map_err(|e| StateError::TransactionFailed {
687                operation: "commit".to_string(),
688                reason: e.to_string(),
689            })?;
690
691            info!("Checkpoint restored successfully");
692            Ok(())
693        })
694        .await
695    }
696
697    /// List all checkpoints
698    pub async fn list_checkpoints(&self) -> StateResult<Vec<CheckpointInfo>> {
699        debug!("Listing checkpoints");
700
701        let rows = sqlx::query(
702            r#"
703            SELECT checkpoint_id, created_at, row_count, data_size, metadata
704            FROM state_checkpoints
705            ORDER BY created_at DESC
706            "#,
707        )
708        .fetch_all(&self.pool)
709        .await
710        .map_err(|e| StateError::StorageError {
711            backend_type: "postgres".to_string(),
712            details: format!("Failed to list checkpoints: {}", e),
713        })?;
714
715        let mut checkpoints = Vec::new();
716        for row in rows {
717            checkpoints.push(CheckpointInfo {
718                checkpoint_id: row.try_get("checkpoint_id").unwrap(),
719                created_at: row.try_get("created_at").unwrap(),
720                row_count: row.try_get("row_count").unwrap(),
721                data_size: row.try_get("data_size").unwrap(),
722                metadata: row.try_get("metadata").ok(),
723            });
724        }
725
726        Ok(checkpoints)
727    }
728
729    /// Get backend statistics
730    pub async fn stats(&self) -> PostgresStats {
731        self.stats.read().await.clone()
732    }
733
734    /// Execute VACUUM on the state table
735    ///
736    /// This reclaims storage and updates statistics for better query performance.
737    pub async fn vacuum(&self) -> StateResult<()> {
738        info!("Running VACUUM on state_entries table");
739
740        sqlx::query("VACUUM ANALYZE state_entries")
741            .execute(&self.pool)
742            .await
743            .map_err(|e| StateError::StorageError {
744                backend_type: "postgres".to_string(),
745                details: format!("VACUUM failed: {}", e),
746            })?;
747
748        info!("VACUUM completed successfully");
749        Ok(())
750    }
751
752    /// Get table size statistics
753    pub async fn table_stats(&self) -> StateResult<(i64, i64)> {
754        let row = sqlx::query(
755            r#"
756            SELECT
757                pg_total_relation_size('state_entries') as total_size,
758                pg_relation_size('state_entries') as table_size
759            "#,
760        )
761        .fetch_one(&self.pool)
762        .await
763        .map_err(|e| StateError::StorageError {
764            backend_type: "postgres".to_string(),
765            details: format!("Failed to get table stats: {}", e),
766        })?;
767
768        let total_size: i64 = row.try_get("total_size").unwrap_or(0);
769        let table_size: i64 = row.try_get("table_size").unwrap_or(0);
770
771        Ok((total_size, table_size))
772    }
773
774    /// Batch put operation
775    ///
776    /// Efficiently insert multiple key-value pairs in a single transaction.
777    pub async fn batch_put(&self, entries: &[(&[u8], &[u8])]) -> StateResult<()> {
778        if entries.is_empty() {
779            return Ok(());
780        }
781
782        debug!("Batch putting {} entries", entries.len());
783
784        self.retry_operation(|| async {
785            let mut tx = self.pool.begin().await.map_err(|e| StateError::TransactionFailed {
786                operation: "begin".to_string(),
787                reason: e.to_string(),
788            })?;
789
790            for (key, value) in entries {
791                sqlx::query(
792                    r#"
793                    INSERT INTO state_entries (key, value, updated_at)
794                    VALUES ($1, $2, NOW())
795                    ON CONFLICT (key) DO UPDATE
796                    SET value = EXCLUDED.value, updated_at = NOW()
797                    "#,
798                )
799                .bind(key)
800                .bind(value)
801                .execute(&mut *tx)
802                .await
803                .map_err(|e| StateError::StorageError {
804                    backend_type: "postgres".to_string(),
805                    details: format!("Batch insert failed: {}", e),
806                })?;
807            }
808
809            tx.commit().await.map_err(|e| StateError::TransactionFailed {
810                operation: "commit".to_string(),
811                reason: e.to_string(),
812            })?;
813
814            let mut stats = self.stats.write().await;
815            stats.put_count += entries.len() as u64;
816            stats.transaction_count += 1;
817
818            Ok(())
819        })
820        .await
821    }
822
823    /// Health check - verify database connectivity
824    pub async fn health_check(&self) -> StateResult<bool> {
825        sqlx::query("SELECT 1")
826            .fetch_one(&self.pool)
827            .await
828            .map(|_| true)
829            .map_err(|e| StateError::StorageError {
830                backend_type: "postgres".to_string(),
831                details: format!("Health check failed: {}", e),
832            })
833    }
834
835    /// Close the connection pool gracefully
836    pub async fn close(&self) {
837        info!("Closing PostgreSQL connection pool");
838        self.pool.close().await;
839    }
840
841    // Helper methods
842
843    fn sanitize_url(url: &str) -> String {
844        // Remove password from URL for logging
845        if let Some(at_pos) = url.rfind('@') {
846            if let Some(scheme_end) = url.find("://") {
847                let scheme = &url[..scheme_end + 3];
848                let host = &url[at_pos + 1..];
849                return format!("{}***@{}", scheme, host);
850            }
851        }
852        url.to_string()
853    }
854
855    fn parse_ssl_mode(mode: &str) -> StateResult<PgSslMode> {
856        match mode.to_lowercase().as_str() {
857            "disable" => Ok(PgSslMode::Disable),
858            "allow" => Ok(PgSslMode::Allow),
859            "prefer" => Ok(PgSslMode::Prefer),
860            "require" => Ok(PgSslMode::Require),
861            "verify-ca" => Ok(PgSslMode::VerifyCa),
862            "verify-full" => Ok(PgSslMode::VerifyFull),
863            _ => Err(StateError::StorageError {
864                backend_type: "postgres".to_string(),
865                details: format!("Invalid SSL mode: {}", mode),
866            }),
867        }
868    }
869
870    async fn retry_operation<F, Fut, T>(&self, mut operation: F) -> StateResult<T>
871    where
872        F: FnMut() -> Fut,
873        Fut: std::future::Future<Output = StateResult<T>>,
874    {
875        let mut attempts = 0;
876        loop {
877            match operation().await {
878                Ok(result) => return Ok(result),
879                Err(e) => {
880                    attempts += 1;
881                    if attempts >= self.config.max_retries {
882                        return Err(e);
883                    }
884
885                    warn!("Operation failed (attempt {}/{}): {:?}", attempts, self.config.max_retries, e);
886
887                    let mut stats = self.stats.write().await;
888                    stats.retry_count += 1;
889                    drop(stats);
890
891                    tokio::time::sleep(self.config.retry_backoff * attempts).await;
892                }
893            }
894        }
895    }
896}
897
898#[async_trait]
899impl StateBackend for PostgresStateBackend {
900    async fn get(&self, key: &[u8]) -> StateResult<Option<Vec<u8>>> {
901        trace!("Getting key from PostgreSQL: {} bytes", key.len());
902
903        self.retry_operation(|| async {
904            let result = sqlx::query(
905                r#"
906                SELECT value FROM state_entries
907                WHERE key = $1
908                  AND (expires_at IS NULL OR expires_at > NOW())
909                "#,
910            )
911            .bind(key)
912            .fetch_optional(&self.pool)
913            .await
914            .map_err(|e| StateError::StorageError {
915                backend_type: "postgres".to_string(),
916                details: format!("Get failed: {}", e),
917            })?;
918
919            let mut stats = self.stats.write().await;
920            stats.get_count += 1;
921            stats.query_count += 1;
922
923            if let Some(row) = result {
924                let value: Vec<u8> = row.try_get("value").map_err(|e| StateError::StorageError {
925                    backend_type: "postgres".to_string(),
926                    details: format!("Failed to extract value: {}", e),
927                })?;
928
929                stats.bytes_read += value.len() as u64;
930                Ok(Some(value))
931            } else {
932                Ok(None)
933            }
934        })
935        .await
936    }
937
938    async fn put(&self, key: &[u8], value: &[u8]) -> StateResult<()> {
939        trace!("Putting key to PostgreSQL: {} bytes", key.len());
940
941        self.retry_operation(|| async {
942            sqlx::query(
943                r#"
944                INSERT INTO state_entries (key, value, updated_at)
945                VALUES ($1, $2, NOW())
946                ON CONFLICT (key) DO UPDATE
947                SET value = EXCLUDED.value, updated_at = NOW()
948                "#,
949            )
950            .bind(key)
951            .bind(value)
952            .execute(&self.pool)
953            .await
954            .map_err(|e| StateError::StorageError {
955                backend_type: "postgres".to_string(),
956                details: format!("Put failed: {}", e),
957            })?;
958
959            let mut stats = self.stats.write().await;
960            stats.put_count += 1;
961            stats.bytes_written += (key.len() + value.len()) as u64;
962            stats.query_count += 1;
963
964            Ok(())
965        })
966        .await
967    }
968
969    async fn delete(&self, key: &[u8]) -> StateResult<()> {
970        trace!("Deleting key from PostgreSQL: {} bytes", key.len());
971
972        self.retry_operation(|| async {
973            sqlx::query("DELETE FROM state_entries WHERE key = $1")
974                .bind(key)
975                .execute(&self.pool)
976                .await
977                .map_err(|e| StateError::StorageError {
978                    backend_type: "postgres".to_string(),
979                    details: format!("Delete failed: {}", e),
980                })?;
981
982            let mut stats = self.stats.write().await;
983            stats.delete_count += 1;
984            stats.query_count += 1;
985
986            Ok(())
987        })
988        .await
989    }
990
991    async fn list_keys(&self, prefix: &[u8]) -> StateResult<Vec<Vec<u8>>> {
992        trace!("Listing keys with prefix: {} bytes", prefix.len());
993
994        self.retry_operation(|| async {
995            let rows = if prefix.is_empty() {
996                sqlx::query("SELECT key FROM state_entries")
997                    .fetch_all(&self.pool)
998                    .await
999            } else {
1000                // Use prefix matching with LIKE and escape special characters
1001                sqlx::query(
1002                    r#"
1003                    SELECT key FROM state_entries
1004                    WHERE key >= $1 AND key < $2
1005                    "#,
1006                )
1007                .bind(prefix)
1008                .bind(Self::next_prefix(prefix))
1009                .fetch_all(&self.pool)
1010                .await
1011            }
1012            .map_err(|e| StateError::StorageError {
1013                backend_type: "postgres".to_string(),
1014                details: format!("List keys failed: {}", e),
1015            })?;
1016
1017            let mut stats = self.stats.write().await;
1018            stats.query_count += 1;
1019            drop(stats);
1020
1021            let keys = rows
1022                .into_iter()
1023                .filter_map(|row| row.try_get::<Vec<u8>, _>("key").ok())
1024                .collect();
1025
1026            Ok(keys)
1027        })
1028        .await
1029    }
1030
1031    async fn clear(&self) -> StateResult<()> {
1032        debug!("Clearing all state entries");
1033
1034        self.retry_operation(|| async {
1035            sqlx::query("TRUNCATE TABLE state_entries")
1036                .execute(&self.pool)
1037                .await
1038                .map_err(|e| StateError::StorageError {
1039                    backend_type: "postgres".to_string(),
1040                    details: format!("Clear failed: {}", e),
1041                })?;
1042
1043            let mut stats = self.stats.write().await;
1044            stats.query_count += 1;
1045
1046            Ok(())
1047        })
1048        .await
1049    }
1050
1051    async fn count(&self) -> StateResult<usize> {
1052        self.retry_operation(|| async {
1053            let row = sqlx::query("SELECT COUNT(*) as count FROM state_entries")
1054                .fetch_one(&self.pool)
1055                .await
1056                .map_err(|e| StateError::StorageError {
1057                    backend_type: "postgres".to_string(),
1058                    details: format!("Count failed: {}", e),
1059                })?;
1060
1061            let count: i64 = row.try_get("count").map_err(|e| StateError::StorageError {
1062                backend_type: "postgres".to_string(),
1063                details: format!("Failed to extract count: {}", e),
1064            })?;
1065
1066            let mut stats = self.stats.write().await;
1067            stats.query_count += 1;
1068
1069            Ok(count as usize)
1070        })
1071        .await
1072    }
1073
1074    async fn contains(&self, key: &[u8]) -> StateResult<bool> {
1075        self.retry_operation(|| async {
1076            let row = sqlx::query(
1077                r#"
1078                SELECT EXISTS(
1079                    SELECT 1 FROM state_entries
1080                    WHERE key = $1
1081                      AND (expires_at IS NULL OR expires_at > NOW())
1082                ) as exists
1083                "#,
1084            )
1085            .bind(key)
1086            .fetch_one(&self.pool)
1087            .await
1088            .map_err(|e| StateError::StorageError {
1089                backend_type: "postgres".to_string(),
1090                details: format!("Contains check failed: {}", e),
1091            })?;
1092
1093            let exists: bool = row.try_get("exists").unwrap_or(false);
1094
1095            let mut stats = self.stats.write().await;
1096            stats.query_count += 1;
1097
1098            Ok(exists)
1099        })
1100        .await
1101    }
1102}
1103
1104impl PostgresStateBackend {
1105    /// Calculate the next prefix for range queries
1106    fn next_prefix(prefix: &[u8]) -> Vec<u8> {
1107        let mut next = prefix.to_vec();
1108        for i in (0..next.len()).rev() {
1109            if next[i] < 255 {
1110                next[i] += 1;
1111                return next;
1112            }
1113        }
1114        // If all bytes are 255, append a zero
1115        next.push(0);
1116        next
1117    }
1118}
1119
1120// Add hex dependency support
1121mod hex {
1122    pub fn encode(bytes: &[u8]) -> String {
1123        bytes.iter().map(|b| format!("{:02x}", b)).collect()
1124    }
1125
1126    pub fn decode(s: &str) -> Result<Vec<u8>, String> {
1127        (0..s.len())
1128            .step_by(2)
1129            .map(|i| {
1130                u8::from_str_radix(&s[i..i + 2], 16)
1131                    .map_err(|e| format!("Invalid hex: {}", e))
1132            })
1133            .collect()
1134    }
1135}
1136
1137#[cfg(test)]
1138mod tests {
1139    use super::*;
1140    use crate::state::backend::tests::*;
1141
1142    // Helper to create test backend (requires running PostgreSQL)
1143    async fn create_test_backend() -> Option<PostgresStateBackend> {
1144        let config = PostgresConfig::new("postgresql://postgres:postgres@localhost/optimizer_test")
1145            .with_pool_size(2, 5);
1146
1147        match PostgresStateBackend::new(config).await {
1148            Ok(backend) => {
1149                backend.clear().await.ok();
1150                Some(backend)
1151            }
1152            Err(_) => {
1153                eprintln!("Skipping PostgreSQL tests - database not available");
1154                None
1155            }
1156        }
1157    }
1158
1159    #[tokio::test]
1160    async fn test_postgres_backend_basic() {
1161        if let Some(backend) = create_test_backend().await {
1162            test_backend_basic_ops(backend).await;
1163        }
1164    }
1165
1166    #[tokio::test]
1167    async fn test_postgres_backend_list_keys() {
1168        if let Some(backend) = create_test_backend().await {
1169            test_backend_list_keys(backend).await;
1170        }
1171    }
1172
1173    #[tokio::test]
1174    async fn test_postgres_backend_clear() {
1175        if let Some(backend) = create_test_backend().await {
1176            test_backend_clear(backend).await;
1177        }
1178    }
1179
1180    #[tokio::test]
1181    async fn test_postgres_backend_contains() {
1182        if let Some(backend) = create_test_backend().await {
1183            test_backend_contains(backend).await;
1184        }
1185    }
1186
1187    #[tokio::test]
1188    async fn test_postgres_metadata() {
1189        if let Some(backend) = create_test_backend().await {
1190            let metadata = serde_json::json!({
1191                "source": "kafka",
1192                "partition": 5,
1193                "offset": 12345
1194            });
1195
1196            backend
1197                .put_with_metadata(b"test_key", b"test_value", metadata)
1198                .await
1199                .unwrap();
1200
1201            let value = backend.get(b"test_key").await.unwrap();
1202            assert_eq!(value, Some(b"test_value".to_vec()));
1203        }
1204    }
1205
1206    #[tokio::test]
1207    async fn test_postgres_ttl() {
1208        if let Some(backend) = create_test_backend().await {
1209            // Put with 1 second TTL
1210            backend
1211                .put_with_ttl(b"ttl_key", b"ttl_value", Duration::from_secs(1))
1212                .await
1213                .unwrap();
1214
1215            // Should exist immediately
1216            assert!(backend.contains(b"ttl_key").await.unwrap());
1217
1218            // Wait for expiration
1219            tokio::time::sleep(Duration::from_secs(2)).await;
1220
1221            // Should be filtered out by query
1222            assert!(!backend.contains(b"ttl_key").await.unwrap());
1223
1224            // Cleanup should remove it
1225            let deleted = backend.cleanup_expired().await.unwrap();
1226            assert_eq!(deleted, 1);
1227        }
1228    }
1229
1230    #[tokio::test]
1231    async fn test_postgres_checkpoint() {
1232        if let Some(backend) = create_test_backend().await {
1233            backend.put(b"key1", b"value1").await.unwrap();
1234            backend.put(b"key2", b"value2").await.unwrap();
1235            backend.put(b"key3", b"value3").await.unwrap();
1236
1237            // Create checkpoint
1238            let checkpoint_id = backend.create_checkpoint().await.unwrap();
1239
1240            // Modify state
1241            backend.put(b"key1", b"modified").await.unwrap();
1242            backend.delete(b"key2").await.unwrap();
1243
1244            // Restore checkpoint
1245            backend.restore_checkpoint(checkpoint_id).await.unwrap();
1246
1247            // Verify original state restored
1248            assert_eq!(
1249                backend.get(b"key1").await.unwrap(),
1250                Some(b"value1".to_vec())
1251            );
1252            assert_eq!(
1253                backend.get(b"key2").await.unwrap(),
1254                Some(b"value2".to_vec())
1255            );
1256        }
1257    }
1258
1259    #[tokio::test]
1260    async fn test_postgres_batch_put() {
1261        if let Some(backend) = create_test_backend().await {
1262            let entries: Vec<(&[u8], &[u8])> = vec![
1263                (b"batch1", b"value1"),
1264                (b"batch2", b"value2"),
1265                (b"batch3", b"value3"),
1266            ];
1267
1268            backend.batch_put(&entries).await.unwrap();
1269
1270            assert_eq!(backend.count().await.unwrap(), 3);
1271            assert_eq!(
1272                backend.get(b"batch1").await.unwrap(),
1273                Some(b"value1".to_vec())
1274            );
1275        }
1276    }
1277
1278    #[tokio::test]
1279    async fn test_postgres_stats() {
1280        if let Some(backend) = create_test_backend().await {
1281            backend.put(b"key1", b"value1").await.unwrap();
1282            backend.get(b"key1").await.unwrap();
1283            backend.delete(b"key1").await.unwrap();
1284
1285            let stats = backend.stats().await;
1286            assert!(stats.put_count >= 1);
1287            assert!(stats.get_count >= 1);
1288            assert!(stats.delete_count >= 1);
1289            assert!(stats.bytes_written > 0);
1290        }
1291    }
1292
1293    #[tokio::test]
1294    async fn test_postgres_health_check() {
1295        if let Some(backend) = create_test_backend().await {
1296            assert!(backend.health_check().await.unwrap());
1297        }
1298    }
1299}