db-test-helpers-postgres 0.1.0__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.
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .venv/
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+ .mypy_cache/
13
+ .env.test
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: db-test-helpers-postgres
3
+ Version: 0.1.0
4
+ Summary: PostgreSQL adapter for db-test-helpers
5
+ Project-URL: Homepage, https://github.com/takaaa220/db-test-helpers
6
+ Project-URL: Repository, https://github.com/takaaa220/db-test-helpers
7
+ Project-URL: Issues, https://github.com/takaaa220/db-test-helpers/issues
8
+ Author: takaaa220
9
+ License-Expression: MIT
10
+ Keywords: database,postgresql,test-helpers,testing
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: db-test-helpers<0.2.0,>=0.1.0
19
+ Requires-Dist: psycopg[binary]>=3.1
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=8.0; extra == 'dev'
22
+ Requires-Dist: ruff>=0.8; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # db-test-helpers-postgres
26
+
27
+ PostgreSQL adapter for [db-test-helpers](https://github.com/takaaa220/db-test-helpers).
28
+
29
+ ## Installation
30
+
31
+ ```bash
32
+ pip install db-test-helpers-postgres
33
+ ```
34
+
35
+ ## Documentation
36
+
37
+ See the [main repository](https://github.com/takaaa220/db-test-helpers) for usage and documentation.
@@ -0,0 +1,13 @@
1
+ # db-test-helpers-postgres
2
+
3
+ PostgreSQL adapter for [db-test-helpers](https://github.com/takaaa220/db-test-helpers).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install db-test-helpers-postgres
9
+ ```
10
+
11
+ ## Documentation
12
+
13
+ See the [main repository](https://github.com/takaaa220/db-test-helpers) for usage and documentation.
@@ -0,0 +1,43 @@
1
+ [project]
2
+ name = "db-test-helpers-postgres"
3
+ version = "0.1.0"
4
+ description = "PostgreSQL adapter for db-test-helpers"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ authors = [{ name = "takaaa220" }]
8
+ keywords = ["testing", "database", "postgresql", "test-helpers"]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Topic :: Software Development :: Testing",
16
+ ]
17
+ requires-python = ">=3.12"
18
+ dependencies = [
19
+ "db-test-helpers>=0.1.0,<0.2.0",
20
+ "psycopg[binary]>=3.1",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/takaaa220/db-test-helpers"
25
+ Repository = "https://github.com/takaaa220/db-test-helpers"
26
+ Issues = "https://github.com/takaaa220/db-test-helpers/issues"
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest>=8.0",
31
+ "ruff>=0.8",
32
+ ]
33
+
34
+ [tool.uv.sources]
35
+ db-test-helpers = { workspace = true }
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ only-include = ["src/db_test_helpers/postgres"]
43
+ sources = ["src"]
@@ -0,0 +1,5 @@
1
+ """db-test-helpers-postgres: PostgreSQL adapter for db-test-helpers."""
2
+
3
+ from db_test_helpers.postgres._adapter import PostgresAdapter
4
+
5
+ __all__ = ["PostgresAdapter"]
@@ -0,0 +1,57 @@
1
+ """PostgreSQL adapter for db-test-helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ import psycopg
8
+ from psycopg import sql
9
+ from psycopg.rows import dict_row
10
+
11
+ from db_test_helpers.core._errors import UnsupportedError
12
+
13
+
14
+ def _check_parent_path(parent_path: str) -> None:
15
+ if parent_path:
16
+ raise UnsupportedError("PostgresAdapter does not support parent_path")
17
+
18
+
19
+ class PostgresAdapter:
20
+ """DatabaseAdapter for PostgreSQL via psycopg.
21
+
22
+ Args:
23
+ conn: An open ``psycopg.Connection``.
24
+ """
25
+
26
+ def __init__(self, conn: psycopg.Connection) -> None:
27
+ self._conn = conn
28
+
29
+ def clear_group(self, group: str, parent_path: str) -> None:
30
+ _check_parent_path(parent_path)
31
+ self._conn.execute(sql.SQL("DELETE FROM {}").format(sql.Identifier(group)))
32
+
33
+ def write_record(self, group: str, parent_path: str, data: dict[str, Any]) -> dict[str, Any]:
34
+ _check_parent_path(parent_path)
35
+ if "__document_id__" in data:
36
+ raise UnsupportedError("PostgresAdapter does not support __document_id__")
37
+ with self._conn.cursor(row_factory=dict_row) as cur:
38
+ if not data:
39
+ cur.execute(sql.SQL("INSERT INTO {} DEFAULT VALUES RETURNING *").format(sql.Identifier(group)))
40
+ else:
41
+ columns = list(data.keys())
42
+ values = list(data.values())
43
+ query = sql.SQL("INSERT INTO {} ({}) VALUES ({}) RETURNING *").format(
44
+ sql.Identifier(group),
45
+ sql.SQL(", ").join(sql.Identifier(c) for c in columns),
46
+ sql.SQL(", ").join(sql.Placeholder() * len(values)),
47
+ )
48
+ cur.execute(query, values)
49
+ row = cur.fetchone()
50
+ assert row is not None
51
+ return row
52
+
53
+ def read_records(self, group: str, parent_path: str) -> list[dict[str, Any]]:
54
+ _check_parent_path(parent_path)
55
+ with self._conn.cursor(row_factory=dict_row) as cur:
56
+ cur.execute(sql.SQL("SELECT * FROM {}").format(sql.Identifier(group)))
57
+ return cur.fetchall()
@@ -0,0 +1,129 @@
1
+ """Tests for PostgresAdapter."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import psycopg
6
+ import pytest
7
+
8
+ from db_test_helpers.core._errors import UnsupportedError
9
+ from db_test_helpers.postgres._adapter import PostgresAdapter
10
+
11
+
12
+ def _create_table(conn: psycopg.Connection, ddl: str) -> None:
13
+ conn.execute(ddl)
14
+
15
+
16
+ def _insert(conn: psycopg.Connection, table: str, data: dict) -> None:
17
+ columns = list(data.keys())
18
+ values = list(data.values())
19
+ query = psycopg.sql.SQL("INSERT INTO {} ({}) VALUES ({})").format(
20
+ psycopg.sql.Identifier(table),
21
+ psycopg.sql.SQL(", ").join(psycopg.sql.Identifier(c) for c in columns),
22
+ psycopg.sql.SQL(", ").join(psycopg.sql.Placeholder() * len(values)),
23
+ )
24
+ conn.execute(query, values)
25
+
26
+
27
+ class TestDocumentIdGuard:
28
+ def test_rejects_document_id(self):
29
+ from unittest.mock import MagicMock
30
+
31
+ adapter = PostgresAdapter(MagicMock())
32
+ with pytest.raises(UnsupportedError, match="PostgresAdapter does not support __document_id__"):
33
+ adapter.write_record("t", "", {"__document_id__": "abc", "name": "Alice"})
34
+
35
+
36
+ class TestParentPathGuard:
37
+ def test_clear_group_rejects_non_empty_parent_path(self):
38
+ from unittest.mock import MagicMock
39
+
40
+ adapter = PostgresAdapter(MagicMock())
41
+ with pytest.raises(UnsupportedError, match="does not support parent_path"):
42
+ adapter.clear_group("t", "some/path")
43
+
44
+ def test_write_record_rejects_non_empty_parent_path(self):
45
+ from unittest.mock import MagicMock
46
+
47
+ adapter = PostgresAdapter(MagicMock())
48
+ with pytest.raises(UnsupportedError, match="does not support parent_path"):
49
+ adapter.write_record("t", "some/path", {"x": 1})
50
+
51
+ def test_read_records_rejects_non_empty_parent_path(self):
52
+ from unittest.mock import MagicMock
53
+
54
+ adapter = PostgresAdapter(MagicMock())
55
+ with pytest.raises(UnsupportedError, match="does not support parent_path"):
56
+ adapter.read_records("t", "some/path")
57
+
58
+
59
+ class TestClearGroup:
60
+ def test_clear_deletes_all_rows(self, conn):
61
+ _create_table(conn, "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
62
+ _insert(conn, "users", {"name": "Alice"})
63
+ _insert(conn, "users", {"name": "Bob"})
64
+
65
+ adapter = PostgresAdapter(conn)
66
+ adapter.clear_group("users", "")
67
+
68
+ rows = conn.execute("SELECT * FROM users").fetchall()
69
+ assert rows == []
70
+
71
+ def test_clear_empty_table(self, conn):
72
+ _create_table(conn, "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
73
+
74
+ adapter = PostgresAdapter(conn)
75
+ adapter.clear_group("users", "")
76
+
77
+ rows = conn.execute("SELECT * FROM users").fetchall()
78
+ assert rows == []
79
+
80
+
81
+ class TestWriteRecord:
82
+ def test_write_inserts_row(self, conn):
83
+ _create_table(conn, "CREATE TABLE users (name TEXT, age INTEGER)")
84
+
85
+ adapter = PostgresAdapter(conn)
86
+ result = adapter.write_record("users", "", {"name": "Alice", "age": 30})
87
+
88
+ assert result["name"] == "Alice"
89
+ assert result["age"] == 30
90
+
91
+ def test_write_returns_db_defaults(self, conn):
92
+ _create_table(conn, "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
93
+
94
+ adapter = PostgresAdapter(conn)
95
+ result = adapter.write_record("users", "", {"name": "Alice"})
96
+
97
+ assert result["id"] is not None
98
+ assert result["name"] == "Alice"
99
+
100
+ def test_write_empty_data_uses_default_values(self, conn):
101
+ _create_table(conn, "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT DEFAULT 'anonymous')")
102
+
103
+ adapter = PostgresAdapter(conn)
104
+ result = adapter.write_record("users", "", {})
105
+
106
+ assert result["id"] is not None
107
+ assert result["name"] == "anonymous"
108
+
109
+
110
+ class TestReadRecords:
111
+ def test_read_empty_table(self, conn):
112
+ _create_table(conn, "CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)")
113
+
114
+ adapter = PostgresAdapter(conn)
115
+ result = adapter.read_records("users", "")
116
+
117
+ assert result == []
118
+
119
+ def test_read_multiple_records(self, conn):
120
+ _create_table(conn, "CREATE TABLE users (name TEXT, age INTEGER)")
121
+ _insert(conn, "users", {"name": "Alice", "age": 30})
122
+ _insert(conn, "users", {"name": "Bob", "age": 25})
123
+
124
+ adapter = PostgresAdapter(conn)
125
+ result = adapter.read_records("users", "")
126
+
127
+ assert len(result) == 2
128
+ names = {r["name"] for r in result}
129
+ assert names == {"Alice", "Bob"}
@@ -0,0 +1,162 @@
1
+ """E2E tests: set_db_data + verify_db_data with PostgreSQL."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from db_test_helpers.core import Config, UnsupportedError, set_db_data, verify_db_data
8
+ from db_test_helpers.postgres import PostgresAdapter
9
+
10
+
11
+ @pytest.fixture
12
+ def _users_table(conn):
13
+ conn.execute("CREATE TABLE users (name TEXT, age INTEGER, active BOOLEAN)")
14
+ yield
15
+ conn.execute("DROP TABLE IF EXISTS users")
16
+
17
+
18
+ class TestE2eBasic:
19
+ @pytest.mark.usefixtures("_users_table")
20
+ def test_set_and_verify_order_independent(self, conn, tmp_path):
21
+ seed = tmp_path / "seed.yaml"
22
+ seed.write_text(
23
+ """\
24
+ users:
25
+ - name: "Alice"
26
+ age: 30
27
+ active: true
28
+ - name: "Bob"
29
+ age: 25
30
+ active: false
31
+ """
32
+ )
33
+ expected = tmp_path / "expected.yaml"
34
+ expected.write_text(
35
+ """\
36
+ users:
37
+ - name: "Bob"
38
+ age: 25
39
+ active: false
40
+ - name: "Alice"
41
+ age: 30
42
+ active: true
43
+ """
44
+ )
45
+ adapter = PostgresAdapter(conn)
46
+ set_db_data(adapter, str(seed))
47
+ verify_db_data(adapter, str(expected))
48
+
49
+ @pytest.mark.usefixtures("_users_table")
50
+ def test_set_replaces_existing(self, conn, tmp_path):
51
+ conn.execute("INSERT INTO users (name, age, active) VALUES ('OldUser', 99, true)")
52
+
53
+ seed = tmp_path / "seed.yaml"
54
+ seed.write_text(
55
+ """\
56
+ users:
57
+ - name: "Alice"
58
+ age: 30
59
+ active: true
60
+ """
61
+ )
62
+ adapter = PostgresAdapter(conn)
63
+ set_db_data(adapter, str(seed))
64
+
65
+ rows = conn.execute("SELECT * FROM users").fetchall()
66
+ assert len(rows) == 1
67
+
68
+
69
+ class TestE2eChildren:
70
+ @pytest.mark.usefixtures("_users_table")
71
+ def test_children_raises_not_implemented(self, conn, tmp_path):
72
+ seed = tmp_path / "seed.yaml"
73
+ seed.write_text(
74
+ """\
75
+ users:
76
+ - name: "Alice"
77
+ age: 30
78
+ active: true
79
+ __children__:
80
+ posts:
81
+ - title: "Hello"
82
+ """
83
+ )
84
+ adapter = PostgresAdapter(conn)
85
+ with pytest.raises(UnsupportedError, match="does not support __children__"):
86
+ set_db_data(adapter, str(seed))
87
+
88
+
89
+ class TestE2eDsl:
90
+ @pytest.mark.usefixtures("_users_table")
91
+ def test_var_and_any(self, conn, tmp_path):
92
+ seed = tmp_path / "seed.yaml"
93
+ seed.write_text(
94
+ """\
95
+ users:
96
+ - name: "$var(user_name)"
97
+ age: 30
98
+ active: true
99
+ """
100
+ )
101
+ expected = tmp_path / "expected.yaml"
102
+ expected.write_text(
103
+ """\
104
+ users:
105
+ - name: "$var(user_name)"
106
+ age: "$any()"
107
+ active: true
108
+ """
109
+ )
110
+ adapter = PostgresAdapter(conn)
111
+ config = Config(variables={"user_name": "Alice"})
112
+ set_db_data(adapter, str(seed), config)
113
+ verify_db_data(adapter, str(expected), config)
114
+
115
+ @pytest.mark.usefixtures("_users_table")
116
+ def test_matchers_in_verify(self, conn, tmp_path):
117
+ seed = tmp_path / "seed.yaml"
118
+ seed.write_text(
119
+ """\
120
+ users:
121
+ - name: "Alice"
122
+ age: 30
123
+ active: true
124
+ """
125
+ )
126
+ expected = tmp_path / "expected.yaml"
127
+ expected.write_text(
128
+ """\
129
+ users:
130
+ - name: "$eq(Alice)"
131
+ age: "$and($gt(0),$lt(100))"
132
+ active: true
133
+ """
134
+ )
135
+ adapter = PostgresAdapter(conn)
136
+ set_db_data(adapter, str(seed))
137
+ verify_db_data(adapter, str(expected))
138
+
139
+ @pytest.mark.usefixtures("_users_table")
140
+ def test_verify_failure(self, conn, tmp_path):
141
+ seed = tmp_path / "seed.yaml"
142
+ seed.write_text(
143
+ """\
144
+ users:
145
+ - name: "Alice"
146
+ age: 30
147
+ active: true
148
+ """
149
+ )
150
+ expected = tmp_path / "expected.yaml"
151
+ expected.write_text(
152
+ """\
153
+ users:
154
+ - name: "Bob"
155
+ age: 30
156
+ active: true
157
+ """
158
+ )
159
+ adapter = PostgresAdapter(conn)
160
+ set_db_data(adapter, str(seed))
161
+ with pytest.raises(AssertionError):
162
+ verify_db_data(adapter, str(expected))
@@ -0,0 +1,32 @@
1
+ """Shared fixtures for PostgreSQL adapter tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ import psycopg
8
+ import pytest
9
+
10
+
11
+ @pytest.fixture
12
+ def conn():
13
+ port = os.environ.get("POSTGRES_PORT", "5432")
14
+ connection = psycopg.connect(
15
+ host="localhost",
16
+ port=int(port),
17
+ user="test",
18
+ password="test",
19
+ dbname="testdb",
20
+ autocommit=True,
21
+ )
22
+ yield connection
23
+ _drop_all_tables(connection)
24
+ connection.close()
25
+
26
+
27
+ def _drop_all_tables(conn: psycopg.Connection) -> None:
28
+ rows = conn.execute(
29
+ "SELECT tablename FROM pg_tables WHERE schemaname = 'public'"
30
+ ).fetchall()
31
+ for (table,) in rows:
32
+ conn.execute(psycopg.sql.SQL("DROP TABLE {} CASCADE").format(psycopg.sql.Identifier(table)))