pgdevkit 0.2.2__tar.gz → 0.2.4__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.
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/PKG-INFO +1 -1
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/db/complex_types.py +22 -4
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/testdb/schema.py +8 -3
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pyproject.toml +1 -1
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/db/test_complex_types.py +41 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/uv.lock +1 -1
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/.github/workflows/python-publish.yml +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/.github/workflows/python-test.yml +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/.gitignore +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/.python-version +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/README.md +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/docs/database-layout.md +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/__init__.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/cli.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/connection.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/db/__init__.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/db/connection.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/db/crud.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/db/loader.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/db/model.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/diff.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/fetch_missing.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/introspect.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/lakebase.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/models.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/parser.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/testdb/__init__.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/testdb/api.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/testdb/config.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/testdb/constants.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/testdb/container.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/testdb/naming.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/pgdevkit/testdb/query.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/skills/pgdevkit/SKILL.md +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/skills/pgdevkit/references/dynamic-sql.md +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/skills/pgdevkit/references/temporal-tables.md +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/__init__.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/conftest.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/db/__init__.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/db/test_connection.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/db/test_crud.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/db/test_loader.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/fixtures/01_schema.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/fixtures/02_types.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/fixtures/03_tables.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/fixtures/04_views.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/fixtures/05_functions.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/fixtures/06_indexes.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/test_cli_compare.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/test_compare.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/test_connection.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/test_fetch_missing.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/test_fetch_missing_cli.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/test_lakebase.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/__init__.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/conftest.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/schema/app.sql +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/test_api.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/test_cli.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/test_config.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/test_constants.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/test_container.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/test_naming.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/test_query.py +0 -0
- {pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/test_schema.py +0 -0
|
@@ -9,7 +9,18 @@ from psycopg.types.composite import CompositeInfo, register_composite
|
|
|
9
9
|
from psycopg.types.enum import EnumInfo, register_enum
|
|
10
10
|
from psycopg.types.json import Jsonb
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
class JsonbArray:
|
|
14
|
+
"""Sentinel for a `jsonb[]` column (a Postgres ARRAY of jsonb) — distinct
|
|
15
|
+
from `Jsonb` (a scalar jsonb column) so `recursive_convert` knows whether
|
|
16
|
+
an incoming Python list *is* the array (each element needs its own
|
|
17
|
+
`Jsonb(...)` wrapper) or is JSON array *content* for one scalar jsonb
|
|
18
|
+
value (the whole list gets wrapped once). Both cases arrive from
|
|
19
|
+
`information_schema.columns` looking identical (a plain Python list) —
|
|
20
|
+
only the column's own array-ness disambiguates them."""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
ComplexTypeInfo = CompositeInfo | EnumInfo | type[Jsonb] | type[JsonbArray] | None
|
|
13
24
|
|
|
14
25
|
|
|
15
26
|
class ComplexHelper:
|
|
@@ -67,7 +78,7 @@ class ComplexHelper:
|
|
|
67
78
|
if res["data_type"].lower() == "jsonb":
|
|
68
79
|
return Jsonb
|
|
69
80
|
if res["data_type"].upper() == "ARRAY" and res["udt_name"] == "_jsonb":
|
|
70
|
-
return
|
|
81
|
+
return JsonbArray
|
|
71
82
|
if (
|
|
72
83
|
not res["is_enum"]
|
|
73
84
|
and not res["is_user_defined"]
|
|
@@ -181,9 +192,16 @@ class ComplexHelper:
|
|
|
181
192
|
return None
|
|
182
193
|
if self.system_complex_type_dict is None:
|
|
183
194
|
await self.load_complex_type_dict()
|
|
195
|
+
if info == JsonbArray:
|
|
196
|
+
# The value here *is* the jsonb[] array -- each element is its
|
|
197
|
+
# own scalar jsonb value, unlike the plain-Jsonb case below.
|
|
198
|
+
assert isinstance(value, list), f"Expected a list for a jsonb[] column, got {type(value)}"
|
|
199
|
+
return [Jsonb(item) for item in value]
|
|
184
200
|
if info == Jsonb:
|
|
185
|
-
# A JSONB column's value is wrapped whole, even if it's a
|
|
186
|
-
#
|
|
201
|
+
# A scalar JSONB column's value is wrapped whole, even if it's a
|
|
202
|
+
# list — that list is JSON array *content* for the one jsonb
|
|
203
|
+
# value, not multiple array elements (see JsonbArray above for
|
|
204
|
+
# the jsonb[] column case).
|
|
187
205
|
return Jsonb(value)
|
|
188
206
|
if isinstance(value, list):
|
|
189
207
|
return [await self.recursive_convert(item, info, con) for item in value]
|
|
@@ -94,7 +94,11 @@ def _get_sql_deps(sql: str) -> set[str]:
|
|
|
94
94
|
continue
|
|
95
95
|
for t in e.find_all(exp.Table):
|
|
96
96
|
if t.args.get("this") is not None and t.args.get("db") is not None:
|
|
97
|
-
|
|
97
|
+
# exp.table_name(), not str(t): str() includes " AS alias" for
|
|
98
|
+
# an aliased reference (e.g. "FROM editing.visit v"), which
|
|
99
|
+
# would never match the plain declared name any dependent
|
|
100
|
+
# file's own CREATE target is recorded under.
|
|
101
|
+
deps.add(exp.table_name(t))
|
|
98
102
|
return deps
|
|
99
103
|
|
|
100
104
|
|
|
@@ -119,7 +123,7 @@ def _iter_sql_files(database_dir: Path):
|
|
|
119
123
|
deps = _get_sql_deps(content)
|
|
120
124
|
if file.parent.name in _SCHEMA_QUALIFIED_TYPES:
|
|
121
125
|
schema = _strip_layer_prefix(file.parent.parent.name)
|
|
122
|
-
full_name = f"{schema}.{file.stem}"
|
|
126
|
+
full_name = f"{schema}.{_strip_layer_prefix(file.stem)}"
|
|
123
127
|
deps.discard(full_name) # the file's own CREATE target is not a real dependency
|
|
124
128
|
all_declared.add(full_name)
|
|
125
129
|
if not deps or all(d in delivered for d in deps):
|
|
@@ -219,7 +223,8 @@ async def apply_schema(
|
|
|
219
223
|
json_file = file.with_suffix(".test_data.json")
|
|
220
224
|
if json_file.exists():
|
|
221
225
|
schema_name = _strip_layer_prefix(file.parent.parent.name)
|
|
222
|
-
|
|
226
|
+
table_stem = _strip_layer_prefix(file.stem)
|
|
227
|
+
await _insert_test_data(json_file, f"{schema_name}.{table_stem}", force_reset, con, complex_helper)
|
|
223
228
|
|
|
224
229
|
failures: list[tuple[Path, str]] = []
|
|
225
230
|
for file, sql in _iter_sql_files(database_dir):
|
|
@@ -80,6 +80,47 @@ async def test_select_list_includes_generated_columns(complex_types_test_db):
|
|
|
80
80
|
assert '"total"' in rendered
|
|
81
81
|
|
|
82
82
|
|
|
83
|
+
@requires_podman
|
|
84
|
+
async def test_jsonb_array_column_wraps_each_element_not_the_whole_list(complex_types_test_db):
|
|
85
|
+
# Regression: a jsonb[] column (a Postgres ARRAY of jsonb) and a plain
|
|
86
|
+
# jsonb column both surface identically from information_schema (a bare
|
|
87
|
+
# Python list, if that's the value) -- previously both were detected as
|
|
88
|
+
# the same `Jsonb` sentinel, and recursive_convert wrapped the whole
|
|
89
|
+
# incoming list as one Jsonb(...) for either case. That's correct for a
|
|
90
|
+
# plain jsonb column storing a JSON array *value*, but wrong for a
|
|
91
|
+
# jsonb[] column, where the list *is* the array and each element needs
|
|
92
|
+
# its own Jsonb(...) wrapper -- psycopg would otherwise try to bind a
|
|
93
|
+
# single jsonb value to an array column and Postgres would raise
|
|
94
|
+
# DatatypeMismatch ("column ... is of type jsonb[] but expression is of
|
|
95
|
+
# type jsonb").
|
|
96
|
+
async with await psycopg.AsyncConnection.connect(_db_dsn(), autocommit=True) as con:
|
|
97
|
+
await con.execute("""
|
|
98
|
+
CREATE TABLE gadget (
|
|
99
|
+
id serial PRIMARY KEY,
|
|
100
|
+
tags jsonb[],
|
|
101
|
+
metadata jsonb
|
|
102
|
+
)
|
|
103
|
+
""")
|
|
104
|
+
helper = ComplexHelper(con)
|
|
105
|
+
types = await helper.load_all_complex_types(("public", "gadget"))
|
|
106
|
+
|
|
107
|
+
tags_value = [{"name": "a"}, {"name": "b"}]
|
|
108
|
+
converted_tags = await helper.recursive_convert(tags_value, types["tags"], con)
|
|
109
|
+
|
|
110
|
+
metadata_value = [1, 2, 3] # JSON array *content* for one scalar jsonb value
|
|
111
|
+
converted_metadata = await helper.recursive_convert(metadata_value, types["metadata"], con)
|
|
112
|
+
|
|
113
|
+
await con.execute(
|
|
114
|
+
"INSERT INTO gadget (id, tags, metadata) VALUES (1, %s, %s)",
|
|
115
|
+
(converted_tags, converted_metadata),
|
|
116
|
+
)
|
|
117
|
+
async with con.cursor() as cur:
|
|
118
|
+
await cur.execute("SELECT tags, metadata FROM gadget WHERE id = 1")
|
|
119
|
+
tags, metadata = await cur.fetchone()
|
|
120
|
+
assert tags == tags_value
|
|
121
|
+
assert metadata == metadata_value
|
|
122
|
+
|
|
123
|
+
|
|
83
124
|
def test_complex_types_cache_is_per_instance_not_shared():
|
|
84
125
|
# Regression: complex_types used to be a mutable class attribute, so a
|
|
85
126
|
# CompositeInfo/EnumInfo (which carries OIDs from one specific
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/widget.test_data.json
RENAMED
|
File without changes
|
|
File without changes
|
{pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pgdevkit-0.2.2 → pgdevkit-0.2.4}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|