Skip to content

Publish local runs to analytics_dev on the real database - #18

Open
lassebenni wants to merge 5 commits into
mainfrom
feat/analytics-dev-schema
Open

Publish local runs to analytics_dev on the real database#18
lassebenni wants to merge 5 commits into
mainfrom
feat/analytics-dev-schema

Conversation

@lassebenni

@lassebenni lassebenni commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

What I built

A third database role, analytics_dev_user, that owns the analytics_dev schema and can only read everything else. Trainees get that credential and publish their in-progress mart to analytics_dev from a local Airflow run; the scheduled pipeline keeps analytics_user and stays the only writer of production analytics. The publish step also stamps the table it writes with the schema and time the rows came from, so a reader can tell a hand-run mart from a scheduled one.

Why this approach

analytics_dev already existed after #19, but analytics_user owned both analytics schemas. That means handing a trainee the credential to write analytics_dev also hands them ownership of production analytics, so the dev/prod split would have rested on people not typing the wrong schema name. Making it a separate role moves the boundary into Postgres, where it is enforced rather than agreed.

This keeps #19's SCHEMA_OWNERS structure exactly as it is, which is better than the two-list version I originally wrote. The whole change is three lines: a new role constant, adding it to ROLES, and pointing analytics_dev at it. The existing loop grants full access on owned schemas and read-only on the rest, so nothing else needed touching.

Rejected: one role with CREATE on analytics_dev and only USAGE on analytics. It works, but the same login then appears in both the trainee's .env and the scheduled pipeline's Key Vault secret, so rotating it for one breaks the other.

docker-compose.override.yml also loses its BACKEND_PG_HOST: db override. It pointed Airflow tasks at a Postgres container while uv run on the same machine wrote the real database, so the two documented ways of running the pipeline silently disagreed about the destination.

Contract impact

None for the backend. analytics.fct_postings keeps its shape and analytics_user keeps writing it. The new analytics_dev schema is trainee-facing only; app_user gains read access to it but nothing reads it yet.

How to run

# Build the three roles on a throwaway database and see the grants
docker run -d --name pg-probe -e POSTGRES_PASSWORD=probe -e POSTGRES_USER=admin \
  -p 55439:5432 postgres:16
POSTGRES_PASSWORD=probe ./scripts/db-setup.py --host localhost --port 55439 --admin-user admin

# Then, as the printed analytics_dev_user password, the boundary holds:
#   create table analytics_dev.t (id int)   -> succeeds
#   create table analytics.t (id int)       -> permission denied for schema analytics
docker rm -f pg-probe

cd data && uv run pytest -q && uv run ruff check . && uv run black --check .

Self-check

  • I ran this and it works
  • Tests pass locally
  • No secrets, tokens, or connection strings in the diff
  • This pull request does one thing

Verified beyond the tests: the same seven-check boundary probe passes both against a database db-setup.py built from scratch and against the live project database, where the role now exists.

Copilot AI lite review requested due to automatic review settings August 13, 2026 11:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates local development publishing to target the real backend Postgres database by default (writing into analytics_dev), and adds a provenance “stamp” onto published tables so teams can identify which warehouse schema last populated the shared dev mart.

Changes:

  • Extend backend DB setup to include an analytics_dev schema and a dedicated analytics_dev_user role with constrained privileges.
  • Add optional source stamping to the publish swap so the final table gets a COMMENT ON TABLE like from <source> at <timestamp>.
  • Update Airflow/local documentation and defaults to publish to analytics_dev with the dev role and TLS required.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
scripts/db-setup.py Adds analytics_dev schema/role and updates the privilege layout to enforce the dev/prod boundary via grants.
data/src/publishing/sync.py Adds optional source parameter and stamps the published table via COMMENT ON TABLE after the swap.
data/tests/publishing/test_sync.py Adds tests asserting the stamp is written and occurs after the rename/swap.
data/README.md Documents new publish schema/user settings and explains shared analytics_dev behavior + stamp visibility.
data/airflow/dags/pipeline_dag.py Publishes to a configurable schema and supplies the source string for stamping.
data/.env.example Updates local env guidance to use the real DB, analytics_dev_user, analytics_dev, and TLS required.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

