feat: Add PostgreSQL and Redis client support
- Add PostgreSQL client functionality for database interactions. - Add Redis client functionality for cache and data store operations. - Extend Rhai scripting with PostgreSQL and Redis client modules. - Add documentation and test cases for both clients.
This commit is contained in:
245
src/postgresclient/README.md
Normal file
245
src/postgresclient/README.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# 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");
|
||||
}
|
||||
```
|
10
src/postgresclient/mod.rs
Normal file
10
src/postgresclient/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// PostgreSQL client module
|
||||
//
|
||||
// This module provides a PostgreSQL client for interacting with PostgreSQL databases.
|
||||
|
||||
mod postgresclient;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
// Re-export the public API
|
||||
pub use postgresclient::*;
|
356
src/postgresclient/postgresclient.rs
Normal file
356
src/postgresclient/postgresclient.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
use lazy_static::lazy_static;
|
||||
use postgres::{Client, Error as PostgresError, NoTls, Row};
|
||||
use std::env;
|
||||
use std::sync::{Arc, Mutex, Once};
|
||||
|
||||
// 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 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>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for PostgreSQL client to handle connection
|
||||
pub struct PostgresClientWrapper {
|
||||
connection_string: String,
|
||||
client: Mutex<Option<Client>>,
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
277
src/postgresclient/tests.rs
Normal file
277
src/postgresclient/tests.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
use super::*;
|
||||
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_integration_tests {
|
||||
use super::*;
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user