postgresql-testing 0.0.1__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {postgresql_testing-0.0.1 → postgresql_testing-0.0.3}/PKG-INFO +19 -3
- postgresql_testing-0.0.3/README.md +44 -0
- {postgresql_testing-0.0.1 → postgresql_testing-0.0.3}/pyproject.toml +1 -1
- {postgresql_testing-0.0.1 → postgresql_testing-0.0.3}/src/postgresql_testing/__init__.py +59 -51
- postgresql_testing-0.0.1/README.md +0 -28
- {postgresql_testing-0.0.1 → postgresql_testing-0.0.3}/src/postgresql_testing/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: postgresql-testing
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: PostgreSQL testing helpers
|
|
5
5
|
Author: Oli Russell
|
|
6
6
|
Author-email: Oli Russell <mrxoliver@gmail.com>
|
|
@@ -12,7 +12,7 @@ Description-Content-Type: text/markdown
|
|
|
12
12
|
|
|
13
13
|
# Postgresql testing
|
|
14
14
|
|
|
15
|
-
Simple Postgres helpers for testing with Python - no docker, brew, apt, etc - uses [postgresql-binaries](https://github.com/leontrolski/postgresql-binaries).
|
|
15
|
+
Simple Postgres helpers for testing with Python - no docker, brew, apt, etc - uses [postgresql-binaries](https://github.com/leontrolski/postgresql-binaries). The interface is simple, but close enough to the metal that you can use it to eg. have a new database per testing thread/use a fresh database from a `TEMPLATE` or archive.
|
|
16
16
|
|
|
17
17
|
```shell
|
|
18
18
|
pip install postgresql-testing 'postgresql-binaries==18.*'
|
|
@@ -37,4 +37,20 @@ There are various useful flags and things - the source code is short enough to j
|
|
|
37
37
|
|
|
38
38
|
<hr>
|
|
39
39
|
|
|
40
|
-
There are a couple of helpers for creating/using template dbs and
|
|
40
|
+
There are a couple of helpers for creating/using template dbs and archives. I have some vague long term plan for some kind of "Docker layers for migrated databases" with clever caching (or not), but I'm not quite sure what it looks like yet.
|
|
41
|
+
|
|
42
|
+
Some benchmarking:
|
|
43
|
+
|
|
44
|
+
| number of tables | create from template | dump to archive | create from archive |
|
|
45
|
+
|---|---|---|---|
|
|
46
|
+
| 100 | 80ms | 80ms (0.2MB) | 150ms |
|
|
47
|
+
| 1000 | 500ms | 300ms (2MB) | 1100ms |
|
|
48
|
+
|
|
49
|
+
On macos using a ramdisk, it is slightly quicker:
|
|
50
|
+
|
|
51
|
+
| number of tables | create from template | dump to archive | create from archive |
|
|
52
|
+
|---|---|---|---|
|
|
53
|
+
| 100 | 70ms | 70ms | 120ms |
|
|
54
|
+
| 1000 | 350ms | 300ms | 1000ms |
|
|
55
|
+
|
|
56
|
+
See claims/advice from planet [Go](https://github.com/peterldowns/pgtestdb).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Postgresql testing
|
|
2
|
+
|
|
3
|
+
Simple Postgres helpers for testing with Python - no docker, brew, apt, etc - uses [postgresql-binaries](https://github.com/leontrolski/postgresql-binaries). The interface is simple, but close enough to the metal that you can use it to eg. have a new database per testing thread/use a fresh database from a `TEMPLATE` or archive.
|
|
4
|
+
|
|
5
|
+
```shell
|
|
6
|
+
pip install postgresql-testing 'postgresql-binaries==18.*'
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Then to use, eg:
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import postgresql_testing
|
|
13
|
+
|
|
14
|
+
@pytest.fixture(scope="session")
|
|
15
|
+
def db() -> Iterator[str]:
|
|
16
|
+
config = postgresql_testing.DatabaseConfig.default("testing-db")
|
|
17
|
+
postgresql_testing.initdb(config.directory, on_existing="use")
|
|
18
|
+
with postgresql_testing.serve(config):
|
|
19
|
+
postgresql_testing.ensure_user(config)
|
|
20
|
+
postgresql_testing.create_database(config, on_existing="replace")
|
|
21
|
+
yield config.dsn
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
There are various useful flags and things - the source code is short enough to just dive in.
|
|
25
|
+
|
|
26
|
+
<hr>
|
|
27
|
+
|
|
28
|
+
There are a couple of helpers for creating/using template dbs and archives. I have some vague long term plan for some kind of "Docker layers for migrated databases" with clever caching (or not), but I'm not quite sure what it looks like yet.
|
|
29
|
+
|
|
30
|
+
Some benchmarking:
|
|
31
|
+
|
|
32
|
+
| number of tables | create from template | dump to archive | create from archive |
|
|
33
|
+
|---|---|---|---|
|
|
34
|
+
| 100 | 80ms | 80ms (0.2MB) | 150ms |
|
|
35
|
+
| 1000 | 500ms | 300ms (2MB) | 1100ms |
|
|
36
|
+
|
|
37
|
+
On macos using a ramdisk, it is slightly quicker:
|
|
38
|
+
|
|
39
|
+
| number of tables | create from template | dump to archive | create from archive |
|
|
40
|
+
|---|---|---|---|
|
|
41
|
+
| 100 | 70ms | 70ms | 120ms |
|
|
42
|
+
| 1000 | 350ms | 300ms | 1000ms |
|
|
43
|
+
|
|
44
|
+
See claims/advice from planet [Go](https://github.com/peterldowns/pgtestdb).
|
|
@@ -8,9 +8,8 @@ import subprocess
|
|
|
8
8
|
from typing import Iterator, Literal, Self
|
|
9
9
|
import postgresql_binaries
|
|
10
10
|
import psycopg
|
|
11
|
-
import tarfile
|
|
12
11
|
|
|
13
|
-
DEFAULT_DIR = Path("/tmp
|
|
12
|
+
DEFAULT_DIR = Path("/tmp/postgresql-testing")
|
|
14
13
|
SUPERUSER = "postgres"
|
|
15
14
|
ROOT_DATABASE = "postgres"
|
|
16
15
|
|
|
@@ -72,7 +71,6 @@ def initdb(
|
|
|
72
71
|
*,
|
|
73
72
|
on_existing: Literal["raise", "use", "replace"] = "raise",
|
|
74
73
|
) -> None:
|
|
75
|
-
"""Call `initdb` with some sensible arguments for testing."""
|
|
76
74
|
if directory.exists():
|
|
77
75
|
if on_existing == "raise":
|
|
78
76
|
raise RuntimeError(f"Directory {directory} already exists")
|
|
@@ -100,7 +98,6 @@ def initdb(
|
|
|
100
98
|
|
|
101
99
|
@contextmanager
|
|
102
100
|
def serve(c: ClusterConfig) -> Iterator[None]:
|
|
103
|
-
"""Call `postgres` with some sensible arguments."""
|
|
104
101
|
c.stderr.parent.mkdir(parents=True, exist_ok=True)
|
|
105
102
|
with open(c.stderr, "wb") as stderr_f:
|
|
106
103
|
cmd = [
|
|
@@ -119,30 +116,16 @@ def serve(c: ClusterConfig) -> Iterator[None]:
|
|
|
119
116
|
_try_connect(c.superuser().dsn)
|
|
120
117
|
yield
|
|
121
118
|
finally:
|
|
119
|
+
# Stop any ongoing queries
|
|
120
|
+
with psycopg.connect(c.superuser().dsn) as conn, conn.cursor() as cur:
|
|
121
|
+
cur.execute("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid()")
|
|
122
|
+
# Shutdown
|
|
122
123
|
process.terminate()
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
c: DatabaseConfig,
|
|
129
|
-
*,
|
|
130
|
-
template: str,
|
|
131
|
-
on_existing: Literal["raise", "replace"] = "replace",
|
|
132
|
-
) -> Iterator[DatabaseConfig]:
|
|
133
|
-
with psycopg.connect(c.superuser().dsn) as conn, conn.cursor() as cur:
|
|
134
|
-
conn.autocommit = True
|
|
135
|
-
template_exists = bool(cur.execute(f"SELECT 1 FROM pg_database WHERE datname = '{template}'").fetchall())
|
|
136
|
-
if template_exists:
|
|
137
|
-
if on_existing == "raise":
|
|
138
|
-
raise RuntimeError(f"Template database {template} already exists")
|
|
139
|
-
if on_existing == "replace":
|
|
140
|
-
cur.execute(f"UPDATE pg_database SET datistemplate = false WHERE datname='{template}'")
|
|
141
|
-
cur.execute(f'DROP DATABASE "{template}"')
|
|
142
|
-
|
|
143
|
-
cur.execute(f'CREATE DATABASE "{template}" OWNER "{c.user}"')
|
|
144
|
-
yield replace(c, database=template)
|
|
145
|
-
cur.execute(f"UPDATE pg_database SET datistemplate = true WHERE datname='{template}'")
|
|
124
|
+
try:
|
|
125
|
+
process.wait(timeout=1)
|
|
126
|
+
except subprocess.TimeoutExpired:
|
|
127
|
+
process.kill()
|
|
128
|
+
process.wait(timeout=5)
|
|
146
129
|
|
|
147
130
|
|
|
148
131
|
def ensure_user(c: DatabaseConfig) -> None:
|
|
@@ -159,11 +142,6 @@ def create_database(
|
|
|
159
142
|
template: str | None = None,
|
|
160
143
|
on_existing: Literal["raise", "replace"] = "replace",
|
|
161
144
|
) -> None:
|
|
162
|
-
"""Create a Postgres database.
|
|
163
|
-
|
|
164
|
-
- Optionally pass in a template.
|
|
165
|
-
- Takes in the tens of ms for a small db.
|
|
166
|
-
"""
|
|
167
145
|
template_str = "" if template is None else f'WITH TEMPLATE "{template}"'
|
|
168
146
|
sql_create_database = f'CREATE DATABASE "{c.database}" {template_str} OWNER "{c.user}" STRATEGY=FILE_COPY'
|
|
169
147
|
|
|
@@ -179,41 +157,71 @@ def create_database(
|
|
|
179
157
|
cur.execute(sql_create_database)
|
|
180
158
|
|
|
181
159
|
|
|
160
|
+
# More experimental functions
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@contextmanager
|
|
164
|
+
def create_template(
|
|
165
|
+
c: DatabaseConfig,
|
|
166
|
+
*,
|
|
167
|
+
template: str,
|
|
168
|
+
on_existing: Literal["raise", "replace"] = "replace",
|
|
169
|
+
) -> Iterator[DatabaseConfig]:
|
|
170
|
+
with psycopg.connect(c.superuser().dsn) as conn, conn.cursor() as cur:
|
|
171
|
+
conn.autocommit = True
|
|
172
|
+
template_exists = bool(cur.execute(f"SELECT 1 FROM pg_database WHERE datname = '{template}'").fetchall())
|
|
173
|
+
if template_exists:
|
|
174
|
+
if on_existing == "raise":
|
|
175
|
+
raise RuntimeError(f"Template database {template} already exists")
|
|
176
|
+
if on_existing == "replace":
|
|
177
|
+
cur.execute(f"UPDATE pg_database SET datistemplate = false WHERE datname='{template}'")
|
|
178
|
+
cur.execute(f'DROP DATABASE "{template}"')
|
|
179
|
+
|
|
180
|
+
cur.execute(f'CREATE DATABASE "{template}" OWNER "{c.user}"')
|
|
181
|
+
yield replace(c, database=template)
|
|
182
|
+
cur.execute(f"UPDATE pg_database SET datistemplate = true WHERE datname='{template}'")
|
|
183
|
+
|
|
184
|
+
|
|
182
185
|
def dump_archive(
|
|
183
|
-
|
|
186
|
+
c: DatabaseConfig,
|
|
184
187
|
archive: Path,
|
|
185
188
|
*,
|
|
186
189
|
on_existing: Literal["raise", "replace"] = "replace",
|
|
187
190
|
) -> None:
|
|
188
|
-
"""Dump the entirety of a Postgres cluster to a tar archive.
|
|
189
|
-
|
|
190
|
-
- Archives are not very portable (between system architectures or Postgres
|
|
191
|
-
versions).
|
|
192
|
-
- Takes in the hundreds of ms for a small db.
|
|
193
|
-
"""
|
|
194
191
|
if archive.exists():
|
|
195
192
|
if on_existing == "raise":
|
|
196
193
|
raise RuntimeError(f"Archive {archive} already exists")
|
|
197
|
-
|
|
194
|
+
if on_existing == "replace":
|
|
195
|
+
archive.unlink()
|
|
198
196
|
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
cmd: list[str] = [
|
|
198
|
+
str(postgresql_binaries.bin() / "pg_dump"),
|
|
199
|
+
c.dsn,
|
|
200
|
+
*("--file", str(archive)),
|
|
201
|
+
*("--format", "t"),
|
|
202
|
+
*("--compress", "0"),
|
|
203
|
+
"--no-sync",
|
|
204
|
+
]
|
|
205
|
+
subprocess.check_call(cmd)
|
|
201
206
|
|
|
202
207
|
|
|
203
208
|
def load_archive(
|
|
204
209
|
archive: Path,
|
|
205
|
-
|
|
210
|
+
c: DatabaseConfig,
|
|
206
211
|
*,
|
|
207
212
|
on_existing: Literal["raise", "replace"] = "replace",
|
|
208
213
|
) -> None:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
214
|
+
cmd: list[str] = [
|
|
215
|
+
str(postgresql_binaries.bin() / "pg_restore"),
|
|
216
|
+
*("--host", c.host),
|
|
217
|
+
*("--port", str(c.port)),
|
|
218
|
+
*("--username", c.user),
|
|
219
|
+
*(("--password", c.password) if c.password else ()),
|
|
220
|
+
*("--dbname", c.database),
|
|
221
|
+
*(("--clean",) if on_existing == "replace" else ()),
|
|
222
|
+
str(archive),
|
|
223
|
+
]
|
|
224
|
+
subprocess.check_call(cmd)
|
|
217
225
|
|
|
218
226
|
|
|
219
227
|
def _try_connect(dsn: str) -> None:
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# Postgresql testing
|
|
2
|
-
|
|
3
|
-
Simple Postgres helpers for testing with Python - no docker, brew, apt, etc - uses [postgresql-binaries](https://github.com/leontrolski/postgresql-binaries).
|
|
4
|
-
|
|
5
|
-
```shell
|
|
6
|
-
pip install postgresql-testing 'postgresql-binaries==18.*'
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
Then to use, eg:
|
|
10
|
-
|
|
11
|
-
```python
|
|
12
|
-
import postgresql_testing
|
|
13
|
-
|
|
14
|
-
@pytest.fixture(scope="session")
|
|
15
|
-
def db() -> Iterator[str]:
|
|
16
|
-
config = postgresql_testing.DatabaseConfig.default("testing-db")
|
|
17
|
-
postgresql_testing.initdb(config.directory, on_existing="use")
|
|
18
|
-
with postgresql_testing.serve(config):
|
|
19
|
-
postgresql_testing.ensure_user(config)
|
|
20
|
-
postgresql_testing.create_database(config, on_existing="replace")
|
|
21
|
-
yield config.dsn
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
There are various useful flags and things - the source code is short enough to just dive in.
|
|
25
|
-
|
|
26
|
-
<hr>
|
|
27
|
-
|
|
28
|
-
There are a couple of helpers for creating/using template dbs and tar files. I have some vague long term plan for some kind of "Docker layers for migrated databases" with clever caching, but I'm not quite sure what it looks like yet.
|
|
File without changes
|