pgdevkit 0.3.0__tar.gz → 0.3.1__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.
Files changed (106) hide show
  1. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/PKG-INFO +1 -1
  2. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/db/crud.py +14 -3
  3. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pyproject.toml +1 -1
  4. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/db/test_crud.py +84 -0
  5. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/uv.lock +1 -1
  6. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/.github/workflows/python-publish.yml +0 -0
  7. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/.github/workflows/python-test.yml +0 -0
  8. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/.gitignore +0 -0
  9. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/.python-version +0 -0
  10. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/README.md +0 -0
  11. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/docs/database-layout.md +0 -0
  12. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/__init__.py +0 -0
  13. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/backends/__init__.py +0 -0
  14. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/backends/base.py +0 -0
  15. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/backends/mssql.py +0 -0
  16. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/backends/postgres.py +0 -0
  17. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/cli.py +0 -0
  18. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/connection.py +0 -0
  19. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/db/__init__.py +0 -0
  20. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/db/complex_types.py +0 -0
  21. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/db/connection.py +0 -0
  22. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/db/loader.py +0 -0
  23. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/db/model.py +0 -0
  24. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/db/mssql_crud.py +0 -0
  25. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/db/mssql_sql.py +0 -0
  26. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/dialect.py +0 -0
  27. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/diff.py +0 -0
  28. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/fetch_missing.py +0 -0
  29. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/introspect.py +0 -0
  30. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/lakebase.py +0 -0
  31. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/models.py +0 -0
  32. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/mssql_introspect.py +0 -0
  33. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/parser.py +0 -0
  34. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/__init__.py +0 -0
  35. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/_docker.py +0 -0
  36. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/api.py +0 -0
  37. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/config.py +0 -0
  38. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/constants.py +0 -0
  39. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/container.py +0 -0
  40. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/mssql/__init__.py +0 -0
  41. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/mssql/api.py +0 -0
  42. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/mssql/constants.py +0 -0
  43. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/mssql/container.py +0 -0
  44. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/naming.py +0 -0
  45. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/query.py +0 -0
  46. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/pgdevkit/testdb/schema.py +0 -0
  47. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/skills/pgdevkit/SKILL.md +0 -0
  48. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/skills/pgdevkit/references/dynamic-sql.md +0 -0
  49. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/skills/pgdevkit/references/temporal-tables.md +0 -0
  50. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/__init__.py +0 -0
  51. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/conftest.py +0 -0
  52. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/db/__init__.py +0 -0
  53. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/db/test_complex_types.py +0 -0
  54. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/db/test_connection.py +0 -0
  55. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/db/test_loader.py +0 -0
  56. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/db/test_mssql_crud_live.py +0 -0
  57. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/db/test_mssql_crud_sql.py +0 -0
  58. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/fixtures/01_schema.sql +0 -0
  59. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/fixtures/02_types.sql +0 -0
  60. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/fixtures/03_tables.sql +0 -0
  61. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/fixtures/04_views.sql +0 -0
  62. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/fixtures/05_functions.sql +0 -0
  63. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/fixtures/06_indexes.sql +0 -0
  64. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_cli_compare.py +0 -0
  65. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_compare.py +0 -0
  66. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_compare_mssql_live.py +0 -0
  67. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_connection.py +0 -0
  68. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_dialect.py +0 -0
  69. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_diff_mssql.py +0 -0
  70. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_fetch_missing.py +0 -0
  71. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_fetch_missing_cli.py +0 -0
  72. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_lakebase.py +0 -0
  73. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_mssql_introspect.py +0 -0
  74. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/test_parser_mssql.py +0 -0
  75. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/__init__.py +0 -0
  76. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/conftest.py +0 -0
  77. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/migrations/001_add_gadget_note.sql +0 -0
  78. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/tables/gadget.sql +0 -0
  79. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/tables/gadget.test_data.json +0 -0
  80. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/tables/widget.sql +0 -0
  81. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/tables/widget.test_data.json +0 -0
  82. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/tables/widget_part.sql +0 -0
  83. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/tables/widget_part_detail.sql +0 -0
  84. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/types/dimensions.sql +0 -0
  85. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/types/mood.sql +0 -0
  86. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/views/a_wrapper_view.sql +0 -0
  87. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/app/views/b_base_view.sql +0 -0
  88. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database/schema/app.sql +0 -0
  89. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database_mssql/app/tables/widget.sql +0 -0
  90. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database_mssql/app/tables/widget.test_data.json +0 -0
  91. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database_mssql/app/views/a_wrapper_view.sql +0 -0
  92. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database_mssql/app/views/b_base_view.sql +0 -0
  93. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/fixtures/database_mssql/schema/app.sql +0 -0
  94. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_api.py +0 -0
  95. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_api_mssql.py +0 -0
  96. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_api_mssql_live.py +0 -0
  97. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_cli.py +0 -0
  98. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_config.py +0 -0
  99. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_config_mssql.py +0 -0
  100. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_constants.py +0 -0
  101. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_container.py +0 -0
  102. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_mssql_constants.py +0 -0
  103. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_naming.py +0 -0
  104. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_query.py +0 -0
  105. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_schema.py +0 -0
  106. {pgdevkit-0.3.0 → pgdevkit-0.3.1}/tests/testdb/test_schema_mssql.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pgdevkit
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: A helper for developing with Postgres
5
5
  Requires-Python: >=3.14
