dataplat 0.2.3__tar.gz → 0.4.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.
Files changed (195) hide show
  1. {dataplat-0.2.3 → dataplat-0.4.0}/.github/workflows/ci.yml +21 -0
  2. {dataplat-0.2.3 → dataplat-0.4.0}/.github/workflows/release.yml +13 -0
  3. {dataplat-0.2.3 → dataplat-0.4.0}/CHANGELOG.md +129 -0
  4. {dataplat-0.2.3 → dataplat-0.4.0}/CONTRIBUTING.md +116 -8
  5. dataplat-0.4.0/PKG-INFO +726 -0
  6. dataplat-0.4.0/README.md +677 -0
  7. dataplat-0.4.0/dataplat/cli/_exit.py +68 -0
  8. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/_lazy.py +104 -3
  9. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/bi/superset.py +26 -27
  10. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/cloud/aws/_common.py +48 -3
  11. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/cloud/aws/rds.py +27 -0
  12. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/cloud/aws/redshift.py +36 -0
  13. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/cloud/aws/secrets.py +90 -5
  14. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/config.py +147 -16
  15. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/__init__.py +105 -15
  16. dataplat-0.4.0/dataplat/cli/db/_common.py +635 -0
  17. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/dbt_orphans.py +91 -19
  18. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/describe.py +73 -13
  19. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/long_queries.py +69 -4
  20. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/role.py +29 -12
  21. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/role_create.py +26 -12
  22. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/role_drop.py +26 -12
  23. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/role_list.py +24 -12
  24. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/top_tables.py +195 -29
  25. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/_common.py +8 -8
  26. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/connections.py +21 -19
  27. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/definitions.py +6 -9
  28. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/tags.py +6 -9
  29. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/templates.py +5 -8
  30. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/workspaces.py +6 -9
  31. dataplat-0.4.0/dataplat/cli/status.py +544 -0
  32. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/core/deps.py +63 -1
  33. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/core/envrc.py +81 -10
  34. dataplat-0.4.0/dataplat/core/errors.py +94 -0
  35. dataplat-0.4.0/dataplat/core/registry.py +381 -0
  36. dataplat-0.4.0/dataplat/core/trace.py +332 -0
  37. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/main.py +22 -3
  38. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/client.py +95 -3
  39. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/aws/auth.py +35 -0
  40. dataplat-0.4.0/dataplat/services/db/capabilities.py +305 -0
  41. dataplat-0.4.0/dataplat/services/db/connection.py +493 -0
  42. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/describe.py +582 -39
  43. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/orphans.py +20 -4
  44. dataplat-0.4.0/dataplat/services/db/top_tables.py +329 -0
  45. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/superset/client.py +50 -1
  46. {dataplat-0.2.3 → dataplat-0.4.0}/pyproject.toml +27 -3
  47. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_airbyte_commands.py +47 -6
  48. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_aws_secrets.py +147 -0
  49. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_cli_smoke.py +282 -3
  50. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_config.py +162 -0
  51. dataplat-0.4.0/tests/cli/test_db_common.py +851 -0
  52. dataplat-0.4.0/tests/cli/test_db_long_queries.py +376 -0
  53. dataplat-0.4.0/tests/cli/test_db_query.py +640 -0
  54. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_dbt_orphans.py +187 -2
  55. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_describe.py +349 -0
  56. dataplat-0.4.0/tests/cli/test_exit.py +129 -0
  57. dataplat-0.4.0/tests/cli/test_rds.py +1001 -0
  58. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_redshift.py +71 -0
  59. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_regression.py +4 -1
  60. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_role.py +169 -0
  61. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_role_create.py +82 -0
  62. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_role_drop.py +63 -0
  63. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_status.py +555 -0
  64. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_superset.py +64 -4
  65. dataplat-0.4.0/tests/cli/test_top_tables.py +472 -0
  66. {dataplat-0.2.3 → dataplat-0.4.0}/tests/core/test_deps.py +85 -1
  67. {dataplat-0.2.3 → dataplat-0.4.0}/tests/core/test_envrc.py +82 -0
  68. dataplat-0.4.0/tests/core/test_errors.py +128 -0
  69. dataplat-0.4.0/tests/core/test_registry.py +495 -0
  70. dataplat-0.4.0/tests/core/test_trace.py +373 -0
  71. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/conftest.py +17 -0
  72. dataplat-0.4.0/tests/integration/duckdb/__init__.py +1 -0
  73. dataplat-0.4.0/tests/integration/duckdb/conftest.py +339 -0
  74. dataplat-0.4.0/tests/integration/duckdb/test_duckdb_services.py +1411 -0
  75. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/test_describe_pg.py +108 -10
  76. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/test_harness.py +94 -0
  77. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/test_orphans_pg.py +96 -1
  78. dataplat-0.4.0/tests/services/airbyte/test_client.py +265 -0
  79. dataplat-0.4.0/tests/services/aws/test_auth.py +290 -0
  80. dataplat-0.4.0/tests/services/db/test_capabilities.py +225 -0
  81. dataplat-0.4.0/tests/services/db/test_connection.py +528 -0
  82. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/db/test_describe.py +700 -0
  83. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/db/test_orphans.py +58 -0
  84. dataplat-0.4.0/tests/services/db/test_top_tables.py +372 -0
  85. dataplat-0.4.0/tests/services/superset/test_client.py +144 -0
  86. {dataplat-0.2.3 → dataplat-0.4.0}/uv.lock +37 -3
  87. dataplat-0.2.3/PKG-INFO +0 -412
  88. dataplat-0.2.3/README.md +0 -367
  89. dataplat-0.2.3/dataplat/cli/db/_common.py +0 -136
  90. dataplat-0.2.3/dataplat/cli/status.py +0 -331
  91. dataplat-0.2.3/dataplat/core/errors.py +0 -23
  92. dataplat-0.2.3/dataplat/core/registry.py +0 -110
  93. dataplat-0.2.3/dataplat/services/db/connection.py +0 -169
  94. dataplat-0.2.3/dataplat/services/db/top_tables.py +0 -189
  95. dataplat-0.2.3/tests/cli/test_db_common.py +0 -130
  96. dataplat-0.2.3/tests/cli/test_db_long_queries.py +0 -217
  97. dataplat-0.2.3/tests/cli/test_db_query.py +0 -199
  98. dataplat-0.2.3/tests/cli/test_rds.py +0 -297
  99. dataplat-0.2.3/tests/cli/test_top_tables.py +0 -240
  100. dataplat-0.2.3/tests/core/test_registry.py +0 -96
  101. dataplat-0.2.3/tests/services/airbyte/test_client.py +0 -43
  102. dataplat-0.2.3/tests/services/aws/test_auth.py +0 -107
  103. dataplat-0.2.3/tests/services/db/test_connection.py +0 -142
  104. dataplat-0.2.3/tests/services/db/test_top_tables.py +0 -125
  105. dataplat-0.2.3/tests/services/superset/test_client.py +0 -54
  106. {dataplat-0.2.3 → dataplat-0.4.0}/.gitignore +0 -0
  107. {dataplat-0.2.3 → dataplat-0.4.0}/.python-version +0 -0
  108. {dataplat-0.2.3 → dataplat-0.4.0}/LICENSE +0 -0
  109. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/__init__.py +0 -0
  110. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/__init__.py +0 -0
  111. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/_missing.py +0 -0
  112. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/_options.py +0 -0
  113. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/_prompt.py +0 -0
  114. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/_render.py +0 -0
  115. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/bi/__init__.py +0 -0
  116. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/bi/app.py +0 -0
  117. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ci/__init__.py +0 -0
  118. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ci/app.py +0 -0
  119. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ci/github/__init__.py +0 -0
  120. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ci/github/app.py +0 -0
  121. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ci/github/runner.py +0 -0
  122. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/cloud/__init__.py +0 -0
  123. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/cloud/app.py +0 -0
  124. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/cloud/aws/__init__.py +0 -0
  125. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/cloud/aws/app.py +0 -0
  126. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/db/_report.py +0 -0
  127. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/__init__.py +0 -0
  128. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/__init__.py +0 -0
  129. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/_cursor.py +0 -0
  130. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/_resource.py +0 -0
  131. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/app.py +0 -0
  132. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/destinations.py +0 -0
  133. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/enums.py +0 -0
  134. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/jobs.py +0 -0
  135. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/sources.py +0 -0
  136. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/airbyte/tui.py +0 -0
  137. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/ingest/app.py +0 -0
  138. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/cli/open.py +0 -0
  139. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/core/__init__.py +0 -0
  140. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/__init__.py +0 -0
  141. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/__init__.py +0 -0
  142. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/_resource.py +0 -0
  143. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/connections.py +0 -0
  144. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/definitions.py +0 -0
  145. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/destinations.py +0 -0
  146. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/jobs.py +0 -0
  147. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/sources.py +0 -0
  148. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/tags.py +0 -0
  149. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/airbyte/workspaces.py +0 -0
  150. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/aws/__init__.py +0 -0
  151. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/__init__.py +0 -0
  152. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/_like.py +0 -0
  153. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/long_queries.py +0 -0
  154. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/role.py +0 -0
  155. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/role_admin.py +0 -0
  156. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/role_dialects.py +0 -0
  157. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/db/targets.py +0 -0
  158. {dataplat-0.2.3 → dataplat-0.4.0}/dataplat/services/superset/__init__.py +0 -0
  159. {dataplat-0.2.3 → dataplat-0.4.0}/tests/__init__.py +0 -0
  160. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/__init__.py +0 -0
  161. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_airbyte_cursor_logic.py +0 -0
  162. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_airbyte_guards.py +0 -0
  163. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_airbyte_tui.py +0 -0
  164. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_aws_secrets_write.py +0 -0
  165. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_github_runner.py +0 -0
  166. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_missing_deps.py +0 -0
  167. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_open.py +0 -0
  168. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_prompt.py +0 -0
  169. {dataplat-0.2.3 → dataplat-0.4.0}/tests/cli/test_render.py +0 -0
  170. {dataplat-0.2.3 → dataplat-0.4.0}/tests/conftest.py +0 -0
  171. {dataplat-0.2.3 → dataplat-0.4.0}/tests/core/__init__.py +0 -0
  172. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/__init__.py +0 -0
  173. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/redshift/__init__.py +0 -0
  174. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/redshift/conftest.py +0 -0
  175. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/redshift/test_conformance.py +0 -0
  176. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/redshift/test_harness.py +0 -0
  177. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/test_long_queries_pg.py +0 -0
  178. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/test_roles_pg.py +0 -0
  179. {dataplat-0.2.3 → dataplat-0.4.0}/tests/integration/test_top_tables_pg.py +0 -0
  180. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/__init__.py +0 -0
  181. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/airbyte/__init__.py +0 -0
  182. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/airbyte/test_connections.py +0 -0
  183. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/airbyte/test_definitions.py +0 -0
  184. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/airbyte/test_destinations.py +0 -0
  185. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/airbyte/test_jobs.py +0 -0
  186. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/airbyte/test_sources.py +0 -0
  187. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/airbyte/test_workspaces.py +0 -0
  188. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/aws/__init__.py +0 -0
  189. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/db/__init__.py +0 -0
  190. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/db/test_long_queries.py +0 -0
  191. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/db/test_role.py +0 -0
  192. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/db/test_role_admin.py +0 -0
  193. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/db/test_role_dialects.py +0 -0
  194. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/db/test_targets.py +0 -0
  195. {dataplat-0.2.3 → dataplat-0.4.0}/tests/services/superset/__init__.py +0 -0
