feat: convert postgresclient module to independent sal-postgresclient package
	
		
			
	
		
	
	
		
	
		
			Some checks are pending
		
		
	
	
		
			
				
	
				Rhai Tests / Run Rhai Tests (push) Waiting to run
				
			
		
		
	
	
				
					
				
			
		
			Some checks are pending
		
		
	
	Rhai Tests / Run Rhai Tests (push) Waiting to run
				
			- Move src/postgresclient/ to postgresclient/ package structure - Add comprehensive test suite (28 tests) with real PostgreSQL operations - Maintain Rhai integration with all 10 wrapper functions - Update workspace configuration and dependencies - Add complete documentation with usage examples - Remove old module and update all references - Ensure zero regressions in existing functionality Closes: postgresclient monorepo conversion
This commit is contained in:
		
							
								
								
									
										355
									
								
								postgresclient/src/installer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										355
									
								
								postgresclient/src/installer.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,355 @@ | ||||
| // PostgreSQL installer module | ||||
| // | ||||
| // This module provides functionality to install and configure PostgreSQL using nerdctl. | ||||
|  | ||||
| use std::collections::HashMap; | ||||
| use std::env; | ||||
| use std::fs; | ||||
| use std::path::Path; | ||||
| use std::process::Command; | ||||
| use std::thread; | ||||
| use std::time::Duration; | ||||
|  | ||||
| use sal_virt::nerdctl::Container; | ||||
| use std::error::Error; | ||||
| use std::fmt; | ||||
|  | ||||
| // Custom error type for PostgreSQL installer | ||||
| #[derive(Debug)] | ||||
| pub enum PostgresInstallerError { | ||||
|     IoError(std::io::Error), | ||||
|     NerdctlError(String), | ||||
|     PostgresError(String), | ||||
| } | ||||
|  | ||||
| impl fmt::Display for PostgresInstallerError { | ||||
|     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||
|         match self { | ||||
|             PostgresInstallerError::IoError(e) => write!(f, "I/O error: {}", e), | ||||
|             PostgresInstallerError::NerdctlError(e) => write!(f, "Nerdctl error: {}", e), | ||||
|             PostgresInstallerError::PostgresError(e) => write!(f, "PostgreSQL error: {}", e), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Error for PostgresInstallerError { | ||||
|     fn source(&self) -> Option<&(dyn Error + 'static)> { | ||||
|         match self { | ||||
|             PostgresInstallerError::IoError(e) => Some(e), | ||||
|             _ => None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<std::io::Error> for PostgresInstallerError { | ||||
|     fn from(error: std::io::Error) -> Self { | ||||
|         PostgresInstallerError::IoError(error) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// PostgreSQL installer configuration | ||||
| pub struct PostgresInstallerConfig { | ||||
|     /// Container name for PostgreSQL | ||||
|     pub container_name: String, | ||||
|     /// PostgreSQL version to install | ||||
|     pub version: String, | ||||
|     /// Port to expose PostgreSQL on | ||||
|     pub port: u16, | ||||
|     /// Username for PostgreSQL | ||||
|     pub username: String, | ||||
|     /// Password for PostgreSQL | ||||
|     pub password: String, | ||||
|     /// Data directory for PostgreSQL | ||||
|     pub data_dir: Option<String>, | ||||
|     /// Environment variables for PostgreSQL | ||||
|     pub env_vars: HashMap<String, String>, | ||||
|     /// Whether to use persistent storage | ||||
|     pub persistent: bool, | ||||
| } | ||||
|  | ||||
| impl Default for PostgresInstallerConfig { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             container_name: "postgres".to_string(), | ||||
|             version: "latest".to_string(), | ||||
|             port: 5432, | ||||
|             username: "postgres".to_string(), | ||||
|             password: "postgres".to_string(), | ||||
|             data_dir: None, | ||||
|             env_vars: HashMap::new(), | ||||
|             persistent: true, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl PostgresInstallerConfig { | ||||
|     /// Create a new PostgreSQL installer configuration with default values | ||||
|     pub fn new() -> Self { | ||||
|         Self::default() | ||||
|     } | ||||
|  | ||||
|     /// Set the container name | ||||
|     pub fn container_name(mut self, name: &str) -> Self { | ||||
|         self.container_name = name.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the PostgreSQL version | ||||
|     pub fn version(mut self, version: &str) -> Self { | ||||
|         self.version = version.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the port to expose PostgreSQL on | ||||
|     pub fn port(mut self, port: u16) -> Self { | ||||
|         self.port = port; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the username for PostgreSQL | ||||
|     pub fn username(mut self, username: &str) -> Self { | ||||
|         self.username = username.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the password for PostgreSQL | ||||
|     pub fn password(mut self, password: &str) -> Self { | ||||
|         self.password = password.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the data directory for PostgreSQL | ||||
|     pub fn data_dir(mut self, data_dir: &str) -> Self { | ||||
|         self.data_dir = Some(data_dir.to_string()); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Add an environment variable | ||||
|     pub fn env_var(mut self, key: &str, value: &str) -> Self { | ||||
|         self.env_vars.insert(key.to_string(), value.to_string()); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set whether to use persistent storage | ||||
|     pub fn persistent(mut self, persistent: bool) -> Self { | ||||
|         self.persistent = persistent; | ||||
|         self | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Install PostgreSQL using nerdctl | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `config` - PostgreSQL installer configuration | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Container, PostgresInstallerError>` - Container instance or error | ||||
| pub fn install_postgres( | ||||
|     config: PostgresInstallerConfig, | ||||
| ) -> Result<Container, PostgresInstallerError> { | ||||
|     // Create the data directory if it doesn't exist and persistent storage is enabled | ||||
|     let data_dir = if config.persistent { | ||||
|         let dir = config.data_dir.unwrap_or_else(|| { | ||||
|             let home_dir = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); | ||||
|             format!("{}/.postgres-data", home_dir) | ||||
|         }); | ||||
|  | ||||
|         if !Path::new(&dir).exists() { | ||||
|             fs::create_dir_all(&dir).map_err(|e| PostgresInstallerError::IoError(e))?; | ||||
|         } | ||||
|  | ||||
|         Some(dir) | ||||
|     } else { | ||||
|         None | ||||
|     }; | ||||
|  | ||||
|     // Build the image name | ||||
|     let image = format!("postgres:{}", config.version); | ||||
|  | ||||
|     // Pull the PostgreSQL image to ensure we have the latest version | ||||
|     println!("Pulling PostgreSQL image: {}...", image); | ||||
|     let pull_result = Command::new("nerdctl") | ||||
|         .args(&["pull", &image]) | ||||
|         .output() | ||||
|         .map_err(|e| PostgresInstallerError::IoError(e))?; | ||||
|  | ||||
|     if !pull_result.status.success() { | ||||
|         return Err(PostgresInstallerError::NerdctlError(format!( | ||||
|             "Failed to pull PostgreSQL image: {}", | ||||
|             String::from_utf8_lossy(&pull_result.stderr) | ||||
|         ))); | ||||
|     } | ||||
|  | ||||
|     // Create the container | ||||
|     let mut container = Container::new(&config.container_name).map_err(|e| { | ||||
|         PostgresInstallerError::NerdctlError(format!("Failed to create container: {}", e)) | ||||
|     })?; | ||||
|  | ||||
|     // Set the image | ||||
|     container.image = Some(image); | ||||
|  | ||||
|     // Set the port | ||||
|     container = container.with_port(&format!("{}:5432", config.port)); | ||||
|  | ||||
|     // Set environment variables | ||||
|     container = container.with_env("POSTGRES_USER", &config.username); | ||||
|     container = container.with_env("POSTGRES_PASSWORD", &config.password); | ||||
|     container = container.with_env("POSTGRES_DB", "postgres"); | ||||
|  | ||||
|     // Add custom environment variables | ||||
|     for (key, value) in &config.env_vars { | ||||
|         container = container.with_env(key, value); | ||||
|     } | ||||
|  | ||||
|     // Add volume for persistent storage if enabled | ||||
|     if let Some(dir) = data_dir { | ||||
|         container = container.with_volume(&format!("{}:/var/lib/postgresql/data", dir)); | ||||
|     } | ||||
|  | ||||
|     // Set restart policy | ||||
|     container = container.with_restart_policy("unless-stopped"); | ||||
|  | ||||
|     // Set detach mode | ||||
|     container = container.with_detach(true); | ||||
|  | ||||
|     // Build and start the container | ||||
|     let container = container.build().map_err(|e| { | ||||
|         PostgresInstallerError::NerdctlError(format!("Failed to build container: {}", e)) | ||||
|     })?; | ||||
|  | ||||
|     // Wait for PostgreSQL to start | ||||
|     println!("Waiting for PostgreSQL to start..."); | ||||
|     thread::sleep(Duration::from_secs(5)); | ||||
|  | ||||
|     // Set environment variables for PostgreSQL client | ||||
|     env::set_var("POSTGRES_HOST", "localhost"); | ||||
|     env::set_var("POSTGRES_PORT", config.port.to_string()); | ||||
|     env::set_var("POSTGRES_USER", config.username); | ||||
|     env::set_var("POSTGRES_PASSWORD", config.password); | ||||
|     env::set_var("POSTGRES_DB", "postgres"); | ||||
|  | ||||
|     Ok(container) | ||||
| } | ||||
|  | ||||
| /// Create a new database in PostgreSQL | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `container` - PostgreSQL container | ||||
| /// * `db_name` - Database name | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<(), PostgresInstallerError>` - Ok if successful, Err otherwise | ||||
| pub fn create_database(container: &Container, db_name: &str) -> Result<(), PostgresInstallerError> { | ||||
|     // Check if container is running | ||||
|     if container.container_id.is_none() { | ||||
|         return Err(PostgresInstallerError::PostgresError( | ||||
|             "Container is not running".to_string(), | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     // Execute the command to create the database | ||||
|     let command = format!( | ||||
|         "createdb -U {} {}", | ||||
|         env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()), | ||||
|         db_name | ||||
|     ); | ||||
|  | ||||
|     container.exec(&command).map_err(|e| { | ||||
|         PostgresInstallerError::NerdctlError(format!("Failed to create database: {}", e)) | ||||
|     })?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Execute a SQL script in PostgreSQL | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `container` - PostgreSQL container | ||||
| /// * `db_name` - Database name | ||||
| /// * `sql` - SQL script to execute | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<String, PostgresInstallerError>` - Output of the command or error | ||||
| pub fn execute_sql( | ||||
|     container: &Container, | ||||
|     db_name: &str, | ||||
|     sql: &str, | ||||
| ) -> Result<String, PostgresInstallerError> { | ||||
|     // Check if container is running | ||||
|     if container.container_id.is_none() { | ||||
|         return Err(PostgresInstallerError::PostgresError( | ||||
|             "Container is not running".to_string(), | ||||
|         )); | ||||
|     } | ||||
|  | ||||
|     // Create a temporary file with the SQL script | ||||
|     let temp_file = "/tmp/postgres_script.sql"; | ||||
|     fs::write(temp_file, sql).map_err(|e| PostgresInstallerError::IoError(e))?; | ||||
|  | ||||
|     // Copy the file to the container | ||||
|     let container_id = container.container_id.as_ref().unwrap(); | ||||
|     let copy_result = Command::new("nerdctl") | ||||
|         .args(&[ | ||||
|             "cp", | ||||
|             temp_file, | ||||
|             &format!("{}:/tmp/script.sql", container_id), | ||||
|         ]) | ||||
|         .output() | ||||
|         .map_err(|e| PostgresInstallerError::IoError(e))?; | ||||
|  | ||||
|     if !copy_result.status.success() { | ||||
|         return Err(PostgresInstallerError::PostgresError(format!( | ||||
|             "Failed to copy SQL script to container: {}", | ||||
|             String::from_utf8_lossy(©_result.stderr) | ||||
|         ))); | ||||
|     } | ||||
|  | ||||
|     // Execute the SQL script | ||||
|     let command = format!( | ||||
|         "psql -U {} -d {} -f /tmp/script.sql", | ||||
|         env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()), | ||||
|         db_name | ||||
|     ); | ||||
|  | ||||
|     let result = container.exec(&command).map_err(|e| { | ||||
|         PostgresInstallerError::NerdctlError(format!("Failed to execute SQL script: {}", e)) | ||||
|     })?; | ||||
|  | ||||
|     // Clean up | ||||
|     fs::remove_file(temp_file).ok(); | ||||
|  | ||||
|     Ok(result.stdout) | ||||
| } | ||||
|  | ||||
| /// Check if PostgreSQL is running | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `container` - PostgreSQL container | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<bool, PostgresInstallerError>` - true if running, false otherwise, or error | ||||
| pub fn is_postgres_running(container: &Container) -> Result<bool, PostgresInstallerError> { | ||||
|     // Check if container is running | ||||
|     if container.container_id.is_none() { | ||||
|         return Ok(false); | ||||
|     } | ||||
|  | ||||
|     // Execute a simple query to check if PostgreSQL is running | ||||
|     let command = format!( | ||||
|         "psql -U {} -c 'SELECT 1'", | ||||
|         env::var("POSTGRES_USER").unwrap_or_else(|_| "postgres".to_string()) | ||||
|     ); | ||||
|  | ||||
|     match container.exec(&command) { | ||||
|         Ok(_) => Ok(true), | ||||
|         Err(_) => Ok(false), | ||||
|     } | ||||
| } | ||||
							
								
								
									
										41
									
								
								postgresclient/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								postgresclient/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| //! SAL PostgreSQL Client | ||||
| //! | ||||
| //! This crate provides a PostgreSQL client for interacting with PostgreSQL databases. | ||||
| //! It offers connection management, query execution, and a builder pattern for flexible configuration. | ||||
| //! | ||||
| //! ## Features | ||||
| //! | ||||
| //! - **Connection Management**: Automatic connection handling and reconnection | ||||
| //! - **Query Execution**: Simple API for executing queries and fetching results | ||||
| //! - **Builder Pattern**: Flexible configuration with authentication support | ||||
| //! - **Environment Variable Support**: Easy configuration through environment variables | ||||
| //! - **Thread Safety**: Safe to use in multi-threaded applications | ||||
| //! - **PostgreSQL Installer**: Install and configure PostgreSQL using nerdctl | ||||
| //! - **Rhai Integration**: Scripting support for PostgreSQL operations | ||||
| //! | ||||
| //! ## Usage | ||||
| //! | ||||
| //! ```rust,no_run | ||||
| //! use sal_postgresclient::{execute, query, query_one}; | ||||
| //! | ||||
| //! fn main() -> Result<(), Box<dyn std::error::Error>> { | ||||
| //!     // Execute a query | ||||
| //!     let rows_affected = execute("CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)", &[])?; | ||||
| //! | ||||
| //!     // Query data | ||||
| //!     let rows = query("SELECT * FROM users", &[])?; | ||||
| //! | ||||
| //!     // Query single row | ||||
| //!     let row = query_one("SELECT * FROM users WHERE id = $1", &[&1])?; | ||||
| //! | ||||
| //!     Ok(()) | ||||
| //! } | ||||
| //! ``` | ||||
|  | ||||
| mod installer; | ||||
| mod postgresclient; | ||||
| pub mod rhai; | ||||
|  | ||||
| // Re-export the public API | ||||
| pub use installer::*; | ||||
| pub use postgresclient::*; | ||||
							
								
								
									
										825
									
								
								postgresclient/src/postgresclient.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										825
									
								
								postgresclient/src/postgresclient.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,825 @@ | ||||
| use lazy_static::lazy_static; | ||||
| use postgres::types::ToSql; | ||||
| use postgres::{Client, Error as PostgresError, NoTls, Row}; | ||||
| use r2d2::Pool; | ||||
| use r2d2_postgres::PostgresConnectionManager; | ||||
| use std::env; | ||||
| use std::sync::{Arc, Mutex, Once}; | ||||
| use std::time::Duration; | ||||
|  | ||||
| // Helper function to create a PostgreSQL error | ||||
| fn create_postgres_error(_message: &str) -> PostgresError { | ||||
|     // Since we can't directly create a PostgresError, we'll create one by | ||||
|     // attempting to connect to an invalid connection string and capturing the error | ||||
|     let result = Client::connect("invalid-connection-string", NoTls); | ||||
|     match result { | ||||
|         Ok(_) => unreachable!(), // This should never happen | ||||
|         Err(e) => { | ||||
|             // We have a valid PostgresError now, but we want to customize the message | ||||
|             // Unfortunately, PostgresError doesn't provide a way to modify the message | ||||
|             // So we'll just return the error we got | ||||
|             e | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Global PostgreSQL client instance using lazy_static | ||||
| lazy_static! { | ||||
|     static ref POSTGRES_CLIENT: Mutex<Option<Arc<PostgresClientWrapper>>> = Mutex::new(None); | ||||
|     static ref POSTGRES_POOL: Mutex<Option<Arc<Pool<PostgresConnectionManager<NoTls>>>>> = | ||||
|         Mutex::new(None); | ||||
|     static ref INIT: Once = Once::new(); | ||||
| } | ||||
|  | ||||
| /// PostgreSQL connection configuration builder | ||||
| /// | ||||
| /// This struct is used to build a PostgreSQL connection configuration. | ||||
| /// It follows the builder pattern to allow for flexible configuration. | ||||
| #[derive(Debug)] | ||||
| pub struct PostgresConfigBuilder { | ||||
|     pub host: String, | ||||
|     pub port: u16, | ||||
|     pub user: String, | ||||
|     pub password: Option<String>, | ||||
|     pub database: String, | ||||
|     pub application_name: Option<String>, | ||||
|     pub connect_timeout: Option<u64>, | ||||
|     pub ssl_mode: Option<String>, | ||||
|     // Connection pool settings | ||||
|     pub pool_max_size: Option<u32>, | ||||
|     pub pool_min_idle: Option<u32>, | ||||
|     pub pool_idle_timeout: Option<Duration>, | ||||
|     pub pool_connection_timeout: Option<Duration>, | ||||
|     pub pool_max_lifetime: Option<Duration>, | ||||
|     pub use_pool: bool, | ||||
| } | ||||
|  | ||||
| impl Default for PostgresConfigBuilder { | ||||
|     fn default() -> Self { | ||||
|         Self { | ||||
|             host: "localhost".to_string(), | ||||
|             port: 5432, | ||||
|             user: "postgres".to_string(), | ||||
|             password: None, | ||||
|             database: "postgres".to_string(), | ||||
|             application_name: None, | ||||
|             connect_timeout: None, | ||||
|             ssl_mode: None, | ||||
|             // Default pool settings | ||||
|             pool_max_size: Some(10), | ||||
|             pool_min_idle: Some(1), | ||||
|             pool_idle_timeout: Some(Duration::from_secs(300)), | ||||
|             pool_connection_timeout: Some(Duration::from_secs(30)), | ||||
|             pool_max_lifetime: Some(Duration::from_secs(1800)), | ||||
|             use_pool: false, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl PostgresConfigBuilder { | ||||
|     /// Create a new PostgreSQL connection configuration builder with default values | ||||
|     pub fn new() -> Self { | ||||
|         Self::default() | ||||
|     } | ||||
|  | ||||
|     /// Set the host for the PostgreSQL connection | ||||
|     pub fn host(mut self, host: &str) -> Self { | ||||
|         self.host = host.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the port for the PostgreSQL connection | ||||
|     pub fn port(mut self, port: u16) -> Self { | ||||
|         self.port = port; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the user for the PostgreSQL connection | ||||
|     pub fn user(mut self, user: &str) -> Self { | ||||
|         self.user = user.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the password for the PostgreSQL connection | ||||
|     pub fn password(mut self, password: &str) -> Self { | ||||
|         self.password = Some(password.to_string()); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the database for the PostgreSQL connection | ||||
|     pub fn database(mut self, database: &str) -> Self { | ||||
|         self.database = database.to_string(); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the application name for the PostgreSQL connection | ||||
|     pub fn application_name(mut self, application_name: &str) -> Self { | ||||
|         self.application_name = Some(application_name.to_string()); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the connection timeout in seconds | ||||
|     pub fn connect_timeout(mut self, seconds: u64) -> Self { | ||||
|         self.connect_timeout = Some(seconds); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the SSL mode for the PostgreSQL connection | ||||
|     pub fn ssl_mode(mut self, ssl_mode: &str) -> Self { | ||||
|         self.ssl_mode = Some(ssl_mode.to_string()); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Enable connection pooling | ||||
|     pub fn use_pool(mut self, use_pool: bool) -> Self { | ||||
|         self.use_pool = use_pool; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the maximum size of the connection pool | ||||
|     pub fn pool_max_size(mut self, size: u32) -> Self { | ||||
|         self.pool_max_size = Some(size); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the minimum number of idle connections in the pool | ||||
|     pub fn pool_min_idle(mut self, size: u32) -> Self { | ||||
|         self.pool_min_idle = Some(size); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the idle timeout for connections in the pool | ||||
|     pub fn pool_idle_timeout(mut self, timeout: Duration) -> Self { | ||||
|         self.pool_idle_timeout = Some(timeout); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the connection timeout for the pool | ||||
|     pub fn pool_connection_timeout(mut self, timeout: Duration) -> Self { | ||||
|         self.pool_connection_timeout = Some(timeout); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set the maximum lifetime of connections in the pool | ||||
|     pub fn pool_max_lifetime(mut self, lifetime: Duration) -> Self { | ||||
|         self.pool_max_lifetime = Some(lifetime); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Build the connection string from the configuration | ||||
|     pub fn build_connection_string(&self) -> String { | ||||
|         let mut conn_string = format!( | ||||
|             "host={} port={} user={} dbname={}", | ||||
|             self.host, self.port, self.user, self.database | ||||
|         ); | ||||
|  | ||||
|         if let Some(password) = &self.password { | ||||
|             conn_string.push_str(&format!(" password={}", password)); | ||||
|         } | ||||
|  | ||||
|         if let Some(app_name) = &self.application_name { | ||||
|             conn_string.push_str(&format!(" application_name={}", app_name)); | ||||
|         } | ||||
|  | ||||
|         if let Some(timeout) = self.connect_timeout { | ||||
|             conn_string.push_str(&format!(" connect_timeout={}", timeout)); | ||||
|         } | ||||
|  | ||||
|         if let Some(ssl_mode) = &self.ssl_mode { | ||||
|             conn_string.push_str(&format!(" sslmode={}", ssl_mode)); | ||||
|         } | ||||
|  | ||||
|         conn_string | ||||
|     } | ||||
|  | ||||
|     /// Build a PostgreSQL client from the configuration | ||||
|     pub fn build(&self) -> Result<Client, PostgresError> { | ||||
|         let conn_string = self.build_connection_string(); | ||||
|         Client::connect(&conn_string, NoTls) | ||||
|     } | ||||
|  | ||||
|     /// Build a PostgreSQL connection pool from the configuration | ||||
|     pub fn build_pool(&self) -> Result<Pool<PostgresConnectionManager<NoTls>>, r2d2::Error> { | ||||
|         let conn_string = self.build_connection_string(); | ||||
|         let manager = PostgresConnectionManager::new(conn_string.parse().unwrap(), NoTls); | ||||
|  | ||||
|         let mut pool_builder = r2d2::Pool::builder(); | ||||
|  | ||||
|         if let Some(max_size) = self.pool_max_size { | ||||
|             pool_builder = pool_builder.max_size(max_size); | ||||
|         } | ||||
|  | ||||
|         if let Some(min_idle) = self.pool_min_idle { | ||||
|             pool_builder = pool_builder.min_idle(Some(min_idle)); | ||||
|         } | ||||
|  | ||||
|         if let Some(idle_timeout) = self.pool_idle_timeout { | ||||
|             pool_builder = pool_builder.idle_timeout(Some(idle_timeout)); | ||||
|         } | ||||
|  | ||||
|         if let Some(connection_timeout) = self.pool_connection_timeout { | ||||
|             pool_builder = pool_builder.connection_timeout(connection_timeout); | ||||
|         } | ||||
|  | ||||
|         if let Some(max_lifetime) = self.pool_max_lifetime { | ||||
|             pool_builder = pool_builder.max_lifetime(Some(max_lifetime)); | ||||
|         } | ||||
|  | ||||
|         pool_builder.build(manager) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Wrapper for PostgreSQL client to handle connection | ||||
| pub struct PostgresClientWrapper { | ||||
|     connection_string: String, | ||||
|     client: Mutex<Option<Client>>, | ||||
| } | ||||
|  | ||||
| /// Transaction functions for PostgreSQL | ||||
| /// | ||||
| /// These functions provide a way to execute queries within a transaction. | ||||
| /// The transaction is automatically committed when the function returns successfully, | ||||
| /// or rolled back if an error occurs. | ||||
| /// | ||||
| /// Example: | ||||
| /// ```no_run | ||||
| /// use sal_postgresclient::{transaction, QueryParams}; | ||||
| /// | ||||
| /// let result = transaction(|client| { | ||||
| ///     // Execute queries within the transaction | ||||
| ///     client.execute("INSERT INTO users (name) VALUES ($1)", &[&"John"])?; | ||||
| ///     client.execute("UPDATE users SET active = true WHERE name = $1", &[&"John"])?; | ||||
| /// | ||||
| ///     // Return a result from the transaction | ||||
| ///     Ok(()) | ||||
| /// }); | ||||
| /// ``` | ||||
| pub fn transaction<F, T>(operations: F) -> Result<T, PostgresError> | ||||
| where | ||||
|     F: FnOnce(&mut Client) -> Result<T, PostgresError>, | ||||
| { | ||||
|     let client = get_postgres_client()?; | ||||
|     let client_mutex = client.get_client()?; | ||||
|     let mut client_guard = client_mutex.lock().unwrap(); | ||||
|  | ||||
|     if let Some(client) = client_guard.as_mut() { | ||||
|         // Begin transaction | ||||
|         client.execute("BEGIN", &[])?; | ||||
|  | ||||
|         // Execute operations | ||||
|         match operations(client) { | ||||
|             Ok(result) => { | ||||
|                 // Commit transaction | ||||
|                 client.execute("COMMIT", &[])?; | ||||
|                 Ok(result) | ||||
|             } | ||||
|             Err(e) => { | ||||
|                 // Rollback transaction | ||||
|                 let _ = client.execute("ROLLBACK", &[]); | ||||
|                 Err(e) | ||||
|             } | ||||
|         } | ||||
|     } else { | ||||
|         Err(create_postgres_error("Failed to get PostgreSQL client")) | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Transaction functions for PostgreSQL using the connection pool | ||||
| /// | ||||
| /// These functions provide a way to execute queries within a transaction using the connection pool. | ||||
| /// The transaction is automatically committed when the function returns successfully, | ||||
| /// or rolled back if an error occurs. | ||||
| /// | ||||
| /// Example: | ||||
| /// ```no_run | ||||
| /// use sal_postgresclient::{transaction_with_pool, QueryParams}; | ||||
| /// | ||||
| /// let result = transaction_with_pool(|client| { | ||||
| ///     // Execute queries within the transaction | ||||
| ///     client.execute("INSERT INTO users (name) VALUES ($1)", &[&"John"])?; | ||||
| ///     client.execute("UPDATE users SET active = true WHERE name = $1", &[&"John"])?; | ||||
| /// | ||||
| ///     // Return a result from the transaction | ||||
| ///     Ok(()) | ||||
| /// }); | ||||
| /// ``` | ||||
| pub fn transaction_with_pool<F, T>(operations: F) -> Result<T, PostgresError> | ||||
| where | ||||
|     F: FnOnce(&mut Client) -> Result<T, PostgresError>, | ||||
| { | ||||
|     let pool = get_postgres_pool()?; | ||||
|     let mut client = pool.get().map_err(|e| { | ||||
|         create_postgres_error(&format!("Failed to get connection from pool: {}", e)) | ||||
|     })?; | ||||
|  | ||||
|     // Begin transaction | ||||
|     client.execute("BEGIN", &[])?; | ||||
|  | ||||
|     // Execute operations | ||||
|     match operations(&mut client) { | ||||
|         Ok(result) => { | ||||
|             // Commit transaction | ||||
|             client.execute("COMMIT", &[])?; | ||||
|             Ok(result) | ||||
|         } | ||||
|         Err(e) => { | ||||
|             // Rollback transaction | ||||
|             let _ = client.execute("ROLLBACK", &[]); | ||||
|             Err(e) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl PostgresClientWrapper { | ||||
|     /// Create a new PostgreSQL client wrapper | ||||
|     fn new(connection_string: String) -> Self { | ||||
|         PostgresClientWrapper { | ||||
|             connection_string, | ||||
|             client: Mutex::new(None), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Get a reference to the PostgreSQL client, creating it if it doesn't exist | ||||
|     fn get_client(&self) -> Result<&Mutex<Option<Client>>, PostgresError> { | ||||
|         let mut client_guard = self.client.lock().unwrap(); | ||||
|  | ||||
|         // If we don't have a client or it's not working, create a new one | ||||
|         if client_guard.is_none() { | ||||
|             *client_guard = Some(Client::connect(&self.connection_string, NoTls)?); | ||||
|         } | ||||
|  | ||||
|         Ok(&self.client) | ||||
|     } | ||||
|  | ||||
|     /// Execute a query on the PostgreSQL connection | ||||
|     pub fn execute( | ||||
|         &self, | ||||
|         query: &str, | ||||
|         params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
|     ) -> Result<u64, PostgresError> { | ||||
|         let client_mutex = self.get_client()?; | ||||
|         let mut client_guard = client_mutex.lock().unwrap(); | ||||
|  | ||||
|         if let Some(client) = client_guard.as_mut() { | ||||
|             client.execute(query, params) | ||||
|         } else { | ||||
|             Err(create_postgres_error("Failed to get PostgreSQL client")) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Execute a query on the PostgreSQL connection and return the rows | ||||
|     pub fn query( | ||||
|         &self, | ||||
|         query: &str, | ||||
|         params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
|     ) -> Result<Vec<Row>, PostgresError> { | ||||
|         let client_mutex = self.get_client()?; | ||||
|         let mut client_guard = client_mutex.lock().unwrap(); | ||||
|  | ||||
|         if let Some(client) = client_guard.as_mut() { | ||||
|             client.query(query, params) | ||||
|         } else { | ||||
|             Err(create_postgres_error("Failed to get PostgreSQL client")) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Execute a query on the PostgreSQL connection and return a single row | ||||
|     pub fn query_one( | ||||
|         &self, | ||||
|         query: &str, | ||||
|         params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
|     ) -> Result<Row, PostgresError> { | ||||
|         let client_mutex = self.get_client()?; | ||||
|         let mut client_guard = client_mutex.lock().unwrap(); | ||||
|  | ||||
|         if let Some(client) = client_guard.as_mut() { | ||||
|             client.query_one(query, params) | ||||
|         } else { | ||||
|             Err(create_postgres_error("Failed to get PostgreSQL client")) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Execute a query on the PostgreSQL connection and return an optional row | ||||
|     pub fn query_opt( | ||||
|         &self, | ||||
|         query: &str, | ||||
|         params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
|     ) -> Result<Option<Row>, PostgresError> { | ||||
|         let client_mutex = self.get_client()?; | ||||
|         let mut client_guard = client_mutex.lock().unwrap(); | ||||
|  | ||||
|         if let Some(client) = client_guard.as_mut() { | ||||
|             client.query_opt(query, params) | ||||
|         } else { | ||||
|             Err(create_postgres_error("Failed to get PostgreSQL client")) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Ping the PostgreSQL server to check if the connection is alive | ||||
|     pub fn ping(&self) -> Result<bool, PostgresError> { | ||||
|         let result = self.query("SELECT 1", &[]); | ||||
|         match result { | ||||
|             Ok(_) => Ok(true), | ||||
|             Err(e) => Err(e), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Get the PostgreSQL client instance | ||||
| pub fn get_postgres_client() -> Result<Arc<PostgresClientWrapper>, PostgresError> { | ||||
|     // Check if we already have a client | ||||
|     { | ||||
|         let guard = POSTGRES_CLIENT.lock().unwrap(); | ||||
|         if let Some(ref client) = &*guard { | ||||
|             return Ok(Arc::clone(client)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Create a new client | ||||
|     let client = create_postgres_client()?; | ||||
|  | ||||
|     // Store the client globally | ||||
|     { | ||||
|         let mut guard = POSTGRES_CLIENT.lock().unwrap(); | ||||
|         *guard = Some(Arc::clone(&client)); | ||||
|     } | ||||
|  | ||||
|     Ok(client) | ||||
| } | ||||
|  | ||||
| /// Create a new PostgreSQL client | ||||
| fn create_postgres_client() -> Result<Arc<PostgresClientWrapper>, PostgresError> { | ||||
|     // Try to get connection details from environment variables | ||||
|     let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| String::from("localhost")); | ||||
|     let port = env::var("POSTGRES_PORT") | ||||
|         .ok() | ||||
|         .and_then(|p| p.parse::<u16>().ok()) | ||||
|         .unwrap_or(5432); | ||||
|     let user = env::var("POSTGRES_USER").unwrap_or_else(|_| String::from("postgres")); | ||||
|     let password = env::var("POSTGRES_PASSWORD").ok(); | ||||
|     let database = env::var("POSTGRES_DB").unwrap_or_else(|_| String::from("postgres")); | ||||
|  | ||||
|     // Build the connection string | ||||
|     let mut builder = PostgresConfigBuilder::new() | ||||
|         .host(&host) | ||||
|         .port(port) | ||||
|         .user(&user) | ||||
|         .database(&database); | ||||
|  | ||||
|     if let Some(pass) = password { | ||||
|         builder = builder.password(&pass); | ||||
|     } | ||||
|  | ||||
|     let connection_string = builder.build_connection_string(); | ||||
|  | ||||
|     // Create the client wrapper | ||||
|     let wrapper = Arc::new(PostgresClientWrapper::new(connection_string)); | ||||
|  | ||||
|     // Test the connection | ||||
|     match wrapper.ping() { | ||||
|         Ok(_) => Ok(wrapper), | ||||
|         Err(e) => Err(e), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Reset the PostgreSQL client | ||||
| pub fn reset() -> Result<(), PostgresError> { | ||||
|     // Clear the existing client | ||||
|     { | ||||
|         let mut client_guard = POSTGRES_CLIENT.lock().unwrap(); | ||||
|         *client_guard = None; | ||||
|     } | ||||
|  | ||||
|     // Create a new client, only return error if it fails | ||||
|     get_postgres_client()?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Execute a query on the PostgreSQL connection | ||||
| pub fn execute( | ||||
|     query: &str, | ||||
|     params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
| ) -> Result<u64, PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.execute(query, params) | ||||
| } | ||||
|  | ||||
| /// Execute a query on the PostgreSQL connection and return the rows | ||||
| pub fn query( | ||||
|     query: &str, | ||||
|     params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
| ) -> Result<Vec<Row>, PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.query(query, params) | ||||
| } | ||||
|  | ||||
| /// Execute a query on the PostgreSQL connection and return a single row | ||||
| pub fn query_one( | ||||
|     query: &str, | ||||
|     params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
| ) -> Result<Row, PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.query_one(query, params) | ||||
| } | ||||
|  | ||||
| /// Execute a query on the PostgreSQL connection and return an optional row | ||||
| pub fn query_opt( | ||||
|     query: &str, | ||||
|     params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
| ) -> Result<Option<Row>, PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.query_opt(query, params) | ||||
| } | ||||
|  | ||||
| /// Create a new PostgreSQL client with custom configuration | ||||
| pub fn with_config(config: PostgresConfigBuilder) -> Result<Client, PostgresError> { | ||||
|     config.build() | ||||
| } | ||||
|  | ||||
| /// Create a new PostgreSQL connection pool with custom configuration | ||||
| pub fn with_pool_config( | ||||
|     config: PostgresConfigBuilder, | ||||
| ) -> Result<Pool<PostgresConnectionManager<NoTls>>, r2d2::Error> { | ||||
|     config.build_pool() | ||||
| } | ||||
|  | ||||
| /// Get the PostgreSQL connection pool instance | ||||
| pub fn get_postgres_pool() -> Result<Arc<Pool<PostgresConnectionManager<NoTls>>>, PostgresError> { | ||||
|     // Check if we already have a pool | ||||
|     { | ||||
|         let guard = POSTGRES_POOL.lock().unwrap(); | ||||
|         if let Some(ref pool) = &*guard { | ||||
|             return Ok(Arc::clone(pool)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // Create a new pool | ||||
|     let pool = create_postgres_pool()?; | ||||
|  | ||||
|     // Store the pool globally | ||||
|     { | ||||
|         let mut guard = POSTGRES_POOL.lock().unwrap(); | ||||
|         *guard = Some(Arc::clone(&pool)); | ||||
|     } | ||||
|  | ||||
|     Ok(pool) | ||||
| } | ||||
|  | ||||
| /// Create a new PostgreSQL connection pool | ||||
| fn create_postgres_pool() -> Result<Arc<Pool<PostgresConnectionManager<NoTls>>>, PostgresError> { | ||||
|     // Try to get connection details from environment variables | ||||
|     let host = env::var("POSTGRES_HOST").unwrap_or_else(|_| String::from("localhost")); | ||||
|     let port = env::var("POSTGRES_PORT") | ||||
|         .ok() | ||||
|         .and_then(|p| p.parse::<u16>().ok()) | ||||
|         .unwrap_or(5432); | ||||
|     let user = env::var("POSTGRES_USER").unwrap_or_else(|_| String::from("postgres")); | ||||
|     let password = env::var("POSTGRES_PASSWORD").ok(); | ||||
|     let database = env::var("POSTGRES_DB").unwrap_or_else(|_| String::from("postgres")); | ||||
|  | ||||
|     // Build the configuration | ||||
|     let mut builder = PostgresConfigBuilder::new() | ||||
|         .host(&host) | ||||
|         .port(port) | ||||
|         .user(&user) | ||||
|         .database(&database) | ||||
|         .use_pool(true); | ||||
|  | ||||
|     if let Some(pass) = password { | ||||
|         builder = builder.password(&pass); | ||||
|     } | ||||
|  | ||||
|     // Create the pool | ||||
|     match builder.build_pool() { | ||||
|         Ok(pool) => { | ||||
|             // Test the connection | ||||
|             match pool.get() { | ||||
|                 Ok(_) => Ok(Arc::new(pool)), | ||||
|                 Err(e) => Err(create_postgres_error(&format!( | ||||
|                     "Failed to connect to PostgreSQL: {}", | ||||
|                     e | ||||
|                 ))), | ||||
|             } | ||||
|         } | ||||
|         Err(e) => Err(create_postgres_error(&format!( | ||||
|             "Failed to create PostgreSQL connection pool: {}", | ||||
|             e | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Reset the PostgreSQL connection pool | ||||
| pub fn reset_pool() -> Result<(), PostgresError> { | ||||
|     // Clear the existing pool | ||||
|     { | ||||
|         let mut pool_guard = POSTGRES_POOL.lock().unwrap(); | ||||
|         *pool_guard = None; | ||||
|     } | ||||
|  | ||||
|     // Create a new pool, only return error if it fails | ||||
|     get_postgres_pool()?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Execute a query using the connection pool | ||||
| pub fn execute_with_pool( | ||||
|     query: &str, | ||||
|     params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
| ) -> Result<u64, PostgresError> { | ||||
|     let pool = get_postgres_pool()?; | ||||
|     let mut client = pool.get().map_err(|e| { | ||||
|         create_postgres_error(&format!("Failed to get connection from pool: {}", e)) | ||||
|     })?; | ||||
|     client.execute(query, params) | ||||
| } | ||||
|  | ||||
| /// Execute a query using the connection pool and return the rows | ||||
| pub fn query_with_pool( | ||||
|     query: &str, | ||||
|     params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
| ) -> Result<Vec<Row>, PostgresError> { | ||||
|     let pool = get_postgres_pool()?; | ||||
|     let mut client = pool.get().map_err(|e| { | ||||
|         create_postgres_error(&format!("Failed to get connection from pool: {}", e)) | ||||
|     })?; | ||||
|     client.query(query, params) | ||||
| } | ||||
|  | ||||
| /// Execute a query using the connection pool and return a single row | ||||
| pub fn query_one_with_pool( | ||||
|     query: &str, | ||||
|     params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
| ) -> Result<Row, PostgresError> { | ||||
|     let pool = get_postgres_pool()?; | ||||
|     let mut client = pool.get().map_err(|e| { | ||||
|         create_postgres_error(&format!("Failed to get connection from pool: {}", e)) | ||||
|     })?; | ||||
|     client.query_one(query, params) | ||||
| } | ||||
|  | ||||
| /// Execute a query using the connection pool and return an optional row | ||||
| pub fn query_opt_with_pool( | ||||
|     query: &str, | ||||
|     params: &[&(dyn postgres::types::ToSql + Sync)], | ||||
| ) -> Result<Option<Row>, PostgresError> { | ||||
|     let pool = get_postgres_pool()?; | ||||
|     let mut client = pool.get().map_err(|e| { | ||||
|         create_postgres_error(&format!("Failed to get connection from pool: {}", e)) | ||||
|     })?; | ||||
|     client.query_opt(query, params) | ||||
| } | ||||
|  | ||||
| /// Parameter builder for PostgreSQL queries | ||||
| /// | ||||
| /// This struct helps build parameterized queries for PostgreSQL. | ||||
| /// It provides a type-safe way to build query parameters. | ||||
| #[derive(Default)] | ||||
| pub struct QueryParams { | ||||
|     params: Vec<Box<dyn ToSql + Sync>>, | ||||
| } | ||||
|  | ||||
| impl QueryParams { | ||||
|     /// Create a new empty parameter builder | ||||
|     pub fn new() -> Self { | ||||
|         Self { params: Vec::new() } | ||||
|     } | ||||
|  | ||||
|     /// Add a parameter to the builder | ||||
|     pub fn add<T: 'static + ToSql + Sync>(&mut self, value: T) -> &mut Self { | ||||
|         self.params.push(Box::new(value)); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Add a string parameter to the builder | ||||
|     pub fn add_str(&mut self, value: &str) -> &mut Self { | ||||
|         self.add(value.to_string()) | ||||
|     } | ||||
|  | ||||
|     /// Add an integer parameter to the builder | ||||
|     pub fn add_int(&mut self, value: i32) -> &mut Self { | ||||
|         self.add(value) | ||||
|     } | ||||
|  | ||||
|     /// Add a float parameter to the builder | ||||
|     pub fn add_float(&mut self, value: f64) -> &mut Self { | ||||
|         self.add(value) | ||||
|     } | ||||
|  | ||||
|     /// Add a boolean parameter to the builder | ||||
|     pub fn add_bool(&mut self, value: bool) -> &mut Self { | ||||
|         self.add(value) | ||||
|     } | ||||
|  | ||||
|     /// Add an optional parameter to the builder | ||||
|     pub fn add_opt<T: 'static + ToSql + Sync>(&mut self, value: Option<T>) -> &mut Self { | ||||
|         if let Some(v) = value { | ||||
|             self.add(v); | ||||
|         } else { | ||||
|             // Add NULL value | ||||
|             self.params.push(Box::new(None::<String>)); | ||||
|         } | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Get the parameters as a slice of references | ||||
|     pub fn as_slice(&self) -> Vec<&(dyn ToSql + Sync)> { | ||||
|         self.params | ||||
|             .iter() | ||||
|             .map(|p| p.as_ref() as &(dyn ToSql + Sync)) | ||||
|             .collect() | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Execute a query with the parameter builder | ||||
| pub fn execute_with_params(query_str: &str, params: &QueryParams) -> Result<u64, PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.execute(query_str, ¶ms.as_slice()) | ||||
| } | ||||
|  | ||||
| /// Execute a query with the parameter builder and return the rows | ||||
| pub fn query_with_params(query_str: &str, params: &QueryParams) -> Result<Vec<Row>, PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.query(query_str, ¶ms.as_slice()) | ||||
| } | ||||
|  | ||||
| /// Execute a query with the parameter builder and return a single row | ||||
| pub fn query_one_with_params(query_str: &str, params: &QueryParams) -> Result<Row, PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.query_one(query_str, ¶ms.as_slice()) | ||||
| } | ||||
|  | ||||
| /// Execute a query with the parameter builder and return an optional row | ||||
| pub fn query_opt_with_params( | ||||
|     query_str: &str, | ||||
|     params: &QueryParams, | ||||
| ) -> Result<Option<Row>, PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.query_opt(query_str, ¶ms.as_slice()) | ||||
| } | ||||
|  | ||||
| /// Execute a query with the parameter builder using the connection pool | ||||
| pub fn execute_with_pool_params( | ||||
|     query_str: &str, | ||||
|     params: &QueryParams, | ||||
| ) -> Result<u64, PostgresError> { | ||||
|     execute_with_pool(query_str, ¶ms.as_slice()) | ||||
| } | ||||
|  | ||||
| /// Execute a query with the parameter builder using the connection pool and return the rows | ||||
| pub fn query_with_pool_params( | ||||
|     query_str: &str, | ||||
|     params: &QueryParams, | ||||
| ) -> Result<Vec<Row>, PostgresError> { | ||||
|     query_with_pool(query_str, ¶ms.as_slice()) | ||||
| } | ||||
|  | ||||
| /// Execute a query with the parameter builder using the connection pool and return a single row | ||||
| pub fn query_one_with_pool_params( | ||||
|     query_str: &str, | ||||
|     params: &QueryParams, | ||||
| ) -> Result<Row, PostgresError> { | ||||
|     query_one_with_pool(query_str, ¶ms.as_slice()) | ||||
| } | ||||
|  | ||||
| /// Execute a query with the parameter builder using the connection pool and return an optional row | ||||
| pub fn query_opt_with_pool_params( | ||||
|     query_str: &str, | ||||
|     params: &QueryParams, | ||||
| ) -> Result<Option<Row>, PostgresError> { | ||||
|     query_opt_with_pool(query_str, ¶ms.as_slice()) | ||||
| } | ||||
|  | ||||
| /// Send a notification on a channel | ||||
| /// | ||||
| /// This function sends a notification on the specified channel with the specified payload. | ||||
| /// | ||||
| /// Example: | ||||
| /// ```no_run | ||||
| /// use sal_postgresclient::notify; | ||||
| /// | ||||
| /// notify("my_channel", "Hello, world!").expect("Failed to send notification"); | ||||
| /// ``` | ||||
| pub fn notify(channel: &str, payload: &str) -> Result<(), PostgresError> { | ||||
|     let client = get_postgres_client()?; | ||||
|     client.execute(&format!("NOTIFY {}, '{}'", channel, payload), &[])?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Send a notification on a channel using the connection pool | ||||
| /// | ||||
| /// This function sends a notification on the specified channel with the specified payload using the connection pool. | ||||
| /// | ||||
| /// Example: | ||||
| /// ```no_run | ||||
| /// use sal_postgresclient::notify_with_pool; | ||||
| /// | ||||
| /// notify_with_pool("my_channel", "Hello, world!").expect("Failed to send notification"); | ||||
| /// ``` | ||||
| pub fn notify_with_pool(channel: &str, payload: &str) -> Result<(), PostgresError> { | ||||
|     let pool = get_postgres_pool()?; | ||||
|     let mut client = pool.get().map_err(|e| { | ||||
|         create_postgres_error(&format!("Failed to get connection from pool: {}", e)) | ||||
|     })?; | ||||
|     client.execute(&format!("NOTIFY {}, '{}'", channel, payload), &[])?; | ||||
|     Ok(()) | ||||
| } | ||||
							
								
								
									
										360
									
								
								postgresclient/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										360
									
								
								postgresclient/src/rhai.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,360 @@ | ||||
| //! Rhai wrappers for PostgreSQL client module functions | ||||
| //! | ||||
| //! This module provides Rhai wrappers for the functions in the PostgreSQL client module. | ||||
|  | ||||
| use crate::{ | ||||
|     create_database, execute, execute_sql, get_postgres_client, install_postgres, | ||||
|     is_postgres_running, query_one, reset, PostgresInstallerConfig, | ||||
| }; | ||||
| use postgres::types::ToSql; | ||||
| use rhai::{Array, Engine, EvalAltResult, Map}; | ||||
| use sal_virt::nerdctl::Container; | ||||
|  | ||||
| /// Register PostgreSQL client module functions with the Rhai engine | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `engine` - The Rhai engine to register the functions with | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<(), Box<EvalAltResult>>` - Ok if registration was successful, Err otherwise | ||||
| pub fn register_postgresclient_module(engine: &mut Engine) -> Result<(), Box<EvalAltResult>> { | ||||
|     // Register PostgreSQL connection functions | ||||
|     engine.register_fn("pg_connect", pg_connect); | ||||
|     engine.register_fn("pg_ping", pg_ping); | ||||
|     engine.register_fn("pg_reset", pg_reset); | ||||
|  | ||||
|     // Register basic query functions | ||||
|     engine.register_fn("pg_execute", pg_execute); | ||||
|     engine.register_fn("pg_query", pg_query); | ||||
|     engine.register_fn("pg_query_one", pg_query_one); | ||||
|  | ||||
|     // Register installer functions | ||||
|     engine.register_fn("pg_install", pg_install); | ||||
|     engine.register_fn("pg_create_database", pg_create_database); | ||||
|     engine.register_fn("pg_execute_sql", pg_execute_sql); | ||||
|     engine.register_fn("pg_is_running", pg_is_running); | ||||
|  | ||||
|     // Builder pattern functions will be implemented in a future update | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| /// Connect to PostgreSQL using environment variables | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise | ||||
| pub fn pg_connect() -> Result<bool, Box<EvalAltResult>> { | ||||
|     match get_postgres_client() { | ||||
|         Ok(_) => Ok(true), | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Ping the PostgreSQL server | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise | ||||
| pub fn pg_ping() -> Result<bool, Box<EvalAltResult>> { | ||||
|     match get_postgres_client() { | ||||
|         Ok(client) => match client.ping() { | ||||
|             Ok(result) => Ok(result), | ||||
|             Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|                 format!("PostgreSQL error: {}", e).into(), | ||||
|                 rhai::Position::NONE, | ||||
|             ))), | ||||
|         }, | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Reset the PostgreSQL client connection | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise | ||||
| pub fn pg_reset() -> Result<bool, Box<EvalAltResult>> { | ||||
|     match reset() { | ||||
|         Ok(_) => Ok(true), | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Execute a query on the PostgreSQL connection | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `query` - The query to execute | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<i64, Box<EvalAltResult>>` - The number of rows affected if successful, error otherwise | ||||
| pub fn pg_execute(query: &str) -> Result<i64, Box<EvalAltResult>> { | ||||
|     // We can't directly pass dynamic parameters from Rhai to PostgreSQL | ||||
|     // So we'll only support parameterless queries for now | ||||
|     let params: &[&(dyn ToSql + Sync)] = &[]; | ||||
|  | ||||
|     match execute(query, params) { | ||||
|         Ok(rows) => Ok(rows as i64), | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Execute a query on the PostgreSQL connection and return the rows | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `query` - The query to execute | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Array, Box<EvalAltResult>>` - The rows if successful, error otherwise | ||||
| pub fn pg_query(query_str: &str) -> Result<Array, Box<EvalAltResult>> { | ||||
|     // We can't directly pass dynamic parameters from Rhai to PostgreSQL | ||||
|     // So we'll only support parameterless queries for now | ||||
|     let params: &[&(dyn ToSql + Sync)] = &[]; | ||||
|  | ||||
|     match crate::query(query_str, params) { | ||||
|         Ok(rows) => { | ||||
|             let mut result = Array::new(); | ||||
|             for row in rows { | ||||
|                 let mut map = Map::new(); | ||||
|                 for column in row.columns() { | ||||
|                     let name = column.name(); | ||||
|                     // We'll convert all values to strings for simplicity | ||||
|                     let value: Option<String> = row.get(name); | ||||
|                     if let Some(val) = value { | ||||
|                         map.insert(name.into(), val.into()); | ||||
|                     } else { | ||||
|                         map.insert(name.into(), rhai::Dynamic::UNIT); | ||||
|                     } | ||||
|                 } | ||||
|                 result.push(map.into()); | ||||
|             } | ||||
|             Ok(result) | ||||
|         } | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Execute a query on the PostgreSQL connection and return a single row | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `query` - The query to execute | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<Map, Box<EvalAltResult>>` - The row if successful, error otherwise | ||||
| pub fn pg_query_one(query: &str) -> Result<Map, Box<EvalAltResult>> { | ||||
|     // We can't directly pass dynamic parameters from Rhai to PostgreSQL | ||||
|     // So we'll only support parameterless queries for now | ||||
|     let params: &[&(dyn ToSql + Sync)] = &[]; | ||||
|  | ||||
|     match query_one(query, params) { | ||||
|         Ok(row) => { | ||||
|             let mut map = Map::new(); | ||||
|             for column in row.columns() { | ||||
|                 let name = column.name(); | ||||
|                 // We'll convert all values to strings for simplicity | ||||
|                 let value: Option<String> = row.get(name); | ||||
|                 if let Some(val) = value { | ||||
|                     map.insert(name.into(), val.into()); | ||||
|                 } else { | ||||
|                     map.insert(name.into(), rhai::Dynamic::UNIT); | ||||
|                 } | ||||
|             } | ||||
|             Ok(map) | ||||
|         } | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Install PostgreSQL using nerdctl | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `container_name` - Name for the PostgreSQL container | ||||
| /// * `version` - PostgreSQL version to install (e.g., "latest", "15", "14") | ||||
| /// * `port` - Port to expose PostgreSQL on | ||||
| /// * `username` - Username for PostgreSQL | ||||
| /// * `password` - Password for PostgreSQL | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise | ||||
| pub fn pg_install( | ||||
|     container_name: &str, | ||||
|     version: &str, | ||||
|     port: i64, | ||||
|     username: &str, | ||||
|     password: &str, | ||||
| ) -> Result<bool, Box<EvalAltResult>> { | ||||
|     // Create the installer configuration | ||||
|     let config = PostgresInstallerConfig::new() | ||||
|         .container_name(container_name) | ||||
|         .version(version) | ||||
|         .port(port as u16) | ||||
|         .username(username) | ||||
|         .password(password); | ||||
|  | ||||
|     // Install PostgreSQL | ||||
|     match install_postgres(config) { | ||||
|         Ok(_) => Ok(true), | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL installer error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Create a new database in PostgreSQL | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `container_name` - Name of the PostgreSQL container | ||||
| /// * `db_name` - Database name to create | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<bool, Box<EvalAltResult>>` - true if successful, error otherwise | ||||
| pub fn pg_create_database(container_name: &str, db_name: &str) -> Result<bool, Box<EvalAltResult>> { | ||||
|     // Create a container reference | ||||
|     let container = Container { | ||||
|         name: container_name.to_string(), | ||||
|         container_id: Some(container_name.to_string()), // Use name as ID for simplicity | ||||
|         image: None, | ||||
|         config: std::collections::HashMap::new(), | ||||
|         ports: Vec::new(), | ||||
|         volumes: Vec::new(), | ||||
|         env_vars: std::collections::HashMap::new(), | ||||
|         network: None, | ||||
|         network_aliases: Vec::new(), | ||||
|         cpu_limit: None, | ||||
|         memory_limit: None, | ||||
|         memory_swap_limit: None, | ||||
|         cpu_shares: None, | ||||
|         restart_policy: None, | ||||
|         health_check: None, | ||||
|         detach: false, | ||||
|         snapshotter: None, | ||||
|     }; | ||||
|  | ||||
|     // Create the database | ||||
|     match create_database(&container, db_name) { | ||||
|         Ok(_) => Ok(true), | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Execute a SQL script in PostgreSQL | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `container_name` - Name of the PostgreSQL container | ||||
| /// * `db_name` - Database name | ||||
| /// * `sql` - SQL script to execute | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<String, Box<EvalAltResult>>` - Output of the command if successful, error otherwise | ||||
| pub fn pg_execute_sql( | ||||
|     container_name: &str, | ||||
|     db_name: &str, | ||||
|     sql: &str, | ||||
| ) -> Result<String, Box<EvalAltResult>> { | ||||
|     // Create a container reference | ||||
|     let container = Container { | ||||
|         name: container_name.to_string(), | ||||
|         container_id: Some(container_name.to_string()), // Use name as ID for simplicity | ||||
|         image: None, | ||||
|         config: std::collections::HashMap::new(), | ||||
|         ports: Vec::new(), | ||||
|         volumes: Vec::new(), | ||||
|         env_vars: std::collections::HashMap::new(), | ||||
|         network: None, | ||||
|         network_aliases: Vec::new(), | ||||
|         cpu_limit: None, | ||||
|         memory_limit: None, | ||||
|         memory_swap_limit: None, | ||||
|         cpu_shares: None, | ||||
|         restart_policy: None, | ||||
|         health_check: None, | ||||
|         detach: false, | ||||
|         snapshotter: None, | ||||
|     }; | ||||
|  | ||||
|     // Execute the SQL script | ||||
|     match execute_sql(&container, db_name, sql) { | ||||
|         Ok(output) => Ok(output), | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Check if PostgreSQL is running | ||||
| /// | ||||
| /// # Arguments | ||||
| /// | ||||
| /// * `container_name` - Name of the PostgreSQL container | ||||
| /// | ||||
| /// # Returns | ||||
| /// | ||||
| /// * `Result<bool, Box<EvalAltResult>>` - true if running, false otherwise, or error | ||||
| pub fn pg_is_running(container_name: &str) -> Result<bool, Box<EvalAltResult>> { | ||||
|     // Create a container reference | ||||
|     let container = Container { | ||||
|         name: container_name.to_string(), | ||||
|         container_id: Some(container_name.to_string()), // Use name as ID for simplicity | ||||
|         image: None, | ||||
|         config: std::collections::HashMap::new(), | ||||
|         ports: Vec::new(), | ||||
|         volumes: Vec::new(), | ||||
|         env_vars: std::collections::HashMap::new(), | ||||
|         network: None, | ||||
|         network_aliases: Vec::new(), | ||||
|         cpu_limit: None, | ||||
|         memory_limit: None, | ||||
|         memory_swap_limit: None, | ||||
|         cpu_shares: None, | ||||
|         restart_policy: None, | ||||
|         health_check: None, | ||||
|         detach: false, | ||||
|         snapshotter: None, | ||||
|     }; | ||||
|  | ||||
|     // Check if PostgreSQL is running | ||||
|     match is_postgres_running(&container) { | ||||
|         Ok(running) => Ok(running), | ||||
|         Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime( | ||||
|             format!("PostgreSQL error: {}", e).into(), | ||||
|             rhai::Position::NONE, | ||||
|         ))), | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user