Skip to content

Commit

Permalink
move diagrams into readme
Browse files Browse the repository at this point in the history
  • Loading branch information
majodev committed Jan 30, 2024
1 parent d454660 commit e71d581
Show file tree
Hide file tree
Showing 3 changed files with 314 additions and 104 deletions.
331 changes: 270 additions & 61 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
<!--
This file contains [mermaid](https://mermaid.js.org) diagrams.
In VSCode:
* install `bierner.markdown-mermaid` to have easy preview.
* install `bpruitt-goddard.mermaid-markdown-syntax-highlighting` for syntax highlighting.
-->

# IntegreSQL

IntegreSQL manages isolated PostgreSQL databases for your integration tests.
Expand All @@ -13,11 +21,26 @@ Do your engineers a favour by allowing them to write fast executing, parallel an
- [Run locally (not recommended)](#run-locally-not-recommended)
- [Run within your CI/CD](#run-within-your-cicd)
- [GitHub Actions](#github-actions)
- [Configuration](#configuration)
- [Integrate](#integrate)
- [Integrate by RESTful JSON calls](#integrate-by-restful-json-calls)
- [Integrate by client lib](#integrate-by-client-lib)
- [Integrate by RESTful JSON calls](#integrate-by-restful-json-calls)
- [Once per test runner/process](#once-per-test-runnerprocess)
- [Testrunner creates a new template database](#testrunner-creates-a-new-template-database)
- [Testrunner reuses an existing template database](#testrunner-reuses-an-existing-template-database)
- [Failure modes while template database setup: 503](#failure-modes-while-template-database-setup-503)
- [Per each test](#per-each-test)
- [New test database per test](#new-test-database-per-test)
- [Optional: Manually unlocking a test database after a readonly test](#optional-manually-unlocking-a-test-database-after-a-readonly-test)
- [Optional: Manually recreating a test database](#optional-manually-recreating-a-test-database)
- [Failure modes while getting a new test database](#failure-modes-while-getting-a-new-test-database)
- [StatusNotFound 404](#statusnotfound-404)
- [StatusGone 410](#statusgone-410)
- [StatusServiceUnavailable 503](#statusserviceunavailable-503)
- [Demo](#demo)
- [Configuration](#configuration)
- [Architecture](#architecture)
- [TestDatabase states](#testdatabase-states)
- [Pool structure](#pool-structure)
- [Background](#background)
- [Approach 0: Leaking database mutations for subsequent tests](#approach-0-leaking-database-mutations-for-subsequent-tests)
- [Approach 1: Isolating by resetting](#approach-1-isolating-by-resetting)
Expand Down Expand Up @@ -177,6 +200,201 @@ jobs:
PGPASSWORD: "dbpass"
```


## Integrate

IntegreSQL is a RESTful JSON API distributed as Docker image and go cli. It's language agnostic and manages multiple [PostgreSQL templates](https://supabase.io/blog/2020/07/09/postgresql-templates/) and their separate pool of test databases for your tests. It keeps the pool of test databases warm (as it's running in the background) and is fit for parallel test execution with multiple test runners / processes.

You will typically want to integrate by a client lib (see below), but you can also integrate by RESTful JSON calls directly. The flow is illustrated in the follow up section.

### Integrate by client lib

The flow above might look intimidating at first glance, but trust us, it's simple to integrate especially if there is already an client library available for your specific language. We currently have those:

* Go: [integresql-client-go](https://github.com/allaboutapps/integresql-client-go) by [Nick Müller - @MorpheusXAUT](https://github.com/MorpheusXAUT)
* Python: [integresql-client-python](https://github.com/msztolcman/integresql-client-python) by [Marcin Sztolcman - @msztolcman](https://github.com/msztolcman)
* .NET: [IntegreSQL.EF](https://github.com/mcctomsk/IntegreSql.EF) by [Artur Drobinskiy - @Shaddix](https://github.com/Shaddix)
* JavaScript/TypeScript: [@devoxa/integresql-client](https://github.com/devoxa/integresql-client) by [Devoxa - @devoxa](https://github.com/devoxa)
* ... *Add your link here and make a PR*

### Integrate by RESTful JSON calls

A really good starting point to write your own integresql-client for a specific language can be found [here (go code)](https://github.com/allaboutapps/integresql-client-go/blob/master/client.go) and [here (godoc)](https://pkg.go.dev/github.com/allaboutapps/integresql-client-go?tab=doc). It's just RESTful JSON after all.

First start IntegreSQL and leave it running in the background (your PostgreSQL template and test database pool will then always be warm). When you trigger your test command (e.g. `make test`), 1..n test runners/processes start in parallel.

#### Once per test runner/process

Each test runner starts and need to communicate with IntegreSQL to setup 1..n template database pools. The following sections describe the flows/scenarios you need to implement.

##### Testrunner creates a new template database

```mermaid
sequenceDiagram
You->>Testrunner: make test
Note right of Testrunner: Compute a hash over all related <br/> files that affect your database<br/> (migrations, fixtures, imports, etc.)
Note over Testrunner,IntegreSQL: Create a new PostgreSQL template database<br/> identified a the same unique hash <br/>payload: {"hash": "string"}
Testrunner->>IntegreSQL: InitializeTemplate: POST /api/v1/templates
IntegreSQL->>PostgreSQL: CREATE DATABASE <br/>template_<hash>
PostgreSQL-->>IntegreSQL:
IntegreSQL-->>Testrunner: StatusOK: 200
Note over Testrunner,PostgreSQL: Parse the received database connection payload and connect to the template database.
Testrunner->>PostgreSQL: Truncate, apply all migrations, seed all fixtures, ..., disconnect.
PostgreSQL-->>Testrunner:
Note over Testrunner,IntegreSQL: Finalize the template so it can be used!
Testrunner->>IntegreSQL: FinalizeTemplate: PUT /api/v1/templates/:hash
IntegreSQL-->>Testrunner: StatusOK: 200
Note over Testrunner,PostgreSQL: You can now get isolated test databases for this hash from the pool!
```

##### Testrunner reuses an existing template database

```mermaid
sequenceDiagram
You->>Testrunner: make test
Note over Testrunner,IntegreSQL: Subsequent testrunners or multiple processes <br/> simply call with the same template hash again.
Testrunner->>IntegreSQL: InitializeTemplate: POST /api/v1/templates
IntegreSQL-->>Testrunner: StatusLocked: 423
Note over Testrunner,IntegreSQL: Some other testrunner / process has already recreated <br/> this PostgreSQL template database identified by this hash<br/> (or is currently doing it), you can just consider<br/> the template ready at this point.
Note over Testrunner,PostgreSQL: You can now get isolated test databases for this hash from the pool!
```

##### Failure modes while template database setup: 503

```mermaid
sequenceDiagram
You->>Testrunner: make test
Testrunner->>IntegreSQL: InitializeTemplate: POST /api/v1/templates
IntegreSQL-->>Testrunner: StatusServiceUnavailable: 503
Note over Testrunner,PostgreSQL: Typically happens if IntegreSQL cannot communicate with<br/>PostgreSQL, fail the test runner process in this case (e.g. exit 1).
```

#### Per each test

##### New test database per test

Well, this is the normal flow to get a new isolated test database (prepopulated as its created from the template) for your test.

```mermaid
sequenceDiagram
Note right of You: ...
Note right of Testrunner: Before each test, get a new isolated test database<br/> from the pool for the template hash.
Testrunner->>IntegreSQL: GetTestDatabase: GET /api/v1/templates/:hash/tests
Note over Testrunner,IntegreSQL: Blocks until the template is finalized
Note right of IntegreSQL: The test databases for the template pool<br/>were already created and are simply returned.
IntegreSQL-->>Testrunner: StatusOK: 200
Note over Testrunner,PostgreSQL: Your runner now has a fully isolated PostgreSQL database<br/>from our already migrated/seeded template database to use within your test.
Testrunner->>PostgreSQL: Directly connect to the test database.
Note over Testrunner,PostgreSQL: Run your test code!
Testrunner-xPostgreSQL: Disconnect from the test database
Note over Testrunner,PostgreSQL: Your test is finished.
```

##### Optional: Manually unlocking a test database after a readonly test

* Returns the given test DB directly to the pool, without cleaning (recreating it).
* **This is optional!** If you don't call this endpoints, the test database will be recreated in a FIFO manner (first in, first out) as soon as possible, even though it actually had no changes.
* This is useful if you are sure, you did not do any changes to the database and thus want to skip the recreation process by returning it to the pool directly.


```mermaid
sequenceDiagram
Note right of You: ...
Testrunner->>IntegreSQL: GetTestDatabase: GET /api/v1/templates/:hash/tests
IntegreSQL-->>Testrunner: StatusOK: 200
Testrunner->>PostgreSQL: Directly connect to the test database.
Note over Testrunner,PostgreSQL: Run your **readonly** test code!
Testrunner-xPostgreSQL: Disconnect from the test database
Note over Testrunner,PostgreSQL: Your **readonly** test is finished.<br/> As you did not modify the test database, you can unlock it again<br/>(immediately available in the pool again).
Testrunner->>IntegreSQL: ReturnTestDatabase: POST /api/v1/templates/:hash/tests/:id/unlock<br/>(previously and soft-deprecated DELETE /api/v1/templates/:hash/tests/:id)
IntegreSQL-->>Testrunner: StatusOK: 200
```

##### Optional: Manually recreating a test database

* Recreates the test DB according to the template and returns it back to the pool.
* **This is optional!** If you don't call this endpoint, the test database will be recreated in a FIFO manner (first in, first out) as soon as possible.
* This is useful if you have parallel testing with a mix of very long and super short tests. Our auto–FIFO recreation handling might block there.

```mermaid
sequenceDiagram
Note right of You: ...
Testrunner->>IntegreSQL: GetTestDatabase: GET /api/v1/templates/:hash/tests
IntegreSQL-->>Testrunner: StatusOK: 200
Testrunner->>PostgreSQL: Directly connect to the test database.
Note over Testrunner,PostgreSQL: Run your test code!
Testrunner-xPostgreSQL: Disconnect from the test database
Note over Testrunner,PostgreSQL: Your test is finished.<br/> As you don't want to wait for FIFO autocleaning,<br/> you can manually recreate the test database.
Testrunner->>IntegreSQL: RecreateTestDatabase: POST /api/v1/templates/:hash/tests/:id/recreate
IntegreSQL-->>Testrunner: StatusOK: 200
```


##### Failure modes while getting a new test database

Some typical status codes you might encounter while getting a new test database.

###### StatusNotFound 404

Well, seems like someone forgot to call InitializeTemplate or it errored out.

###### StatusGone 410

There was an error during test setup with our fixtures, someone called `DiscardTemplate`, thus this template cannot be used.

###### StatusServiceUnavailable 503

Well, typically a PostgreSQL connectivity problem

#### Demo

If you want to take a look on how we integrate IntegreSQL - 🤭 - please just try our [go-starter](https://github.com/allaboutapps/go-starter) project or take a look at our [test_database setup code](https://github.com/allaboutapps/go-starter/blob/master/internal/test/test_database.go).

## Configuration

IntegreSQL requires little configuration, all of which has to be provided via environment variables (due to the intended usage in a Docker environment). The following settings are available:
Expand Down Expand Up @@ -223,73 +441,64 @@ IntegreSQL requires little configuration, all of which has to be provided via en
| Should the console logger pretty-print the log (instead of json)? | `INTEGRESQL_LOGGER_PRETTY_PRINT_CONSOLE` | | `false` |


## Integrate
## Architecture

IntegreSQL is a RESTful JSON API distributed as Docker image or go cli. It's language agnostic and manages multiple [PostgreSQL templates](https://supabase.io/blog/2020/07/09/postgresql-templates/) and their separate pool of test databases for your tests. It keeps the pool of test databases warm (as it's running in the background) and is fit for parallel test execution with multiple test runners / processes.
### TestDatabase states

You will typically want to integrate by a client lib (see below), but you can also integrate by RESTful JSON calls directly. The flow is introducd below.
The following describes the state and transitions of a TestDatabase.

### Integrate by RESTful JSON calls

Your development/testing flow should look like this:

* **Start IntegreSQL** and leave it running **in the background** (your PostgreSQL template and test database pool will always be warm)
* ...
* You trigger your test command. 1..n test runners/processes start in parallel
* **Once** per test runner/process:
* Get migrations/fixtures files `hash` over all related database files
* `InitializeTemplate: POST /api/v1/templates`: attempt to create a new PostgreSQL template database identified by the above hash `payload: {"hash": "string"}`
* `StatusOK: 200`
* Truncate
* Apply all migrations
* Seed all fixtures
* `FinalizeTemplate: PUT /api/v1/templates/:hash`
* If you encountered any template setup errors call `DiscardTemplate: DELETE /api/v1/templates/:hash`
* `StatusLocked: 423`
* Some other process has already recreated a PostgreSQL template database for this `hash` (or is currently doing it), you can just consider the template ready at this point.
* `StatusServiceUnavailable: 503`
* Typically happens if IntegreSQL cannot communicate with PostgreSQL, fail the test runner process
* **Before each** test `GetTestDatabase: GET /api/v1/templates/:hash/tests`
* Blocks until the template database is finalized (via `FinalizeTemplate`)
* `StatusOK: 200`
* You get a fully isolated PostgreSQL database from our already migrated/seeded template database to use within your test
* `StatusNotFound: 404`
* Well, seems like someone forgot to call `InitializeTemplate` or it errored out.
* `StatusGone: 410`
* There was an error during test setup with our fixtures, someone called `DiscardTemplate`, thus this template cannot be used.
* `StatusServiceUnavailable: 503`
* Well, typically a PostgreSQL connectivity problem
* Utilizing the isolated PostgreSQL test database received from IntegreSQL for each (parallel) test:
* **Run your test code**
* **After each** test **optional**:
* `RecreateTestDatabase: POST /api/v1/templates/:hash/tests/:id/recreate`
* Recreates the test DB according to the template and returns it back to the pool.
* **This is optional!** If you don't call this endpoint, the test database will be recreated in a FIFO manner (first in, first out) as soon as possible.
* This is useful if you have parallel testing with a mix of very long and super short tests. Our auto–FIFO recreation handling might block there.
* `ReturnTestDatabase: POST /api/v1/templates/:hash/tests/:id/unlock` (previously and soft-deprecated `DELETE /api/v1/templates/:hash/tests/:id`)
* Returns the given test DB directly to the pool, without cleaning (recreating it).
* **This is optional!** If you don't call this endpoints, the test database will be recreated in a FIFO manner (first in, first out) as soon as possible, even though it actually had no changes.
* This is useful if you are sure, you did not do any changes to the database and thus want to skip the recreation process by returning it to the pool directly.

* 1..n test runners end
* ...
* Subsequent 1..n test runners start/end in parallel and reuse the above logic

A really good starting point to write your own integresql-client for a specific language can be found [here (go code)](https://github.com/allaboutapps/integresql-client-go/blob/master/client.go) and [here (godoc)](https://pkg.go.dev/github.com/allaboutapps/integresql-client-go?tab=doc). It's just RESTful JSON after all.
```mermaid
stateDiagram-v2
### Integrate by client lib
HashPool --> TestDatabase: Task EXTEND
The flow above might look intimidating at first glance, but trust us, it's simple to integrate especially if there is already an client library available for your specific language. We currently have those:
state TestDatabase {
[*] --> ready: init
ready --> dirty: GetTestDatabase()
dirty --> ready: ReturnTestDatabase()
dirty --> recreating: RecreateTestDatabase()\nTask CLEAN_DIRTY
recreating --> ready: generation++
recreating --> recreating: retry (still in use)
}
```

* Go: [integresql-client-go](https://github.com/allaboutapps/integresql-client-go) by [Nick Müller - @MorpheusXAUT](https://github.com/MorpheusXAUT)
* Python: [integresql-client-python](https://github.com/msztolcman/integresql-client-python) by [Marcin Sztolcman - @msztolcman](https://github.com/msztolcman)
* .NET: [IntegreSQL.EF](https://github.com/mcctomsk/IntegreSql.EF) by [Artur Drobinskiy - @Shaddix](https://github.com/Shaddix)
* JavaScript/TypeScript: [@devoxa/integresql-client](https://github.com/devoxa/integresql-client) by [Devoxa - @devoxa](https://github.com/devoxa)
* ... *Add your link here and make a PR*
### Pool structure

The following describes the relationship between the components of IntegreSQL.

```mermaid
erDiagram
Server ||--o| Manager : owns
Manager {
Template[] templateCollection
HashPool[] poolCollection
}
Manager ||--o{ HashPool : has
Manager ||--o{ Template : has
Template {
TemplateDatabase database
}
HashPool {
TestDatabase database
}
HashPool ||--o{ TestDatabase : "manages"
Template ||--|| TemplateDatabase : "sets"
TestDatabase {
int ID
Database database
}
TemplateDatabase {
Database database
}
Database {
string TemplateHash
Config DatabaseConfig
}
TestDatabase o|--|| Database : "is"
TemplateDatabase o|--|| Database : "is"
```

#### Demo

If you want to take a look on how we integrate IntegreSQL - 🤭 - please just try our [go-starter](https://github.com/allaboutapps/go-starter) project or take a look at our [test_database setup code](https://github.com/allaboutapps/go-starter/blob/master/internal/test/test_database.go).

## Background

Expand Down
Loading

0 comments on commit e71d581

Please sign in to comment.