Skip to content

refactor(data): make the starter a Databricks template, not a solution - #4

Closed
lassebenni wants to merge 11 commits into
feat/data-starterfrom
feat/data-starter-databricks
Closed

refactor(data): make the starter a Databricks template, not a solution#4
lassebenni wants to merge 11 commits into
feat/data-starterfrom
feat/data-starter-databricks

Conversation

@lassebenni

@lassebenni lassebenni commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

What I built

Rebases the data starter onto the stack every team will actually use (Databricks, ACA jobs, Airflow) and turns it from a working pipeline into a template with TODOs. Targets feat/data-starter so it lands as a review on top of #1 rather than a competing branch.

Oversized: it rewrites the same starter #1 introduced, so splitting it would leave the branch in a half-Postgres, half-Databricks state that no reviewer could evaluate.

Why this approach

Three decisions taken after #1 was opened:

  1. All three teams use Databricks, ACA jobs and Airflow. The starter was Postgres end to end, and the two-stack choice it assumed no longer exists.
  2. Airflow also publishes from the lakehouse into the backend's Postgres. That step was missing, and it is the one that turns a pipeline into a data product.
  3. Trainees get starter templates, not full solutions.

Postgres stays, but in its real role: the backend's database, which the pipeline publishes into, not where raw data lives. Raw lands as files in the team's Unity Catalog volume, because a file is exactly what the source sent and can be re-read when a column changes shape.

Staging reads the volume with read_files rather than a declared dbt source, so a new day's file needs no code change.

Considered and rejected: keeping a Postgres path for local development alongside Databricks. It gives every concept two implementations and doubles what a trainee has to hold in their head.

Contract impact

None. fct_postings keeps the same columns and the same _fct_postings.yml. One description changed: tags no longer mentions Postgres JSONB operators, since it is a Databricks array until the sync writes it out.

How to run

