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:
@@ -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;
|
||||
|
@@ -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");
|
||||
}
|
||||
```
|
@@ -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(©_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),
|
||||
}
|
||||
}
|
@@ -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::*;
|
@@ -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, ¶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(())
|
||||
}
|
@@ -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",
|
||||
¶ms,
|
||||
);
|
||||
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, ¶ms);
|
||||
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());
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
))),
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user