1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! 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.
//!
//! ## Configuring the IntegreSQL server
//!
//! 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.
//!
//! ## Configuring the IntegreSQL client library
//!
//! 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.
//!
//! The full list of environment variables that can be set to configure this library
//! is:
//! - `INTEGRESQL_BASE_URL`: The base URL of the IntegreSQL server
//! - `INTEGRESQL_TIMEOUT_SECONDS`: The timeout in seconds for HTTP requests (the default is 6)
//! - `INTEGRESQL_DB_HOST`: A hostname that will override all Postgres hostnames returned by the IntegreSQL server (default: none)
//! - `INTEGRESQL_DB_PORT`: A port that will override all Postgres ports returned by the IntegreSQL server (default: none)
//!
//! Example usage:
//! ```rust
//! 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);
//! }
//! ```
pub use IntegresqlError;
pub use ;