6
6
  Requires-Dist: docker>=7.1.0
@@ -157,8 +157,15 @@ async def pg_update_dict(
157
157
  table_name: tuple[str, str],
158
158
  data: dict,
159
159
  primary_keys: Sequence[str],
160
+ *,
161
+ complex_helper: ComplexHelper | None = None,
160
162
  ) -> Any | None:
161
- """Update a row identified by primary_keys. Returns the raw row tuple."""
163
+ """Update a row identified by primary_keys. Returns the raw row tuple.
164
+
165
+ Pass `complex_helper` when the table has composite/enum columns among the
166
+ values being set — omitted, this behaves exactly as before (plain values
167
+ passed straight through to psycopg)."""
168
+ data = await _convert_complex_values(con, table_name, data, complex_helper)
162
169
  set_parts = [
163
170
  SQL("{col} = {val}").format(col=Identifier(k), val=Placeholder(k)) for k in data if k not in primary_keys
164
171
  ]
@@ -173,9 +180,13 @@ async def pg_update_dict(
173
180
  return await cur.fetchone()
174
181
 
175
182
 
176
- async def pg_update(con: AsyncConnection, data: T, data_type: type[T]) -> Any | None:
183
+ async def pg_update(
184
+ con: AsyncConnection, data: T, data_type: type[T], *, complex_helper: ComplexHelper | None = None
185
+ ) -> Any | None:
177
186
  """Update a typed model instance."""
178
- return await pg_update_dict(con, data_type.get_table_name(), data.model_dump(), data_type.get_primary_key())
187
+ return await pg_update_dict(
188
+ con, data_type.get_table_name(), data.model_dump(), data_type.get_primary_key(), complex_helper=complex_helper
189
+ )
179
190
 
180
191
 
181
192
  async def pg_upsert_dict(
@@ -11,7 +11,7 @@ packages = ["pgdevkit"]
11
11
 
12
12
  [project]
13
13
  name = "pgdevkit"
14
- version = "0.3.0"
14
+ version = "0.3.1"
15
15
  description = "A helper for developing with Postgres"
16
16
  readme = "README.md"
17
17
  requires-python = ">=3.14"
@@ -16,6 +16,7 @@ from pgdevkit.db import (
16
16
  pg_retrieve,
17
17
  pg_retrieve_many,
18
18
  pg_update,
19
+ pg_update_dict,
19
20
  pg_upsert,
20
21
  pg_upsert_many,
21
22
  pg_upsert_many_dict,
@@ -60,6 +61,20 @@ class Gizmo(PostgresTableModel):
60
61
  return ["id"]
61
62
 
62
63
 
64
+ class Gadget(PostgresTableModel):
65
+ model_config = ConfigDict(from_attributes=True)
66
+ id: int
67
+ status: str
68
+
69
+ @staticmethod
70
+ def get_table_name() -> tuple[str, str]:
71
+ return ("public", "gadget")
72
+
73
+ @staticmethod
74
+ def get_primary_key() -> Sequence[str]:
75
+ return ["id"]
76
+
77
+
63
78
  @pytest.fixture
64
79
  async def pool(monkeypatch: pytest.MonkeyPatch):
65
80
  ensure_container()
@@ -70,6 +85,8 @@ async def pool(monkeypatch: pytest.MonkeyPatch):
70
85
  con.execute("CREATE TABLE widget (id serial PRIMARY KEY, name text NOT NULL)")
71
86
  con.execute("CREATE TYPE label_pair AS (en text, de text)")
72
87
  con.execute("CREATE TABLE gizmo (id serial PRIMARY KEY, label label_pair NOT NULL)")
88
+ con.execute("CREATE TYPE gadget_status AS ENUM ('active', 'inactive')")
89
+ con.execute("CREATE TABLE gadget (id serial PRIMARY KEY, status gadget_status NOT NULL)")
73
90
 
74
91
  monkeypatch.setenv(f"{ENV_PREFIX}HOST", constants.HOST)
75
92
  monkeypatch.setenv(f"{ENV_PREFIX}PORT", str(constants.PORT))
@@ -184,3 +201,70 @@ async def test_insert_retrieve_composite_column_with_normalizer(pool: PgPool):
184
201
  fetched = await pg_retrieve(con, Gizmo, {"id": gizmo_id}, complex_helper=helper)
185
202
  assert fetched is not None
186
203
  assert fetched.label == {"en": "Hello", "de": "[Hello]"}
204
+
205
+
206
+ @requires_podman
207
+ async def test_update_dict_without_complex_helper_still_works_on_plain_columns(pool: PgPool):
208
+ # complex_helper is a new optional kwarg -- omitting it (the pre-existing call
209
+ # shape) must behave exactly as before for tables with no composite/enum columns.
210
+ async with pool.connection() as con:
211
+ inserted = await pg_insert(con, ("public", "widget"), {"name": "sprocket"})
212
+ widget_id = inserted["id"]
213
+
214
+ await pg_update_dict(con, ("public", "widget"), {"id": widget_id, "name": "renamed"}, ["id"])
215
+
216
+ fetched = await pg_retrieve(con, Widget, {"id": widget_id})
217
+ assert fetched is not None
218
+ assert fetched.name == "renamed"
219
+
220
+
221
+ @requires_podman
222
+ async def test_update_dict_converts_enum_column_with_complex_helper(pool: PgPool):
223
+ # psycopg already binds a plain str to an enum-typed UPDATE target column
224
+ # fine on its own (Postgres resolves it via the usual assignment cast), so
225
+ # this isn't a failing-without/passing-with case the way composite columns
226
+ # are below -- it just confirms complex_helper doesn't break the enum path
227
+ # and round-trips correctly end to end.
228
+ async with pool.connection() as con:
229
+ helper = ComplexHelper(con)
230
+ inserted = await pg_insert(con, ("public", "gadget"), {"status": "active"}, complex_helper=helper)
231
+ gadget_id = inserted["id"]
232
+
233
+ await pg_update_dict(
234
+ con, ("public", "gadget"), {"id": gadget_id, "status": "inactive"}, ["id"], complex_helper=helper
235
+ )
236
+ fetched = await pg_retrieve(con, Gadget, {"id": gadget_id}, complex_helper=helper)
237
+ assert fetched is not None
238
+ assert fetched.status == "inactive"
239
+
240
+ # pg_update (typed-model variant) threads complex_helper through the same way.
241
+ fetched.status = "active"
242
+ await pg_update(con, fetched, Gadget, complex_helper=helper)
243
+ refetched = await pg_retrieve(con, Gadget, {"id": gadget_id}, complex_helper=helper)
244
+ assert refetched is not None
245
+ assert refetched.status == "active"
246
+
247
+
248
+ @requires_podman
249
+ async def test_update_dict_composite_column_requires_complex_helper(pool: PgPool):
250
+ # Unlike enums, psycopg has no built-in way to adapt a plain Python dict
251
+ # to a composite column type -- this is the real case pg_update_dict's
252
+ # complex_helper support fixes.
253
+ async with pool.connection() as con:
254
+ helper = ComplexHelper(con)
255
+ inserted = await pg_insert(con, ("public", "gizmo"), {"label": {"en": "Hello", "de": "Hallo"}}, complex_helper=helper)
256
+ gizmo_id = inserted["id"]
257
+
258
+ with pytest.raises(psycopg.ProgrammingError):
259
+ await pg_update_dict(con, ("public", "gizmo"), {"id": gizmo_id, "label": {"en": "Bye", "de": "Tschuess"}}, ["id"])
260
+
261
+ await pg_update_dict(
262
+ con,
263
+ ("public", "gizmo"),
264
+ {"id": gizmo_id, "label": {"en": "Bye", "de": "Tschuess"}},
265
+ ["id"],
266
+ complex_helper=helper,
267
+ )
268
+ fetched = await pg_retrieve(con, Gizmo, {"id": gizmo_id}, complex_helper=helper)
269
+ assert fetched is not None
270
+ assert fetched.label == {"en": "Bye", "de": "Tschuess"}
@@ -313,7 +313,7 @@ wheels = [
313
313
 
314
314
  [[package]]
315
315
  name = "pgdevkit"
316
- version = "0.3.0"
316
+ version = "0.3.1"
317
317
  source = { editable = "." }
318
318
  dependencies = [
319
319
  { name = "docker" },
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