"fct_postings",
columns,
rows,
source=f"{Warehouse.from_env().catalog}.{setting('DBT_SCHEMA')}",
Comment thread data/README.md
Comment on lines 318 to +320
| `DBT_SCHEMA` | `dev_yourname` | `analytics` |
| `BACKEND_PG_HOST` | your own Postgres in Docker | the backend's database |
| `BACKEND_PG_PUBLISH_SCHEMA` | `analytics_dev` | `analytics` |
| `BACKEND_PG_USER` | `analytics_dev_user` | `analytics_user` |
Lasse Benninga added 5 commits August 13, 2026 22:30
…duction may not

Local development published to a Postgres in Docker, which simulates the one
thing that most reliably breaks on the first scheduled run: connecting to a
real database. TLS, the firewall and the grants all went untested until
production tested them. So trainees now publish to the real database, into
their own schema.

Three schemas, and the boundary is permissions rather than naming:

  analytics_user      full on analytics,     reads app
  analytics_dev_user  full on analytics_dev, reads app and analytics
  app_user            full on app,           reads both

analytics_dev_user cannot write analytics, so an .env still pointing at
production fails on the drop with a permission error instead of replacing what
the backend serves. analytics_user cannot even read analytics_dev, so nothing
production builds can come to depend on a table someone is editing from a
laptop. The backend reads both, so its developers can build against a mart
before it reaches production.

Table names are the same in both schemas, so promotion changes the schema and
nothing else.

Verified against a throwaway Postgres 16, all six directions:
dev writes analytics_dev, dev refused on analytics, dev reads analytics,
airflow writes analytics, airflow refused writing and reading analytics_dev.

SchemaAccess.read_only becomes reads, a tuple, because the dev role needs two.
Two data trainees share one analytics_dev schema, and the publish drops and
renames, so the last one to run wins. That is the right behaviour for a place
the two tracks meet: the backend wants one table called fct_postings, not three
named after people. What it lacked was any way to answer 'why did the columns
change this morning'.

So the swap now ends with a comment naming the warehouse schema the rows came
from: 'from team_a.dev_alex at 2026-08-13T11:21Z'. Visible in psql and every
GUI, needs no new setting because the DAG already knows DBT_SCHEMA, and it
describes the table rather than widening what the backend selects.

Useful in production too, where it reads team_a.analytics and dates the last
successful publish.

Verified against Postgres 16: published as dev_alex, republished as dev_maria,
comment followed the second writer and the rows were replaced, which is the
collision the stamp exists to explain.
Settings and README followed the old model, where a trainee published to a
Postgres in Docker. They now name the real host, analytics_dev_user, and
analytics_dev, and say why each is not the production equivalent: analytics_user
writes production and only the team's Airflow VM can read its password.

sslmode goes back to require, since the target is Azure rather than a local
container with no certificate.

Also documents the one consequence of sharing a dev schema: the last publish
wins, and the table comment is how you tell whose run you are looking at.
…atabase

Verifying the documented dev workflow end to end found this. The settings now
send local runs to analytics_dev on the team's real Postgres, but this file
still pinned BACKEND_PG_HOST to the local `db` container and sslmode to
prefer. So `uv run` published to Azure and the same task under `astro dev
start` published somewhere else entirely, silently, which is the exact failure
this file exists to prevent.

The overrides go, and with them the external `finalproject` network, which was
only there to reach that container. That also removes a trap: `astro dev start`
used to fail unless you had first run `docker compose up -d db`, a database the
data pipeline no longer touches.

Verified after the change, against real infrastructure:
  astro dev start                      came up with no local db and no network
  airflow tasks test dbt_build         PASS=37 WARN=0 ERROR=0
  airflow tasks test publish_to_backend  published 176 rows to analytics_dev
CI runs `black --check` alongside ruff; the stamp test was added with only
ruff run locally, so the whole branch failed on formatting.
@lassebenni
lassebenni force-pushed the feat/analytics-dev-schema branch from ff3e667 to 80ae425 Compare August 13, 2026 20:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants