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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: postgresql-testing
3
- Version: 0.0.1
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 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.
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).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "postgresql-testing"
3
- version = "0.0.1"
3
+ version = "0.0.3"
4
4
  description = "PostgreSQL testing helpers"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -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/.postgresql_testing")
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
- process.wait()
124
-
125
-
126
- @contextmanager
127
- def create_template(
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
- directory: Path,
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
- archive.unlink()
194
+ if on_existing == "replace":
195
+ archive.unlink()
198
196
 
199
- with tarfile.open(archive, "w") as tar:
200
- tar.add(directory, arcname=directory.name)
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
- directory: Path,
210
+ c: DatabaseConfig,
206
211
  *,
207
212
  on_existing: Literal["raise", "replace"] = "replace",
208
213
  ) -> None:
209
- """Dump the entirety of a Postgres cluster from a tar archive."""
210
- if directory.exists():
211
- if on_existing == "raise":
212
- raise RuntimeError(f"Directory {directory} already exists")
213
- shutil.rmtree(directory)
214
-
215
- with tarfile.open(archive) as tar:
216
- tar.extractall(directory.parent)
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.