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};