db-test-helpers-sqlite 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,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: db-test-helpers-sqlite
3
+ Version: 0.1.0
4
+ Summary: SQLite 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,sqlite,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
+ Provides-Extra: dev
20
+ Requires-Dist: pytest>=8.0; extra == 'dev'
21
+ Requires-Dist: ruff>=0.8; extra == 'dev'
22
+ Description-Content-Type: text/markdown
23
+
24
+ # db-test-helpers-sqlite
25
+
26
+ SQLite adapter for [db-test-helpers](https://github.com/takaaa220/db-test-helpers).
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install db-test-helpers-sqlite
32
+ ```
33
+
34
+ ## Documentation
35
+
36
+ See the [main repository](https://github.com/takaaa220/db-test-helpers) for usage and documentation.
@@ -0,0 +1,13 @@
1
+ # db-test-helpers-sqlite
2
+
3
+ SQLite adapter for [db-test-helpers](https://github.com/takaaa220/db-test-helpers).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pip install db-test-helpers-sqlite
9
+ ```
10
+
11
+ ## Documentation
12
+
13
+ See the [main repository](https://github.com/takaaa220/db-test-helpers) for usage and documentation.
@@ -0,0 +1,42 @@
1
+ [project]
2
+ name = "db-test-helpers-sqlite"
3
+ version = "0.1.0"
4
+ description = "SQLite adapter for db-test-helpers"
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ authors = [{ name = "takaaa220" }]
8
+ keywords = ["testing", "database", "sqlite", "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
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/takaaa220/db-test-helpers"
24
+ Repository = "https://github.com/takaaa220/db-test-helpers"
25
+ Issues = "https://github.com/takaaa220/db-test-helpers/issues"
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=8.0",
30
+ "ruff>=0.8",
31
+ ]
32
+
33
+ [tool.uv.sources]
34
+ db-test-helpers = { workspace = true }
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [tool.hatch.build.targets.wheel]
41
+ only-include = ["src/db_test_helpers/sqlite"]
42
+ sources = ["src"]
@@ -0,0 +1,3 @@
1
+ from db_test_helpers.sqlite._adapter import SqliteAdapter
2
+
3
+ __all__ = ["SqliteAdapter"]
@@ -0,0 +1,56 @@
1
+ """SQLite adapter for db-test-helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ from typing import Any
8
+
9
+ from db_test_helpers.core._errors import UnsupportedError
10
+
11
+
12
+ def _quote_identifier(name: str) -> str:
13
+ if not name:
14
+ raise ValueError("identifier must not be empty")
15
+ return '"' + name.replace('"', '""') + '"'
16
+
17
+
18
+ def _check_parent_path(parent_path: str) -> None:
19
+ if parent_path:
20
+ raise UnsupportedError("SqliteAdapter does not support parent_path")
21
+
22
+
23
+ class SqliteAdapter:
24
+ """DatabaseAdapter for SQLite via the standard library sqlite3 module.
25
+
26
+ Args:
27
+ conn: An open ``sqlite3.Connection``.
28
+ """
29
+
30
+ def __init__(self, conn: sqlite3.Connection) -> None:
31
+ self._conn = conn
32
+
33
+ def clear_group(self, group: str, parent_path: str) -> None:
34
+ _check_parent_path(parent_path)
35
+ self._conn.execute(f"DROP TABLE IF EXISTS {_quote_identifier(group)}")
36
+ self._conn.commit()
37
+
38
+ def write_record(self, group: str, parent_path: str, data: dict[str, Any]) -> dict[str, Any]:
39
+ _check_parent_path(parent_path)
40
+ if "__document_id__" in data:
41
+ raise UnsupportedError("SqliteAdapter does not support __document_id__")
42
+ table = _quote_identifier(group)
43
+ self._conn.execute(f"CREATE TABLE IF NOT EXISTS {table} (_data TEXT NOT NULL)")
44
+ self._conn.execute(f"INSERT INTO {table} (_data) VALUES (?)", (json.dumps(data),))
45
+ self._conn.commit()
46
+ return dict(data)
47
+
48
+ def read_records(self, group: str, parent_path: str) -> list[dict[str, Any]]:
49
+ _check_parent_path(parent_path)
50
+ try:
51
+ cursor = self._conn.execute(f"SELECT _data FROM {_quote_identifier(group)}")
52
+ except sqlite3.OperationalError as e:
53
+ if "no such table" in str(e):
54
+ return []
55
+ raise
56
+ return [json.loads(row[0]) for row in cursor.fetchall()]
@@ -0,0 +1,161 @@
1
+ import sqlite3
2
+
3
+ import pytest
4
+
5
+ from db_test_helpers.core._errors import UnsupportedError
6
+ from db_test_helpers.sqlite import SqliteAdapter
7
+ from db_test_helpers.sqlite._adapter import _quote_identifier
8
+
9
+
10
+ class TestQuoteIdentifier:
11
+ def test_normal_identifier(self):
12
+ assert _quote_identifier("users") == '"users"'
13
+
14
+ def test_identifier_with_double_quote(self):
15
+ assert _quote_identifier('my"table') == '"my""table"'
16
+
17
+ def test_empty_string_raises(self):
18
+ with pytest.raises(ValueError):
19
+ _quote_identifier("")
20
+
21
+
22
+ class TestDocumentIdGuard:
23
+ def test_rejects_document_id(self, conn: sqlite3.Connection):
24
+ adapter = SqliteAdapter(conn)
25
+ with pytest.raises(UnsupportedError, match="SqliteAdapter does not support __document_id__"):
26
+ adapter.write_record("t", "", {"__document_id__": "abc", "name": "Alice"})
27
+
28
+
29
+ class TestParentPathGuard:
30
+ def test_clear_group_rejects_non_empty_parent_path(self, conn: sqlite3.Connection):
31
+ adapter = SqliteAdapter(conn)
32
+ with pytest.raises(UnsupportedError):
33
+ adapter.clear_group("t", "some/path")
34
+
35
+ def test_write_record_rejects_non_empty_parent_path(self, conn: sqlite3.Connection):
36
+ adapter = SqliteAdapter(conn)
37
+ with pytest.raises(UnsupportedError):
38
+ adapter.write_record("t", "some/path", {"x": 1})
39
+
40
+ def test_read_records_rejects_non_empty_parent_path(self, conn: sqlite3.Connection):
41
+ adapter = SqliteAdapter(conn)
42
+ with pytest.raises(UnsupportedError):
43
+ adapter.read_records("t", "some/path")
44
+
45
+
46
+ class TestWriteRecord:
47
+ def test_basic(self, conn: sqlite3.Connection):
48
+ adapter = SqliteAdapter(conn)
49
+ result = adapter.write_record("users", "", {"name": "Alice", "age": 30})
50
+ assert result == {"name": "Alice", "age": 30}
51
+
52
+ def test_multiple_fields(self, conn: sqlite3.Connection):
53
+ adapter = SqliteAdapter(conn)
54
+ data = {"a": 1, "b": "two", "c": 3.0, "d": True, "e": None}
55
+ result = adapter.write_record("t", "", data)
56
+ assert result == {"a": 1, "b": "two", "c": 3.0, "d": True, "e": None}
57
+
58
+ def test_none_value(self, conn: sqlite3.Connection):
59
+ adapter = SqliteAdapter(conn)
60
+ result = adapter.write_record("t", "", {"x": None})
61
+ assert result == {"x": None}
62
+
63
+ def test_nested_dict_and_list(self, conn: sqlite3.Connection):
64
+ adapter = SqliteAdapter(conn)
65
+ data = {"nested": {"a": 1}, "items": [1, "two", True]}
66
+ result = adapter.write_record("t", "", data)
67
+ assert result == {"nested": {"a": 1}, "items": [1, "two", True]}
68
+
69
+ def test_bool_preserved(self, conn: sqlite3.Connection):
70
+ adapter = SqliteAdapter(conn)
71
+ adapter.write_record("t", "", {"flag": True, "other": False})
72
+ records = adapter.read_records("t", "")
73
+ assert records[0]["flag"] is True
74
+ assert records[0]["other"] is False
75
+
76
+ def test_table_name_with_double_quote(self, conn: sqlite3.Connection):
77
+ adapter = SqliteAdapter(conn)
78
+ adapter.write_record('my"table', "", {"x": 1})
79
+ records = adapter.read_records('my"table', "")
80
+ assert records == [{"x": 1}]
81
+
82
+ def test_table_name_with_sql_keyword(self, conn: sqlite3.Connection):
83
+ adapter = SqliteAdapter(conn)
84
+ adapter.write_record("select", "", {"x": 1})
85
+ records = adapter.read_records("select", "")
86
+ assert records == [{"x": 1}]
87
+
88
+ def test_does_not_mutate_input(self, conn: sqlite3.Connection):
89
+ adapter = SqliteAdapter(conn)
90
+ data = {"name": "Alice"}
91
+ adapter.write_record("t", "", data)
92
+ assert data == {"name": "Alice"}
93
+
94
+
95
+ class TestReadRecords:
96
+ def test_empty_table(self, conn: sqlite3.Connection):
97
+ adapter = SqliteAdapter(conn)
98
+ conn.execute('CREATE TABLE "t" (_data TEXT NOT NULL)')
99
+ assert adapter.read_records("t", "") == []
100
+
101
+ def test_multiple_records(self, conn: sqlite3.Connection):
102
+ adapter = SqliteAdapter(conn)
103
+ adapter.write_record("t", "", {"x": 1})
104
+ adapter.write_record("t", "", {"x": 2})
105
+ records = adapter.read_records("t", "")
106
+ assert len(records) == 2
107
+ assert {"x": 1} in records
108
+ assert {"x": 2} in records
109
+
110
+ def test_table_not_exists_returns_empty(self, conn: sqlite3.Connection):
111
+ adapter = SqliteAdapter(conn)
112
+ assert adapter.read_records("nonexistent", "") == []
113
+
114
+ def test_type_roundtrip(self, conn: sqlite3.Connection):
115
+ adapter = SqliteAdapter(conn)
116
+ data = {
117
+ "int_val": 42,
118
+ "float_val": 3.14,
119
+ "bool_val": True,
120
+ "none_val": None,
121
+ "dict_val": {"nested": "yes"},
122
+ "list_val": [1, 2, 3],
123
+ }
124
+ adapter.write_record("t", "", data)
125
+ records = adapter.read_records("t", "")
126
+ assert len(records) == 1
127
+ record = records[0]
128
+ assert record["int_val"] == 42
129
+ assert isinstance(record["int_val"], int)
130
+ assert record["float_val"] == 3.14
131
+ assert isinstance(record["float_val"], float)
132
+ assert record["bool_val"] is True
133
+ assert record["none_val"] is None
134
+ assert record["dict_val"] == {"nested": "yes"}
135
+ assert record["list_val"] == [1, 2, 3]
136
+
137
+
138
+ class TestClearGroup:
139
+ def test_drops_table(self, conn: sqlite3.Connection):
140
+ adapter = SqliteAdapter(conn)
141
+ adapter.write_record("t", "", {"x": 1})
142
+ adapter.clear_group("t", "")
143
+ assert adapter.read_records("t", "") == []
144
+
145
+ def test_nonexistent_table_no_error(self, conn: sqlite3.Connection):
146
+ adapter = SqliteAdapter(conn)
147
+ adapter.clear_group("nonexistent", "")
148
+
149
+ def test_clear_does_not_affect_other_tables(self, conn: sqlite3.Connection):
150
+ adapter = SqliteAdapter(conn)
151
+ adapter.write_record("a", "", {"x": 1})
152
+ adapter.write_record("b", "", {"x": 2})
153
+ adapter.clear_group("a", "")
154
+ assert adapter.read_records("a", "") == []
155
+ assert adapter.read_records("b", "") == [{"x": 2}]
156
+
157
+ def test_table_name_with_double_quote(self, conn: sqlite3.Connection):
158
+ adapter = SqliteAdapter(conn)
159
+ adapter.write_record('my"table', "", {"x": 1})
160
+ adapter.clear_group('my"table', "")
161
+ assert adapter.read_records('my"table', "") == []
@@ -0,0 +1,142 @@
1
+ """E2E tests: set_db_data + verify_db_data with SQLite."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlite3
6
+
7
+ import pytest
8
+
9
+ from db_test_helpers.core import Config, UnsupportedError, set_db_data, verify_db_data
10
+ from db_test_helpers.sqlite import SqliteAdapter
11
+
12
+
13
+ class TestE2eBasic:
14
+ def test_set_and_verify(self, conn: sqlite3.Connection, tmp_path):
15
+ seed = tmp_path / "seed.yaml"
16
+ seed.write_text(
17
+ """\
18
+ users:
19
+ - name: "Alice"
20
+ age: 30
21
+ - name: "Bob"
22
+ age: 25
23
+ """
24
+ )
25
+ expected = tmp_path / "expected.yaml"
26
+ expected.write_text(
27
+ """\
28
+ users:
29
+ - name: "Bob"
30
+ age: 25
31
+ - name: "Alice"
32
+ age: 30
33
+ """
34
+ )
35
+ adapter = SqliteAdapter(conn)
36
+ set_db_data(adapter, str(seed))
37
+ verify_db_data(adapter, str(expected))
38
+
39
+ def test_set_replaces_existing(self, conn: sqlite3.Connection, tmp_path):
40
+ adapter = SqliteAdapter(conn)
41
+
42
+ adapter.write_record("users", "", {"name": "OldUser"})
43
+
44
+ seed = tmp_path / "seed.yaml"
45
+ seed.write_text(
46
+ """\
47
+ users:
48
+ - name: "Alice"
49
+ """
50
+ )
51
+ set_db_data(adapter, str(seed))
52
+
53
+ records = adapter.read_records("users", "")
54
+ assert len(records) == 1
55
+ assert records[0]["name"] == "Alice"
56
+
57
+
58
+ class TestE2eDsl:
59
+ def test_var_and_any(self, conn: sqlite3.Connection, tmp_path):
60
+ seed = tmp_path / "seed.yaml"
61
+ seed.write_text(
62
+ """\
63
+ users:
64
+ - name: "$var(user_name)"
65
+ role: "admin"
66
+ - name: "Bob"
67
+ role: "member"
68
+ """
69
+ )
70
+ expected = tmp_path / "expected.yaml"
71
+ expected.write_text(
72
+ """\
73
+ users:
74
+ - name: "$var(user_name)"
75
+ role: "admin"
76
+ - name: "$any()"
77
+ role: "member"
78
+ """
79
+ )
80
+ adapter = SqliteAdapter(conn)
81
+ config = Config(variables={"user_name": "Alice"})
82
+
83
+ set_db_data(adapter, str(seed), config)
84
+ verify_db_data(adapter, str(expected), config)
85
+
86
+ def test_matchers_in_verify(self, conn: sqlite3.Connection, tmp_path):
87
+ seed = tmp_path / "seed.yaml"
88
+ seed.write_text(
89
+ """\
90
+ users:
91
+ - name: "Alice"
92
+ age: 30
93
+ """
94
+ )
95
+ expected = tmp_path / "expected.yaml"
96
+ expected.write_text(
97
+ """\
98
+ users:
99
+ - name: "$eq(Alice)"
100
+ age: "$and($gt(0),$lt(100))"
101
+ """
102
+ )
103
+ adapter = SqliteAdapter(conn)
104
+ set_db_data(adapter, str(seed))
105
+ verify_db_data(adapter, str(expected))
106
+
107
+ def test_verify_failure(self, conn: sqlite3.Connection, tmp_path):
108
+ seed = tmp_path / "seed.yaml"
109
+ seed.write_text(
110
+ """\
111
+ users:
112
+ - name: "Alice"
113
+ """
114
+ )
115
+ expected = tmp_path / "expected.yaml"
116
+ expected.write_text(
117
+ """\
118
+ users:
119
+ - name: "Bob"
120
+ """
121
+ )
122
+ adapter = SqliteAdapter(conn)
123
+ set_db_data(adapter, str(seed))
124
+ with pytest.raises(AssertionError):
125
+ verify_db_data(adapter, str(expected))
126
+
127
+
128
+ class TestE2eChildren:
129
+ def test_children_raises_not_implemented(self, conn: sqlite3.Connection, tmp_path):
130
+ seed = tmp_path / "seed.yaml"
131
+ seed.write_text(
132
+ """\
133
+ users:
134
+ - name: "Alice"
135
+ __children__:
136
+ posts:
137
+ - title: "Hello"
138
+ """
139
+ )
140
+ adapter = SqliteAdapter(conn)
141
+ with pytest.raises(UnsupportedError):
142
+ set_db_data(adapter, str(seed))
@@ -0,0 +1,10 @@
1
+ import sqlite3
2
+
3
+ import pytest
4
+
5
+
6
+ @pytest.fixture
7
+ def conn():
8
+ conn = sqlite3.connect(":memory:")
9
+ yield conn
10
+ conn.close()