1use 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#[derive(Debug, Clone)]
73pub struct PostgresConfig {
74 pub database_url: String,
76 pub min_connections: u32,
78 pub max_connections: u32,
80 pub table_name: String,
82 pub schema_name: String,
84 pub connect_timeout: Duration,
86 pub query_timeout: Duration,
88 pub ssl_mode: String,
90 pub max_retries: u32,
92 pub retry_backoff: Duration,
94 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 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 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 pub fn with_table_name<S: Into<String>>(mut self, name: S) -> Self {
151 self.table_name = name.into();
152 self
153 }
154
155 pub fn with_schema_name<S: Into<String>>(mut self, name: S) -> Self {
157 self.schema_name = name.into();
158 self
159 }
160
161 pub fn with_timeout(mut self, timeout: Duration) -> Self {
163 self.connect_timeout = timeout;
164 self.query_timeout = timeout;
165 self
166 }
167
168 pub fn with_ssl_mode<S: Into<String>>(mut self, mode: S) -> Self {
170 self.ssl_mode = mode.into();
171 self
172 }
173
174 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 pub fn with_statement_logging(mut self, enabled: bool) -> Self {
183 self.log_statements = enabled;
184 self
185 }
186}
187
188#[derive(Debug, Clone, Default, Serialize, Deserialize)]
190pub struct PostgresStats {
191 pub get_count: u64,
193 pub put_count: u64,
195 pub delete_count: u64,
197 pub query_count: u64,
199 pub transaction_count: u64,
201 pub retry_count: u64,
203 pub bytes_written: u64,
205 pub bytes_read: u64,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct CheckpointInfo {
212 pub checkpoint_id: Uuid,
214 pub created_at: chrono::DateTime<chrono::Utc>,
216 pub row_count: i64,
218 pub data_size: i64,
220 pub metadata: Option<serde_json::Value>,
222}
223
224pub struct PostgresStateBackend {
230 pool: PgPool,
232 config: PostgresConfig,
234 stats: Arc<RwLock<PostgresStats>>,
236}
237
238impl PostgresStateBackend {
239 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 let ssl_mode = Self::parse_ssl_mode(&config.ssl_mode)?;
270
271 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 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 backend.run_migrations().await?;
301
302 info!("PostgreSQL state backend initialized successfully");
303
304 Ok(backend)
305 }
306
307 async fn run_migrations(&self) -> StateResult<()> {
311 info!("Running database migrations");
312
313 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 migrations.sort();
341
342 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 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 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 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 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 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 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 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 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 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 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 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 pub async fn stats(&self) -> PostgresStats {
731 self.stats.read().await.clone()
732 }
733
734 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 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 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 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 pub async fn close(&self) {
837 info!("Closing PostgreSQL connection pool");
838 self.pool.close().await;
839 }
840
841 fn sanitize_url(url: &str) -> String {
844 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 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 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 next.push(0);
1116 next
1117 }
1118}
1119
1120mod 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 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 backend
1211 .put_with_ttl(b"ttl_key", b"ttl_value", Duration::from_secs(1))
1212 .await
1213 .unwrap();
1214
1215 assert!(backend.contains(b"ttl_key").await.unwrap());
1217
1218 tokio::time::sleep(Duration::from_secs(2)).await;
1220
1221 assert!(!backend.contains(b"ttl_key").await.unwrap());
1223
1224 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 let checkpoint_id = backend.create_checkpoint().await.unwrap();
1239
1240 backend.put(b"key1", b"modified").await.unwrap();
1242 backend.delete(b"key2").await.unwrap();
1243
1244 backend.restore_checkpoint(checkpoint_id).await.unwrap();
1246
1247 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}