@@ -32,6 +32,10 @@ jobs:
32
32
  - name: Setup Python
33
33
  run: uv python install ${{ matrix.python-version }}
34
34
 
35
+ # --all-extras is what installs psycopg (the "db" extra) and duckdb (the
36
+ # "duckdb" extra). The DuckDB step below needs the latter and has no skip
37
+ # path, so dropping this flag turns into a hard failure rather than a
38
+ # quietly shrinking test run.
35
39
  - name: Install dependencies
36
40
  run: uv sync --group dev --all-extras
37
41
 
@@ -53,6 +57,23 @@ jobs:
53
57
  - name: Pytest (unit)
54
58
  run: uv run pytest -m "not integration"
55
59
 
60
+ # The third engine's real-SQL tier, and the only one that belongs in this
61
+ # job: DuckDB is a library, so it needs no container, no credentials and no
62
+ # wait loop -- just the extra `uv sync` already installed. Running it in
63
+ # both matrix legs is the point, since an embedded engine is the one place
64
+ # an interpreter difference could plausibly show up in a database.
65
+ #
66
+ # Selected by path rather than by marker. tests/integration/duckdb/ sits
67
+ # under tests/integration/, so its conftest chain stamps every test with
68
+ # the `integration` marker the step above deselects; a `duckdb` marker
69
+ # would have to be registered in pyproject.toml, and one line here is
70
+ # cheaper than a marker that then has to be kept in sync with a directory.
71
+ # These tests are consequently also collected by the `integration` job
72
+ # below -- harmless, since they run in under two seconds and touch no
73
+ # server, but *this* is where DuckDB is proved, on both interpreters.
74
+ - name: Pytest (DuckDB integration)
75
+ run: uv run pytest tests/integration/duckdb
76
+
56
77
  # A separate job, not extra steps in the matrix above, for two reasons: the
