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.
- dataplat-0.2.2/.github/workflows/ci.yml +171 -0
- dataplat-0.2.2/.github/workflows/release.yml +227 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/CHANGELOG.md +100 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/PKG-INFO +55 -3
- {dataplat-0.2.0 → dataplat-0.2.2}/README.md +54 -2
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/dbt_orphans.py +63 -15
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/role.py +19 -2
- dataplat-0.2.2/dataplat/services/db/_like.py +31 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/describe.py +98 -20
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/long_queries.py +39 -4
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/orphans.py +85 -10
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/role.py +86 -12
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/top_tables.py +3 -7
- {dataplat-0.2.0 → dataplat-0.2.2}/pyproject.toml +7 -1
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_role.py +5 -0
- dataplat-0.2.2/tests/integration/__init__.py +6 -0
- dataplat-0.2.2/tests/integration/conftest.py +528 -0
- dataplat-0.2.2/tests/integration/test_describe_pg.py +1614 -0
- dataplat-0.2.2/tests/integration/test_harness.py +733 -0
- dataplat-0.2.2/tests/integration/test_long_queries_pg.py +690 -0
- dataplat-0.2.2/tests/integration/test_orphans_pg.py +841 -0
- dataplat-0.2.2/tests/integration/test_roles_pg.py +1357 -0
- dataplat-0.2.2/tests/integration/test_top_tables_pg.py +320 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_describe.py +29 -1
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_long_queries.py +94 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_orphans.py +84 -3
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_role.py +72 -18
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_top_tables.py +5 -5
- {dataplat-0.2.0 → dataplat-0.2.2}/uv.lock +1 -1
- dataplat-0.2.0/.github/workflows/ci.yml +0 -48
- dataplat-0.2.0/.github/workflows/release.yml +0 -119
- {dataplat-0.2.0 → dataplat-0.2.2}/.gitignore +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/.python-version +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/LICENSE +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_lazy.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_missing.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_options.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_prompt.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/_render.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/bi/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/bi/app.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/bi/superset.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/app.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/github/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/github/app.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ci/github/runner.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/app.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/_common.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/app.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/rds.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/redshift.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/cloud/aws/secrets.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/config.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/_common.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/_report.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/describe.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/long_queries.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/role_create.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/role_drop.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/role_list.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/db/top_tables.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/_common.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/_cursor.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/_resource.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/app.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/connections.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/definitions.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/destinations.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/enums.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/jobs.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/sources.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/tags.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/templates.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/tui.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/airbyte/workspaces.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/ingest/app.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/open.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/cli/status.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/deps.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/envrc.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/errors.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/core/registry.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/main.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/_resource.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/client.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/connections.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/definitions.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/destinations.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/jobs.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/sources.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/tags.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/airbyte/workspaces.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/aws/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/aws/auth.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/connection.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/role_admin.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/role_dialects.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/db/targets.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/superset/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/dataplat/services/superset/client.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_airbyte_commands.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_airbyte_cursor_logic.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_airbyte_guards.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_airbyte_tui.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_aws_secrets.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_aws_secrets_write.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_cli_smoke.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_config.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_db_common.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_db_long_queries.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_db_query.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_dbt_orphans.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_describe.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_github_runner.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_missing_deps.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_open.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_prompt.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_rds.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_redshift.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_regression.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_render.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_role_create.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_role_drop.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_status.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_superset.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/cli/test_top_tables.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/conftest.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/core/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/core/test_deps.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/core/test_envrc.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/core/test_registry.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_client.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_connections.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_definitions.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_destinations.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_jobs.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_sources.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/airbyte/test_workspaces.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/aws/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/aws/test_auth.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/__init__.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_connection.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_role_admin.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_role_dialects.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/db/test_targets.py +0 -0
- {dataplat-0.2.0 → dataplat-0.2.2}/tests/services/superset/__init__.py +0 -0
- {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.
|
|
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
|
|
345
|
-
build and publish to PyPI
|
|
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
|
|
300
|
-
build and publish to PyPI
|
|
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
|