feat: convert postgresclient module to independent sal-postgresclient package
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:
Mahmoud-Emad
2025-06-23 03:12:26 +03:00
parent 455f84528b
commit b737cd6337
22 changed files with 1276 additions and 56 deletions

View File

@@ -41,7 +41,7 @@ pub mod cmd;
pub use sal_mycelium as mycelium;
pub use sal_net as net;
pub use sal_os as os;
pub mod postgresclient;
pub use sal_postgresclient as postgresclient;
pub use sal_process as process;
pub use sal_redisclient as redisclient;
pub mod rhai;

View File

@@ -1,245 +0,0 @@
# PostgreSQL Client Module
The PostgreSQL client module provides a simple and efficient way to interact with PostgreSQL databases in Rust. 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
## Usage
### Basic Usage
```rust
use sal::postgresclient::{execute, query, query_one};
// Execute a query
let create_table_query = "CREATE TABLE IF NOT EXISTS users (id SERIAL PRIMARY KEY, name TEXT)";
execute(create_table_query, &[]).expect("Failed to create table");
// Insert data
let insert_query = "INSERT INTO users (name) VALUES ($1) RETURNING id";
let rows = query(insert_query, &[&"John Doe"]).expect("Failed to insert data");
let id: i32 = rows[0].get(0);
// Query data
let select_query = "SELECT id, name FROM users WHERE id = $1";
let row = query_one(select_query, &[&id]).expect("Failed to query data");
let name: String = row.get(1);
println!("User: {} (ID: {})", name, id);
```
### Connection Management
The module manages connections automatically, but you can also reset the connection if needed:
```rust
use sal::postgresclient::reset;
// Reset the PostgreSQL client connection
reset().expect("Failed to reset connection");
```
### Builder Pattern
The module provides a builder pattern for flexible configuration:
```rust
use sal::postgresclient::{PostgresConfigBuilder, with_config};
// Create a configuration builder
let config = PostgresConfigBuilder::new()
.host("db.example.com")
.port(5432)
.user("postgres")
.password("secret")
.database("mydb")
.application_name("my-app")
.connect_timeout(30)
.ssl_mode("require");
// Connect with the configuration
let client = with_config(config).expect("Failed to connect");
```
## Configuration
### Environment Variables
The module uses the following environment variables for configuration:
- `POSTGRES_HOST`: PostgreSQL server host (default: localhost)
- `POSTGRES_PORT`: PostgreSQL server port (default: 5432)
- `POSTGRES_USER`: PostgreSQL username (default: postgres)
- `POSTGRES_PASSWORD`: PostgreSQL password
- `POSTGRES_DB`: PostgreSQL database name (default: postgres)
### Connection String
The connection string is built from the configuration options:
```
host=localhost port=5432 user=postgres dbname=postgres
```
With authentication:
```
host=localhost port=5432 user=postgres password=secret dbname=postgres
```
With additional options:
```
host=localhost port=5432 user=postgres dbname=postgres application_name=my-app connect_timeout=30 sslmode=require
```
## API Reference
### Connection Functions
- `get_postgres_client() -> Result<Arc<PostgresClientWrapper>, PostgresError>`: Get the PostgreSQL client instance
- `reset() -> Result<(), PostgresError>`: Reset the PostgreSQL client connection
### Query Functions
- `execute(query: &str, params: &[&(dyn postgres::types::ToSql + Sync)]) -> Result<u64, PostgresError>`: Execute a query and return the number of affected rows
- `query(query: &str, params: &[&(dyn postgres::types::ToSql + Sync)]) -> Result<Vec<Row>, PostgresError>`: Execute a query and return the results as a vector of rows
- `query_one(query: &str, params: &[&(dyn postgres::types::ToSql + Sync)]) -> Result<Row, PostgresError>`: Execute a query and return a single row
- `query_opt(query: &str, params: &[&(dyn postgres::types::ToSql + Sync)]) -> Result<Option<Row>, PostgresError>`: Execute a query and return an optional row
### Configuration Functions
- `PostgresConfigBuilder::new() -> PostgresConfigBuilder`: Create a new PostgreSQL configuration builder
- `with_config(config: PostgresConfigBuilder) -> Result<Client, PostgresError>`: Create a new PostgreSQL client with custom configuration
## Error Handling
The module uses the `postgres::Error` type for error handling:
```rust
use sal::postgresclient::{query, query_one};
// Handle errors
match query("SELECT * FROM users", &[]) {
Ok(rows) => {
println!("Found {} users", rows.len());
},
Err(e) => {
eprintln!("Error querying users: {}", e);
}
}
// Using query_one with no results
match query_one("SELECT * FROM users WHERE id = $1", &[&999]) {
Ok(_) => {
println!("User found");
},
Err(e) => {
eprintln!("User not found: {}", e);
}
}
```
## Thread Safety
The PostgreSQL client module is designed to be thread-safe. It uses `Arc` and `Mutex` to ensure safe concurrent access to the client instance.
## Examples
### Basic CRUD Operations
```rust
use sal::postgresclient::{execute, query, query_one};
// Create
let create_query = "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id";
let rows = query(create_query, &[&"Alice", &"alice@example.com"]).expect("Failed to create user");
let id: i32 = rows[0].get(0);
// Read
let read_query = "SELECT id, name, email FROM users WHERE id = $1";
let row = query_one(read_query, &[&id]).expect("Failed to read user");
let name: String = row.get(1);
let email: String = row.get(2);
// Update
let update_query = "UPDATE users SET email = $1 WHERE id = $2";
let affected = execute(update_query, &[&"new.alice@example.com", &id]).expect("Failed to update user");
// Delete
let delete_query = "DELETE FROM users WHERE id = $1";
let affected = execute(delete_query, &[&id]).expect("Failed to delete user");
```
### Transactions
Transactions are not directly supported by the module, but you can use the PostgreSQL client to implement them:
```rust
use sal::postgresclient::{execute, query};
// Start a transaction
execute("BEGIN", &[]).expect("Failed to start transaction");
// Perform operations
let insert_query = "INSERT INTO accounts (user_id, balance) VALUES ($1, $2)";
execute(insert_query, &[&1, &1000.0]).expect("Failed to insert account");
let update_query = "UPDATE users SET has_account = TRUE WHERE id = $1";
execute(update_query, &[&1]).expect("Failed to update user");
// Commit the transaction
execute("COMMIT", &[]).expect("Failed to commit transaction");
// Or rollback in case of an error
// execute("ROLLBACK", &[]).expect("Failed to rollback transaction");
```
## Testing
The module includes comprehensive tests for both unit and integration testing:
```rust
// Unit tests
#[test]
fn test_postgres_config_builder() {
let config = PostgresConfigBuilder::new()
.host("test-host")
.port(5433)
.user("test-user");
let conn_string = config.build_connection_string();
assert!(conn_string.contains("host=test-host"));
assert!(conn_string.contains("port=5433"));
assert!(conn_string.contains("user=test-user"));
}
// Integration tests
#[test]
fn test_basic_postgres_operations() {
// Skip if PostgreSQL is not available
if !is_postgres_available() {
return;
}
// Create a test table
let create_table_query = "CREATE TEMPORARY TABLE test_table (id SERIAL PRIMARY KEY, name TEXT)";
execute(create_table_query, &[]).expect("Failed to create table");
// Insert data
let insert_query = "INSERT INTO test_table (name) VALUES ($1) RETURNING id";
let rows = query(insert_query, &[&"test"]).expect("Failed to insert data");
let id: i32 = rows[0].get(0);
// Query data
let select_query = "SELECT name FROM test_table WHERE id = $1";
let row = query_one(select_query, &[&id]).expect("Failed to query data");
let name: String = row.get(0);
assert_eq!(name, "test");
}
```

