Crate integresql

Crate integresql 

Source
Expand description

A Rust client for the IntegreSQL Postgres testing tool

IntegreSQL makes it easy and fast to write integration tests that require a Postgres database, letting you give each test its own fresh, isolated database containing all the schema resources the test requires, with minimal performance overhead. It does this by leveraging Postgres’s support for template databases- databases that can be cloned cheaply to create new databases. By combining the use of template databases with a pre-populated pool of test databases, IntegreSQL lets you spin up and destroy large numbers of ephemeral test databases in roughly 10-50 milliseconds each on modern development machines.

§Setup

In typical usage, you will run a containerized Postgres server and a containerized IntegreSQL server independently of your test code, such as via docker-compose configuration for your development environment. This library doesn’t get involved in starting or stopping those servers, that’s up to you. The IntegreSQL server needs credentials to connect to the Postgres server, which it obtains from the following environment variables:

  • PGHOST: The hostname of the Postgres server (default: 127.0.0.1).
  • PGPORT: The port of the Postgres server (default: 5432).
  • PGUSER: The username to connect to the Postgres server (default: postgres).
  • PGPASSWORD: The password to connect to the Postgres server (default: ``).
  • INTEGRESQL_PGDATABASE: The name of the Postgres database that IntegreSQL will connect to initially (default: postgres).

There are many other environment variables that can optionally be set to control the IntegreSQL server, such as separate credentials for connecting to the template and test databases.

The primary configuration setting for this library is the INTEGRESQL_BASE_URL environment variable, which specifies the base URL of the IntegreSQL server. It should end with /api, such as http://localhost:5000/api. This library doesn’t need Postgres connection credentials- it doesn’t interact with Postgres directly, only with the IntegreSQL API server. Your test code will obtain its own Postgres credentials via this library, which in turn will obtain them from the IntegreSQL server.

Example usage:

use integresql::{DbManager, TemplateDb, ConnectionSettings};
use tokio_postgres::{Client, NoTls};
use std::time::Instant;
use std::error::Error;
use tokio::task;
use futures::future::join_all;
 
const SCHEMA_SQL: &str = "CREATE SCHEMA test_schema;
 
CREATE TABLE test_schema.pet_owners (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    pet_count INTEGER NOT NULL
);
";
 
async fn connect(conn_str: impl AsRef<str>) -> Result<Client, tokio_postgres::Error> {
    let (client, connection) = tokio_postgres::connect(conn_str.as_ref(), NoTls).await?;
    tokio::spawn(async move { 
        if let Err(e) = connection.await {
            eprintln!("Connection error: {}", e);
        }
    });
    Ok(client)
}
 
/// Sets up the template database with your schema 
async fn setup_template(config: ConnectionSettings) -> Result<(), Box<dyn Error + 'static>> {
   let conn_str = config.to_libpq_url();
   let client = connect(&conn_str).await?;

   client.batch_execute(SCHEMA_SQL).await?;

   drop(client);
   tokio::task::yield_now().await; // Yield to ensure the connection is closed
   Ok(())
}
 
#[tokio::main]
async fn main() {
    let db_manager = DbManager::from_env();
    let template_db = db_manager.get_template_db_async(SCHEMA_SQL, setup_template).await
        .expect("Error creating template database");
    
    let start = Instant::now();
    let mut handles = Vec::new();
    for i in 0..20 {
        let test_db = template_db.get_writable_test_db()
           .expect("Error getting test database");
        handles.push(task::spawn(async move {
            let conn_str = test_db.to_libpq_url();
            let client = connect(&conn_str).await
                .expect("Error connecting to test database");
 
            let insert_sql = "
                INSERT INTO test_schema.pet_owners (name, pet_count) 
                VALUES ($1, $2), $($3, $4);";
            client.execute(insert_sql, &[&"Alice", &1, &"Bob", &2]).await.unwrap();
            let row_count: i64 = client.query_one("SELECT COUNT(*) FROM test_schema.pet_owners", &[]).await
                .map(|row| row.get(0))
                .unwrap();
            assert_eq!(row_count, 2);
        }));
    }
    join_all(handles).await;
 
    let elapsed = start.elapsed().as_secs_f64();
    eprintln!("Created and used 20 test databases in {:.2} seconds", elapsed);
}

Structs§

ConnectionSettings
Credentials to connect to a database managed by IntegreSQL- either a template database or a test database.
DbManager
A manager for IntegreSQL template databases- the main entry point for this crate.
TemplateDb
A template database that can be used to create test databases.

Enums§

IntegresqlError
The various error types that can be returned by an IntegreSQL request.

Traits§

AsyncTemplateInitializer
A template database setup function that should be called asynchronously.
TemplateInitializer
A template database setup function that should be called synchronously.

Type Aliases§

InitializeResult
The result type for template database initialization functions.