postgresql-testing 0.0.1__tar.gz → 0.0.2__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.2
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.2"
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 = [
@@ -123,28 +120,6 @@ def serve(c: ClusterConfig) -> Iterator[None]:
123
120
  process.wait()
124
121
 
125
122
 
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}'")
146
-
147
-
148
123
  def ensure_user(c: DatabaseConfig) -> None:
149
124
  with psycopg.connect(c.superuser().dsn) as conn, conn.cursor() as cur:
150
125
  try:
@@ -159,11 +134,6 @@ def create_database(
159
134
  template: str | None = None,
160
135
  on_existing: Literal["raise", "replace"] = "replace",
161
136
  ) -> 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
137
  template_str = "" if template is None else f'WITH TEMPLATE "{template}"'
168
138
  sql_create_database = f'CREATE DATABASE "{c.database}" {template_str} OWNER "{c.user}" STRATEGY=FILE_COPY'
169
139
 
@@ -179,41 +149,71 @@ def create_database(
179
149
  cur.execute(sql_create_database)
180
150
 
181
151
 
152
+ # More experimental functions
153
+
154
+
155
+ @contextmanager
156
+ def create_template(
157
+ c: DatabaseConfig,
158
+ *,
159
+ template: str,
160
+ on_existing: Literal["raise", "replace"] = "replace",
161
+ ) -> Iterator[DatabaseConfig]:
162
+ with psycopg.connect(c.superuser().dsn) as conn, conn.cursor() as cur:
163
+ conn.autocommit = True
164
+ template_exists = bool(cur.execute(f"SELECT 1 FROM pg_database WHERE datname = '{template}'").fetchall())
165
+ if template_exists:
166
+ if on_existing == "raise":
167
+ raise RuntimeError(f"Template database {template} already exists")
168
+ if on_existing == "replace":
169
+ cur.execute(f"UPDATE pg_database SET datistemplate = false WHERE datname='{template}'")
170
+ cur.execute(f'DROP DATABASE "{template}"')
171
+
172
+ cur.execute(f'CREATE DATABASE "{template}" OWNER "{c.user}"')
173
+ yield replace(c, database=template)
174
+ cur.execute(f"UPDATE pg_database SET datistemplate = true WHERE datname='{template}'")
175
+
176
+
182
177
  def dump_archive(
183
- directory: Path,
178
+ c: DatabaseConfig,
184
179
  archive: Path,
185
180
  *,
186
181
  on_existing: Literal["raise", "replace"] = "replace",
187
182
  ) -> 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
183
  if archive.exists():
195
184
  if on_existing == "raise":
196
185
  raise RuntimeError(f"Archive {archive} already exists")
197
- archive.unlink()
186
+ if on_existing == "replace":
187
+ archive.unlink()
198
188
 
199
- with tarfile.open(archive, "w") as tar:
200
- tar.add(directory, arcname=directory.name)
189
+ cmd: list[str] = [
190
+ str(postgresql_binaries.bin() / "pg_dump"),
191
+ c.dsn,
192
+ *("--file", str(archive)),
193
+ *("--format", "t"),
194
+ *("--compress", "0"),
195
+ "--no-sync",
196
+ ]
197
+ subprocess.check_call(cmd)
201
198
 
202
199
 
203
200
  def load_archive(
204
201
  archive: Path,
205
- directory: Path,
202
+ c: DatabaseConfig,
206
203
  *,
207
204
  on_existing: Literal["raise", "replace"] = "replace",
208
205
  ) -> 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)
206
+ cmd: list[str] = [
207
+ str(postgresql_binaries.bin() / "pg_restore"),
208
+ *("--host", c.host),
209
+ *("--port", str(c.port)),
210
+ *("--username", c.user),
211
+ *(("--password", c.password) if c.password else ()),
212
+ *("--dbname", c.database),
213
+ *(("--clean",) if on_existing == "replace" else ()),
214
+ str(archive),
215
+ ]
216
+ subprocess.check_call(cmd)
217
217
 
218
218
 
219
219
  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.