gh pr checkout 4
cd data
python3 -m py_compile src/*.py airflow/dags/pipeline_dag.py   # imports cleanly
grep -rn "NotImplementedError" src/ airflow/dags/            # what trainees write

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

What changed

Databricks first. src/storage.py lands raw JSON in /Volumes/<catalog>/landing/raw/<source>. dbt/profiles.yml targets dbt-databricks against the team catalog.

Three-step DAG. ingest (trigger the ACA job and wait) → dbt_build (uvx on Python 3.11 with dbt-databricks) → publish_to_backend. Publishing runs after dbt, so a failed test never reaches the backend.

Template, not solution. Finished: config.py, ingest.py, the pipeline.py wiring, the Dockerfile, the contract .yml, the two custom tests. Left to the trainees, each with a docstring saying what it must do: land_raw_json, landing_file_name, read_mart, publish, and the three DAG task bodies.

Local dev. docker compose starts one Postgres named backend-db, standing in for the backend database so the sync can be built before the real one exists.

Removed optional/databricks/, which described Databricks as an optional upgrade from Postgres.

🤖 Generated with Claude Code

Lasse Benninga and others added 4 commits August 10, 2026 13:18
Three changes, all following from decisions taken after this branch was
opened: every team uses Databricks, ACA jobs and Airflow, and the starter
should hand trainees a shape rather than a finished pipeline.

Databricks first. Ingestion lands raw JSON files in the team's Unity Catalog
volume instead of upserting into Postgres, dbt targets dbt-databricks against
the team catalog, and staging reads the volume with read_files. Postgres stays
in the picture as the backend's database, which is what the pipeline publishes
into, not where raw data lives.

Airflow owns three steps: trigger the Container Apps job, build dbt, then
publish the mart into the backend database. The publish step exists because a
mart nobody can read is not a data product, and it runs after dbt so a failed
test never reaches the backend.

Template, not solution. The wiring, the config loading and the conventions are
finished. land_raw_json, the sync, and the three DAG task bodies raise
NotImplementedError with a docstring saying what they must do. The dbt models
are skeletons carrying the naming conventions and the contract .yml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Raw JSON now lands in an ADLS Gen2 container the team creates themselves,
which is what week_15__7_architecture_stacks.md already describes, rather than
going straight into a Unity Catalog volume.

The win is authentication. The container writes with DefaultAzureCredential:
the trainee's az login locally, the Container Apps job's managed identity in
Azure. No Databricks token is involved in ingestion, and account keys are
switched off on the storage account, so there is no key to put in .env.

infra/ is bicep for the storage account, the raw container, a lifecycle rule
and the role assignment for the job identity. It replaces the bare storage
account under optional/, because storage is now on the critical path.

dbt is untouched. A volume in the team's catalog points at the container, so
models keep reading /Volumes/<catalog>/landing/raw and never name the storage
account.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each team has its own Databricks service principal, so dbt and the sync use
OAuth client credentials instead of a personal access token. A personal token
means the pipeline runs as whoever pasted it, breaks when they rotate it, and
keeps working after they leave.

Both values live in Key Vault as fp-databricks-client-id-team-<x> and
fp-databricks-client-secret-team-<x>. The team's Airflow VM has an identity
allowed to read its own team's secrets and nobody else's, so the DAG fetches
them at run time and nothing is stored on the VM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DATABRICKS_HTTP_PATH was left blank; it is the shared warehouse and now
verified end to end with a team service principal. The infra README said
<your-rg>, but role assignments only succeed in rg-hyf-data, so the second
deploy would have failed with AuthorizationFailed and looked like a bug in
the template.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

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

Refactors the data starter into a Databricks-first training template using ADLS, ACA jobs, Airflow, dbt, and backend Postgres publishing.

Changes:

  • Replaces Postgres raw storage with ADLS and Databricks models.
  • Adds three-stage Airflow orchestration and outbound synchronization scaffolding.
  • Promotes Azure infrastructure and removes obsolete optional modules.

Reviewed changes

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

Show a summary per file
File Description
data/src/sync.py Adds backend publishing template.
data/src/storage.py Replaces Postgres writes with ADLS landing template.
data/src/pipeline.py Wires ingestion to raw-file landing.
data/src/config.py Adds storage-oriented configuration.
data/README.md Documents the new architecture and workflow.
data/pyproject.toml Adds Azure, Databricks, and sync dependencies.
data/optional/README.md Removes promoted optional components.
data/optional/databricks/README.md Removes obsolete Databricks upgrade guide.
data/optional/bicep/modules/storage.bicep Removes old optional storage module.
data/optional/bicep/main.bicep Removes old optional deployment entry point.
data/infra/README.md Documents required Azure infrastructure.
data/infra/modules/storage.bicep Provisions secured ADLS storage.
data/infra/main.bicep Adds deployment and role assignment wiring.
data/Dockerfile Installs Azure storage dependencies.
data/docker-compose.yml Reframes Postgres as the backend database.
data/dbt/tests/assert_posted_at_not_in_future.sql Adapts timestamp syntax for Databricks.
data/dbt/profiles.yml Switches dbt to Databricks OAuth.
data/dbt/models/staging/stg_postings.sql Reads raw files through read_files.
data/dbt/models/staging/_sources.yml Removes the Postgres source declaration.
data/dbt/models/marts/fct_postings.sql Updates the mart for Databricks staging.
data/dbt/models/marts/_fct_postings.yml Updates the tags contract description.
data/dbt/dbt_project.yml Adds the landing-volume path variable.
data/airflow/requirements.txt Replaces embedded dbt dependencies.
data/airflow/dags/pipeline_dag.py Adds ingest, dbt, and publish orchestration.
data/.env.example Adds storage, Databricks, and backend settings.

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

Comment thread data/src/pipeline.py
account=config.storage_account,
container=config.storage_container,
blob_path=blob_name(config.source_name, run_date),
records=[record.model_dump(mode="json") for record in parsed],
Comment on lines +35 to 38
created_at as posted_at,
source_file,
ingested_at
from source
bash_command=(
"uvx --python 3.11 --from 'dbt-core==1.10.*' "
"--with 'dbt-databricks==1.10.*' "
"dbt build --project-dir /opt/airflow/include/dbt --profiles-dir /opt/airflow/include/dbt"
Comment thread data/airflow/dags/pipeline_dag.py Outdated
Comment on lines +17 to +21
AZURE_RESOURCE_GROUP, ACA_JOB_NAME,
DATABRICKS_HOST, DATABRICKS_HTTP_PATH,
DATABRICKS_CLIENT_ID, DATABRICKS_CLIENT_SECRET,
DATABRICKS_CATALOG, DBT_SCHEMA,
BACKEND_PG_HOST, BACKEND_PG_DB, BACKEND_PG_USER, BACKEND_PG_PASSWORD
Comment thread data/src/sync.py
Comment on lines +16 to +24
The pattern that avoids a half-written table is write-then-swap:

1. create <table>_new
2. insert every row into it
3. rename <table> -> <table>_old, <table>_new -> <table>, drop <table>_old
(all three inside one transaction)

Readers see the old table until the swap, then the new one. They never see an
empty or partial table.
Comment thread data/src/pipeline.py
Comment on lines +36 to +38
run_date: the day this run belongs to, as YYYY-MM-DD. Airflow passes
its logical date so a re-run of an old day overwrites that day's
file rather than today's.
Lasse Benninga and others added 7 commits August 10, 2026 16:10
Ingestion now uploads to /Volumes/<catalog>/landing/raw through the Databricks
Files API, using the same team credential dbt already uses. That removes the
storage account, its bicep, the role assignment that only succeeds in one
resource group, and the teacher step that had to create an external location
before anything in dbt could run. Verified end to end against team_a: the
upload returns 204 and read_files() reads the rows back.

An empty batch now fails the run instead of reporting success, because landing
zero rows leaves yesterday's mart in place with every test still passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
datetime.fromtimestamp without a timezone uses whatever zone the machine is
in, so the same record produced a different posted_at on a laptop than in the
container. Raised by review on PR #1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
payload.get("data", payload) raises AttributeError when the API returns a
list at the top level, which is the shape the docstring claimed to support.
Anything that is not a list now fails with a message naming the type it got.
Raised by review on PR #1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The compose service became backend-db and this file still said db, still set
POSTGRES_DB=finalproject where compose sets backend, and told students to
start a service named postgres that does not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The documented streamlit command failed on a clean checkout: neither streamlit
nor pandas was in pyproject.toml. Raised by review on PR #1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Findings from the end-to-end rehearsal on team A's real infrastructure:

- The Airflow image has no az binary and no Container Apps provider, so the
  ingest task is an HTTPS call to the management API with a token from the
  VM's own identity. The DAG now ships azure_token() and keyvault() rather
  than pointing students at a CLI that is not installed.
- Secrets move from Airflow variables to Key Vault, fetched per run.
- V2__analytics_schema.sql adds the schema and both sync roles as a migration
  in the backend repo, where the boundary belongs. ALTER DEFAULT PRIVILEGES
  names the writer role: without FOR ROLE the application cannot read a single
  mart, because the sync creates a new table on every publish.
'dbt-core==1.10.*' resolves to 1.10.13, and dbt-databricks 1.10.11 requires
dbt-core <1.10.10, so the wildcard pair no longer resolves at all. A rehearsal
run on the team A VM failed on exactly this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lassebenni

Copy link
Copy Markdown
Collaborator Author

Superseded by #9, which lands the final state of both branches on current main.

This one no longer merges: main has since gained the Spring backend, the Next.js frontend, the root compose stack and CI, all of which postdate this branch.

#9 carries the six live review comments from #4 as fixes, and notes in its description which comments here are already stale (the naive datetime, the undeclared Streamlit dependencies, the postgres versus db service name, the zero-record success, the top-level-list fallback, and the SQL injection in the old Postgres storage.py, plus two comments on files that no longer exist).

Leaving this open for you to close.

@lassebenni lassebenni closed this Aug 11, 2026
@lassebenni
lassebenni deleted the feat/data-starter-databricks branch August 11, 2026 23:07
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