dataplat 0.2.0__tar.gz → 0.2.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.
Files changed (163) hide show
  1. dataplat-0.2.2/.github/workflows/ci.yml +171 -0
  2. dataplat-0.2.2/.github/workflows/release.yml +227 -0
  3. {dataplat-0.2.0 → dataplat-0.2.2}/CHANGELOG.md +100 -0
  4. {dataplat-0.2.0 → dataplat-0.2.2}/PKG-INFO +55 -3
  5. {dataplat-0.2.0 → dataplat-0.2.2}/README.md +54 -2
  6. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/dbt_orphans.py +63 -15
  7. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/role.py +19 -2
  8. dataplat-0.2.2/dataplat/services/db/_like.py +31 -0
  9. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/describe.py +98 -20
  10. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/long_queries.py +39 -4
  11. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/orphans.py +85 -10
  12. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/role.py +86 -12
  13. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/top_tables.py +3 -7
  14. {dataplat-0.2.0 → dataplat-0.2.2}/pyproject.toml +7 -1
  15. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_role.py +5 -0
  16. dataplat-0.2.2/tests/integration/__init__.py +6 -0
  17. dataplat-0.2.2/tests/integration/conftest.py +528 -0
  18. dataplat-0.2.2/tests/integration/test_describe_pg.py +1614 -0
  19. dataplat-0.2.2/tests/integration/test_harness.py +733 -0
  20. dataplat-0.2.2/tests/integration/test_long_queries_pg.py +690 -0
  21. dataplat-0.2.2/tests/integration/test_orphans_pg.py +841 -0
  22. dataplat-0.2.2/tests/integration/test_roles_pg.py +1357 -0
  23. dataplat-0.2.2/tests/integration/test_top_tables_pg.py +320 -0
  24. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_describe.py +29 -1
  25. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_long_queries.py +94 -0
  26. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_orphans.py +84 -3
  27. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_role.py +72 -18
  28. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_top_tables.py +5 -5
  29. {dataplat-0.2.0 → dataplat-0.2.2}/uv.lock +1 -1
  30. dataplat-0.2.0/.github/workflows/ci.yml +0 -48
  31. dataplat-0.2.0/.github/workflows/release.yml +0 -119
  32. {dataplat-0.2.0 → dataplat-0.2.2}/.gitignore +0 -0
  33. {dataplat-0.2.0 → dataplat-0.2.2}/.python-version +0 -0
  34. {dataplat-0.2.0 → dataplat-0.2.2}/LICENSE +0 -0
  35. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/__init__.py +0 -0
  36. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/__init__.py +0 -0
  37. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_lazy.py +0 -0
  38. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_missing.py +0 -0
  39. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_options.py +0 -0
  40. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_prompt.py +0 -0
  41. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_render.py +0 -0
  42. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/bi/__init__.py +0 -0
  43. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/bi/app.py +0 -0
  44. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/bi/superset.py +0 -0
  45. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/__init__.py +0 -0
  46. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/app.py +0 -0
  47. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/github/__init__.py +0 -0
  48. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/github/app.py +0 -0
  49. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/github/runner.py +0 -0
  50. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/__init__.py +0 -0
  51. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/app.py +0 -0
  52. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/__init__.py +0 -0
  53. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/_common.py +0 -0
  54. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/app.py +0 -0
  55. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/rds.py +0 -0
  56. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/redshift.py +0 -0
  57. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/secrets.py +0 -0
  58. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/config.py +0 -0
  59. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/__init__.py +0 -0
  60. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/_common.py +0 -0
  61. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/_report.py +0 -0
  62. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/describe.py +0 -0
  63. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/long_queries.py +0 -0
  64. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/role_create.py +0 -0
  65. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/role_drop.py +0 -0
  66. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/role_list.py +0 -0
  67. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/top_tables.py +0 -0
  68. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/__init__.py +0 -0
  69. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/__init__.py +0 -0
  70. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/_common.py +0 -0
  71. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/_cursor.py +0 -0
  72. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/_resource.py +0 -0
  73. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/app.py +0 -0
  74. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/connections.py +0 -0
  75. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/definitions.py +0 -0
  76. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/destinations.py +0 -0
  77. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/enums.py +0 -0
  78. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/jobs.py +0 -0
  79. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/sources.py +0 -0
  80. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/tags.py +0 -0
  81. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/templates.py +0 -0
  82. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/tui.py +0 -0
  83. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/workspaces.py +0 -0
  84. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/app.py +0 -0
  85. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/open.py +0 -0
  86. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/status.py +0 -0
  87. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/__init__.py +0 -0
  88. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/deps.py +0 -0
  89. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/envrc.py +0 -0
  90. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/errors.py +0 -0
  91. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/registry.py +0 -0
  92. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/main.py +0 -0
  93. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/__init__.py +0 -0
  94. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/__init__.py +0 -0
  95. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/_resource.py +0 -0
  96. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/client.py +0 -0
  97. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/connections.py +0 -0
  98. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/definitions.py +0 -0
  99. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/destinations.py +0 -0
  100. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/jobs.py +0 -0
  101. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/sources.py +0 -0
  102. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/tags.py +0 -0
  103. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/workspaces.py +0 -0
  104. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/aws/__init__.py +0 -0
  105. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/aws/auth.py +0 -0
  106. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/__init__.py +0 -0
  107. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/connection.py +0 -0
  108. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/role_admin.py +0 -0
  109. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/role_dialects.py +0 -0
  110. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/targets.py +0 -0
  111. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/superset/__init__.py +0 -0
  112. {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/superset/client.py +0 -0
  113. {dataplat-0.2.0 → dataplat-0.2.2}/tests/__init__.py +0 -0
  114. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/__init__.py +0 -0
  115. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_airbyte_commands.py +0 -0
  116. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_airbyte_cursor_logic.py +0 -0
  117. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_airbyte_guards.py +0 -0
  118. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_airbyte_tui.py +0 -0
  119. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_aws_secrets.py +0 -0
  120. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_aws_secrets_write.py +0 -0
  121. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_cli_smoke.py +0 -0
  122. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_config.py +0 -0
  123. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_db_common.py +0 -0
  124. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_db_long_queries.py +0 -0
  125. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_db_query.py +0 -0
  126. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_dbt_orphans.py +0 -0
  127. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_describe.py +0 -0
  128. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_github_runner.py +0 -0
  129. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_missing_deps.py +0 -0
  130. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_open.py +0 -0
  131. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_prompt.py +0 -0
  132. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_rds.py +0 -0
  133. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_redshift.py +0 -0
  134. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_regression.py +0 -0
  135. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_render.py +0 -0
  136. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_role_create.py +0 -0
  137. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_role_drop.py +0 -0
  138. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_status.py +0 -0
  139. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_superset.py +0 -0
  140. {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_top_tables.py +0 -0
  141. {dataplat-0.2.0 → dataplat-0.2.2}/tests/conftest.py +0 -0
  142. {dataplat-0.2.0 → dataplat-0.2.2}/tests/core/__init__.py +0 -0
  143. {dataplat-0.2.0 → dataplat-0.2.2}/tests/core/test_deps.py +0 -0
  144. {dataplat-0.2.0 → dataplat-0.2.2}/tests/core/test_envrc.py +0 -0
  145. {dataplat-0.2.0 → dataplat-0.2.2}/tests/core/test_registry.py +0 -0
  146. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/__init__.py +0 -0
  147. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/__init__.py +0 -0
  148. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_client.py +0 -0
  149. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_connections.py +0 -0
  150. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_definitions.py +0 -0
  151. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_destinations.py +0 -0
  152. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_jobs.py +0 -0
  153. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_sources.py +0 -0
  154. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_workspaces.py +0 -0
  155. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/aws/__init__.py +0 -0
  156. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/aws/test_auth.py +0 -0
  157. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/__init__.py +0 -0
  158. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_connection.py +0 -0
  159. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_role_admin.py +0 -0
  160. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_role_dialects.py +0 -0
  161. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_targets.py +0 -0
  162. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/superset/__init__.py +0 -0
  163. {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/superset/test_client.py +0 -0
@@ -0,0 +1,171 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["**"]
6
+ # A tag push matches no branch, so without this a release tag would run no
7
+ # checks at all before release.yml publishes it.
8
+ tags: ["[0-9]*.[0-9]*.[0-9]*"]
9
+ pull_request:
10
+
11
+ jobs:
12
+ checks:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ # The floor and the ceiling break for different reasons; report both
16
+ # rather than hiding one behind the other.
17
+ fail-fast: false
18
+ matrix:
19
+ # Lowest version requires-python allows, plus the dev pin.
20
+ python-version: ["3.12", "3.13"]
21
+ env:
22
+ # Overrides .python-version (3.13). Without it every uv command in the
23
+ # 3.12 leg would still pick the pin and we would test 3.13 twice.
24
+ UV_PYTHON: ${{ matrix.python-version }}
25
+ steps:
26
+ - name: Checkout
27
+ uses: actions/checkout@v4
28
+
29
+ - name: Setup uv
30
+ uses: astral-sh/setup-uv@v5
31
+
32
+ - name: Setup Python
33
+ run: uv python install ${{ matrix.python-version }}
34
+
35
+ - name: Install dependencies
36
+ run: uv sync --group dev --all-extras
37
+
38
+ - name: Ruff
39
+ run: uv run ruff check .
40
+
41
+ - name: Ruff format
42
+ run: uv run ruff format --check .
43
+
44
+ - name: MyPy
45
+ run: uv run mypy dataplat
46
+
47
+ # Deselected rather than left to skip. With no server reachable the
48
+ # integration tests would skip on their own, but naming the marker keeps
49
+ # this leg an honest database-free signal instead of a growing pile of
50
+ # skips reported twice per push. Marker deselection happens after
51
+ # collection, so an import error under tests/integration/ still fails
52
+ # here and does not have to wait for the integration job.
53
+ - name: Pytest (unit)
54
+ run: uv run pytest -m "not integration"
55
+
56
+ # A separate job, not extra steps in the matrix above, for two reasons: the
57
+ # unit legs are the fast signal and must not queue behind a container boot,
58
+ # and a database per Python version would buy nothing -- the SQL these tests
59
+ # validate is identical on every interpreter.
60
+ integration:
61
+ runs-on: ubuntu-latest
62
+ strategy:
63
+ fail-fast: false
64
+ matrix:
65
+ # An explicit major, never "latest": a silent server upgrade must not be
66
+ # able to change what this job proves. Adding a second major is a
67
+ # one-token edit here if catalog drift across majors ever earns the
68
+ # minutes.
69
+ postgres-version: ["16"]
70
+ env:
71
+ # The dev pin only -- see the job comment. The 3.12 floor is imported,
72
+ # linted and type-checked by `checks` above.
73
+ UV_PYTHON: "3.13"
74
+ # Matches the default in tests/integration/conftest.py, set explicitly so
75
+ # the published port below and the DSN cannot drift apart unnoticed.
76
+ DP_TEST_PG_DSN: postgresql://postgres:postgres@127.0.0.1:55432/dataplat_test
77
+ # The single most important line in this file. Without it an unreachable
78
+ # server turns every integration test into a skip: the job goes green, the
79
+ # badge stays green, and CI reports success while executing none of the
80
+ # SQL this suite exists to validate. Truthy here means the harness raises
81
+ # instead of skipping.
82
+ DP_TEST_PG_REQUIRED: "1"
83
+ steps:
84
+ - name: Checkout
85
+ uses: actions/checkout@v4
86
+
87
+ # `docker run`, not a `services:` block. A service container is started
88
+ # with the image's default command and Actions gives no way to append
89
+ # arguments to it, so `-c shared_preload_libraries=pg_stat_statements`
90
+ # cannot be passed. Without that preload the extension still installs and
91
+ # its columns are still describable, but every read of the view raises
92
+ # "pg_stat_statements must be loaded via shared_preload_libraries" -- that
93
+ # errors the harness's own pg_stat_statements test and leaves the
94
+ # version-branching second half of fetch_query_history_postgres
95
+ # unexecuted, which is precisely the coverage this job exists for.
96
+ #
97
+ # Detached and placed before `uv sync` so initdb runs while dependencies
98
+ # install; by the time the wait step runs the server is normally up.
99
+ - name: Start PostgreSQL ${{ matrix.postgres-version }}
100
+ run: |
101
+ docker run --detach --name dp-pg-ci \
102
+ --publish 55432:5432 \
103
+ --env POSTGRES_PASSWORD=postgres \
104
+ --env POSTGRES_DB=dataplat_test \
105
+ --health-cmd "pg_isready -h 127.0.0.1 -U postgres -d dataplat_test" \
106
+ --health-interval 2s \
107
+ --health-timeout 5s \
108
+ --health-retries 30 \
109
+ postgres:${{ matrix.postgres-version }} \
110
+ -c shared_preload_libraries=pg_stat_statements
111
+
112
+ - name: Setup uv
113
+ uses: astral-sh/setup-uv@v5
114
+
115
+ - name: Setup Python
116
+ run: uv python install 3.13
117
+
118
+ # --all-extras is what installs psycopg (the optional "db" extra). The
119
+ # harness routes a missing psycopg through the same gate as an unreachable
120
+ # server, so under DP_TEST_PG_REQUIRED=1 dropping it is a hard error
121
+ # rather than a quiet skip.
122
+ - name: Install dependencies
123
+ run: uv sync --group dev --all-extras
124
+
125
+ # `-h 127.0.0.1` in the health command above is deliberate: the official
126
+ # entrypoint runs a temporary socket-only server while it initialises the
127
+ # data directory, so a Unix-socket probe reports "ready" before the real
128
+ # server is listening on TCP -- which is how the tests connect.
129
+ - name: Wait for PostgreSQL
130
+ run: |
131
+ for _ in $(seq 60); do
132
+ state="$(
133
+ docker inspect \
134
+ --format '{{.State.Running}}/{{.State.Health.Status}}' dp-pg-ci
135
+ )"
136
+ case "$state" in
137
+ true/healthy)
138
+ echo "PostgreSQL is accepting TCP connections."
139
+ exit 0
140
+ ;;
141
+ # An exited or unhealthy container (bad flag, image change, OOM)
142
+ # will never turn healthy: fail now with its log instead of
143
+ # stalling for the full two minutes.
144
+ false/* | */unhealthy)
145
+ break
146
+ ;;
147
+ esac
148
+ sleep 2
149
+ done
150
+ echo "::error::PostgreSQL never became healthy (state: $state)."
151
+ docker logs dp-pg-ci
152
+ exit 1
153
+
154
+ # Preloading the library only makes the view available server-wide; the
155
+ # extension itself is per-database and has to be created once. Nothing in
156
+ # the suite can do it -- the fixtures roll back every transaction.
157
+ - name: Enable pg_stat_statements
158
+ run: |
159
+ docker exec dp-pg-ci psql \
160
+ --username postgres --dbname dataplat_test \
161
+ --set ON_ERROR_STOP=1 \
162
+ --command 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'
163
+
164
+ - name: Pytest (integration)
165
+ run: uv run pytest -m integration
166
+
167
+ # For the failures that are about the database rather than the SQL:
168
+ # refused connections, a crashed backend, the OOM killer.
169
+ - name: PostgreSQL server log
170
+ if: failure()
171
+ run: docker logs dp-pg-ci
@@ -0,0 +1,227 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["[0-9]*.[0-9]*.[0-9]*"]
6
+
7
+ jobs:
8
+ # Nothing downstream re-checks the tree, so this job and `integration` are the
9
+ # only things standing between an untested commit and PyPI. Publishing is
10
+ # irreversible: a version can be yanked but never replaced.
11
+ verify:
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ # Must mirror ci.yml: the wheel advertises >=3.12, so the floor has to
17
+ # pass before we publish, not just the dev pin.
18
+ python-version: ["3.12", "3.13"]
19
+ env:
20
+ UV_PYTHON: ${{ matrix.python-version }}
21
+ steps:
22
+ - name: Checkout
23
+ uses: actions/checkout@v4
24
+
25
+ - name: Setup uv
26
+ uses: astral-sh/setup-uv@v5
27
+
28
+ - name: Setup Python
29
+ run: uv python install ${{ matrix.python-version }}
30
+
31
+ # Before the expensive sync: a mismatch means the tag would either
32
+ # publish the wrong version number or die at upload against an already
33
+ # occupied filename.
34
+ - name: Assert tag matches project version
35
+ run: |
36
+ uv run --no-project python - <<'PY'
37
+ import os
38
+ import sys
39
+ import tomllib
40
+
41
+ tag = os.environ["GITHUB_REF_NAME"]
42
+ with open("pyproject.toml", "rb") as handle:
43
+ version = tomllib.load(handle)["project"]["version"]
44
+ if tag != version:
45
+ sys.exit(
46
+ f"Tag '{tag}' does not match [project].version '{version}' in "
47
+ "pyproject.toml. Bump the version, commit, and re-tag; "
48
+ "refusing to publish a mismatched artifact."
49
+ )
50
+ print(f"Tag '{tag}' matches [project].version.")
51
+ PY
52
+
53
+ - name: Install dependencies
54
+ run: uv sync --group dev --all-extras
55
+
56
+ - name: Ruff
57
+ run: uv run ruff check .
58
+
59
+ - name: Ruff format
60
+ run: uv run ruff format --check .
61
+
62
+ - name: MyPy
63
+ run: uv run mypy dataplat
64
+
65
+ # Mirrors ci.yml: the database-backed half of the suite is the
66
+ # `integration` job below, which gates `build` alongside this one.
67
+ - name: Pytest (unit)
68
+ run: uv run pytest -m "not integration"
69
+
70
+ # This gate exists because the alternative is shipping SQL no server has ever
71
+ # parsed. dataplat's db services are thousands of lines of generated SQL, and
72
+ # the fake-cursor tests in `verify` prove only that execute() was called -- an
73
+ # invalid column, a Redshift-only construct on the PostgreSQL path, or a broken
74
+ # GRANT passes them all and fails on a user's warehouse. Since a bad version
75
+ # can only be yanked, never replaced, the few minutes this adds to tagging are
76
+ # cheap next to a release that has to be withdrawn.
77
+ #
78
+ # Deliberately NOT `needs: verify`: it runs concurrently with the verify
79
+ # matrix, so the added wall-clock is the amount by which it outlasts verify,
80
+ # not its full runtime. Every executable line is identical to ci.yml's
81
+ # integration job -- only the surrounding prose differs -- so the tag gate
82
+ # cannot quietly become weaker than the per-push one. (A reusable workflow
83
+ # would remove the duplication; two copies this size do not yet justify the
84
+ # extra indirection.)
85
+ integration:
86
+ runs-on: ubuntu-latest
87
+ strategy:
88
+ fail-fast: false
89
+ matrix:
90
+ postgres-version: ["16"]
91
+ env:
92
+ UV_PYTHON: "3.13"
93
+ DP_TEST_PG_DSN: postgresql://postgres:postgres@127.0.0.1:55432/dataplat_test
94
+ # Load-bearing: without it an unreachable server makes every integration
95
+ # test skip, and this gate would wave through exactly the unexecuted SQL it
96
+ # was added to catch.
97
+ DP_TEST_PG_REQUIRED: "1"
98
+ steps:
99
+ - name: Checkout
100
+ uses: actions/checkout@v4
101
+
102
+ # `docker run` rather than a `services:` block: a service container runs
103
+ # the image's default command with no way to append arguments, so
104
+ # shared_preload_libraries cannot be set -- and without it every read of
105
+ # pg_stat_statements raises "must be loaded via shared_preload_libraries".
106
+ # Detached and before `uv sync` so initdb overlaps the install.
107
+ - name: Start PostgreSQL ${{ matrix.postgres-version }}
108
+ run: |
109
+ docker run --detach --name dp-pg-ci \
110
+ --publish 55432:5432 \
111
+ --env POSTGRES_PASSWORD=postgres \
112
+ --env POSTGRES_DB=dataplat_test \
113
+ --health-cmd "pg_isready -h 127.0.0.1 -U postgres -d dataplat_test" \
114
+ --health-interval 2s \
115
+ --health-timeout 5s \
116
+ --health-retries 30 \
117
+ postgres:${{ matrix.postgres-version }} \
118
+ -c shared_preload_libraries=pg_stat_statements
119
+
120
+ - name: Setup uv
121
+ uses: astral-sh/setup-uv@v5
122
+
123
+ - name: Setup Python
124
+ run: uv python install 3.13
125
+
126
+ # --all-extras installs psycopg; the harness treats it as missing
127
+ # infrastructure, so under DP_TEST_PG_REQUIRED=1 omitting it errors.
128
+ - name: Install dependencies
129
+ run: uv sync --group dev --all-extras
130
+
131
+ # `-h 127.0.0.1` in the health command matters: the entrypoint runs a
132
+ # socket-only server during initdb, so a socket probe says "ready" before
133
+ # TCP -- the transport the tests actually use -- is listening.
134
+ - name: Wait for PostgreSQL
135
+ run: |
136
+ for _ in $(seq 60); do
137
+ state="$(
138
+ docker inspect \
139
+ --format '{{.State.Running}}/{{.State.Health.Status}}' dp-pg-ci
140
+ )"
141
+ case "$state" in
142
+ true/healthy)
143
+ echo "PostgreSQL is accepting TCP connections."
144
+ exit 0
145
+ ;;
146
+ # An exited or unhealthy container (bad flag, image change, OOM)
147
+ # will never turn healthy: fail now with its log instead of
148
+ # stalling for the full two minutes.
149
+ false/* | */unhealthy)
150
+ break
151
+ ;;
152
+ esac
153
+ sleep 2
154
+ done
155
+ echo "::error::PostgreSQL never became healthy (state: $state)."
156
+ docker logs dp-pg-ci
157
+ exit 1
158
+
159
+ # Server-wide preload only exposes the view; the extension is per-database.
160
+ - name: Enable pg_stat_statements
161
+ run: |
162
+ docker exec dp-pg-ci psql \
163
+ --username postgres --dbname dataplat_test \
164
+ --set ON_ERROR_STOP=1 \
165
+ --command 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'
166
+
167
+ - name: Pytest (integration)
168
+ run: uv run pytest -m integration
169
+
170
+ - name: PostgreSQL server log
171
+ if: failure()
172
+ run: docker logs dp-pg-ci
173
+
174
+ build:
175
+ # Both gates, so neither a lint/type/unit regression nor SQL that no server
176
+ # accepts can reach the publish job.
177
+ needs: [verify, integration]
178
+ runs-on: ubuntu-latest
179
+ steps:
180
+ - name: Checkout
181
+ uses: actions/checkout@v4
182
+
183
+ - name: Setup uv
184
+ uses: astral-sh/setup-uv@v5
185
+
186
+ - name: Build sdist and wheel
187
+ run: uv build
188
+
189
+ - name: Upload artifacts
190
+ uses: actions/upload-artifact@v4
191
+ with:
192
+ name: dist
193
+ path: dist/
194
+
195
+ publish:
196
+ needs: build
197
+ runs-on: ubuntu-latest
198
+ environment: pypi
199
+ permissions:
200
+ id-token: write # PyPI Trusted Publishing (OIDC)
201
+ steps:
202
+ - name: Download artifacts
203
+ uses: actions/download-artifact@v4
204
+ with:
205
+ name: dist
206
+ path: dist/
207
+
208
+ - name: Publish to PyPI
209
+ uses: pypa/gh-action-pypi-publish@release/v1
210
+
211
+ github-release:
212
+ needs: publish
213
+ runs-on: ubuntu-latest
214
+ permissions:
215
+ contents: write
216
+ steps:
217
+ - name: Download artifacts
218
+ uses: actions/download-artifact@v4
219
+ with:
220
+ name: dist
221
+ path: dist/
222
+
223
+ - name: Create GitHub release
224
+ uses: softprops/action-gh-release@v2
225
+ with:
226
+ files: dist/*
227
+ generate_release_notes: true
@@ -1,5 +1,105 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.2
4
+
5
+ Closes the six defects 0.2.1's integration suite found and pinned as expected
6
+ failures. No expected failures remain.
7
+
8
+ ### Fixed
9
+
10
+ - `dp db role show` claimed `Password set: yes` for every role, including a
11
+ passwordless `NOLOGIN` group. The attribute query read `rolpassword` from
12
+ `pg_roles`, whose view definition returns the literal `'********'` and can
13
+ never be NULL. Only `pg_authid` holds the real verifier and it is
14
+ superuser-only, so the field is now tri-state and reports `unknown` — with a
15
+ hint saying why — when the connecting role cannot read it. The privilege is
16
+ probed before the query rather than discovered by catching an error, since a
17
+ permission failure would abort the surrounding transaction.
18
+
19
+ - `dp db describe <schema>` never reported `USAGE` grants. The query read
20
+ `information_schema.usage_privileges`, which on PostgreSQL does not cover
21
+ schemas at all, so the USAGE half of the union always returned nothing. The
22
+ PostgreSQL path now reads the schema ACL directly.
23
+
24
+ - `dp db role show` under-counted a role's tables when it owned a partitioned
25
+ table. Both `relkind` `'r'` and `'p'` map to the label "table" and the
26
+ aggregation assigned rather than accumulated, so one group overwrote the
27
+ other while the total was summed separately and stayed right — the per-schema
28
+ breakdown contradicted its own total.
29
+
30
+ - `dp db dbt-orphans purge` aborted the whole batch when a single relation had
31
+ vanished between scan and purge. The generated statement now uses
32
+ `IF EXISTS`, matching `top-tables`, so a missing relation is a no-op. When a
33
+ dependent object genuinely blocks a drop the purge still stops — that
34
+ all-or-nothing property is deliberate for a destructive batch — but it now
35
+ names the relation and everything depending on it instead of surfacing a raw
36
+ driver error, and records the blockage in the audit log.
37
+
38
+ - `dp db describe <relation>` raised nothing but returned an invalid result for
39
+ a non-view relation: `pg_get_viewdef()` yields NULL rather than erroring, and
40
+ the PostgreSQL branch returned a view definition whose `sql` was `None`,
41
+ violating its own annotation. It now raises, as the Redshift branch already
42
+ did.
43
+
44
+ - `dp db long-queries --history` died with a driver traceback on any server
45
+ where `pg_stat_statements` is installed but not preloaded — the most common
46
+ misconfiguration. The guard that was meant to catch this probed the view's
47
+ columns, which PostgreSQL answers from the view definition without invoking
48
+ the extension, so the probe always succeeded. The failure is now reported as
49
+ an actionable error naming `shared_preload_libraries` and the required
50
+ restart.
51
+
52
+ ### Changed
53
+
54
+ - `RoleAttributes.password_set` widened from `bool` to `bool | None`, where
55
+ `None` means "not determinable by this connection". Relevant only if you
56
+ import the dataclass; the CLI renders the third state as `unknown`.
57
+
58
+ - On PostgreSQL, `dp db describe <schema>` privileges now come from the schema
59
+ ACL rather than a `has_schema_privilege` scan. A role holding `CREATE` only
60
+ through membership in a granted role is no longer listed as its own row, and
61
+ `grantor` and `WITH GRANT OPTION` are now real values where the previous
62
+ CREATE half hardcoded them.
63
+
64
+ ## 0.2.1
65
+
66
+ ### Fixed
67
+
68
+ - `dp db dbt-orphans` could report — and with `purge --include-unknown`, drop —
69
+ a relation it had never renamed. The scan matched `LIKE '%_deprecated'` with
70
+ no `ESCAPE`, and an unescaped `_` matches any single character, so a table
71
+ named `legacydeprecated` came back as a purge candidate. Found by running the
72
+ scan against a real PostgreSQL; `top_tables.py` had carried the escaping fix
73
+ for this since the beginning, one module over, which is exactly why the
74
+ helper now lives in one place both modules share.
75
+
76
+ - `dp db describe` reported `Size 0 B` and no row estimate for every ordinary
77
+ table, matview and view. All five header size aggregates read from
78
+ `pg_partition_tree()`, which returns no rows for a relation that is neither
79
+ partitioned nor a partition; `SUM` over no rows is `NULL`, and a
80
+ `COALESCE(..., 0)` inside each subquery presented that as a confident zero.
81
+ Partitioned tables had the mirror-image bug and double-counted rows, because
82
+ `reltuples` on an analyzed parent already aggregates its partitions.
83
+
84
+ - `dp db role show` under-reported a role's privileges. The recursive
85
+ membership walk collapsed duplicate paths with `DISTINCT ON`, and when an
86
+ ancestor was reachable by several equal-depth paths the surviving row — and
87
+ therefore whether `INHERIT` was honoured — was decided by scan order.
88
+ PostgreSQL reported the privilege as held while dataplat showed it missing.
89
+
90
+ - `dp db long-queries` could return an empty snapshot. The scan measured age
91
+ with `now()`, which is `transaction_timestamp()`, so any query that started
92
+ after the scanning transaction began had a negative age and failed the
93
+ threshold filter silently.
94
+
95
+ ### Added
96
+
97
+ - An integration suite that executes the database service layer against a real
98
+ PostgreSQL, in CI and locally (`tests/integration/`, see the README). It
99
+ found all four fixes above, plus seven further defects now pinned as strict
100
+ expected failures. Releases are gated on it: `build` will not run until the
101
+ suite passes.
102
+
3
103
  ## 0.2.0
4
104
 
5
105
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataplat
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: One command to manage any shape of data platform: databases, ingestion, BI, cloud, and CI.
5
5
  Project-URL: Homepage, https://github.com/hanslemm/dataplat
6
6
  Project-URL: Repository, https://github.com/hanslemm/dataplat
@@ -337,12 +337,64 @@ uv run mypy dataplat
337
337
  CI runs those four across Python 3.12 and 3.13 — the floor the wheel
338
338
  advertises as well as the pinned dev version.
339
339
 
340
+ ### Integration tests against a real PostgreSQL
341
+
342
+ Most of the suite drives a fake database cursor. That proves a code path
343
+ *called* `execute`, never that the SQL it built is valid. The tests in
344
+ `tests/integration/` close that gap: they run the real statements against a
345
+ live PostgreSQL and check the results, so an invalid column reference or a
346
+ broken `GRANT` fails here instead of on your warehouse.
347
+
348
+ `uv run pytest` stays green **without Docker** — the suite skips itself when
349
+ no server is reachable. You only need the container to actually exercise it:
350
+
351
+ ```bash
352
+ docker run -d --name dp-pg-test -p 55432:5432 \
353
+ -e POSTGRES_PASSWORD=postgres \
354
+ -e POSTGRES_DB=dataplat_test \
355
+ postgres:16 -c shared_preload_libraries=pg_stat_statements
356
+
357
+ # Once per database: pg_stat_statements is a per-database extension.
358
+ docker exec dp-pg-test psql -U postgres -d dataplat_test \
359
+ -c 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'
360
+
361
+ DP_TEST_PG_REQUIRED=1 uv run pytest -m integration
362
+ ```
363
+
364
+ The `-c shared_preload_libraries=pg_stat_statements` is not optional for full
365
+ coverage: without it the extension installs but every *read* of the view fails
366
+ with `pg_stat_statements must be loaded via shared_preload_libraries`, so
367
+ `dp db long-queries --history` stays untested.
368
+
369
+ | Variable | Purpose |
370
+ | --- | --- |
371
+ | `DP_TEST_PG_DSN` | Connection string for the test server. Default: `postgresql://postgres:postgres@127.0.0.1:55432/dataplat_test`. |
372
+ | `DP_TEST_PG_REQUIRED` | Truthy ⇒ an unreachable server is a hard **error**. Unset ⇒ the tests skip. |
373
+
374
+ Set `DP_TEST_PG_REQUIRED=1` whenever a skip would be a lie — that is, always
375
+ in CI. Without it a broken container makes the tests vanish and the run goes
376
+ green having validated no SQL at all. CI runs the integration job with it set,
377
+ against a pinned PostgreSQL major, and the release workflow runs the same job
378
+ as a gate before publishing.
379
+
380
+ Each test runs in a transaction that is rolled back afterwards, so tests never
381
+ see each other's objects and nothing survives a failed run. To select the
382
+ fast, database-free subset explicitly, use `-m "not integration"`.
383
+
384
+ **Known gap: Redshift.** Redshift cannot be containerized, so every
385
+ Redshift-specific code path remains fake-tested only — its SQL is generated and
386
+ asserted against a fake cursor, never executed. Treat changes to Redshift
387
+ paths as unverified by CI and test them against a real cluster.
388
+
340
389
  ## Releasing
341
390
 
342
391
  Bump `[project].version`, then tag `X.Y.Z` (bare semver, no `v` prefix) on
343
392
  `main`. The release workflow refuses to publish unless the tag matches that
344
- version, and runs the full check matrix first; only then does GitHub Actions
345
- build and publish to PyPI via Trusted Publishing. Commits follow
393
+ version, and runs the full check matrix *plus* the integration suite against a
394
+ real PostgreSQL first; only then does GitHub Actions build and publish to PyPI
395
+ via Trusted Publishing. A published version can be yanked but never replaced,
396
+ so the extra minutes buy a guarantee that the shipped SQL has at least been
397
+ parsed by a server. Commits follow
346
398
  [Conventional Commits](https://www.conventionalcommits.org/).
347
399
 
348
400
  ## License
@@ -292,12 +292,64 @@ uv run mypy dataplat
292
292
  CI runs those four across Python 3.12 and 3.13 — the floor the wheel
293
293
  advertises as well as the pinned dev version.
294
294
 
295
+ ### Integration tests against a real PostgreSQL
296
+
297
+ Most of the suite drives a fake database cursor. That proves a code path
298
+ *called* `execute`, never that the SQL it built is valid. The tests in
299
+ `tests/integration/` close that gap: they run the real statements against a
300
+ live PostgreSQL and check the results, so an invalid column reference or a
301
+ broken `GRANT` fails here instead of on your warehouse.
302
+
303
+ `uv run pytest` stays green **without Docker** — the suite skips itself when
304
+ no server is reachable. You only need the container to actually exercise it:
305
+
306
+ ```bash
307
+ docker run -d --name dp-pg-test -p 55432:5432 \
308
+ -e POSTGRES_PASSWORD=postgres \
309
+ -e POSTGRES_DB=dataplat_test \
310
+ postgres:16 -c shared_preload_libraries=pg_stat_statements
311
+
312
+ # Once per database: pg_stat_statements is a per-database extension.
313
+ docker exec dp-pg-test psql -U postgres -d dataplat_test \
314
+ -c 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'
315
+
316
+ DP_TEST_PG_REQUIRED=1 uv run pytest -m integration
317
+ ```
318
+
319
+ The `-c shared_preload_libraries=pg_stat_statements` is not optional for full
320
+ coverage: without it the extension installs but every *read* of the view fails
321
+ with `pg_stat_statements must be loaded via shared_preload_libraries`, so
322
+ `dp db long-queries --history` stays untested.
323
+
324
+ | Variable | Purpose |
325
+ | --- | --- |
326
+ | `DP_TEST_PG_DSN` | Connection string for the test server. Default: `postgresql://postgres:postgres@127.0.0.1:55432/dataplat_test`. |
327
+ | `DP_TEST_PG_REQUIRED` | Truthy ⇒ an unreachable server is a hard **error**. Unset ⇒ the tests skip. |
328
+
329
+ Set `DP_TEST_PG_REQUIRED=1` whenever a skip would be a lie — that is, always
330
+ in CI. Without it a broken container makes the tests vanish and the run goes
331
+ green having validated no SQL at all. CI runs the integration job with it set,
332
+ against a pinned PostgreSQL major, and the release workflow runs the same job
333
+ as a gate before publishing.
334
+
335
+ Each test runs in a transaction that is rolled back afterwards, so tests never
336
+ see each other's objects and nothing survives a failed run. To select the
337
+ fast, database-free subset explicitly, use `-m "not integration"`.
338
+
339
+ **Known gap: Redshift.** Redshift cannot be containerized, so every
340
+ Redshift-specific code path remains fake-tested only — its SQL is generated and
341
+ asserted against a fake cursor, never executed. Treat changes to Redshift
342
+ paths as unverified by CI and test them against a real cluster.
343
+
295
344
  ## Releasing
296
345
 
297
346
  Bump `[project].version`, then tag `X.Y.Z` (bare semver, no `v` prefix) on
298
347
  `main`. The release workflow refuses to publish unless the tag matches that
299
- version, and runs the full check matrix first; only then does GitHub Actions
300
- build and publish to PyPI via Trusted Publishing. Commits follow
348
+ version, and runs the full check matrix *plus* the integration suite against a
349
+ real PostgreSQL first; only then does GitHub Actions build and publish to PyPI
350
+ via Trusted Publishing. A published version can be yanked but never replaced,
351
+ so the extra minutes buy a guarantee that the shipped SQL has at least been
352
+ parsed by a server. Commits follow
301
353
  [Conventional Commits](https://www.conventionalcommits.org/).
302
354
 
303
355
  ## License