57
78
  # unit legs are the fast signal and must not queue behind a container boot,
58
79
  # and a database per Python version would buy nothing -- the SQL these tests
@@ -50,6 +50,9 @@ jobs:
50
50
  print(f"Tag '{tag}' matches [project].version.")
51
51
  PY
52
52
 
53
+ # --all-extras installs psycopg ("db") and duckdb ("duckdb"). The wheel
54
+ # advertises both as extras, so both are installed before the tag is
55
+ # allowed through.
53
56
  - name: Install dependencies
54
57
  run: uv sync --group dev --all-extras
55
58
 
@@ -67,6 +70,16 @@ jobs:
67
70
  - name: Pytest (unit)
68
71
  run: uv run pytest -m "not integration"
69
72
 
73
+ # Also mirrors ci.yml, and it belongs in the tag gate for the same reason
74
+ # the `integration` job does: a version can be yanked but never replaced,
75
+ # and `verify`'s fake cursors cannot tell whether a `duckdb_*()` query
76
+ # parses. This one costs no container and runs on both interpreters, so
77
+ # there is no argument for leaving the third engine's SQL unexecuted at
78
+ # publish time. Path-selected because the tier inherits the `integration`
79
+ # marker the step above deselects -- see the note in ci.yml.
80
+ - name: Pytest (DuckDB integration)
81
+ run: uv run pytest tests/integration/duckdb
82
+
70
83
  # This gate exists because the alternative is shipping SQL no server has ever
