feat: Add PostgreSQL connection pooling support
Some checks failed
Rhai Tests / Run Rhai Tests (pull_request) Has been cancelled

- Implement connection pooling using `r2d2` and `r2d2_postgres`
- Add connection pool configuration options to `PostgresConfigBuilder`
- Introduce transaction functions with automatic commit/rollback
- Add functions for executing queries using the connection pool
- Add `QueryParams` struct for building parameterized queries
- Add tests for connection pooling and transaction functions
This commit is contained in:
Mahmoud Emad
2025-05-09 10:45:53 +03:00
parent 22f87b320e
commit 114d63e590
3 changed files with 851 additions and 43 deletions

View File

@@ -4,7 +4,7 @@ use std::env;
#[cfg(test)]
mod postgres_client_tests {
use super::*;
#[test]
fn test_env_vars() {
// Save original environment variables to restore later
@@ -13,24 +13,24 @@ mod postgres_client_tests {
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);
@@ -48,11 +48,11 @@ mod postgres_client_tests {
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");
@@ -63,7 +63,7 @@ mod postgres_client_tests {
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")
@@ -74,7 +74,7 @@ mod postgres_client_tests {
.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");
@@ -84,11 +84,11 @@ mod postgres_client_tests {
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();
@@ -97,7 +97,7 @@ mod postgres_client_tests {
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")
@@ -108,7 +108,7 @@ mod postgres_client_tests {
.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"));
@@ -119,11 +119,11 @@ mod postgres_client_tests {
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
@@ -137,7 +137,8 @@ mod postgres_client_tests {
#[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() {
@@ -145,28 +146,120 @@ mod postgres_integration_tests {
Err(_) => false,
}
}
#[test]
fn test_postgres_client_integration() {
if !is_postgres_available() {
println!("Skipping PostgreSQL integration tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL integration tests...");
// Test basic operations
test_basic_postgres_operations();
// Test error handling
test_error_handling();
}
#[test]
fn test_connection_pool() {
if !is_postgres_available() {
println!("Skipping PostgreSQL connection pool tests - PostgreSQL server not available");
return;
}
run_connection_pool_test();
}
fn run_connection_pool_test() {
println!("Running PostgreSQL connection pool tests...");
// Test creating a connection pool
let config = PostgresConfigBuilder::new()
.use_pool(true)
.pool_max_size(5)
.pool_min_idle(1)
.pool_connection_timeout(Duration::from_secs(5));
let pool_result = config.build_pool();
assert!(pool_result.is_ok());
let pool = pool_result.unwrap();
// Test getting a connection from the pool
let conn_result = pool.get();
assert!(conn_result.is_ok());
// Test executing a query with the connection
let mut conn = conn_result.unwrap();
let query_result = conn.query("SELECT 1", &[]);
assert!(query_result.is_ok());
// Test the global pool
let global_pool_result = get_postgres_pool();
assert!(global_pool_result.is_ok());
// Test executing queries with the pool
let create_table_query = "
CREATE TEMPORARY TABLE pool_test (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
)
";
let create_result = execute_with_pool(create_table_query, &[]);
assert!(create_result.is_ok());
// Test with parameters
let insert_result = execute_with_pool(
"INSERT INTO pool_test (name) VALUES ($1) RETURNING id",
&[&"test_pool"],
);
assert!(insert_result.is_ok());
// Test with QueryParams
let mut params = QueryParams::new();
params.add_str("test_pool_params");
let insert_params_result = execute_with_pool_params(
"INSERT INTO pool_test (name) VALUES ($1) RETURNING id",
&params,
);
assert!(insert_params_result.is_ok());
// Test query functions
let query_result = query_with_pool("SELECT * FROM pool_test", &[]);
assert!(query_result.is_ok());
let rows = query_result.unwrap();
assert_eq!(rows.len(), 2);
// Test query_one
let query_one_result =
query_one_with_pool("SELECT * FROM pool_test WHERE name = $1", &[&"test_pool"]);
assert!(query_one_result.is_ok());
// Test query_opt
let query_opt_result =
query_opt_with_pool("SELECT * FROM pool_test WHERE name = $1", &[&"nonexistent"]);
assert!(query_opt_result.is_ok());
assert!(query_opt_result.unwrap().is_none());
// Test resetting the pool
let reset_result = reset_pool();
assert!(reset_result.is_ok());
// Test getting the pool again after reset
let pool_after_reset = get_postgres_pool();
assert!(pool_after_reset.is_ok());
}
fn test_basic_postgres_operations() {
if !is_postgres_available() {
return;
}
// Create a test table
let create_table_query = "
CREATE TEMPORARY TABLE test_table (
@@ -175,103 +268,347 @@ mod postgres_integration_tests {
value INTEGER
)
";
let create_result = execute(create_table_query, &[]);
assert!(create_result.is_ok());
// Insert data
let insert_query = "
INSERT INTO test_table (name, value)
VALUES ($1, $2)
RETURNING id
";
let insert_result = query(insert_query, &[&"test_name", &42]);
assert!(insert_result.is_ok());
let rows = insert_result.unwrap();
assert_eq!(rows.len(), 1);
let id: i32 = rows[0].get(0);
assert!(id > 0);
// Query data
let select_query = "
SELECT id, name, value
FROM test_table
WHERE id = $1
";
let select_result = query_one(select_query, &[&id]);
assert!(select_result.is_ok());
let row = select_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
assert_eq!(name, "test_name");
assert_eq!(value, 42);
// Update data
let update_query = "
UPDATE test_table
SET value = $1
WHERE id = $2
";
let update_result = execute(update_query, &[&100, &id]);
assert!(update_result.is_ok());
assert_eq!(update_result.unwrap(), 1); // 1 row affected
// Verify update
let verify_query = "
SELECT value
FROM test_table
WHERE id = $1
";
let verify_result = query_one(verify_query, &[&id]);
assert!(verify_result.is_ok());
let row = verify_result.unwrap();
let updated_value: i32 = row.get(0);
assert_eq!(updated_value, 100);
// Delete data
let delete_query = "
DELETE FROM test_table
WHERE id = $1
";
let delete_result = execute(delete_query, &[&id]);
assert!(delete_result.is_ok());
assert_eq!(delete_result.unwrap(), 1); // 1 row affected
}
#[test]
fn test_query_params() {
if !is_postgres_available() {
println!("Skipping PostgreSQL parameter tests - PostgreSQL server not available");
return;
}
run_query_params_test();
}
#[test]
fn test_transactions() {
if !is_postgres_available() {
println!("Skipping PostgreSQL transaction tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL transaction tests...");
// Test successful transaction
let result = transaction(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_test (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_test (name) VALUES ($1)",
&[&"test_transaction"],
)?;
// Query data
let rows = client.query(
"SELECT * FROM transaction_test WHERE name = $1",
&[&"test_transaction"],
)?;
assert_eq!(rows.len(), 1);
let name: String = rows[0].get(1);
assert_eq!(name, "test_transaction");
// Return success
Ok(true)
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
// Test failed transaction
let result = transaction(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_test_fail (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_test_fail (name) VALUES ($1)",
&[&"test_transaction_fail"],
)?;
// Cause an error with invalid SQL
client.execute("THIS IS INVALID SQL", &[])?;
// This should not be reached
Ok(false)
});
assert!(result.is_err());
// Verify that the table was not created (transaction was rolled back)
let verify_result = query("SELECT * FROM transaction_test_fail", &[]);
assert!(verify_result.is_err());
// Test transaction with pool
let result = transaction_with_pool(|client| {
// Create a temporary table
client.execute(
"CREATE TEMPORARY TABLE transaction_pool_test (id SERIAL PRIMARY KEY, name TEXT NOT NULL)",
&[],
)?;
// Insert data
client.execute(
"INSERT INTO transaction_pool_test (name) VALUES ($1)",
&[&"test_transaction_pool"],
)?;
// Query data
let rows = client.query(
"SELECT * FROM transaction_pool_test WHERE name = $1",
&[&"test_transaction_pool"],
)?;
assert_eq!(rows.len(), 1);
let name: String = rows[0].get(1);
assert_eq!(name, "test_transaction_pool");
// Return success
Ok(true)
});
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
fn run_query_params_test() {
println!("Running PostgreSQL parameter tests...");
// Create a test table
let create_table_query = "
CREATE TEMPORARY TABLE param_test (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
value INTEGER,
active BOOLEAN,
score REAL
)
";
let create_result = execute(create_table_query, &[]);
assert!(create_result.is_ok());
// Test QueryParams builder
let mut params = QueryParams::new();
params.add_str("test_name");
params.add_int(42);
params.add_bool(true);
params.add_float(3.14);
// Insert data using QueryParams
let insert_query = "
INSERT INTO param_test (name, value, active, score)
VALUES ($1, $2, $3, $4)
RETURNING id
";
let insert_result = query_with_params(insert_query, &params);
assert!(insert_result.is_ok());
let rows = insert_result.unwrap();
assert_eq!(rows.len(), 1);
let id: i32 = rows[0].get(0);
assert!(id > 0);
// Query data using QueryParams
let mut query_params = QueryParams::new();
query_params.add_int(id);
let select_query = "
SELECT id, name, value, active, score
FROM param_test
WHERE id = $1
";
let select_result = query_one_with_params(select_query, &query_params);
assert!(select_result.is_ok());
let row = select_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
let active: bool = row.get(3);
let score: f64 = row.get(4);
assert_eq!(name, "test_name");
assert_eq!(value, 42);
assert_eq!(active, true);
assert_eq!(score, 3.14);
// Test optional parameters
let mut update_params = QueryParams::new();
update_params.add_int(100);
update_params.add_opt::<String>(None);
update_params.add_int(id);
let update_query = "
UPDATE param_test
SET value = $1, name = COALESCE($2, name)
WHERE id = $3
";
let update_result = execute_with_params(update_query, &update_params);
assert!(update_result.is_ok());
assert_eq!(update_result.unwrap(), 1); // 1 row affected
// Verify update
let verify_result = query_one_with_params(select_query, &query_params);
assert!(verify_result.is_ok());
let row = verify_result.unwrap();
let name: String = row.get(1);
let value: i32 = row.get(2);
assert_eq!(name, "test_name"); // Name should be unchanged
assert_eq!(value, 100); // Value should be updated
// Test query_opt_with_params
let mut nonexistent_params = QueryParams::new();
nonexistent_params.add_int(9999); // ID that doesn't exist
let opt_query = "
SELECT id, name
FROM param_test
WHERE id = $1
";
let opt_result = query_opt_with_params(opt_query, &nonexistent_params);
assert!(opt_result.is_ok());
assert!(opt_result.unwrap().is_none());
// Clean up
let delete_query = "
DELETE FROM param_test
WHERE id = $1
";
let delete_result = execute_with_params(delete_query, &query_params);
assert!(delete_result.is_ok());
assert_eq!(delete_result.unwrap(), 1); // 1 row affected
}
fn test_error_handling() {
if !is_postgres_available() {
return;
}
// Test invalid SQL
let invalid_query = "SELECT * FROM nonexistent_table";
let invalid_result = query(invalid_query, &[]);
assert!(invalid_result.is_err());
// Test parameter type mismatch
let mismatch_query = "SELECT $1::integer";
let mismatch_result = query(mismatch_query, &[&"not_an_integer"]);
assert!(mismatch_result.is_err());
// Test query_one with no results
let empty_query = "SELECT * FROM pg_tables WHERE tablename = 'nonexistent_table'";
let empty_result = query_one(empty_query, &[]);
assert!(empty_result.is_err());
// Test query_opt with no results
let opt_query = "SELECT * FROM pg_tables WHERE tablename = 'nonexistent_table'";
let opt_result = query_opt(opt_query, &[]);
assert!(opt_result.is_ok());
assert!(opt_result.unwrap().is_none());
}
#[test]
fn test_notify() {
if !is_postgres_available() {
println!("Skipping PostgreSQL notification tests - PostgreSQL server not available");
return;
}
println!("Running PostgreSQL notification tests...");
// Test sending a notification
let result = notify("test_channel", "test_payload");
assert!(result.is_ok());
// Test sending a notification with the pool
let result = notify_with_pool("test_channel_pool", "test_payload_pool");
assert!(result.is_ok());
}
}