View File

@@ -1,355 +0,0 @@
// 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(&copy_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),
}
}

View File

@@ -1,12 +0,0 @@
// PostgreSQL client module
//
// This module provides a PostgreSQL client for interacting with PostgreSQL databases.
mod installer;
mod postgresclient;
#[cfg(test)]
mod tests;
// Re-export the public API
pub use installer::*;
pub use postgresclient::*;

View File

@@ -1,825 +0,0 @@
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:
/// ```
/// 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:
/// ```
/// 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, &params.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, &params.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, &params.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, &params.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, &params.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, &params.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, &params.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, &params.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(())
}

View File

@@ -1,843 +0,0 @@
use super::*;
use std::collections::HashMap;
use std::env;
#[cfg(test)]
mod postgres_client_tests {
use super::*;
#[test]
fn test_env_vars() {
// Save original environment variables to restore later
let original_host = env::var("POSTGRES_HOST").ok();
let original_port = env::var("POSTGRES_PORT").ok();
let original_user = env::var("POSTGRES_USER").ok();
let original_password = env::var("POSTGRES_PASSWORD").ok();
let original_db = env::var("POSTGRES_DB").ok();
// Set test environment variables
env::set_var("POSTGRES_HOST", "test-host");
env::set_var("POSTGRES_PORT", "5433");
env::set_var("POSTGRES_USER", "test-user");
env::set_var("POSTGRES_PASSWORD", "test-password");
env::set_var("POSTGRES_DB", "test-db");
// Test with invalid port
env::set_var("POSTGRES_PORT", "invalid");
// Test with unset values
env::remove_var("POSTGRES_HOST");
env::remove_var("POSTGRES_PORT");
env::remove_var("POSTGRES_USER");
env::remove_var("POSTGRES_PASSWORD");
env::remove_var("POSTGRES_DB");
// Restore original environment variables
if let Some(host) = original_host {
env::set_var("POSTGRES_HOST", host);
}
if let Some(port) = original_port {
env::set_var("POSTGRES_PORT", port);
}
if let Some(user) = original_user {
env::set_var("POSTGRES_USER", user);
}
if let Some(password) = original_password {
env::set_var("POSTGRES_PASSWORD", password);
}
if let Some(db) = original_db {
env::set_var("POSTGRES_DB", db);
}
}
#[test]
fn test_postgres_config_builder() {
// Test the PostgreSQL configuration builder
// Test default values
let config = PostgresConfigBuilder::new();
assert_eq!(config.host, "localhost");
assert_eq!(config.port, 5432);
assert_eq!(config.user, "postgres");
assert_eq!(config.password, None);
assert_eq!(config.database, "postgres");
assert_eq!(config.application_name, None);
assert_eq!(config.connect_timeout, None);
assert_eq!(config.ssl_mode, None);
// Test setting values
let config = PostgresConfigBuilder::new()
.host("pg.example.com")
.port(5433)
.user("test-user")
.password("test-password")
.database("test-db")
.application_name("test-app")
.connect_timeout(30)
.ssl_mode("require");
assert_eq!(config.host, "pg.example.com");
assert_eq!(config.port, 5433);
assert_eq!(config.user, "test-user");
assert_eq!(config.password, Some("test-password".to_string()));
assert_eq!(config.database, "test-db");
assert_eq!(config.application_name, Some("test-app".to_string()));
assert_eq!(config.connect_timeout, Some(30));
assert_eq!(config.ssl_mode, Some("require".to_string()));
}
#[test]
fn test_connection_string_building() {
// Test building connection strings
// Test default connection string
let config = PostgresConfigBuilder::new();
let conn_string = config.build_connection_string();
assert!(conn_string.contains("host=localhost"));
assert!(conn_string.contains("port=5432"));
assert!(conn_string.contains("user=postgres"));
assert!(conn_string.contains("dbname=postgres"));
assert!(!conn_string.contains("password="));
// Test with all options
let config = PostgresConfigBuilder::new()
.host("pg.example.com")
.port(5433)
.user("test-user")
.password("test-password")
.database("test-db")
.application_name("test-app")
.connect_timeout(30)
.ssl_mode("require");
let conn_string = config.build_connection_string();
assert!(conn_string.contains("host=pg.example.com"));
assert!(conn_string.contains("port=5433"));
assert!(conn_string.contains("user=test-user"));
assert!(conn_string.contains("password=test-password"));
assert!(conn_string.contains("dbname=test-db"));
assert!(conn_string.contains("application_name=test-app"));
assert!(conn_string.contains("connect_timeout=30"));
assert!(conn_string.contains("sslmode=require"));
}
#[test]
fn test_reset_mock() {
// This is a simplified test that doesn't require an actual PostgreSQL server
// Just verify that the reset function doesn't panic
if let Err(_) = reset() {
// If PostgreSQL is not available, this is expected to fail
// So we don't assert anything here
}
}
}
// Integration tests that require a real PostgreSQL server
// These tests will be skipped if PostgreSQL is not available
#[cfg(test)]
mod postgres_installer_tests {
use super::*;
use sal_virt::nerdctl::Container;
#[test]
fn test_postgres_installer_config() {
// Test default configuration
let config = PostgresInstallerConfig::default();
assert_eq!(config.container_name, "postgres");
assert_eq!(config.version, "latest");
assert_eq!(config.port, 5432);
assert_eq!(config.username, "postgres");
assert_eq!(config.password, "postgres");
assert_eq!(config.data_dir, None);
assert_eq!(config.env_vars.len(), 0);
assert_eq!(config.persistent, true);
// Test builder pattern
let config = PostgresInstallerConfig::new()
.container_name("my-postgres")
.version("15")
.port(5433)
.username("testuser")
.password("testpass")
.data_dir("/tmp/pgdata")
.env_var("POSTGRES_INITDB_ARGS", "--encoding=UTF8")
.persistent(false);
assert_eq!(config.container_name, "my-postgres");
assert_eq!(config.version, "15");
assert_eq!(config.port, 5433);
assert_eq!(config.username, "testuser");
assert_eq!(config.password, "testpass");
assert_eq!(config.data_dir, Some("/tmp/pgdata".to_string()));
assert_eq!(config.env_vars.len(), 1);
assert_eq!(
config.env_vars.get("POSTGRES_INITDB_ARGS").unwrap(),
"--encoding=UTF8"
);
assert_eq!(config.persistent, false);
}
#[test]
fn test_postgres_installer_error() {
// Test IoError
let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "File not found");
let installer_error = PostgresInstallerError::IoError(io_error);
assert!(format!("{}", installer_error).contains("I/O error"));
// Test NerdctlError
let nerdctl_error = PostgresInstallerError::NerdctlError("Container not found".to_string());
assert!(format!("{}", nerdctl_error).contains("Nerdctl error"));
// Test PostgresError
let postgres_error =
PostgresInstallerError::PostgresError("Database not found".to_string());
assert!(format!("{}", postgres_error).contains("PostgreSQL error"));
}
#[test]
fn test_install_postgres_with_defaults() {
// This is a unit test that doesn't actually install PostgreSQL
// It just tests the configuration and error handling
// Test with default configuration
let config = PostgresInstallerConfig::default();
// We expect this to fail because nerdctl is not available
let result = install_postgres(config);
assert!(result.is_err());
// Check that the error is a NerdctlError or IoError
match result {
Err(PostgresInstallerError::NerdctlError(_)) => {
// This is fine, we expected a NerdctlError
}
Err(PostgresInstallerError::IoError(_)) => {
// This is also fine, we expected an error
}
_ => panic!("Expected NerdctlError or IoError"),
}
}
#[test]
fn test_install_postgres_with_custom_config() {
// Test with custom configuration
let config = PostgresInstallerConfig::new()
.container_name("test-postgres")
.version("15")
.port(5433)
.username("testuser")
.password("testpass")
.data_dir("/tmp/pgdata")
.env_var("POSTGRES_INITDB_ARGS", "--encoding=UTF8")
.persistent(true);
// We expect this to fail because nerdctl is not available
let result = install_postgres(config);
assert!(result.is_err());
// Check that the error is a NerdctlError or IoError
match result {
Err(PostgresInstallerError::NerdctlError(_)) => {
// This is fine, we expected a NerdctlError
}
Err(PostgresInstallerError::IoError(_)) => {
// This is also fine, we expected an error
}
_ => panic!("Expected NerdctlError or IoError"),
}
}
#[test]
fn test_create_database() {
// Create a mock container
// In a real test, we would use mockall to create a mock container
// But for this test, we'll just test the error handling
// We expect this to fail because the container is not running
let result = create_database(
&Container {
name: "test-postgres".to_string(),
container_id: None,
image: Some("postgres:15".to_string()),
config: HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: 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,
},
"testdb",
);
assert!(result.is_err());
// Check that the error is a PostgresError
match result {
Err(PostgresInstallerError::PostgresError(msg)) => {
assert!(msg.contains("Container is not running"));
}
_ => panic!("Expected PostgresError"),
}
}
#[test]
fn test_execute_sql() {
// Create a mock container
// In a real test, we would use mockall to create a mock container
// But for this test, we'll just test the error handling
// We expect this to fail because the container is not running
let result = execute_sql(
&Container {
name: "test-postgres".to_string(),
container_id: None,
image: Some("postgres:15".to_string()),
config: HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: 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,
},
"testdb",
"SELECT 1",
);
assert!(result.is_err());
// Check that the error is a PostgresError
match result {
Err(PostgresInstallerError::PostgresError(msg)) => {
assert!(msg.contains("Container is not running"));
}
_ => panic!("Expected PostgresError"),
}
}
#[test]
fn test_is_postgres_running() {
// Create a mock container
// In a real test, we would use mockall to create a mock container
// But for this test, we'll just test the error handling
// We expect this to return false because the container is not running
let result = is_postgres_running(&Container {
name: "test-postgres".to_string(),
container_id: None,
image: Some("postgres:15".to_string()),
config: HashMap::new(),
ports: Vec::new(),
volumes: Vec::new(),
env_vars: 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,
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
}
}
#[cfg(test)]
mod postgres_integration_tests {
use super::*;
use std::time::Duration;
// Helper function to check if PostgreSQL is available
fn is_postgres_available() -> bool {
match get_postgres_client() {
Ok(_) => true,
Err(_) => false,
}
}
#[test]
fn test_postgres_client_integration() {
if !is_postgres_available() {
println!("Skipping PostgreSQL integration tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL integration tests...");
// Test basic operations
test_basic_postgres_operations();
// Test error handling
test_error_handling();
}
#[test]
fn test_connection_pool() {
if !is_postgres_available() {
println!("Skipping PostgreSQL connection pool tests - PostgreSQL server not available");
return;
}
run_connection_pool_test();
}
fn run_connection_pool_test() {
println!("Running PostgreSQL connection pool tests...");
// Test creating a connection pool
let config = PostgresConfigBuilder::new()
.use_pool(true)
.pool_max_size(5)
.pool_min_idle(1)
.pool_connection_timeout(Duration::from_secs(5));
let pool_result = config.build_pool();
assert!(pool_result.is_ok());
let pool = pool_result.unwrap();
// Test getting a connection from the pool
let conn_result = pool.get();
assert!(conn_result.is_ok());
// Test executing a query with the connection
let mut conn = conn_result.unwrap();
let query_result = conn.query("SELECT 1", &[]);
assert!(query_result.is_ok());
// Test the global pool
let global_pool_result = get_postgres_pool();
assert!(global_pool_result.is_ok());
// Test executing queries with the pool
let create_table_query = "
CREATE TEMPORARY TABLE pool_test (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
)
";
let create_result = execute_with_pool(create_table_query, &[]);
assert!(create_result.is_ok());
// Test with parameters
let insert_result = execute_with_pool(
"INSERT INTO pool_test (name) VALUES ($1) RETURNING id",
&[&"test_pool"],
);
assert!(insert_result.is_ok());
// Test with QueryParams
let mut params = QueryParams::new();
params.add_str("test_pool_params");
let insert_params_result = execute_with_pool_params(
"INSERT INTO pool_test (name) VALUES ($1) RETURNING id",
&params,
);
assert!(insert_params_result.is_ok());
// Test query functions
let query_result = query_with_pool("SELECT * FROM pool_test", &[]);
assert!(query_result.is_ok());
let rows = query_result.unwrap();
assert_eq!(rows.len(), 2);
// Test query_one
let query_one_result =
query_one_with_pool("SELECT * FROM pool_test WHERE name = $1", &[&"test_pool"]);
assert!(query_one_result.is_ok());
// Test query_opt
let query_opt_result =
query_opt_with_pool("SELECT * FROM pool_test WHERE name = $1", &[&"nonexistent"]);
assert!(query_opt_result.is_ok());
assert!(query_opt_result.unwrap().is_none());
// Test resetting the pool
let reset_result = reset_pool();
assert!(reset_result.is_ok());
// Test getting the pool again after reset
let pool_after_reset = get_postgres_pool();
assert!(pool_after_reset.is_ok());
}
fn test_basic_postgres_operations() {
if !is_postgres_available() {
return;
}
// Create a test table
let create_table_query = "
CREATE TEMPORARY TABLE test_table (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER
)
";
let create_result = execute(create_table_query, &[]);
assert!(create_result.is_ok());
// Insert data
let insert_query = "
INSERT INTO test_table (name, value)
VALUES ($1, $2)
RETURNING id
";
let insert_result = query(insert_query, &[&"test_name", &42]);
assert!(insert_result.is_ok());
let rows = insert_result.unwrap();
assert_eq!(rows.len(), 1);
let id: i32 = rows[0].get(0);
assert!(id > 0);
// Query data
let select_query = "
SELECT id, name, value
FROM test_table
WHERE id = $1
";
let select_result = query_one(select_query, &[&id]);
assert!(select_result.is_ok());
let row = select_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
assert_eq!(name, "test_name");
assert_eq!(value, 42);
// Update data
let update_query = "
UPDATE test_table
SET value = $1
WHERE id = $2
";
let update_result = execute(update_query, &[&100, &id]);
assert!(update_result.is_ok());
assert_eq!(update_result.unwrap(), 1); // 1 row affected
// Verify update
let verify_query = "
SELECT value
FROM test_table
WHERE id = $1
";
let verify_result = query_one(verify_query, &[&id]);
assert!(verify_result.is_ok());
let row = verify_result.unwrap();
let updated_value: i32 = row.get(0);
assert_eq!(updated_value, 100);
// Delete data
let delete_query = "
DELETE FROM test_table
WHERE id = $1
";
let delete_result = execute(delete_query, &[&id]);
assert!(delete_result.is_ok());
assert_eq!(delete_result.unwrap(), 1); // 1 row affected
}
#[test]
fn test_query_params() {
if !is_postgres_available() {
println!("Skipping PostgreSQL parameter tests - PostgreSQL server not available");
return;
}
run_query_params_test();
}
#[test]
fn test_transactions() {
if !is_postgres_available() {
println!("Skipping PostgreSQL transaction tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL transaction tests...");
// Test successful transaction
let result = transaction(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_test (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_test (name) VALUES ($1)",
&[&"test_transaction"],
)?;
// Query data
let rows = client.query(
"SELECT * FROM transaction_test WHERE name = $1",
&[&"test_transaction"],
)?;
assert_eq!(rows.len(), 1);
let name: String = rows[0].get(1);
assert_eq!(name, "test_transaction");
// Return success
Ok(true)
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
// Test failed transaction
let result = transaction(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_test_fail (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_test_fail (name) VALUES ($1)",
&[&"test_transaction_fail"],
)?;
// Cause an error with invalid SQL
client.execute("THIS IS INVALID SQL", &[])?;
// This should not be reached
Ok(false)
});
assert!(result.is_err());
// Verify that the table was not created (transaction was rolled back)
let verify_result = query("SELECT * FROM transaction_test_fail", &[]);
assert!(verify_result.is_err());
// Test transaction with pool
let result = transaction_with_pool(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_pool_test (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_pool_test (name) VALUES ($1)",
&[&"test_transaction_pool"],
)?;
// Query data
let rows = client.query(
"SELECT * FROM transaction_pool_test WHERE name = $1",
&[&"test_transaction_pool"],
)?;
assert_eq!(rows.len(), 1);
let name: String = rows[0].get(1);
assert_eq!(name, "test_transaction_pool");
// Return success
Ok(true)
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
fn run_query_params_test() {
println!("Running PostgreSQL parameter tests...");
// Create a test table
let create_table_query = "
CREATE TEMPORARY TABLE param_test (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER,
active BOOLEAN,
score REAL
)
";
let create_result = execute(create_table_query, &[]);
assert!(create_result.is_ok());
// Test QueryParams builder
let mut params = QueryParams::new();
params.add_str("test_name");
params.add_int(42);
params.add_bool(true);
params.add_float(3.14);
// Insert data using QueryParams
let insert_query = "
INSERT INTO param_test (name, value, active, score)
VALUES ($1, $2, $3, $4)
RETURNING id
";
let insert_result = query_with_params(insert_query, &params);
assert!(insert_result.is_ok());
let rows = insert_result.unwrap();
assert_eq!(rows.len(), 1);
let id: i32 = rows[0].get(0);
assert!(id > 0);
// Query data using QueryParams
let mut query_params = QueryParams::new();
query_params.add_int(id);
let select_query = "
SELECT id, name, value, active, score
FROM param_test
WHERE id = $1
";
let select_result = query_one_with_params(select_query, &query_params);
assert!(select_result.is_ok());
let row = select_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
let active: bool = row.get(3);
let score: f64 = row.get(4);
assert_eq!(name, "test_name");
assert_eq!(value, 42);
assert_eq!(active, true);
assert_eq!(score, 3.14);
// Test optional parameters
let mut update_params = QueryParams::new();
update_params.add_int(100);
update_params.add_opt::<String>(None);
update_params.add_int(id);
let update_query = "
UPDATE param_test
SET value = $1, name = COALESCE($2, name)
WHERE id = $3
";
let update_result = execute_with_params(update_query, &update_params);
assert!(update_result.is_ok());
assert_eq!(update_result.unwrap(), 1); // 1 row affected
// Verify update
let verify_result = query_one_with_params(select_query, &query_params);
assert!(verify_result.is_ok());
let row = verify_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
assert_eq!(name, "test_name"); // Name should be unchanged
assert_eq!(value, 100); // Value should be updated
// Test query_opt_with_params
let mut nonexistent_params = QueryParams::new();
nonexistent_params.add_int(9999); // ID that doesn't exist
let opt_query = "
SELECT id, name
FROM param_test
WHERE id = $1
";
let opt_result = query_opt_with_params(opt_query, &nonexistent_params);
assert!(opt_result.is_ok());
assert!(opt_result.unwrap().is_none());
// Clean up
let delete_query = "
DELETE FROM param_test
WHERE id = $1
";
let delete_result = execute_with_params(delete_query, &query_params);
assert!(delete_result.is_ok());
assert_eq!(delete_result.unwrap(), 1); // 1 row affected
}
fn test_error_handling() {
if !is_postgres_available() {
return;
}
// Test invalid SQL
let invalid_query = "SELECT * FROM nonexistent_table";
let invalid_result = query(invalid_query, &[]);
assert!(invalid_result.is_err());
// Test parameter type mismatch
let mismatch_query = "SELECT $1::integer";
let mismatch_result = query(mismatch_query, &[&"not_an_integer"]);
assert!(mismatch_result.is_err());
// Test query_one with no results
let empty_query = "SELECT * FROM pg_tables WHERE tablename = 'nonexistent_table'";
let empty_result = query_one(empty_query, &[]);
assert!(empty_result.is_err());
// Test query_opt with no results
let opt_query = "SELECT * FROM pg_tables WHERE tablename = 'nonexistent_table'";
let opt_result = query_opt(opt_query, &[]);
assert!(opt_result.is_ok());
assert!(opt_result.unwrap().is_none());
}
#[test]
fn test_notify() {
if !is_postgres_available() {
println!("Skipping PostgreSQL notification tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL notification tests...");
// Test sending a notification
let result = notify("test_channel", "test_payload");
assert!(result.is_ok());
// Test sending a notification with the pool
let result = notify_with_pool("test_channel_pool", "test_payload_pool");
assert!(result.is_ok());
}
}

View File

@@ -7,7 +7,7 @@ mod core;
pub mod error;
// OS module is now provided by sal-os package
// Platform module is now provided by sal-os package
mod postgresclient;
// PostgreSQL module is now provided by sal-postgresclient package
// Virt modules (buildah, nerdctl, rfs) are now provided by sal-virt package
mod vault;
@@ -44,7 +44,7 @@ pub use sal_os::rhai::{
pub use sal_redisclient::rhai::register_redisclient_module;
// Re-export PostgreSQL client module registration function
pub use postgresclient::register_postgresclient_module;
pub use sal_postgresclient::rhai::register_postgresclient_module;
pub use sal_process::rhai::{
kill,
@@ -158,7 +158,7 @@ pub fn register(engine: &mut Engine) -> Result<(), Box<rhai::EvalAltResult>> {
sal_redisclient::rhai::register_redisclient_module(engine)?;
// Register PostgreSQL client module functions
postgresclient::register_postgresclient_module(engine)?;
sal_postgresclient::rhai::register_postgresclient_module(engine)?;
// Platform functions are now registered by sal-os package

View File

@@ -1,356 +0,0 @@
//! Rhai wrappers for PostgreSQL client module functions
//!
//! This module provides Rhai wrappers for the functions in the PostgreSQL client module.
use crate::postgresclient;
use postgres::types::ToSql;
use rhai::{Array, Engine, EvalAltResult, Map};
/// 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 postgresclient::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 postgresclient::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 postgresclient::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 postgresclient::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) -> 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 postgresclient::query(query, 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 postgresclient::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 = postgresclient::PostgresInstallerConfig::new()
.container_name(container_name)
.version(version)
.port(port as u16)
.username(username)
.password(password);
// Install PostgreSQL
match postgresclient::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 = crate::virt::nerdctl::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 postgresclient::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 = crate::virt::nerdctl::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 postgresclient::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 = crate::virt::nerdctl::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 postgresclient::is_postgres_running(&container) {
Ok(running) => Ok(running),
Err(e) => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("PostgreSQL error: {}", e).into(),
rhai::Position::NONE,
))),
}
}