71
84
  # parsed. dataplat's db services are thousands of lines of generated SQL, and
72
85
  # the fake-cursor tests in `verify` prove only that execute() was called -- an
@@ -1,5 +1,134 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Added
6
+
7
+ - **DuckDB is a supported engine.** Set `<NAME>_ENGINE=duckdb` and
8
+ `<NAME>_PATH=/path/to.duckdb` (or `:memory:`), and install
9
+ `dataplat[duckdb]`. `<NAME>_DATABASE` works as a fallback for the path, and
10
+ `<NAME>_READ_ONLY=1` opens the file read-only.
11
+
12
+ DuckDB is not a smaller PostgreSQL — it runs inside the `dp` process, backed by
13
+ a file, with a single implicit user — so only the commands that mean something
14
+ there are available:
15
+
16
+ | command | DuckDB | why |
17
+ | --- | --- | --- |
18
+ | `db query` | works | |
19
+ | `db describe` | works | sizes and view definitions come from DuckDB's own catalog |
20
+ | `db top-tables` | works | sizes are DuckDB estimates, not comparable to PostgreSQL's |
21
+ | `db role *` | refused | there are no users or roles to describe |
22
+ | `db long-queries`, `db kill` | refused | in-process: there are no other sessions |
23
+ | `db dbt-orphans` | refused | it works by renaming, and DuckDB refuses to rename a relation a view depends on |
24
+
25
+ A refused command exits 2 and says which engine and why. These are properties
26
+ of the database, not gaps — the messages say so, because a user deserves to
27
+ know a thing can never work rather than assume it is coming.
28
+
29
+ `db describe` reports what DuckDB has no concept of — materialized views,
30
+ partitions, triggers, row-level security, privileges — as not applicable, with
31
+ the reason, rather than as empty sections that would read as "you have none".
32
+
33
+ Unlike PostgreSQL (needs a container) and Redshift (needs a cluster), the
34
+ DuckDB test tier needs nothing at all, so it never skips: this is the first
35
+ dialect with unconditional executing coverage in CI, and the second real-SQL
36
+ target the shared queries have ever been checked against.
37
+
38
+ ### Fixed
39
+
40
+ - `dp config doctor` reported failures for a valid DuckDB target: it demanded
41
+ `<NAME>_HOST`, `<NAME>_USER` and `<NAME>_PASSWORD`, then failed the connection
42
+ check by dialing a server that does not exist. It now asks for the variables
43
+ the engine actually uses, and probes a DuckDB target by opening its file —
44
+ which is the health check.
45
+
46
+ ## 0.3.0
47
+
48
+ ### Changed
49
+
50
+ - **Exit codes now say what went wrong.** Every failure used to be `1`, so a
51
+ wrapper script could not tell "your config is wrong" from "the warehouse is
52
+ down" — and only one of those is worth retrying. Typed failures now carry
53
+ their own code: `2` invalid input, `3` configuration, `4` authentication,
54
+ `5` external service. `0`, `1` and `2` keep their conventional meanings, and
55
+ `2` is deliberately shared with Click's own usage error, because
56
+ `--format nope` and `-t nosuchtarget` are one condition to the caller.
57
+
58
+ An unreachable warehouse exits `5`, since that is the retryable case. A bad
59
+ statement against a reachable server stays `1`: retrying a syntax error would
60
+ fail identically forever. Untyped failures and a declined confirmation also
61
+ stay `1`. The full table is in the README.
62
+
63
+ **Scripts that branch on a non-zero exit will see new numbers.** Anything
64
+ testing `== 1` for a config or auth problem needs updating.
65
+
66
+ - **`dp db dbt-orphans` is more aggressive.** Its "which models are live" query
67
+ interpolated the dbt project name into a `LIKE` pattern without escaping it,
68
+ and dbt project names are snake_case — so the `_` in `my_project` matched any
69
+ character and a *sibling* project's models (`my2project`) sharing the
70
+ `dbt_artifacts` schema were counted as live. The same applied to
71
+ `DP_DBT_INVOCATION_COMMAND`, where `_` is ordinary in a dbt selector.
72
+
73
+ The direction is the point: an over-large live set makes **fewer** objects look
74
+ orphaned. Escaping it shrinks the live set, so dbt-orphans will now rename —
75
+ and after the grace period drop — objects it previously left alone. Run
76
+ `dp db dbt-orphans` (dry-run is the default) and read the plan before you
77
+ `--no-dry-run` the first time after upgrading.
78
+
79
+ - **`dp status` runs its checks concurrently**: 40.7s to 10.4s with five
80
+ targets, one unreachable. Sections run in parallel and each database target is
81
+ probed in parallel within them, which is where the time actually went — a 10s
82
+ connect timeout per target was paid serially. Key order and section order are
83
+ unchanged, because both pools iterate the declared mapping rather than
84
+ completion order. The AWS section stays serial and last: it may hand the
85
+ terminal to an interactive `aws sso login`.
86
+
87
+ ### Added
88
+
89
+ - **`--verbose` (or `DP_VERBOSE=1`) shows what dataplat actually sent** — SQL
90
+ statements, HTTP requests with status and duration, and AWS service calls.
91
+ It writes to stderr only, so `--json` and `--format csv` stay pipeable, and
92
+ everything passes through a redactor: passwords, secret values, bearer tokens
93
+ and API keys are never traced. Parameter values and response bodies are not
94
+ traced either — those are the data, not the request.
95
+
96
+ - **Third-party command areas.** `dp` discovers areas declared in the
97
+ `dataplat.areas` entry-point group, so a package can add a command area
98
+ without a change here. Discovery reads only the entry-point metadata, never
99
+ imports the plugin, so `dp --version` and `dp --help` stay import-free and
100
+ fast. A plugin that fails to import warns on stderr and leaves the built-in
101
+ areas working; it cannot shadow a built-in area.
102
+
103
+ - `dp config doctor` warns when a loaded `.envrc` value still contains an
104
+ unexpanded `$VAR`. dataplat does not run a shell, so
105
+ `export PGHOST=$DB_HOST` loads the literal text — which then surfaces as a
106
+ baffling connection failure rather than as the configuration mistake it is.
107
+
108
+ - Shell completion is documented (`dp --install-completion`). It always worked;
109
+ the README never said so.
110
+
111
+ ### Fixed
112
+
113
+ - `dp db query --format json|csv` could emit output that would not parse. The
114
+ progress spinner painted to stdout, which was invisible while Rich only did
115
+ that for a real terminal — the frames are erased — but `FORCE_COLOR` makes
116
+ Rich treat a pipe as a terminal too, and then the escape sequences ended up in
117
+ the redirected file. The spinner now follows the same sink as the notices.
118
+
119
+ - `dp db describe` reported the owner's grant option two different ways
120
+ depending on whether you asked about a schema or a relation. PostgreSQL grants
121
+ an owner every grant option implicitly and never records it in the ACL, so
122
+ reading the ACL reported "cannot delegate" about a role that demonstrably can.
123
+ Schema privileges now agree with relation privileges.
124
+
125
+ - The `Operating System :: OS Independent` classifier was an overclaim and has
126
+ been narrowed. Four things break on Windows: `dp config init` creates a
127
+ symlink, the dependency auto-install re-execs through `os.execvp`, the runner
128
+ commands shell out to `docker` with a POSIX default workdir, and the
129
+ credentials file is written with a `0o600` mode Windows ignores — after which
130
+ dataplat reports its own file as insecurely permissioned. CI tests Linux only.
131
+
3
132
  ## 0.2.3
