integresql/
lib.rs

1//! A Rust client for the IntegreSQL Postgres testing tool
2//!
3//! IntegreSQL makes it easy and fast to write integration tests that require a
4//! Postgres database, letting you give each test its own fresh, isolated
5//! database containing all the schema resources the test requires, with minimal
6//! performance overhead.  It does this by leveraging Postgres's support for
7//! template databases- databases that can be cloned cheaply to create new
8//! databases. By combining the use of template databases with a pre-populated
9//! pool of test databases, IntegreSQL lets you spin up and destroy large
10//! numbers of ephemeral test databases in roughly 10-50 milliseconds each on
11//! modern development machines.
12//! 
13//! ## Setup
14//! 
15//! In typical usage, you will run a containerized Postgres server and a
16//! containerized IntegreSQL server independently of your test code, such as via
17//! docker-compose configuration for your development environment. This library
18//! doesn't get involved in starting or stopping those servers, that's up to
19//! you. The IntegreSQL server needs credentials to connect to the Postgres
20//! server, which it obtains from the following environment variables:
21//! 
22//! - `PGHOST`: The hostname of the Postgres server (default: `127.0.0.1`).
23//! - `PGPORT`: The port of the Postgres server (default: `5432`).
24//! - `PGUSER`: The username to connect to the Postgres server (default: `postgres`).
25//! - `PGPASSWORD`: The password to connect to the Postgres server (default: ``).
26//! - `INTEGRESQL_PGDATABASE`: The name of the Postgres database that IntegreSQL
27//!    will connect to initially (default: `postgres`).
28//! 
29//! There are many other environment variables that can optionally be set to
30//! control the IntegreSQL server, such as separate credentials for connecting
31//! to the template and test databases.
32//! 
33//! The primary configuration setting for this library is the `INTEGRESQL_BASE_URL`
34//! environment variable, which specifies the base URL of the IntegreSQL server. 
35//! It should end with `/api`, such as `http://localhost:5000/api`. This library
36//! doesn't need Postgres connection credentials- it doesn't interact with Postgres
37//! directly, only with the IntegreSQL API server. Your test code will obtain
38//! its own Postgres credentials via this library, which in turn will obtain
39//! them from the IntegreSQL server.
40//! 
41//! Example usage:
42//! ```rust
43//! use integresql::{DbManager, TemplateDb, ConnectionSettings};
44//! use tokio_postgres::{Client, NoTls};
45//! use std::time::Instant;
46//! use std::error::Error;
47//! use tokio::task;
48//! use futures::future::join_all;
49//! 
50//! const SCHEMA_SQL: &str = "CREATE SCHEMA test_schema;
51//! 
52//! CREATE TABLE test_schema.pet_owners (
53//!     id SERIAL PRIMARY KEY,
54//!     name TEXT NOT NULL,
55//!     pet_count INTEGER NOT NULL
56//! );
57//! ";
58//! 
59//! async fn connect(conn_str: impl AsRef<str>) -> Result<Client, tokio_postgres::Error> {
60//!     let (client, connection) = tokio_postgres::connect(conn_str.as_ref(), NoTls).await?;
61//!     tokio::spawn(async move { 
62//!         if let Err(e) = connection.await {
63//!             eprintln!("Connection error: {}", e);
64//!         }
65//!     });
66//!     Ok(client)
67//! }
68//! 
69//! /// Sets up the template database with your schema 
70//! async fn setup_template(config: ConnectionSettings) -> Result<(), Box<dyn Error + 'static>> {
71//!    let conn_str = config.to_libpq_url();
72//!    let client = connect(&conn_str).await?;
73//!
74//!    client.batch_execute(SCHEMA_SQL).await?;
75//!
76//!    drop(client);
77//!    tokio::task::yield_now().await; // Yield to ensure the connection is closed
78//!    Ok(())
79//! }
80//! 
81//! #[tokio::main]
82//! async fn main() {
83//!     let db_manager = DbManager::from_env();
84//!     let template_db = db_manager.get_template_db_async(SCHEMA_SQL, setup_template).await
85//!         .expect("Error creating template database");
86//!    
87//!     let start = Instant::now();
88//!     let mut handles = Vec::new();
89//!     for i in 0..20 {
90//!         let test_db = template_db.get_writable_test_db()
91//!            .expect("Error getting test database");
92//!         handles.push(task::spawn(async move {
93//!             let conn_str = test_db.to_libpq_url();
94//!             let client = connect(&conn_str).await
95//!                 .expect("Error connecting to test database");
96//! 
97//!             let insert_sql = "
98//!                 INSERT INTO test_schema.pet_owners (name, pet_count) 
99//!                 VALUES ($1, $2), $($3, $4);";
100//!             client.execute(insert_sql, &[&"Alice", &1, &"Bob", &2]).await.unwrap();
101//!             let row_count: i64 = client.query_one("SELECT COUNT(*) FROM test_schema.pet_owners", &[]).await
102//!                 .map(|row| row.get(0))
103//!                 .unwrap();
104//!             assert_eq!(row_count, 2);
105//!         }));
106//!     }
107//!     join_all(handles).await;
108//! 
109//!     let elapsed = start.elapsed().as_secs_f64();
110//!     eprintln!("Created and used 20 test databases in {:.2} seconds", elapsed);
111//! }
112//! ```
113
114mod client;
115mod db_manager;
116mod server_models;
117
118pub use client::IntegresqlError;
119pub use db_manager::{
120    AsyncTemplateInitializer, DbManager, TemplateDb, TemplateInitializer, ConnectionSettings, InitializeResult
121};