4
133
 
5
134
  Redshift-only fixes. Nothing changes for PostgreSQL targets.
@@ -19,8 +19,10 @@ uv run ruff format --check .
19
19
  uv run mypy dataplat
20
20
  ```
21
21
 
22
- `uv run pytest` is green without Docker: the database-backed tests skip. To run
23
- them, start a server and point the suite at it:
22
+ `uv run pytest` is green without Docker: the PostgreSQL-backed tests skip. The
23
+ DuckDB ones do not — there is no server for them to miss, so they always run
24
+ (see below). To exercise the PostgreSQL half, start a server and point the suite
25
+ at it:
24
26
 
25
27
  ```bash
26
28
  docker run -d --name dp-pg-test \
@@ -30,7 +32,7 @@ docker exec dp-pg-test psql -U postgres -d dataplat_test \
30
32
  -c 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'
31
33
 
32
34
  DP_TEST_PG_REQUIRED=1 uv run pytest # everything
33
- uv run pytest -m "not integration" # skip the database half
35
+ uv run pytest -m "not integration" # deselect the real-database tiers
34
36
  docker rm -f -v dp-pg-test # -v, or the volume dangles
35
37
  ```
36
38
 
@@ -40,6 +42,40 @@ and still report success.
40
42
 
41
43
  Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
42
44
 
45
+ ## Testing against DuckDB
46
+
47
+ There is nothing to provision. DuckDB is a library that opens a file inside the
48
+ test process, so the fixtures in `tests/integration/duckdb/` build a throwaway
49
+ database in pytest's `tmp_path`:
50
+
51
+ ```bash
52
+ uv run pytest tests/integration/duckdb # ~1s, no server, no environment
53
+ ```
54
+
55
+ This tier deliberately has **none** of the availability machinery the other two
56
+ have: no DSN variable, no `_REQUIRED` switch, no disposability gate, no
57
+ read-only guard, and no skip path. Its `conftest.py` argues each omission at
58
+ length; the short version is that `DP_TEST_*_REQUIRED` exists because a
59
+ silently-skipping tier reports green having executed none of the SQL it was
60
+ written to validate — and here there is nothing that *can* be unavailable. The
61
+ one thing that can be missing is the `duckdb` package, which is a broken dev
62
+ environment rather than absent infrastructure (`uv sync --group dev
63
+ --all-extras`), so it is a hard collection error carrying the install hint,
64
+ never a skip.
65
+
66
+ It carries the `integration` marker, because everything under
67
+ `tests/integration/` does — that is the safety net which keeps
68
+ `-m 'not integration'` airtight. Select it by path, as above, when you want it
69
+ without the PostgreSQL container.
70
+
71
+ Isolation is a fresh database file per test rather than the PostgreSQL harness's
72
+ BEGIN/ROLLBACK. DuckDB *does* roll back DDL, so the trick would work
73
+ mechanically — but a table created inside an open transaction reports
74
+ `estimated_size = 0`, and `pragma_database_size()` reports `0 bytes` until a
75
+ `CHECKPOINT` that cannot run in a writing transaction, so every row-estimate and
76
+ size assertion would have passed against zeros. Both facts are pinned by tests,
77
+ so the decision can be re-checked instead of re-derived.
78
+
43
79
  ## Testing against a real Redshift cluster
44
80
 
45
81
  Redshift is a managed service, so there is no container and CI cannot cover it.
@@ -108,9 +144,58 @@ has taught nobody anything.
108
144
 
109
145
  ## Dialect changes: what counts as evidence
110
146
 
111
- `dataplat/services/db` targets PostgreSQL and Redshift. PostgreSQL has a real
112
- integration suite behind it. Redshift has none and cannot get one cheaply — it
113
- is a managed service, so there is no container to run in CI.
147
+ `dataplat/services/db` targets three engines, and they are not equally knowable:
148
+
149
+ | Engine | SQL really executed? | What it costs to find out |
150
+ | --- | --- | --- |
151
+ | PostgreSQL | yes, in CI | a container |
152
+ | DuckDB | yes, in CI — and its tier cannot skip | nothing: a library and a temp file |
153
+ | Redshift | **no** — generated and asserted, never run | a cluster you own; CI cannot have one |
154
+
155
+ That asymmetry is the whole reason this section exists, and it is now lopsided in
156
+ one direction only. PostgreSQL has a real integration suite behind it. So does
157
+ DuckDB, and DuckDB is the cheap case: there is no server to be unreachable and
158
+ nothing to provision, so a claim about it can always be checked *before* it is
159
+ made. Redshift has none and cannot get one cheaply — it is a managed service, so
160
+ there is no container to run in CI.
161
+
162
+ Which *commands* an engine refuses, and why, is declared in one place:
163
+ `dataplat/services/db/capabilities.py` — one entry per engine with no defaults,
164
+ so adding an engine and forgetting what it can do is a construction error there
165
+ rather than a wrong answer somewhere downstream. Change what an engine can be
166
+ asked there and nowhere else, and cite the code or the probe the entry rests on,
167
+ exactly as the existing ones do.
168
+
169
+ A command that still runs but has to leave a *section* out is the other case, and
170
+ it declares that next to the section (`services/db/describe.py`'s
171
+ `NotApplicable`) and prints it — `dp db describe` on DuckDB ends with the list of
172
+ sections the engine has no concept of, with a reason each. Either way the reason
173
+ is written once, next to the fact, and a refusal never says "not implemented"
174
+ about something the engine simply is not.
175
+
176
+ ### DuckDB: the rules apply, and the excuse does not
177
+
178
+ The evidence classes below apply to DuckDB unchanged in principle — same
179
+ ordering, same requirement to record in the code *why* a line is the way it is.
180
+ In practice you will never reach past the first one, because the strongest class
181
+ (0: confirmed against a real engine) is always available: `uv run pytest
182
+ tests/integration/duckdb` runs in about a second, needs no server, no
183
+ credentials and no container, and cannot skip.
184
+
185
+ So, plainly: **"I could not test it" is never an acceptable reason for a DuckDB
186
+ change.** If a DuckDB behaviour is in doubt, open a connection and find out —
187
+ then turn the probe into a test in `tests/integration/duckdb/`, so the next
188
+ person inherits the answer instead of the doubt. A comment recording an
189
+ *unverified* DuckDB assumption is not a legitimate outcome here the way it is for
190
+ Redshift; it is a probe someone declined to run.
191
+
192
+ The corollary applies to the capability matrix too. Every DuckDB `False` in
193
+ `capabilities.py` names something the engine does not have, and
194
+ `tests/integration/duckdb/test_duckdb_services.py` asserts the declaration
195
+ *alongside the missing catalog* — so a future DuckDB that grows `pg_roles` fails
196
+ the suite instead of quietly going on being refused.
197
+
198
+ ### Redshift: weighing evidence without a server
114
199
 
115
200
  For a while the rule was simply "don't touch SQL that runs on Redshift, because
116
201
  you can't test it." That is a good instinct and a bad rule. Applied literally it
@@ -159,10 +244,20 @@ comment is what lets the next person re-evaluate instead of rediscovering.
159
244
 
160
245
  - **Keep the engine constants split.** Never edit a `_*_SQL_REDSHIFT` constant
161
246
  to fix a PostgreSQL bug. If a shared statement needs to diverge, split it and
162
- leave the Redshift half byte-for-byte as it was.
247
+ leave the Redshift half byte-for-byte as it was. The DuckDB statements are
248
+ split the same way (`_*_SQL_DUCKDB`, `_DUCKDB_*_SQL`), with one difference: you
249
+ can run them, so a DuckDB constant you touched comes back through
250
+ `tests/integration/duckdb/`, not through inspection.
163
251
  - **Add a fake-cursor test** asserting what the Redshift branch emits. It is the
164
252
  only mechanism that covers that path at all, and it catches the common
165
- accident of "fixed both branches when I meant one".
253
+ accident of "fixed both branches when I meant one". DuckDB's equivalent is an
254
+ *executing* test — a fake cursor there proves less than the real engine does,
255
+ for the same effort.
256
+ - **Do not translate placeholders between engines.** psycopg binds `%s`; DuckDB
257
+ binds `?`. Rewriting one into the other means deciding which `%` in a
258
+ statement is a literal, and getting that wrong changes the SQL — so
259
+ `DuckDbCursor` passes statements through untouched, and DuckDB SQL is written
260
+ for DuckDB.
166
261
  - **Record the evidence in the code**, not just the commit message. A future
167
262
  reader deciding whether they may touch the line needs to see why it is the way
168
263
  it is.
@@ -175,3 +270,16 @@ never be NULL. A report that states something false is worse than one that
175
270
  admits a gap — especially a report someone is using for an audit. When the
176
271
  server will not tell you, say so, and say why in the same breath: a bare
177
272
  "unknown" reads as a tool defect.
273
+
274
+ `dp db top-tables` on DuckDB is the same rule applied to a whole column, and it
275
+ shows where the rule ends. DuckDB has no per-relation size function and no
276
+ catalog column carrying bytes, so every `size_bytes` there is `None` — never `0`,
277
+ because a table whose size cannot be read is not an empty table, and `--json`
278
+ consumers have to be able to tell those apart.
279
+
280
+ The table rendering then goes one step further and *drops* the Size and
281
+ "% of disk" columns instead of filling them with `—`, printing the basis for the
282
+ numbers it does show. Withdrawing a claim is not enough on its own when the gap
283
+ is an entire column: a report of dashes under a `0 B (0.0% of disk)` footer reads
284
+ as a broken tool, not as the engine's answer. Say what you cannot know, then
285
+ show what you can.